├── .gitignore ├── LICENSE ├── README.md ├── flet_mvc ├── __init__.py ├── alert.py ├── controller.py ├── model.py ├── routing.py └── view.py ├── imgs ├── Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.001.png ├── Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.002.png ├── Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.003.png ├── Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.004.png └── Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.005.png ├── mvc_commands ├── __init__.py ├── cli.py └── templates │ ├── basic │ ├── app.py │ ├── controllers │ │ └── main.py │ ├── models │ │ └── main.py │ └── views │ │ └── main.py │ ├── routes │ ├── app.py │ ├── controllers │ │ ├── home.py │ │ └── secondary.py │ ├── models │ │ ├── home.py │ │ └── secondary.py │ └── views │ │ ├── home.py │ │ └── secondary.py │ └── tabs │ ├── app.py │ ├── controllers │ ├── home.py │ ├── index.py │ └── settings.py │ ├── models │ ├── home.py │ ├── index.py │ └── settings.py │ └── views │ ├── home.py │ ├── index.py │ └── settings.py ├── setup.py └── tests ├── app_test_multi_ref ├── controller.py ├── main.py ├── model.py └── view.py ├── data └── component_attribute.py ├── test_all_controls ├── controller.py ├── main.py ├── model.py └── view.py └── test_datapoints.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | flet_mvc.egg-info/ 4 | .history 5 | .DS_Store 6 | myenv/ 7 | .coverage 8 | __pycache__ 9 | .coveragerc 10 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Carlos Adrian Monroy Garcia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | [DEPRECATED, NO LONGER UPDATED DUE WORK + CHANGED TO REACT FOR PERSONAL PROJECTS, BUT STILL: FEEL FREE TO COLLABORATE] 4 |
5 |
6 | [LOGO IN PROGRESS] 7 |
8 |
9 | Flet-MVC 10 |
11 |

12 | 13 |

The coding best practices for Flet. (Works with Flet v0.9.0)

14 | 15 |

16 | 17 | 18 | 19 |

20 |
21 | 22 | # Content 23 | * [Quick Start](#quick-start) 24 | * [What's next?](#next-updates) 25 | * [Before we start](#before) 26 | * [What is MVC?](#whatismvc) 27 | * [Flet and MVC](#fletandmvc) 28 | * [The flet-mvc package](#package) 29 | * [Installation](#installation) 30 | * [Usage – How to implement MVC to a Flet App?](#usage) 31 | * [Codebase structure:](#codebase) 32 | * [Basic structure](#structure) 33 | * [Explanation of the basic structure](#explanation) 34 | * [Controller Extra abilities](#extra) 35 | * [Datapoints](#datapoints) 36 | * [Supported Controls](#controls) 37 | * [Common Mistakes](#common-mistakes) 38 | 39 | 40 | 41 | 42 | # Quick Start: `flet-mvc` cli 43 | Flet-mvc v.0.1.5 now includes new **quick start** commands that you can use in the **terminal**. They will allow you to start developing right away, without the need of going though the effort of: looking for examples, copying old code as guidance, creating snippets or even reading this documentation every time you are in the need of starting a new Flet project with the MVC structure. 44 | 45 | **The commands:** 46 | 47 | - `flet-mvc start`: Creates the most basic MVC template for a flet app. It also includes the most basic usage of [Datapoints](#datapoints). 48 | - `flet-mvc routes`: Creates the basic MVC template for a routed flet app. 49 | - `flet-mvc tabs`: Creates the basic MVC template for a flet app that uses tabs. (advanced) 50 | 51 | NOTE: The `flet-mvc tabs` command includes a concept of inheriting a modal inside other modal. This would illustrate **one** solution when in the need of making a View interact/modify other views. Remember that these are just templates, feel free to modify as needed. 52 | 53 | 54 | # Version History 55 | ### **Flet-mvc v0.1.5 - The template update:** 56 | - Bug fixes 57 | - Added new flet-mvc cli. See "Quick Start: flet-mvc cli" [STABLE VERSION] 58 | 59 | ### **Flet-mvc v0.1.1 - v0.1.4 - Unstable versions!:** 60 | - Bug fixes 61 | - Added new flet-mvc cli [ERRORS FOUND] 62 | 63 | 64 | ### **Flet-mvc v0.1.0 - The datapoint update:** 65 | 66 | - Added Ref Datapoints:
Creating Ref objects is no longer a pain, the need of using `__init__` and attributes in the model has been removed. It's now as easy as creating a new datapoint and setting it up inside a flet control: `ft.Text(ref=model.MyRefDatapoint)`.
They will show the returned value to the flet Control automatically and change it's value whenever a value is set to this datapoints. For more information see [Datapoints](#datapoints). 67 | - Added RefOnly Datapoints:
This datapoins can be using by declaring the following decorator: `@data.RefOnly`, the difference between the normal Ref datapoints is that this won't set any value to it's control by default, instead you will use it as any normal flet ref obj by calling `.current` and any needed attribute. 68 | - Added `.has_set_value()` method to the datapoint object, which returns a boolean whenever a datapoint has a value set after using ".set_value()". Resets to false after calling `.reset()` 69 | - Added unit tests and an app that tests all supported controls at: `/tests/test_all_controls/main.py` 70 | - Added Dependancy between refs datapoints. If you create a datapoint with a certain Ref Object which default value is another flet control, to which you add another ref datapoint, the parent control will be updated when the child control does. 71 | - Important Notes: 72 | - Memory: After testing the impact in the usage of this library seem to be minimal almost null. Still, more testing will be in progress. 73 | - See all supported controls and their corresponding default values at [Supported Controls](#controls) 74 | 75 |
76 | 77 | 78 | 79 | # What's next? 80 | 81 | The flet-mvc 0.1.0 it's a very stable and complete versions of this library. But we are still a few steps away from making it the real flet-mvc v1.0.0 package. Below you will find the topic for the next updates and what will they consist of: 82 | 83 | 1. **The User's Control update**: This will cover the creation of a Flet User Control using the flet-mvc best practices and the corresponding send/receive decorator to comunicate between controls. 84 | 2. **The Controller update**: This will include new controller functions that will make flet develpment and building controls easier; more alerts, options for dropdowns maybe, etc. (suggestions are very well received) 85 | 3. **The DataTable update**: This will include practical controller functions that will allow the developer creating tables faster. Methods like: 86 | - stablish_columns(columns: List[str]) 87 | - stablish_rows(rows: List[str]) 88 | - create_from_df(data=pd.DataFrame) 89 | - create_from_dict(data=List[dict]) 90 | - download_df() 91 | 92 | *These method names are not official and will be part of the datapoints methods. 93 | 94 | Also some styling maybe, and pretty much everything realted to DataTables. 95 | 4. **The Depancy Graph update**: This will be the biggest update and may take months of develoment. Were datapoints can depend on other datapoints (without the need of being ref objects) and can automatically affect the node/root values whenever the leaf changes. This will mark the era of the flet-mvc v1.0.0 (in the meantime use refs/datapoints for simple dependancies) 96 | 97 | 98 | - **bug-fixes-updates**: simple bug-fixes, better typing exceptions, etc... 99 | 100 |
101 | 102 | 103 | # Before we start 104 | In case you want to go to a straighforward code example, please see the flet app where I am testing almost all flet controls with this library, and of course, following the strucure described here. The app is at: 105 | `/tests/test_all_controls/main.py` 106 | 107 | **KEEP IN MIND**, that it is a testing app, of course you don't need to add refdatapoints to every control in the ui. It's just showing the possibilities for all supported controls. 108 | 109 | **SPECIAL NOTE**: I made this library with love <3 - I really hope you can find this guide is useful! in case it's not, please feel free to reach me out and I will further explain any topic that is not clear or add the necessary changes to the code and documentation. 110 | 111 | You can find me in the Flet discort chanel which can be found in it's page: Flet at `third-party-packaged/flet-mvc` 112 | 113 |
114 |
115 | 116 | 117 | # What is MVC? 118 | 119 | MVC stands for Model-View-Controller. It is a design pattern that is used to create software applications with a clear separation of concerns. 120 | 121 | - The Model represents the data and the business logic of the application. It is responsible for handling the data and performing any necessary computations. 122 | 123 | - The View is responsible for displaying the data to the user. It receives updates from the Model and renders them on the screen. 124 | 125 | - The Controller is responsible for handling user input and updating the Model and the View accordingly. It receives user input from the View and updates the Model with the new data, and then instructs the View to refresh the display with the new data. 126 | 127 | The MVC pattern helps in separation of concern, **maintainability** and testability** of the application and also **making it scalable.** 128 | 129 | In even simple terms: 130 | 131 | - The Model can update the view when it’s values are changed by the controller. or just a "container" of data used in the app. 132 | 133 | - The View is what you see and can interact with 134 | 135 | - The Controller receives user input from the View and updates the Model with the new data, and then instructs the View to refresh the display with the new data. 136 | 137 | Here is an image that explains this graphically: 138 | 139 | ![](imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.001.png) 140 | 141 | Image taken from Wikipedia.org 142 | 143 | 144 |
145 | # Flet and MVC 146 | 147 | In Flet you can build a whole running application in just one file. But “just because you can, doesn’t mean you should”. 148 | 149 | Here two disadvantages of not using an architecture (any): 150 | 151 | - Poor maintainability: A lack of separation of concerns can make it harder to maintain the codebase over time. This can lead to bugs and performance issues that are difficult to fix. 152 | 153 | - Lack of scalability: Applications that are not designed using the MVC pattern can be harder to scale as the number of users or amount of data increases. This is because the codebase may become more complex and harder to understand as it grows. 154 | 155 | - Just imagine receiving a +3000 lines of code app python file where every 100 lines a new Flet Control is added to the view, methods all over the place and variables too. Good luck trying to fix a bug over there 156 | 157 | **Flet-mvc** python package provides the necessary components to assimilate an implementation of the MVC architectural pattern in Flet. 158 | 159 | 160 | 161 | # The flet-mvc package 162 | 163 | 164 | 165 | ## Installation 166 | ``` 167 | python3 -m pip install flet-mvc 168 | ``` 169 | 170 | 171 | ## Usage – How to implement MVC to a Flet App? 172 | 173 | 174 | 175 | ### Codebase structure: 176 | In the following image you will find a basic code base structure to start working in a new app. 177 | 178 | ![](imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.002.png) 179 | 180 | This will keep the app maintenance and scalability on point. 181 | 182 | **One more example** 183 | 184 | Imagine a flet app that looks like this: 185 | 186 | ![](imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.003.png) 187 | 188 | This app example has an index view, this index view contains a layer and a navigation menu that will allow the user to change the view to three different tabs: Home, Statistics and Dashboard. Each view containing unique logic. 189 | 190 | How do we keep organized the code of this app? Using a similar MVC codebase structure. Something like this: 191 | 192 | ![](imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.004.png) 193 | 194 | This way we can keep the project completely scalable and maintainable. 195 | 196 | 197 | 198 | ### Basic structure 199 | 200 | In the following paragraphs I will show you the basic template that each of the MVC python scripts contain. You can start building your app on top of these templates right away. 201 | 202 | **NOTE**: You can now get the basic template when creating a new flet project by running the command `flet-mvc start` on v.0.1.5 - I would also encourage you to create User snippets. 203 | 204 | 205 | **Model:** 206 | 207 | 208 | Model -> ./models/main.py 209 | 210 | ```python 211 | from flet_mvc import FletModel, data 212 | import flet as ft 213 | 214 | # Model 215 | class Model(FletModel): 216 | @data 217 | def example(self): 218 | return ... 219 | ``` 220 | **View** 221 | 222 | ./views/main.py 223 | 224 | ```python 225 | from flet_mvc import FletView 226 | import flet as ft 227 | 228 | # View 229 | class MainView(FletView): 230 | def __init__(self, controller, model): 231 | view = [ 232 | ... # List of flet controls 233 | ] 234 | super().__init__(model, view, controller) 235 | ``` 236 | 237 | 238 | **Controller** 239 | 240 | ./controllers/main.py 241 | 242 | ```python 243 | from flet_mvc import FletController 244 | 245 | # Controller 246 | class Controller(FletController): 247 | ... 248 | ``` 249 | 250 | 251 | **App** 252 | 253 | app.py 254 | 255 | ```python 256 | import flet as ft 257 | from .controllers.main import Controller 258 | from .views.main import MainView 259 | from .models.main import Model 260 | 261 | def main(page: ft.Page): 262 | # MVC set-up 263 | model = Model() 264 | controller = Controller(page, model) 265 | model.controller = controller # Optional, in case controller it's needed in the model. 266 | view = MainView(controller, model) 267 | 268 | # Settings 269 | page.title = "" 270 | 271 | # Run 272 | page.add( 273 | *view.content 274 | ) 275 | 276 | ft.app(target=main) 277 | ``` 278 | 279 | 280 | 281 | ### Explanation of the basic structure: 282 | 283 | **App** 284 | 285 | Everything starts with the app.py, this script may be a little familiar to you if you have already worked with flet. Here you will: 286 | 287 | - Set-up the MVC classes (I will explain each of them in a moment) 288 | - Stablish the basic setting of your app (page), like the title, scroll, horizontal alignment, theme mode, etc. see for more page properties 289 | - And finally run the app by reading the content of your View class. 290 | 291 | One of the question that you may have is, *how do I add controls to the page properties like banner or overlay?* The answer to that question is that you will add every control into a view attributes and set them up in the "Settings" section. example: 292 | 293 | ```python 294 | # Settings 295 | page.overlay.append(view.audio) 296 | page.overlay.append(view.bottom_sheet) 297 | page.overlay.append(view.file_picker) 298 | page.appbar = view.app_bar 299 | page.banner = view.banner 300 | page.snack_bar = view.snack_bar 301 | page.floating_action_button = view.fab 302 | 303 | ``` 304 | 305 | Just keep in mind that if you want to dinamically change them, you will be doing this set-up in the controller like: 306 | 307 | ```python 308 | # Inside a controller function 309 | self.page.banner = self.model.Banner.current # <- ref datapoint 310 | ``` 311 | 312 | Let’s continue to the view 313 | 314 | **View** 315 | 316 | Now let’s talk about the View class (which, as mentioned, I recommend having in the corresponding “views” folder). 317 | 318 | Here we will inherit *FletView* base class and create it’s “content” attribute as list of all the flet controls that our app will contain.
The *FletView* base class will have access to the model and controller. This way we can set the flet controls to use our controller methods when an event occurs.
Here is an example of a flet TextField control using a ref object from the model and a controller method in the “on\_click“ and “on\_submit” argument: 319 | 320 | ```python 321 | ft.TextField( 322 | hint_text="0.0", 323 | ref=model.TextFieldDatapoint, # Setting the ref obj 324 | border=ft.InputBorder.NONE, 325 | filled=True, 326 | expand=True, 327 | keyboard_type="number", 328 | on_change=controller.check_digits_only, # Setting the function 329 | on_submit=controller.create_labels, # Setting the function 330 | ), 331 | ``` 332 | 333 | **NOTE:** Keep in mind that datapoints have default values which will be set in the controls automaitcally. In this case, the value of TextFieldDatapoint will modify the attribute "value" of the TextField control. To learn more about this see [Datapoints](#datapoints). 334 | 335 | Also, as we mentioned in the **App** section, in the view we can declare the controls that can be added to the flet page object. In this case we will need to add them into separate attributes. Following the example of the banner we mentioned before, the banner will look like this in the view: 336 | 337 | ```python 338 | class MainView(FletView): 339 | def __init__(self, controller, model): 340 | self.banner = ft.Banner( # <-- 341 | ref=model.Banner, 342 | bgcolor=ft.colors.AMBER_100, 343 | leading=ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size=40), 344 | actions=[ 345 | ... 346 | ], 347 | ) 348 | 349 | view = [ 350 | ... # List of "normal" flet controls 351 | ] 352 | super().__init__(model, view, controller) 353 | ``` 354 | 355 | and that is how now the banner could be set to the page by using the datapoint in the controller.py or by using this attribute in the app.py at the "Settings" section. 356 | 357 | Now let’s talk about the model before talking about the controller. 358 | 359 | **Model** 360 | 361 | The Model class inherit the *FletModel* base class. Here is where we create methods with the flet-mvc @data decorator, which I like to call: **Datapoints**. 362 | 363 | In a VERY simple terms (maybe someone can get mad at me for saying this), the Model is a class that contains all the “variables” that the app will work with. These “variables” can have anything: from a Pandas DF obtained from SQL, Classes, Controls; to simple data types like strings, lists, int, etc. 364 | 365 | In order to create a “variable”/datapoint in the model, you only need to create a method, return it’s default value (can be computed with any logic that you want to use) and use the @data decorator in it. 366 | 367 | The benefits of having a datapoint is that it can be accessed from the Controller and **can be used as any other python datatype**, but **additionally**: it will allow you to **set** **it’s value** in case it’s needed and most importantly **reset** **it’s value**! 368 | 369 | \*\* Since version *0.1.0: The datapoint update*, Datapoint can also work as Flet Ref objects. For more information see: [Datapoints](#datapoints) 370 | 371 | 372 | **Controller** 373 | 374 | I like to think that the Controller is the most relevant class from an application because it handles all the interactions of the user within the app (events). 375 | 376 | Essentially the controller class will inherit from flet-mvc *FletController* base class and in it you will define all the methods that *Flet controls* use to handle the events. (as mentioned in the View explanation) 377 | 378 | Let's talk about all the possibilities that the FletController allows you to do: 379 | 380 | - **Access a model datapoint:** 381 | 382 | Let’s imagine in our Model we declare a “number” datapoint that return 5 by calculating 2+3: 383 | ```python 384 | @data 385 | def number(self): 386 | return 2 + 3 387 | ``` 388 | 389 | now in our Controller we have a “clicked” method (which we assume is called when a button is clicked), and we want to print the “number” value. We will achieve this by doing the following: 390 | 391 | ```python 392 | class Controller(FletController): 393 | 394 | def clicked(self, e=None): 395 | print(self.model.number()) # will print: 5 396 | ``` 397 | 398 | \*Notice how we call the datapoint by using the parethesis (), this returns the current value of the datapoint. 399 | 400 | - **Set a model datapoint with any other value:** 401 | 402 | Now let’s say we want to set our “number” datapoint value to 1 after we "click" it. Then we will use the **set\_value** property of the datapoint: 403 | 404 | ```python 405 | class Controller(FletController): 406 | 407 | def clicked(self, e=None): 408 | print(self.model.number()) # will print: 5 409 | self.model.number.set_value(1) 410 | print(self.model.number()) # will print: 1 411 | ``` 412 | This number is saved and can be latter used by another other controller method or even other Controller class as long as it has the model instance reference: 413 | 414 | ```python 415 | def clicked2(self, e=None): 416 | print(self.model.number()) # will print: 1 417 | 418 | ``` 419 | 420 | - Reset a model datapoint 421 | 422 | If we no longer need the set value and want to revert our change to this datapoint, it’s as easy as use the **reset** property: 423 | 424 | ```python 425 | class Controller(FletController): 426 | 427 | def clicked(self, e=None): 428 | print(self.model.number()) # will print: 5 429 | self.model.number.set_value(1) 430 | print(self.model.number()) # will print: 1 431 | self.model.number.reset() 432 | print(self.model.number()) # will print: 5 again after calculating 2+3 again. 433 | ``` 434 | 435 | - Set the default value of a datapoint 436 | 437 | If by any chance we want to set a new default value whenever we reset a datapoint, we will use **set_default** property: 438 | 439 | ```python 440 | class Controller(FletController): 441 | 442 | def clicked(self, e=None): 443 | print(self.model.number()) # will print: 5 444 | self.model.number.set_value(1) 445 | print(self.model.number()) # will print: 1 446 | self.model.number.set_default(3) 447 | self.model.number.reset() 448 | print(self.model.number()) # will print: 3 which is the new default 449 | ``` 450 | 451 | To learn more about datapoints please see [Datapoints](#datapoints). 452 | 453 | 454 | 455 | 456 | ### Controller Extra abilities 457 | 458 | - The FletController base class also allows out app Controller to update the view, as we would do it with page.update() but just by running self.update() in a controller method, like this: 459 | 460 | ```python 461 | def clicked(self, e=None): 462 | self.update() 463 | ``` 464 | 465 | - You can also access flet “page” by doing self.page 466 | - Create alerts, you can send an alert whenever you need it as snackbar by using: 467 | 468 | self.alert(“{your alert message}”, {alert type}) 469 | 470 | 471 | Example: 472 | ```python 473 | self.alert("Alert Message", alert.WARNING) # from flet_mvc import alert 474 | ``` 475 | 476 | alert shown: 477 | 478 | ![](imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.005.png) 479 | 480 | There are 5 different alerts, feel free to experiment with any of them. 481 | ```python 482 | WARNING = "warning" 483 | ERROR = "error" 484 | SUCCESS = "success" 485 | INFO = "info" 486 | ADVICE = "advice" 487 | ``` 488 | 489 | 490 | 491 | # Datapoints 492 | 493 | 494 | The data decorator in the Flet MVC framework is a powerful tool for defining Datapoints within a model. These datapoints form the backbone of your model's state and can be used to track, manipulate, and reference important data throughout the lifecycle of the Flet app. 495 | 496 | I recommend using PascalCase to define your datapoints. () 497 | 498 | It's important to mention that there are three types of datapoints: 499 | 500 | - Normal Datapoint: This Datapoints are used as normal "variables" that can be used at any time of the App lifecycle, they can be any type of data that you need: Pandas Dataframes, objects, integers, strings, etc. The way to define this datapoints is simply by creating a method in the model with the @data decorator: 501 | 502 | ```python 503 | class Model(FletModel): 504 | @data 505 | def Example(self): 506 | return "string type" 507 | ``` 508 | 509 | - Normal Reference Data Points: Normal reference data points are used when you want a Datapoint to affect the state of a Flet Control whenever these datapoints change their value. Also the returned type of this datapoint will be set to a specific attribute of the control, depending which one is it. Please see the section "Supported Flet Controls" below to learn more about the attributes affected. The way to define these datapoints is by declaring a normal datapoint, but adding it to a flet control `ref` attribute: 510 | 511 | ```python 512 | class Model(FletModel): 513 | @data 514 | def Example(self): 515 | return "string type value" 516 | 517 | class MainView(FletView): 518 | def __init__(self, controller, model): 519 | view = [ 520 | ft.Text(ref=model.Example) # This converts the datapoint automatically to a Ref Obj 521 | ] 522 | super().__init__(model, view, controller) 523 | ``` 524 | 525 | - Reference-Only Datapoints (RefOnly): Lastly, RefOnly Datapoints are meant to be used only for referencing. They cannot be directly set, reset, appended to, or retrieved like normal data points; also they should return a None value since it's returned value don't affect the state of the Flet Control. Any attempts to directly interact with a RefOnly data point will raise a TypeError. 526 | 527 | Essentially they will fully work as normal Flet Ref Obj and in order to access the attributes of a control you will need to invoke the `.current` property. Don't forget to add it to the `ref` attribute of a Flet Control. The way to define them is by using the `@data.RefOnly` decorator: 528 | 529 | ```python 530 | class Model(FletModel): 531 | @data.RefOnly # <-- 532 | def Example(self): 533 | return None # Any other value will result in useless logic 534 | 535 | class MainView(FletView): 536 | def __init__(self, controller, model): 537 | view = [ 538 | ft.Text(ref=model.Example, value="RefOnly") # Assigning 539 | ] 540 | super().__init__(model, view, controller) 541 | ``` 542 | 543 | 544 | As seen in the Controller section, here are some of the functionalities of the data decorator: 545 | 546 | ## 1. Setting and Getting Data Point Values 547 | 548 | Once a data point is defined, its value can be set using the `set_value()` function, and retrieved simply by calling the data point as a function. For example: 549 | 550 | ```python 551 | model = MockModel() 552 | model.datapoint.set_value("Test Value") 553 | print(model.datapoint()) # Prints: Test Value 554 | ``` 555 | If the Datapoint is a ref obj datapoint, it will only accept the expected type of the control. For example, seting a string value for a datapoint that needs an integer for it's defined attributes (see *Supported flet controls* section below), will raise an exception. Same error will be applied if the default returned value is not consintent with the control. 556 | ## 2. Resetting Data Point Values 557 | 558 | The value of a data point can be reset to its initial value using the `reset()` function. For example: 559 | 560 | ```python 561 | model.datapoint.reset() 562 | print(model.datapoint()) # Resets to it's initial defined value (unless it's set to a new default) 563 | ``` 564 | 565 | ## 3. Setting a new default value 566 | 567 | In the case that you need to set a new default for a datapoint, you can always use the `set_default` function. For Example: 568 | 569 | ```python 570 | print(model.datapoint()) # Initial declared value 571 | model.datapoint.set_default("new value") 572 | model.datapoint.reset() 573 | print(model.datapoint()) # Shows "new value" 574 | ``` 575 | 576 | 577 | ## 4. Appending to List Data Points 578 | 579 | If a data point is a list, values can be appended to it using the `append()` function. Note that a normal append operation won't modify the `has_set_value` attribute of the data point. 580 | 581 | ```python 582 | model.datapoint_list().append("Test Value") 583 | print(model.datapoint_list()) # Prints: ["Test Value"] 584 | ``` 585 | In case the Datapoint is a ref value with a data type of list (usually list of controls), then the append method can be directly called from the datapoint, it won't work otherwise. 586 | 587 | ```python 588 | model.datapoint_list.append(ft.Text()"Test Value")) 589 | print(model.datapoint_list()) # Shows the full list of controls that the datapoint has. 590 | ``` 591 | 592 | ## 5. Check if it has a value set 593 | 594 | Sometime you may want to see if a datapoint has changed of state and a value has been set. In order to solve that you can use the `has_set_value()` function, which will return a bool when the state has been set. 595 | 596 | ```python 597 | print(model.datapoint.has_set_value()) # False 598 | model.datapoint.set_value(None) 599 | print(model.datapoint.has_set_value()) # True 600 | model.datapoint.reset() 601 | print(model.datapoint.has_set_value()) # False 602 | ``` 603 | 604 | ## 6. Hard Reset (WARNING) 605 | 606 | There is also a hard reset method called `__hard_reset__()` which will completely reset the datapoint to a state where it was never assigned to any flet Control, loosing all reference and being in the limbo. I have been using this to test the same datapoint in multiple flet controls by losing reference of the previous ones. 607 | 608 | ## 7. Multireference 609 | 610 | A Datapoint can save the reference of multiple controls at the same time, in other words, we are assinging the same datapoint to multiple controls (that's why in order to assign a datapoint to another control you first need to call the __hard__reset__() method as mentioned above). This should be only used when you are sure the controls have the same attribute to be set by the ref datapoint. This can be useful when you have multiple copies of the same control. Not sure if it's needed but added this possibility. 611 | 612 | ## 8. Current property 613 | 614 | The `.current` property can only be used by Ref Datapoints, either normal ones or RefOnly datapoints. The value returned by this property it's the referenced flet Control itself. This way you can access all the attributes of a control in the controller. 615 | 616 | ## 9. Control Dependencies 617 | 618 | This is where the magic can happen. Let's say you have a dialog component: 619 | 620 | 621 | ```python 622 | ft.AlertDialog(ref=model.Dialog, title=ft.Text("Title")) 623 | ``` 624 | 625 | If you look at the section below you will see that the Ref Datapoint will affect the content of the Dialog, but not the title, but what if we also what to have the text of the title in another datapoint so that it can change of state depending on a controller event? Well, we could define another ref datapoint to this text! 626 | 627 | ```python 628 | ft.AlertDialog(ref=model.Dialog, title=ft.Text(ref=model.Text)) 629 | ``` 630 | 631 | Hence our model datapoint will look like this: 632 | 633 | ```python 634 | @data 635 | def Text(self): 636 | return "Title" 637 | 638 | @data 639 | def Dialog(self): 640 | return ft.Text("This is my dialog content") 641 | ``` 642 | 643 | but as you can see there is even more levels to the Dialog datapoint, so we can even add more ref objects from the "self" model. 644 | 645 | ```python 646 | @data 647 | def Text(self): 648 | return "Title" 649 | 650 | @data 651 | def Dialog(self): 652 | return ft.Text(ref=self.DialogContentText) 653 | 654 | @data 655 | def DialogContentText(self): 656 | return "This is my dialog content" 657 | ``` 658 | you see where I am going? I know this example is a little useless, but it can illustrate the power of ref datapoint, and even more because if we do a `self.model.Text.set_value("new_title")` in the controller, it will automatically affect the root Control which will be the AlertDialog. 659 | 660 | Just for the record, another way to do this will be the following: 661 | 662 | ```python 663 | dialog = ft.AlertDialog(content=ft.Text(ref=model.DialogContentText) title=ft.Text(ref=model.Text)) 664 | 665 | # assigned to variable so I can set it in the page dialog attribute. *See the banner example at the View Section above. 666 | ``` 667 | this way I am only using two datapoint instead of three, but sometimes have that third one is useful. 668 | 669 |
670 | 671 | ## Supported flet controls: 672 | 673 | | Control | Attribute | Type | 674 | |-------------------------|--------------|------------| 675 | | ft.TextField | value | str | 676 | | ft.AlertDialog | content | Control | 677 | | ft.AnimatedSwitcher | content | Control | 678 | | ft.AppBar | actions | list | 679 | | ft.Audio | src | str | 680 | | ft.Banner | content | Control | 681 | | ft.BottomSheet | content | Control | 682 | | ft.Card | content | Control | 683 | | ft.Checkbox | value | Bool | 684 | | ft.CircleAvatar | content | Control | 685 | | ft.Column | controls | list | 686 | | ft.Container | content | Control(s?)| 687 | | ft.DataTable | rows | list | 688 | | ft.DataRow | cells | list | 689 | | ft.Draggable | content | Control | 690 | | ft.DragTarget | content | Control | 691 | | ft.Dropdown | options | list | 692 | | ft.ElevatedButton | text | str | 693 | | ft.FilledButton | text | str | 694 | | ft.FilledTonalButton | text | str | 695 | | ft.FloatingActionButton | text | str | 696 | | ft.GestureDetector | content | Control | 697 | | ft.GridView | controls | list | 698 | | ft.Icon | name | str | 699 | | ft.Image | src | str | 700 | | ft.ListTile | title | Control | 701 | | ft.ListView | controls | list | 702 | | ft.Markdown | value | str | 703 | | ft.NavigationBar | destinations | list | 704 | | ft.NavigationRail | destinations | list | 705 | | ft.OutlinedButton | text | str | 706 | | ft.PopupMenuItem | text | str | 707 | | ft.PopupMenuButton | items | list | 708 | | ft.ProgressBar | value | float | 709 | | ft.ProgressRing | value | float | 710 | | ft.Radio | value | str | 711 | | ft.RadioGroup | value | str | 712 | | ft.ResponsiveRow | controls | list | 713 | | ft.Row | controls | list | 714 | | ft.ShaderMask | content | Control | 715 | | ft.Slider | value | int | 716 | | ft.Stack | controls | list | 717 | | ft.Switch | value | bool | 718 | | ft.Tabs | tabs | list | 719 | | ft.Text | value | str | 720 | | ft.TextButton | text | str | 721 | | ft.TextField | value | str | 722 | | ft.TextSpan | text | str | 723 | | ft.Tooltip | content | Control | 724 | | ft.TransparentPointer | content | ---- | 725 | | ft.WindowDragArea | content | Control | 726 | | ft.canvas.Canvas | shapes | list | 727 | 728 | 729 | Other supported controls are, but I would recommend using them as RefOnly datapoints: 730 | 731 | | Control | 732 | |------------------------| 733 | | ft.DataColumn | 734 | | ft.Divider | 735 | | ft.DataCell | 736 | | ft.FilePicker | 737 | | ft.HapticFeedback | 738 | | ft.IconButton | 739 | | ft.InlineSpan | 740 | | ft.Semantics | 741 | | ft.ShakeDetector | 742 | | ft.SnackBar | 743 | | ft.VerticalDivider | 744 | 745 | 746 | MatplotlibChart, PlotlyChart, LineChart, BarChart, PieChart Controls are still missing some testing. 747 | 748 | 749 | 750 | # Common Mistakes 751 | If you have any issues in your code is possible that these are the possible mistakes: 752 | 753 | 1. Forget to add the Ref-Datapoint to a Flet Control: Keep in consideration that if you want to use a model datapoint as Ref-Object or RefOnly-Object **you need to assign it to a Flet control!** Else you won't be seeing any change being made in the UI. 754 | 755 | Example: 756 | 757 | ```python 758 | ft.Text(ref=model.MyRefDatapoint) 759 | ``` 760 | 761 | 2. Forget to use current attribute of a Ref-Object Datapoint in the Controller: When working with Ref-Object Datatpoints, it's possible to forget using the *current* property. This one will actually return the Flet Control that you are referencing. So it's important to call current when accessing any other arrtribute of a Control. 762 | 763 | Example: 764 | ```python 765 | # Inside a controller function: 766 | self.model.MyRefDatatpoint.current.icon 767 | ``` 768 | 769 | 3. Calling the model datapoints and controller functions in the Flet controls: In order to attach a datapoint to a Flet control, or adding the function to be triggered by an event of a flet control, it's important to specify only the object! not calling it! 770 | 771 | Example: 772 | ```python 773 | # Wrong: 774 | ft.ElevatedButton(ft.model.ButtonDatapoint(), on_click=controller.button_click()) 775 | # Correct (no parenthesis): 776 | ft.ElevatedButton(ft.model.ButtonDatapoint, on_click=controller.button_click) 777 | ``` 778 | -------------------------------------------------------------------------------- /flet_mvc/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import * # noqa 2 | from .controller import * # noqa 3 | from .view import FletView # noqa 4 | from .routing import RouteHandler # noqa 5 | from . import alert # noqa 6 | -------------------------------------------------------------------------------- /flet_mvc/alert.py: -------------------------------------------------------------------------------- 1 | """ Alert constants """ 2 | 3 | WARNING = "warning" 4 | ERROR = "error" 5 | SUCCESS = "success" 6 | INFO = "info" 7 | ADVICE = "advice" 8 | -------------------------------------------------------------------------------- /flet_mvc/controller.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flet Base Controller Class 3 | """ 4 | 5 | from typing import Type 6 | import flet as ft 7 | from .alert import ERROR, WARNING, SUCCESS, ADVICE, INFO 8 | from .model import FletModel 9 | 10 | 11 | class FletController: 12 | def __init__(self, page: ft.Page, model: Type[FletModel]): 13 | self.page = page 14 | self.model = model 15 | 16 | def update(self): 17 | """Updates the page by using self.update() instead of self.page.update()""" 18 | self.page.update() 19 | 20 | def alert(self, msg="", alert_type=ERROR): 21 | """Show Alert""" 22 | if alert_type == ERROR: 23 | self.page.snack_bar = ft.SnackBar( 24 | content=ft.Row( 25 | controls=[ 26 | ft.Icon(ft.icons.ERROR_OUTLINE, color=ft.colors.WHITE), 27 | ft.Text(msg), 28 | ], 29 | ), 30 | bgcolor=ft.colors.ERROR, 31 | ) 32 | self.page.snack_bar.open = True 33 | self.update() 34 | 35 | elif alert_type == SUCCESS: 36 | self.page.snack_bar = ft.SnackBar( 37 | content=ft.Row( 38 | controls=[ 39 | ft.Icon(ft.icons.CHECK_CIRCLE, color=ft.colors.WHITE), 40 | ft.Text(msg), 41 | ], 42 | ), 43 | action="OK", 44 | action_color=ft.colors.BLACK, 45 | bgcolor=ft.colors.GREEN_300, 46 | ) 47 | self.page.snack_bar.open = True 48 | self.update() 49 | 50 | elif alert_type == ADVICE: 51 | self.page.snack_bar = ft.SnackBar( 52 | content=ft.Text(msg), 53 | action="Understood", 54 | action_color=ft.colors.BLUE_200, 55 | ) 56 | self.page.snack_bar.open = True 57 | self.update() 58 | 59 | elif alert_type == INFO: 60 | self.page.snack_bar = ft.SnackBar( 61 | content=ft.Row( 62 | controls=[ 63 | ft.Icon(ft.icons.INFO_OUTLINED, color=ft.colors.WHITE), 64 | ft.Text(msg), 65 | ], 66 | ), 67 | bgcolor=ft.colors.BLUE_200, 68 | ) 69 | self.page.snack_bar.open = True 70 | self.update() 71 | 72 | elif alert_type == WARNING: 73 | self.page.snack_bar = ft.SnackBar( 74 | content=ft.Row( 75 | controls=[ 76 | ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.WHITE), 77 | ft.Text(msg), 78 | ], 79 | ), 80 | action="OK", 81 | action_color=ft.colors.BLACK, 82 | bgcolor=ft.colors.AMBER, 83 | ) 84 | self.page.snack_bar.open = True 85 | self.update() 86 | -------------------------------------------------------------------------------- /flet_mvc/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Data decorator. Originally created to be used in every method of the model class 3 | it will convert the method to a datapoint/node that can be set and reset. 4 | 5 | It allows using a reference datapoints automatically when sent in 6 | the "ref" parameter of a Flet Control. 7 | 8 | Next Feature: Dependency between datapoints (currently we can't set default values 9 | with other datapoints and make them change automatically when the parent changes). 10 | Send/Receive decorators. 11 | """ 12 | 13 | import logging 14 | from dataclasses import dataclass 15 | from typing import Any, Optional, Callable, List 16 | 17 | logging.basicConfig(level=logging.WARNING) 18 | 19 | __all__ = ["data", "FletModel"] 20 | 21 | 22 | class FletModel: 23 | def __init__(self, **kwargs): 24 | self.ref_objs = [] 25 | self.controller = None 26 | 27 | 28 | class _Null: 29 | """Null class that will be used when the datapoint is still empty""" 30 | 31 | def __bool__(self): 32 | return False 33 | 34 | def __repr__(self): 35 | return "Null" 36 | 37 | 38 | NULL = _Null() 39 | 40 | 41 | @dataclass 42 | class RefDatapoint: 43 | current: Optional[Any] = NULL 44 | ref_attr: Optional[str] = NULL 45 | ref_type: Optional[Any] = NULL 46 | 47 | 48 | class data: 49 | """ 50 | data decorator 51 | Converts a class method (intended for the Model class) to a datapoint 52 | """ 53 | 54 | def __init__(self, f: Callable[..., Any]): 55 | self.is_instanciated = False 56 | self.func = f 57 | self.name: str = f.__name__ 58 | self.__has_set_value = False 59 | self.__value: Optional[Any] = NULL 60 | self.__new_default: Optional[Any] = NULL 61 | self.__ref_only = False 62 | 63 | # Ref obj 64 | self.__ref_datapoints: List[RefDatapoint] = [] 65 | self.is_ref_obj: bool = False 66 | self.is_container_ref: bool = False 67 | self.is_ref_multi_referenced: bool = False 68 | 69 | @classmethod 70 | def RefOnly(cls, f): 71 | instance = cls(f) 72 | instance.__ref_only = True # noqa 73 | return instance 74 | 75 | def __call__(self) -> Any: 76 | """ 77 | Return current value when datapoint is called 78 | e.g. print(self.model.fruit()) -> 'Apple' 79 | """ 80 | # ref_only 81 | if self.__ref_only: 82 | raise TypeError( 83 | f"'Ref Only' datapoint is not callable; perhaps you meant 'self.model.{self.name}.current' to access Referenced object." 84 | ) 85 | 86 | if self.is_ref_obj: 87 | # Expecting for all of them to be the same, else don't use multi datapoints if they are going to be different. 88 | return getattr( 89 | self.__ref_datapoints[0].current, self.__ref_datapoints[0].ref_attr 90 | ) 91 | else: 92 | return self.value 93 | 94 | def __hard_reset__(self) -> None: 95 | """Used on testing only so far""" 96 | self.is_instanciated = False 97 | self.__has_set_value = False 98 | self.__value: Optional[Any] = NULL 99 | self.__new_default: Optional[Any] = NULL 100 | self.__ref_datapoints: List[RefDatapoint] = [] 101 | self.is_ref_obj: bool = False 102 | self.is_container_ref: bool = False 103 | self.is_ref_multi_referenced: bool = False 104 | self.__get__(self.model_instance, None) 105 | 106 | def __get__(self, instance, owner): 107 | """ 108 | Return value when accessing the datapoint instance. 109 | This will return the self instance of the datapoint in order to 110 | access the other methods of the datapoint. 111 | """ 112 | if self.is_instanciated: 113 | return self 114 | 115 | self.model_instance = instance 116 | self.model_class = self.model_instance.__class__ 117 | self.is_instanciated = True 118 | 119 | if not hasattr(self.model_instance, "ref_objs"): 120 | super(self.model_class, self.model_instance).__init__() 121 | 122 | # ref only warning 123 | if self.__ref_only: 124 | if self.func(instance) != None: 125 | logging.warning( 126 | f"The datapoint '{self.name}' is Ref Only, the returned value '{self.func(instance)}' won't have any impact. Consider returning 'None' value." 127 | ) 128 | return self 129 | 130 | if self.value is NULL: 131 | self.value = self.func(instance) 132 | 133 | return self 134 | 135 | def has_set_value(self): 136 | return self.__has_set_value 137 | 138 | @property 139 | def value(self): 140 | if self.__ref_only: 141 | raise TypeError( 142 | f"'Ref Only' datapoint doesn't have a 'value' functionality; perhaps you meant 'self.model.{self.name}.current.' to retrieve a value" 143 | ) 144 | return self.__value 145 | 146 | @value.setter 147 | def value(self, value): 148 | # triggered by __get__ when setting the initial value 149 | # triggered by reset 150 | # triggered by set_value 151 | if self.__ref_only: 152 | raise TypeError( 153 | f"'Ref Only' datapoint doesn't have a 'value' property, to set a value use 'self.model.{self.name}.current. = {value}'" 154 | ) 155 | 156 | self.__value = value 157 | if self.is_ref_obj: 158 | # this should automatically modify content and controls properties of the control instance. 159 | for ref_obj in self.__ref_datapoints: 160 | if ref_obj.ref_type is None or isinstance(value, ref_obj.ref_type): 161 | setattr(ref_obj.current, ref_obj.ref_attr, value) 162 | else: 163 | help_str = ( 164 | "" 165 | if value 166 | else " Perhaps you meant to use the @data.RefOnly decorator." 167 | ) 168 | raise TypeError( 169 | f"The {type(value)} value of the Datapoint '{self.name}' does not match expected type '{ref_obj.ref_type}' for '{ref_obj.current}'.{help_str}" 170 | ) 171 | 172 | @property 173 | def current(self): 174 | """ 175 | If the datapoint is a ref object, and want to access the Flet control for any other propeties 176 | use this property, it works as a Ref Object. 177 | 178 | In case the datapoint is used in several Flet controls, it returns the list of all of them. 179 | """ 180 | return ( 181 | [ref_obj.current for ref_obj in self.__ref_datapoints] 182 | if self.is_ref_multi_referenced 183 | else self.__ref_datapoints[0].current 184 | if self.__ref_datapoints 185 | else None 186 | ) 187 | 188 | @current.setter 189 | def current(self, current): 190 | """First triggered when adding to ref property of a control, this is how we will set the datapoint as a ref obj""" 191 | if self.__ref_only: 192 | # if is ref_only and being assigned to another control 193 | if len(self.__ref_datapoints) == 1: 194 | raise ValueError( 195 | f"The datapoint '{self.name}' is a (Single) Ref object only and has already been asigned to {self.__ref_datapoints[0].current.__class__}, cannot be linked to multiple Controls" 196 | ) 197 | self.__ref_datapoints.append(RefDatapoint(current=current)) 198 | self.is_ref_obj = True 199 | return 200 | 201 | ref_datapoint = RefDatapoint(current=current) 202 | potential_attributes = { 203 | # order matters, controls may have more than one atribute. 204 | # DO NOT TOUCH ORDER, they were carefully selected. 205 | "options": list, 206 | "value": None, # accept any value 207 | "label": str, 208 | "src": str, 209 | "text": str, 210 | "name": str, 211 | "items": list, 212 | "shapes": list, 213 | "content": None, # Either List or Flet Control. | Why? don't know, inconsistency on Flet's attrs 214 | "actions": list, 215 | "controls": list, 216 | "figure": None, # accept any value 217 | "title": None, # accept any value 218 | "rows": list, 219 | "cells": list, 220 | "destinations": list, 221 | "tabs": list, 222 | "paint": None, 223 | } 224 | for attr, attr_type in potential_attributes.items(): 225 | if hasattr(current.__class__, attr): 226 | setattr(ref_datapoint.current, attr, self.value) 227 | ref_datapoint.ref_attr = attr 228 | ref_datapoint.ref_type = attr_type 229 | break 230 | 231 | self.__ref_datapoints.append(ref_datapoint) 232 | self.is_container_ref = ref_datapoint.ref_attr in [ 233 | "controls", 234 | "content", 235 | "figure", 236 | ] 237 | self.model_instance.ref_objs.append(self) 238 | self.is_ref_obj = True 239 | # if it's assigned to other controls 240 | if len(self.__ref_datapoints) > 1: 241 | self.is_ref_multi_referenced = True 242 | 243 | def set_value(self, x: Any) -> None: 244 | """ 245 | Set value method 246 | e.g. 247 | self.model.fruit.set_value('Pear') 248 | print(model.fruit()) -> 'Pear' 249 | """ 250 | # set data value, calling value setter 251 | if self.__ref_only: 252 | raise TypeError( 253 | f"'Ref Only' datapoint is not settable; perhaps you meant to change a value using 'self.model.{self.name}.current. = {x}' to modify the Referenced object." 254 | ) 255 | self.__has_set_value = True 256 | self.value = x 257 | 258 | def reset(self) -> None: 259 | """ 260 | Reset methods - Resets a datapoint to their initial value 261 | e.g. 262 | print(self.model.fruit()) -> 'Apple' 263 | self.model.fruit.set_value('Pear') 264 | print(self.model.fruit()) -> 'Pear' 265 | self.model.fruit.reset() 266 | print(model.fruit()) -> 'Apple' 267 | """ 268 | # calling value setter 269 | if self.__ref_only: 270 | raise TypeError(f"'Ref Only' datapoint cannot be reset") 271 | self.value = ( 272 | self.func(self.model_instance) 273 | if self.__new_default is NULL 274 | else self.__new_default 275 | ) 276 | self.__has_set_value = False 277 | 278 | def set_default(self, x: Any) -> None: 279 | """ 280 | WARNING: once you set a new default, every time you hit reset, 281 | it will always return this new value. Haven't thought of a use 282 | case, but adding the possibility just in case. 283 | """ 284 | if self.__ref_only: 285 | raise TypeError(f"'Ref Only' datapoint cannot have a default value") 286 | self.__new_default = x 287 | 288 | def append(self, newitem: Any) -> None: 289 | if self.__ref_only: 290 | raise TypeError( 291 | f"'Ref Only' datapoint doesn't have 'append' functionality; perhaps you meant 'self.model.{self.name}.current..append({newitem})' to append a new object." 292 | ) 293 | if self.is_container_ref: 294 | # Using first element since they share the same list reference. 295 | getattr( 296 | self.__ref_datapoints[0].current, self.__ref_datapoints[0].ref_attr 297 | ).append(newitem) 298 | self.__has_set_value = False 299 | elif type(self.__value) != list: 300 | raise AttributeError( 301 | f"'{type(self.__value)}' object has no attribute 'append'" 302 | ) 303 | else: 304 | raise TypeError( 305 | f"Append failed. Datapoint '{self.name}' is not a ref obj. Maybe you meant 'self.model.{self.name}().append({newitem})'" 306 | ) 307 | 308 | def __set__(self, instance, value): 309 | raise AttributeError("Can't set attribute, please use attr.set_value() instead") 310 | 311 | def __bool__(self): 312 | # Always true, since it's used by the Control class in Flet library control.py 313 | return True 314 | 315 | def __repr__(self) -> str: 316 | return f"" 317 | 318 | def __len__(self): 319 | return len(self.value) 320 | -------------------------------------------------------------------------------- /flet_mvc/routing.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class RouteHandler: 5 | def __init__(self, page: ft.Page, fallback_content: list = None): 6 | self.page = page 7 | self.routes: dict[str, list] = {} 8 | self.fallback = fallback_content 9 | 10 | def register_route(self, route: str, view_content: list): 11 | self.routes[route] = view_content 12 | 13 | def route_change(self, e): 14 | self.page.views.clear() 15 | view_content = self.routes.get(e.route, self.fallback) 16 | self.page.views.append(ft.View(e.route, view_content)) 17 | self.page.update() 18 | -------------------------------------------------------------------------------- /flet_mvc/view.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flet Base View Class 3 | """ 4 | from .controller import FletController 5 | 6 | 7 | class FletView: 8 | def __init__( 9 | self, model=None, content: list = None, controller: FletController = None 10 | ): 11 | self.controller = controller 12 | self.model = model 13 | self.content = content 14 | 15 | # Flet sets the values/content of a control before it is actually instanciated [shown in ui], 16 | # that is why we call them again here so we can set the control with our default value. 17 | for ref_obj in self.model.ref_objs: 18 | ref_obj.reset() 19 | -------------------------------------------------------------------------------- /imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o0Adrian/flet-mvc/a57709a31b2e549277523c4d91db2c51e2bccb1e/imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.001.png -------------------------------------------------------------------------------- /imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o0Adrian/flet-mvc/a57709a31b2e549277523c4d91db2c51e2bccb1e/imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.002.png -------------------------------------------------------------------------------- /imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o0Adrian/flet-mvc/a57709a31b2e549277523c4d91db2c51e2bccb1e/imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.003.png -------------------------------------------------------------------------------- /imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o0Adrian/flet-mvc/a57709a31b2e549277523c4d91db2c51e2bccb1e/imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.004.png -------------------------------------------------------------------------------- /imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o0Adrian/flet-mvc/a57709a31b2e549277523c4d91db2c51e2bccb1e/imgs/Aspose.Words.efcdc9e9-d587-4000-ba2a-a36665e1390e.005.png -------------------------------------------------------------------------------- /mvc_commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o0Adrian/flet-mvc/a57709a31b2e549277523c4d91db2c51e2bccb1e/mvc_commands/__init__.py -------------------------------------------------------------------------------- /mvc_commands/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import click 4 | from pathlib import Path 5 | 6 | 7 | def _copy_template_files(template_path_str): 8 | library_path = Path(__file__).parent 9 | template_path = library_path / template_path_str 10 | 11 | if not template_path.exists(): 12 | print( 13 | "Error: The 'templates' directory is missing. Please check that the library is properly installed and has not been modified." 14 | ) 15 | return 16 | 17 | pwd = Path.cwd() 18 | 19 | for item in template_path.iterdir(): 20 | if item.is_file(): 21 | shutil.copy(item, pwd) 22 | elif item.is_dir(): 23 | shutil.copytree(item, pwd / item.name) 24 | 25 | print(f"Templates have been copied into {pwd}") 26 | 27 | 28 | @click.command() 29 | def start(): 30 | """Creates a basic Flet-MVC template.""" 31 | _copy_template_files("templates/basic") 32 | 33 | 34 | @click.command() 35 | def routes(): 36 | """Creates Flet-MVC template for a routed app.""" 37 | _copy_template_files("templates/routes") 38 | 39 | @click.command() 40 | def tabs(): 41 | """Creates Flet-MVC template for an app with tabs.""" 42 | _copy_template_files("templates/tabs") 43 | 44 | 45 | @click.group() 46 | def cli(): 47 | pass 48 | 49 | 50 | cli.add_command(start) 51 | cli.add_command(routes) 52 | cli.add_command(tabs) 53 | 54 | 55 | 56 | if __name__ == "__main__": 57 | cli() 58 | -------------------------------------------------------------------------------- /mvc_commands/templates/basic/app.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from controllers.main import Controller 3 | from views.main import MainView 4 | from models.main import Model 5 | 6 | def main(page: ft.Page): 7 | # MVC set-up 8 | model = Model() 9 | controller = Controller(page, model) 10 | model.controller = controller 11 | view = MainView(controller, model) 12 | 13 | # Settings 14 | page.title = "" 15 | 16 | # Run 17 | page.add( 18 | *view.content 19 | ) 20 | 21 | ft.app(target=main) -------------------------------------------------------------------------------- /mvc_commands/templates/basic/controllers/main.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletController 2 | 3 | 4 | class Controller(FletController): 5 | def example_function(self, e=None): 6 | """Example of all the operations that you can perform on a model's datapoint""" 7 | self.model.example() # value = "Hello World" 8 | self.model.example.set_value("Title") # value = "Title" 9 | self.model.example.has_set_value() # True 10 | self.model.example.reset() # value = "Hello World" 11 | self.model.set_default("Hello Flet") # value = "Hello World" 12 | self.model.example.reset() # value = "Hello Flet" 13 | self.model.example.has_set_value() # False 14 | 15 | self.update() # flet page.update() command 16 | -------------------------------------------------------------------------------- /mvc_commands/templates/basic/models/main.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletModel, data 2 | import flet as ft 3 | 4 | 5 | class Model(FletModel): 6 | @data 7 | def example_title(self): 8 | return "Hello World" 9 | -------------------------------------------------------------------------------- /mvc_commands/templates/basic/views/main.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletView 2 | import flet as ft 3 | 4 | 5 | class MainView(FletView): 6 | def __init__(self, controller, model): 7 | view = [ 8 | ft.Text(ref=model.example_title, size=30) 9 | ] # List of flet controls 10 | super().__init__(model, view, controller) 11 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/app.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from flet_mvc import RouteHandler 3 | 4 | # Models 5 | from models.home import HomeModel 6 | from models.secondary import SecondaryModel 7 | 8 | # Views 9 | from views.home import HomeView 10 | from views.secondary import SecondaryView 11 | 12 | # Controllers 13 | from controllers.home import HomeController 14 | from controllers.secondary import SecondaryController 15 | 16 | 17 | def main(page: ft.Page): 18 | ### MVC set-up ### 19 | routes_handler = RouteHandler(page) 20 | 21 | # main view 22 | home_model = HomeModel() 23 | home_controller = HomeController(page, home_model) 24 | home_model.controller = home_controller 25 | home_view = HomeView(home_controller, home_model) 26 | routes_handler.register_route("/", home_view.content) 27 | 28 | # secondary view 29 | secondary_model = SecondaryModel() 30 | secondary_controller = SecondaryController(page, secondary_model) 31 | secondary_model.controller = secondary_controller 32 | secondary_view = SecondaryView(secondary_controller, secondary_model) 33 | routes_handler.register_route("/secondary", secondary_view.content) 34 | 35 | ### Page Settings ### 36 | theme = ft.Theme() 37 | platforms = ["android", "ios", "macos", "linux", "windows"] 38 | for platform in platforms: # Removing animation on route change. 39 | setattr(theme.page_transitions, platform, ft.PageTransitionTheme.NONE) 40 | 41 | page.title = "" 42 | page.theme = theme 43 | page.on_route_change = routes_handler.route_change # route change 44 | 45 | # Run 46 | page.go(page.route) 47 | 48 | 49 | ft.app(target=main) 50 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/controllers/home.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletController 2 | 3 | 4 | class HomeController(FletController): 5 | def navigate_secondary(self, e): 6 | """Example route change""" 7 | self.page.go("/secondary") 8 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/controllers/secondary.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletController 2 | 3 | 4 | class SecondaryController(FletController): 5 | def return_home(self, e): 6 | """Example route change""" 7 | self.page.go("/") 8 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/models/home.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletModel, data 2 | import flet as ft 3 | 4 | 5 | class HomeModel(FletModel): 6 | @data 7 | def example_title(self): 8 | return "This is the Home View!" 9 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/models/secondary.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletModel, data 2 | import flet as ft 3 | 4 | 5 | class SecondaryModel(FletModel): 6 | @data 7 | def example_title(self): 8 | return "This is the secondary view!" 9 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/views/home.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletView 2 | import flet as ft 3 | 4 | 5 | class HomeView(FletView): 6 | def __init__(self, controller, model): 7 | view = [ 8 | ft.Text(ref=model.example_title, size=30), 9 | ft.ElevatedButton( 10 | "Go to secondary view", on_click=controller.navigate_secondary 11 | ), 12 | ] 13 | super().__init__(model, view, controller) 14 | -------------------------------------------------------------------------------- /mvc_commands/templates/routes/views/secondary.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletView 2 | import flet as ft 3 | 4 | 5 | class SecondaryView(FletView): 6 | def __init__(self, controller, model): 7 | view = [ 8 | ft.Text(ref=model.example_title, size=30), 9 | ft.ElevatedButton("Go to home", on_click=controller.return_home), 10 | ] 11 | super().__init__(model, view, controller) 12 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Notice how I am not importing index model. (controller is unused) 3 | I am using the model in the home model, so that the home model can have 4 | access to the index model. 5 | 6 | It's no neccesary but showing the possibility in case you want to have 7 | control of the index view inside the Home tab. Like a button that adds 8 | tabs which only leaves in the home tab. 9 | 10 | You could easily use normal setup, but that would mean that views cannot 11 | interact with each other (which in most cases, is the expected behaivour) 12 | """ 13 | import flet as ft 14 | 15 | # Models 16 | from models.home import HomeModel 17 | from models.settings import SettingsModel 18 | 19 | # Views 20 | from views.index import IndexView 21 | from views.home import HomeView 22 | from views.settings import SettingsView 23 | 24 | # Controllers 25 | 26 | from controllers.home import HomeController 27 | from controllers.settings import SettingsController 28 | 29 | 30 | def main(page: ft.Page): 31 | ### MVC set-up ### 32 | 33 | # Models 34 | home_model = HomeModel() 35 | settings_model = SettingsModel() 36 | 37 | # Controllers 38 | home_controller = HomeController(page, home_model) 39 | settings_controller = SettingsController(page, settings_model) 40 | 41 | # Views 42 | home_view = HomeView(home_controller, home_model) 43 | settings_view = SettingsView(settings_controller, settings_model) 44 | 45 | # Lastly set up index view 46 | 47 | # Here you could add the index controller and model in case you want the normal set-up 48 | # but as mentioned, using this weird approach to show that it's possible and that you can connect two models. 49 | index_view = IndexView(home_controller, home_model, home_view, settings_view) 50 | 51 | ### Page Settings ### 52 | page.title = "" 53 | 54 | # Run 55 | page.add(*index_view.content) 56 | 57 | 58 | ft.app(target=main) 59 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/controllers/home.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletController 2 | 3 | 4 | class HomeController(FletController): 5 | ... -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/controllers/index.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class is not being used, you could delete this file, 3 | but leaving it here in case you need it. 4 | """ 5 | from flet_mvc import FletController 6 | 7 | 8 | class IndexController(FletController): 9 | ... -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/controllers/settings.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletController 2 | 3 | 4 | class SettingsController(FletController): 5 | ... -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/models/home.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import data 2 | from .index import IndexModel 3 | import flet as ft 4 | 5 | 6 | class HomeModel(IndexModel): 7 | @data 8 | def example_content(self): 9 | return "\n\n\tThis is the Home Tab!" 10 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/models/index.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class being used in the home model. See docstring at app.py to understand why. 3 | This structure is optional but just showing it's possible. 4 | """ 5 | from flet_mvc import FletModel, data 6 | import flet as ft 7 | 8 | 9 | class IndexModel(FletModel): 10 | ... 11 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/models/settings.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletModel, data 2 | import flet as ft 3 | 4 | 5 | class SettingsModel(FletModel): 6 | @data 7 | def example_content(self): 8 | return "\n\n\tThis is the settings tab!" 9 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/views/home.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletView 2 | import flet as ft 3 | 4 | 5 | class HomeView(FletView): 6 | def __init__(self, controller, model): 7 | view = [ 8 | ft.Text(ref=model.example_content), 9 | ] 10 | super().__init__(model, view, controller) 11 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/views/index.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletView 2 | import flet as ft 3 | 4 | 5 | class IndexView(FletView): 6 | """ 7 | When working with tabs you can create an Index view that will orquestrate 8 | the tabs content. I would recommend the following approaches: 9 | 10 | 1. You could add as many parameters as needed for all the views, into the index view 11 | class. That way you could personalize every tab individually. (nice for small 12 | amount of tabs) 13 | 14 | 2. Another option would be to expect multiple views but without the need to add 15 | a parameter every time, just use *tab_views (unpacking). 16 | 17 | 3. The third option is an extension of the above, where insead of expecting an 18 | undefined number of arguments/views, you could use a dictionary or maybe list of 19 | sets/dicts/tuples that contain the properites of all the tabs. Properties like 20 | - Tab name (text) 21 | - Tab icon (icon) 22 | - Tab style (tab_content) 23 | - Tab view (content) 24 | 25 | In this template I am using aproach 1 for simplicity (home_view and settings_view 26 | params). I would highly recommend approach 3. 27 | """ 28 | 29 | def __init__(self, controller, model, home_view, settings_view): 30 | view = [ 31 | ft.Text("Tabs template", size=30), 32 | ft.Tabs( 33 | tabs=[ 34 | # Remember tab's "content" property expects ONE control, not a list !! 35 | # Here I am using Column and ListView to illustrate that, since the 36 | # view's "content" property is a list. (unless your specify the contrary) 37 | ft.Tab( 38 | text="Home", 39 | icon=ft.icons.HOME, 40 | content=ft.ListView(home_view.content), 41 | ), 42 | ft.Tab( 43 | text="Settings", 44 | icon=ft.icons.SETTINGS, 45 | content=ft.Column(settings_view.content), 46 | ), 47 | ft.Tab( 48 | text="Other Tab", 49 | icon=ft.icons.BUILD, 50 | content=ft.Text("\n\n\tTab under construction!"), 51 | ), 52 | ], 53 | selected_index=0, 54 | animation_duration=0, # No animation 55 | ), 56 | ] 57 | super().__init__(model, view, controller) 58 | -------------------------------------------------------------------------------- /mvc_commands/templates/tabs/views/settings.py: -------------------------------------------------------------------------------- 1 | from flet_mvc import FletView 2 | import flet as ft 3 | 4 | 5 | class SettingsView(FletView): 6 | def __init__(self, controller, model): 7 | view = [ 8 | ft.Text(ref=model.example_content), 9 | ] 10 | super().__init__(model, view, controller) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from pathlib import Path 3 | 4 | this_directory = Path(__file__).parent 5 | long_description = (this_directory / "README.md").read_text() 6 | 7 | VERSION = "0.1.5" 8 | DESCRIPTION = ( 9 | "This package will allow the developer to use an mvc structure in a Flet project." 10 | ) 11 | 12 | # Setting up 13 | setup( 14 | name="flet-mvc", 15 | version=VERSION, 16 | author="o0Adrian (C. Adrián Monroy)", 17 | author_email="", 18 | description=DESCRIPTION, 19 | url="https://github.com/o0Adrian/flet-mvc", 20 | long_description_content_type="text/markdown", 21 | long_description=long_description, 22 | packages=find_packages(), 23 | package_data={ 24 | "mvc_commands": [ 25 | "templates/*", 26 | "templates/*/*", 27 | "templates/*/*/*", 28 | ], 29 | }, 30 | install_requires=[ 31 | "flet>=0.7.4", 32 | "click>=8.1.3", 33 | ], 34 | keywords=[ 35 | "mvc", 36 | "flet", 37 | "flet mvc", 38 | "model", 39 | "view", 40 | "controller", 41 | "node", 42 | "datapoint", 43 | ], 44 | entry_points={ 45 | "console_scripts": [ 46 | "flet-mvc=mvc_commands.cli:cli", 47 | ], 48 | }, 49 | classifiers=[ 50 | "Development Status :: 4 - Beta", 51 | "Intended Audience :: Developers", 52 | "Programming Language :: Python :: 3", 53 | "Operating System :: Unix", 54 | "Operating System :: MacOS :: MacOS X", 55 | "Operating System :: Microsoft :: Windows", 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /tests/app_test_multi_ref/controller.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 4 | from flet_mvc import FletController, ADVICE 5 | import flet as ft 6 | 7 | class TaskController(FletController): 8 | def add_task(self, e): 9 | new_task = self.model.new_task() 10 | if new_task.strip() == "": 11 | self.alert("Task can't be empty!!", ADVICE) 12 | return 13 | self.model.tasks.append( 14 | ft.Row( 15 | controls=[ 16 | ft.Checkbox(ref=self.model.check), 17 | ft.Text(new_task, size=18), 18 | ] 19 | ), 20 | ) 21 | # IMPORTANT: Check changes it's value and modifies the node, but to change the UI we still need to modify the older 22 | # checkboxes, a possible solition would be iterating over self.model.check.current which will return a list of currents. 23 | # and then set the value self.model.check.current.value = self.model.check(). Also.. there is already an on_change method... 24 | self.model.check.set_value(self.model.check()) 25 | self.model.new_task.set_value("") 26 | self.update() 27 | 28 | def on_keyboard(self, e: ft.KeyboardEvent): 29 | if e.key == "D" and e.alt and e.shift: 30 | self.page.show_semantics_debugger = not self.page.show_semantics_debugger 31 | self.page.update() 32 | -------------------------------------------------------------------------------- /tests/app_test_multi_ref/main.py: -------------------------------------------------------------------------------- 1 | from controller import TaskController 2 | from view import TaskView 3 | from model import TaskModel 4 | 5 | import flet as ft 6 | 7 | def main(page): 8 | # MVC set-up 9 | model = TaskModel() 10 | controller = TaskController(page, model) 11 | view = TaskView(controller, model) 12 | 13 | # model operations 14 | model.controller = controller 15 | # model.create_tasks() 16 | 17 | # Settings 18 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 19 | page.on_keyboard_event = controller.on_keyboard 20 | page.theme_mode = "light" 21 | page.padding = 20 22 | page.window_width = 580 23 | page.window_always_on_top = True 24 | page.window_resizable = False 25 | page.window_height = 500 26 | 27 | # Run 28 | page.add( 29 | *view.content 30 | ) 31 | 32 | ft.app(target=main) 33 | -------------------------------------------------------------------------------- /tests/app_test_multi_ref/model.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 4 | from flet_mvc import FletModel, data 5 | import flet as ft 6 | 7 | class TaskModel(FletModel): 8 | @data 9 | def new_task(self): 10 | return "" 11 | 12 | @data 13 | def tasks(self): 14 | return [] 15 | 16 | @data.RefOnly 17 | def title(self): 18 | return "title" 19 | 20 | @data 21 | def check(self): 22 | return False -------------------------------------------------------------------------------- /tests/app_test_multi_ref/view.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 4 | from flet_mvc import FletView 5 | import flet as ft 6 | 7 | class TaskView(FletView): 8 | def __init__(self, controller, model): 9 | view = [ 10 | ft.Text( 11 | "Task List", 12 | size=36, 13 | weight="w700", 14 | ), 15 | ft.ListView( 16 | ref=model.tasks, 17 | expand=True 18 | ), 19 | ft.ListView( 20 | ref=model.tasks, 21 | expand=True 22 | ), 23 | ft.ListView( 24 | expand=True 25 | ), 26 | ft.Row( 27 | controls=[ 28 | ft.TextField( 29 | ref=model.new_task, 30 | hint_text="Enter new task", 31 | border=ft.InputBorder.NONE, 32 | filled=True, 33 | expand=True, 34 | on_submit=controller.add_task 35 | ), 36 | ft.ElevatedButton( 37 | content=ft.Text("Add Task", size=18, weight="w700"), 38 | on_click=controller.add_task, 39 | bgcolor="#f7ce7c", 40 | color="black", 41 | width=220, 42 | height=50, 43 | ), 44 | ], 45 | alignment=ft.MainAxisAlignment.END, 46 | ), 47 | ] 48 | super().__init__(model, view, controller) 49 | -------------------------------------------------------------------------------- /tests/data/component_attribute.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from flet.matplotlib_chart import MatplotlibChart 3 | from flet.plotly_chart import PlotlyChart 4 | 5 | component_value_attr_map = { 6 | ft.TextField: "value", 7 | ft.AlertDialog: "content", 8 | ft.AnimatedSwitcher: "content", 9 | ft.AppBar: "actions", 10 | ft.Audio: "src", 11 | ft.Banner: "content", 12 | ft.BottomSheet: "content", 13 | ft.Card: "content", 14 | ft.Checkbox: "value", 15 | ft.CircleAvatar: "content", 16 | ft.Column: "controls", 17 | ft.Container: "content", 18 | ft.DataTable: "rows", 19 | ft.DataCell: "content", 20 | # ft.DataColumn: None, 21 | ft.DataRow: "cells", 22 | # ft.Divider: None, 23 | ft.Draggable: "content", 24 | ft.DragTarget: "content", 25 | ft.Dropdown: "options", 26 | ft.ElevatedButton: "text", 27 | # ft.FilePicker: None, 28 | ft.FilledButton: "text", 29 | ft.FilledTonalButton: "text", 30 | ft.FloatingActionButton: "text", 31 | ft.GestureDetector: "content", 32 | ft.GridView: "controls", 33 | # ft.HapticFeedback: None, 34 | ft.Icon: "name", 35 | ft.IconButton: "content", 36 | ft.Image: "src", 37 | # ft.InlineSpan: None, 38 | ft.ListTile: "title", 39 | ft.ListView: "controls", 40 | ft.Markdown: "value", 41 | ft.NavigationBar: "destinations", 42 | ft.NavigationRail: "destinations", 43 | ft.OutlinedButton: "text", 44 | # MatplotlibChart: "figure", ******** MISSING TESTING 45 | # PlotlyChart: "figure", ******** MISSING TESTING 46 | # LineChart. ******** MISSING TESTING 47 | # BarChart, ******** MISSING TESTING 48 | # PieChart, ******** MISSING TESTING 49 | ft.PopupMenuItem: "text", 50 | ft.PopupMenuButton: "items", 51 | ft.ProgressBar: "value", 52 | ft.ProgressRing: "value", 53 | ft.Radio: "value", 54 | ft.RadioGroup: "value", 55 | ft.ResponsiveRow: "controls", 56 | ft.Row: "controls", 57 | # ft.Semantics: None, 58 | ft.ShaderMask: "content", 59 | # ft.ShakeDetector: None, 60 | ft.Slider: "value", 61 | ft.SnackBar: "content", 62 | ft.Stack: "controls", 63 | ft.Switch: "value", 64 | ft.Tabs: "tabs", 65 | ft.Text: "value", 66 | ft.TextButton: "text", 67 | ft.TextField: "value", 68 | ft.TextSpan: "text", 69 | ft.Tooltip: "content", 70 | ft.TransparentPointer: "content", 71 | # ft.VerticalDivider: None, 72 | ft.WindowDragArea: "content", 73 | ft.canvas.Canvas: "shapes", 74 | } 75 | 76 | potential_attributes = { 77 | "options": list, 78 | "value": None, 79 | "label": str, 80 | "src": str, 81 | "text": str, 82 | "name": str, 83 | "items": list, 84 | "shapes": list, 85 | "content": None, 86 | "actions": list, 87 | "controls": list, 88 | "figure": None, 89 | "title": None, 90 | "rows": list, 91 | "cells": list, 92 | "destinations": list, 93 | "tabs": list, 94 | "paint": None, 95 | } 96 | -------------------------------------------------------------------------------- /tests/test_all_controls/controller.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append( 4 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | ) 6 | from flet_mvc import FletController, ADVICE 7 | import flet as ft 8 | 9 | 10 | class TestController(FletController): 11 | def on_keyboard(self, e: ft.KeyboardEvent): 12 | if e.key == "D" and e.alt and e.shift: 13 | self.page.show_semantics_debugger = not self.page.show_semantics_debugger 14 | self.page.update() 15 | 16 | def open_dialog(self, e): 17 | """1. AlertDialog""" 18 | self.page.Dialog = self.model.Dialog.current 19 | self.model.Dialog.current.open = True 20 | self.page.update() 21 | 22 | def animate(self, e): 23 | """2. Animated Switcher""" 24 | if self.model.AnimatedSwitcher.has_set_value(): 25 | self.model.AnimatedSwitcher.reset() 26 | else: 27 | self.model.AnimatedSwitcher.set_value( 28 | ft.Container( 29 | ft.Text("Bye!", size=50), 30 | alignment=ft.alignment.center, 31 | width=200, 32 | height=200, 33 | bgcolor=ft.colors.YELLOW, 34 | ) 35 | ) 36 | 37 | # This shouldn't be here, but just testing that a refdatapoint can change when containing other refdatapoint 38 | self.model.PopupMenuButton.set_value( 39 | [ 40 | ft.PopupMenuItem(text="Changed Field"), 41 | ft.PopupMenuItem(), # divider 42 | ft.PopupMenuItem(text="True Checked", checked=True), 43 | ] 44 | ) 45 | self.update() 46 | 47 | def close_banner(self, e): 48 | """4. Banner""" 49 | self.page.banner.open = False 50 | self.update() 51 | 52 | def open_banner(self, e): 53 | """4. Banner""" 54 | self.page.banner.open = True 55 | self.update() 56 | 57 | def close_bs(self, e): 58 | """5. Bottom Sheet""" 59 | self.model.BottomSheet.current.open = False 60 | self.update() 61 | 62 | def open_bs(self, e): 63 | """5. Bottom Sheet""" 64 | self.model.BottomSheet.current.open = True 65 | self.update() 66 | 67 | def bs_dismissed(self, e): 68 | """5. Bottom Sheet""" 69 | print("Dismissed!") 70 | 71 | def drag_will_accept(self, e): 72 | """14. Draggable""" 73 | e.control.content.border = ft.border.all( 74 | 2, ft.colors.BLACK45 if e.data == "true" else ft.colors.RED 75 | ) 76 | e.control.update() 77 | 78 | def drag_accept(self, e: ft.DragTargetAcceptEvent): 79 | """14. Draggable""" 80 | src = self.page.get_control(e.src_id) 81 | e.control.content.bgcolor = src.content.bgcolor 82 | e.control.content.border = None 83 | e.control.update() 84 | 85 | def drag_leave(self, e): 86 | """14. Draggable""" 87 | e.control.content.border = None 88 | e.control.update() 89 | 90 | def button_clicked(self, e): 91 | """50. Snackbar""" 92 | self.model.SnackBar.set_value( 93 | ft.Text(f"Dropdown value is: {self.model.Dropdown.current.value}") 94 | ) 95 | self.page.snack_bar.open = True 96 | self.page.update() 97 | 98 | def see_file_on_close(self, e: ft.FilePickerResultEvent): 99 | """18. FilePicker""" 100 | print(e.files) 101 | 102 | def see_file(self, e): 103 | """18. FilePicker""" 104 | # Using the ref object 105 | # TODO: Maybe I could add all this methods in the datapoint (FilePicker.files), 106 | # and just raise exception if it's not a supported component to use that method. 107 | # others: icon, set_options, columns, rows, set_dataframe 108 | print( 109 | self.model.FilePicker.current.result.files 110 | if self.model.FilePicker.current.result 111 | else "No file selected" 112 | ) 113 | 114 | def fab_pressed(self, e): 115 | """22. Floating Action Button""" 116 | self.alert("FAB Pressed", alert_type="info") 117 | 118 | def on_pan_update(self, e: ft.DragUpdateEvent): 119 | """23. Floating Action Button""" 120 | e.control.top = max(0, e.control.top + e.delta_y) 121 | e.control.left = max(0, e.control.left + e.delta_x) 122 | e.control.update() 123 | 124 | def icon_button_click(self, e): 125 | """27. Floating Action Button""" 126 | self.alert("Icon button Pressed", alert_type="info") -------------------------------------------------------------------------------- /tests/test_all_controls/main.py: -------------------------------------------------------------------------------- 1 | from controller import TestController 2 | from view import TestView 3 | from model import TestModel 4 | 5 | import flet as ft 6 | 7 | 8 | def main(page): 9 | # MVC set-up 10 | model = TestModel() 11 | controller = TestController(page, model) 12 | model.controller = controller # important to set controller in model (in needed) before view 13 | view = TestView(controller, model) 14 | 15 | # Settings 16 | 17 | # NOTE: adding in setting, but remember controller has access to page too; so 18 | # you can set this values in a function of the controller, which I recommend to do. 19 | page.appbar = view.app_bar 20 | page.overlay.append(view.audio) 21 | page.overlay.append(view.bottom_sheet) 22 | page.overlay.append(view.file_picker) 23 | page.banner = view.banner 24 | page.snack_bar = view.snack_bar 25 | page.floating_action_button = view.fab 26 | 27 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 28 | page.on_keyboard_event = controller.on_keyboard 29 | page.theme_mode = "light" 30 | page.padding = 20 31 | page.window_width = 600 32 | page.window_always_on_top = True 33 | page.window_resizable = False 34 | page.window_height = 500 35 | 36 | # Run 37 | page.add(*view.content) 38 | 39 | 40 | ft.app(target=main) 41 | -------------------------------------------------------------------------------- /tests/test_all_controls/model.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import pandas 4 | import math 5 | 6 | sys.path.append( 7 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | ) 9 | from flet_mvc import FletModel, data 10 | import flet as ft 11 | 12 | 13 | class TestModel(FletModel): 14 | @data 15 | def AppBar(self): 16 | """0. App Bar | Attribute: actions""" 17 | return [ 18 | ft.IconButton(ft.icons.WB_SUNNY_OUTLINED), 19 | ft.IconButton(ft.icons.FILTER_3), 20 | ft.PopupMenuButton(ref=self.PopupMenuButton), 21 | ] 22 | 23 | # NOTE: When the attribute of a Cotnrol is content, sometimes can be a list 24 | # sometimes a single control, and sometimes both. Inconsistency of flet. 25 | @data 26 | def Dialog(self): 27 | """1. Dialog datapoint | Attribute: content""" 28 | return ft.Text("test") 29 | 30 | @data 31 | def AnimatedSwitcher(self): 32 | """2. Animated Switcher | Attribute: content""" 33 | return ft.Container( 34 | ft.Text("Hello!", style=ft.TextThemeStyle.HEADLINE_MEDIUM), 35 | alignment=ft.alignment.center, 36 | width=200, 37 | height=200, 38 | bgcolor=ft.colors.GREEN, 39 | ) 40 | 41 | @data 42 | def Audio(self): 43 | """3. Audio | Attribute: src""" 44 | return "https://luan.xyz/files/audio/ambient_c_motion.mp3" 45 | 46 | @data 47 | def Banner(self): 48 | """4. Banner | Attribute: content""" 49 | return ft.Text( 50 | "4. Banner. Imagine this is an error. What would you like me to do?" 51 | ) 52 | 53 | @data 54 | def BottomSheet(self): 55 | """5. Bottom Sheet | Attribute: content""" 56 | return ft.Container( 57 | ft.Column( 58 | [ 59 | ft.Text("This is sheet's content!"), 60 | ft.ElevatedButton( 61 | "Close bottom sheet", on_click=self.controller.close_bs 62 | ), 63 | ], 64 | tight=True, 65 | ), 66 | padding=10, 67 | width=680, 68 | ) 69 | 70 | @data 71 | def Card(self): 72 | """6. Card | Attribute: content""" 73 | return ft.Container( 74 | ref=self.Container, 75 | width=400, 76 | padding=10, 77 | ) 78 | 79 | @data 80 | def Checkbox(self): 81 | """7. Checkbox | Attribute: value""" 82 | return False 83 | 84 | @data 85 | def CircleAvatar(self): 86 | """8. CircleAvatar | Attribute: content""" 87 | return ft.Text("CA") 88 | 89 | @data 90 | def Column(self): 91 | """10. Column | Attribute: controls""" 92 | return [ 93 | ft.ListTile( 94 | ref=self.ListTile, 95 | leading=ft.Icon(ref=self.Icon), 96 | subtitle=ft.Text("Music by Julie Gable. Lyrics by Sidney Stein."), 97 | ), 98 | ft.Row( 99 | ref=self.Row, 100 | alignment=ft.MainAxisAlignment.END, 101 | ), 102 | ] 103 | 104 | @data 105 | def Container(self): 106 | """11. Container | Attribute: content""" 107 | return ft.Column(ref=self.Column) 108 | 109 | @data 110 | def DataTable(self): 111 | """12. DataTable | Attribute: rows""" 112 | 113 | def createRows(): 114 | """ 115 | This is an example of a function that creates rows from a dataframe 116 | This could be used in a function of the controller and then set the value 117 | of this node (set_value) with the result of that function for the datatable 118 | to be interactive. 119 | Keep in mind this is a RefNode, so you can also access the columns in the controller 120 | with 'self.model.DataTable.current.columns = $yourNewColumns' 121 | """ 122 | df = pandas.DataFrame( 123 | [ 124 | {"First name": "John", "Last name": "Smith", "Age": "43"}, 125 | {"First name": "Jack", "Last name": "Brown", "Age": "19"}, 126 | {"First name": "Alice", "Last name": "Wong", "Age": "25"}, 127 | ] 128 | ) 129 | return [ 130 | ft.DataRow(cells=[ft.DataCell(ft.Text(value)) for value in row]) 131 | for row in df.values.tolist() 132 | ] 133 | 134 | rows = createRows() 135 | rows.append( 136 | ft.DataRow( 137 | ref=self.DataRow, 138 | selected=True, 139 | on_select_changed=lambda e: print(f"row select changed: {e.data}"), 140 | ), 141 | ) 142 | return rows 143 | 144 | @data 145 | def DataRow(self): 146 | """12.1 DataRow | Attribute: cells (DataCell and Column not supported)""" 147 | return [ 148 | ft.DataCell(ft.Text("Adrián")), 149 | ft.DataCell(ft.Text("Monroy")), 150 | ft.DataCell(ft.Text("23")), 151 | ] 152 | 153 | @data.RefOnly 154 | def Divider(self): 155 | """13. Divider | Attribute: NONE""" 156 | return None 157 | 158 | @data 159 | def Draggable(self): 160 | """14. Draggable | Attribute: content""" 161 | return ft.Container( 162 | width=50, 163 | height=50, 164 | bgcolor=ft.colors.CYAN, 165 | border_radius=5, 166 | ) 167 | 168 | @data 169 | def DragTarget(self): 170 | """15. DragTarget | Attribute: content""" 171 | return ft.Container( 172 | width=50, 173 | height=50, 174 | bgcolor=ft.colors.BLUE_GREY_100, 175 | border_radius=5, 176 | ) 177 | 178 | @data 179 | def Dropdown(self): 180 | """16. Dropdown | Attribute: options""" 181 | 182 | def set_options( 183 | options, 184 | ) -> list: # TODO: This could live in the controller or datapoint. 185 | return [ft.dropdown.Option(option) for option in options] 186 | 187 | return set_options(options=["Red", "Green", "Blue"]) 188 | 189 | @data 190 | def ElevatedButton(self): 191 | """17. ElevatedButton | Attribute: text""" 192 | return "Submit" 193 | 194 | @data.RefOnly 195 | def FilePicker(self): 196 | """18. FilePicker | Attribute: None""" 197 | return None 198 | 199 | @data 200 | def FilledButton(self): 201 | """19. FilledButton | Attribute: text""" 202 | return "Disabled Button" 203 | 204 | @data 205 | def FilledTonalButton(self): 206 | """20. FilledTonalButton | Attribute: text""" 207 | return "Disabled Button" 208 | 209 | # 21. FletApp | Ignore 210 | 211 | @data 212 | def FloatingActionButton(self): 213 | """22. FloatingActionButton | Attribute: text""" 214 | return "22. FAB" 215 | 216 | @data 217 | def GestureDetector(self): 218 | """23. GestureDetector | Attribute: content""" 219 | return ft.Container( 220 | content=ft.Text("23", style=ft.TextThemeStyle.LABEL_LARGE), 221 | bgcolor=ft.colors.BLUE, 222 | alignment=ft.alignment.center, 223 | width=50, 224 | height=50, 225 | border_radius=5, 226 | ) 227 | 228 | @data 229 | def GridView(self): 230 | """24. GridView | Attribute: controls""" 231 | images = [] 232 | for i in range(0, 7): 233 | images.append( 234 | ft.Image( 235 | src=f"https://picsum.photos/150/150?{i}", 236 | fit=ft.ImageFit.NONE, 237 | repeat=ft.ImageRepeat.NO_REPEAT, 238 | border_radius=ft.border_radius.all(10), 239 | ) 240 | ) 241 | images.append( 242 | ft.Image( 243 | ref=self.Image, 244 | fit=ft.ImageFit.NONE, 245 | repeat=ft.ImageRepeat.NO_REPEAT, 246 | border_radius=ft.border_radius.all(10), 247 | ) 248 | ) 249 | return images 250 | 251 | # 25. HapticFeedback | Ignore | can still have ref though. 252 | 253 | @data.RefOnly 254 | def IconButton(self): 255 | """27. IconButton | Attribute: content * Use RefOnly Please""" 256 | return None 257 | 258 | @data 259 | def Image(self): 260 | """28. Image | Attribute: src""" 261 | return "https://picsum.photos/150/150?8" 262 | 263 | # 29. Ignore 264 | 265 | @data 266 | def ListView(self): 267 | """31. ListView | Attribute: controls""" 268 | items = [] 269 | for i in range(100): 270 | items.append(ft.Text(f"Line {i}")) 271 | return items 272 | 273 | @data 274 | def Markdown(self): 275 | """32. Markdown | Attribute: value""" 276 | return """ 277 | ## Tables 278 | 279 | |Syntax |Result | 280 | |---------------------------------------|-------------------------------------| 281 | |`*italic 1*` |*italic 1* | 282 | |`_italic 2_` | _italic 2_ | 283 | |`**bold 1**` |**bold 1** | 284 | |`__bold 2__` |__bold 2__ | 285 | |`This is a ~~strikethrough~~` |This is a ~~strikethrough~~ | 286 | |`***italic bold 1***` |***italic bold 1*** | 287 | |`___italic bold 2___` |___italic bold 2___ | 288 | |`***~~italic bold strikethrough 1~~***`|***~~italic bold strikethrough 1~~***| 289 | |`~~***italic bold strikethrough 2***~~`|~~***italic bold strikethrough 2***~~| 290 | 291 | ## Styling 292 | 293 | Style text as _italic_, __bold__, ~~strikethrough~~, or `inline code`. 294 | 295 | - Use bulleted lists 296 | - To better clarify 297 | - Your points 298 | 299 | ## Code blocks 300 | 301 | Formatted Dart code looks really pretty too: 302 | 303 | ```dart 304 | import 'package:flet/flet.dart'; 305 | import 'package:flutter/material.dart'; 306 | 307 | void main() async { 308 | await setupDesktop(); 309 | runApp(const MyApp()); 310 | } 311 | class MyApp extends StatelessWidget { 312 | const MyApp({super.key}); 313 | 314 | @override 315 | Widget build(BuildContext context) { 316 | return const MaterialApp( 317 | title: 'Flet Flutter Demo', 318 | home: FletApp(pageUrl: "http://localhost:8550"), 319 | ); 320 | } 321 | } 322 | ``` 323 | """ 324 | 325 | # TEST COMPLETE: 326 | # @data 327 | # def MatplotlibChart(self): 328 | # """33. MatplotlibChart | Attribute: figure""" 329 | # return None 330 | 331 | # @data 332 | # def NavigationBar(self): 333 | # """34. NavigationBar | Attribute: destinations""" 334 | # return None 335 | 336 | # @data 337 | # def NavigationRail(self): 338 | # """35. NavigationRail | Attribute: destinations""" 339 | # return None 340 | 341 | # @data 342 | # def NavigationDestination(self): 343 | # """34.5 NavigationDestination | Attribute: label""" 344 | # return None 345 | 346 | @data 347 | def OutlinedButton(self): 348 | """36. OutlinedButton | Attribute: text""" 349 | return "Disabled button" 350 | 351 | @data 352 | def PlotlyChart(self): 353 | """37. PlotlyChart | Attribute: figure""" 354 | return None 355 | 356 | @data 357 | def ProgressBar(self): 358 | """40. ProgressBar | Attribute: value""" 359 | return 0.5 360 | 361 | @data 362 | def ProgressRing(self): 363 | """41. ProgressRing | Attribute: value""" 364 | return 0.75 365 | 366 | @data 367 | def Radio(self): 368 | """42. Radio | Attribute: value""" 369 | return "red" 370 | 371 | @data 372 | def RadioGroup(self): 373 | """43. RadioGroup | Attribute: value (current selection, not content!)""" 374 | return "blue" 375 | 376 | @data 377 | def ResponsiveRow(self): 378 | """44. ResponsiveRow | Attribute: controls""" 379 | return [ 380 | ft.Container( 381 | ft.Text("Column 1"), 382 | padding=5, 383 | bgcolor=ft.colors.YELLOW, 384 | col={"sm": 6, "md": 4, "xl": 2}, 385 | ), 386 | ft.Container( 387 | ft.Text("Column 2"), 388 | padding=5, 389 | bgcolor=ft.colors.GREEN, 390 | col={"sm": 6, "md": 4, "xl": 2}, 391 | ), 392 | ft.Container( 393 | ft.Text("Column 3"), 394 | padding=5, 395 | bgcolor=ft.colors.BLUE, 396 | col={"sm": 6, "md": 4, "xl": 2}, 397 | ), 398 | ft.Container( 399 | ft.Text("Column 4"), 400 | padding=5, 401 | bgcolor=ft.colors.PINK_300, 402 | col={"sm": 6, "md": 4, "xl": 2}, 403 | ), 404 | ] 405 | 406 | # 46. Semantics? | Ignore | label 407 | 408 | @data 409 | def ShaderMask(self): 410 | """47. ShaderMask | Attribute: content""" 411 | return ft.Image( 412 | src="https://picsum.photos/200/200?1", 413 | width=200, 414 | height=200, 415 | fit=ft.ImageFit.FILL, 416 | ) 417 | 418 | # 48. ShakeDetector | Attribute: UNSOPPORTED UNTIL THERE'S AN USE CASE""" 419 | 420 | @data 421 | def Slider(self): 422 | """49. Slider | Attribute: value""" 423 | return 25 424 | 425 | @data 426 | def SnackBar(self): 427 | """50. SnackBar | Attribute: content *not automatically set""" 428 | # NOTE: This is not supported to be automatically set, but can still be used 429 | # as normal ref, like DataColumn or DataCell, but if you are going to use this 430 | # I recommend using it as @data.RefOnly instead. but showing that can still work 431 | # with set valueon the controller at button_clicked method. 432 | # Actually just use flet-mvc alert instead. 433 | return None 434 | 435 | @data 436 | def Switch(self): 437 | """52. Switch | Attribute: value""" 438 | return True 439 | 440 | @data 441 | def Tabs(self): 442 | """53. Tabs | Attribute: tabs""" 443 | return [ 444 | ft.Tab( 445 | ref=self.Tab, 446 | content=ft.Container( 447 | content=ft.Text("This is Tab 1"), alignment=ft.alignment.center 448 | ), 449 | ), 450 | ft.Tab( 451 | tab_content=ft.Icon(ft.icons.SEARCH), 452 | content=ft.Text("This is Tab 2"), 453 | ), 454 | ft.Tab( 455 | text="Tab 3", 456 | icon=ft.icons.SETTINGS, 457 | content=ft.Text("This is Tab 3"), 458 | ), 459 | ] 460 | 461 | @data 462 | def Tab(self): 463 | """53.1 Tab | Attribute: text""" 464 | return "Tab 1" 465 | 466 | @data 467 | def TextField(self): 468 | """56. TextField | Attribute: value""" 469 | return "Initial text value" 470 | 471 | @data 472 | def TextSpan(self): 473 | """57. TextSpan | Attribute: text""" 474 | return "Greetings, planet!" 475 | 476 | @data 477 | def Tooltip(self): 478 | """58. Tooltip | Attribute: content""" 479 | return ft.Text("Hover to see tooltip", weight=ft.FontWeight.W_100) 480 | 481 | # 59. TransparentPointer | See Flet docs | content 482 | 483 | @data.RefOnly 484 | def VerticalDivider(self): 485 | """60. VerticalDivider | Attribute: None""" 486 | return None 487 | 488 | @data 489 | def WindowDragArea(self): 490 | """61. WindowDragArea | Attribute: content""" 491 | return ft.Container( 492 | ft.Text("Drag this area to move, maximize and restore application window."), 493 | bgcolor=ft.colors.AMBER_300, 494 | padding=10, 495 | ) 496 | 497 | @data 498 | def Canvas(self): 499 | """62. Canvas | Attribute: shapes""" 500 | return [ 501 | ft.canvas.Circle(100, 100, 50, self.PaintStroke()), 502 | ft.canvas.Circle(80, 90, 10, self.PaintStroke()), 503 | ft.canvas.Circle(84, 87, 5, self.PaintFill()), 504 | ft.canvas.Circle(120, 90, 10, self.PaintStroke()), 505 | ft.canvas.Circle(124, 87, 5, self.PaintFill()), 506 | ft.canvas.Arc(70, 95, 60, 40, 0, math.pi, paint=self.PaintStroke()), 507 | ] 508 | 509 | def PaintStroke(self): 510 | return ft.Paint(stroke_width=2, style=ft.PaintingStyle.STROKE) 511 | 512 | def PaintFill(self): 513 | return ft.Paint(style=ft.PaintingStyle.FILL) 514 | 515 | @data 516 | def LineChart(self): 517 | """63. LineChart | Attribute: MISSING""" 518 | return None 519 | 520 | @data 521 | def BarChart(self): 522 | """64. BarChart | Attribute: MISSING""" 523 | return None 524 | 525 | @data 526 | def PieChart(self): 527 | """65. PieChart | Attribute: MISSING""" 528 | return None 529 | 530 | ### 531 | 532 | @data 533 | def Icon(self): 534 | """26. Icon | Attribute: name""" 535 | return ft.icons.ALBUM 536 | 537 | @data 538 | def ListTile(self): 539 | """30. ListTile | Attribute: title""" 540 | return ft.Text(ref=self.Text) 541 | 542 | @data 543 | def Row(self): 544 | """45. Row | Attribute: controls""" 545 | return [ 546 | ft.Checkbox(ref=self.Checkbox, label="7. Checkbox"), 547 | ft.TextButton(ref=self.TextButton), 548 | ] 549 | 550 | @data 551 | def Stack(self): 552 | """51. Stack | Attribute: controls""" 553 | return [ 554 | ft.CircleAvatar(ref=self.CircleAvatar), 555 | ft.Container( 556 | content=ft.CircleAvatar(bgcolor=ft.colors.GREEN, radius=5), 557 | alignment=ft.alignment.bottom_left, 558 | ), 559 | ] 560 | 561 | @data 562 | def Text(self): 563 | """54. Text | Attribute: value""" 564 | return "54. Text | The Enchanted Nightingale" 565 | 566 | @data 567 | def TextButton(self): 568 | """55. TextButton | Attribute: text""" 569 | return "55. TextButton" 570 | 571 | @data 572 | def PopupMenuButton(self): 573 | """39. PopupMenuButton | Attribute: items""" 574 | return [ 575 | ft.PopupMenuItem(text="39. PopupMenuButton"), 576 | ft.PopupMenuItem(), # divider 577 | ft.PopupMenuItem(content=ft.TextField()), 578 | ft.PopupMenuItem(ref=self.PopupMenuItem, checked=False), 579 | ] 580 | 581 | @data 582 | def PopupMenuItem(self): 583 | """38. PopupMenuItem | Attribute: text""" 584 | return "38. PopupMenuItem" 585 | -------------------------------------------------------------------------------- /tests/test_all_controls/view.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 4 | from flet_mvc import FletView 5 | import flet as ft 6 | import math 7 | 8 | class TestView(FletView): 9 | def __init__(self, controller, model): 10 | self.app_bar = ft.AppBar( 11 | ref=model.AppBar, 12 | leading=ft.Icon(ft.icons.PALETTE), 13 | leading_width=40, 14 | title=ft.Text("0. AppBar | This app contains all controls with refs"), 15 | center_title=False, 16 | bgcolor=ft.colors.SURFACE_VARIANT, 17 | ) 18 | 19 | self.audio = ft.Audio(ref=model.Audio) 20 | 21 | self.banner = ft.Banner( 22 | ref=model.Banner, 23 | bgcolor=ft.colors.AMBER_100, 24 | leading=ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size=40), 25 | actions=[ 26 | ft.TextButton("Retry", on_click=controller.close_banner), 27 | ft.TextButton("Ignore", on_click=controller.close_banner), 28 | ft.TextButton("Cancel", on_click=controller.close_banner), 29 | ], 30 | ) 31 | 32 | self.bottom_sheet = ft.BottomSheet( 33 | ref=model.BottomSheet, 34 | open=False, 35 | on_dismiss=controller.bs_dismissed, 36 | ) 37 | self.snack_bar = ft.SnackBar( 38 | ref=model.SnackBar, 39 | content=ft.Text("Hello, world!"), 40 | action="Alright!", 41 | ) 42 | 43 | self.fab = ft.FloatingActionButton( 44 | ref=model.FloatingActionButton, 45 | icon=ft.icons.ADD, 46 | on_click=controller.fab_pressed, 47 | bgcolor=ft.colors.LIME_300, 48 | height=30, 49 | mini=True, 50 | ) 51 | 52 | self.file_picker = ft.FilePicker(ref=model.FilePicker, on_result=controller.see_file) 53 | 54 | view = [ 55 | ft.ListView( 56 | controls=[ 57 | # 1. AlertDialog 58 | ft.Text("1. AlertDialog"), 59 | ft.AlertDialog(ref=model.Dialog, title=ft.Text("Test Title")), 60 | ft.ElevatedButton("Open Dialog", on_click=controller.open_dialog), 61 | 62 | # 2. AnimatedSwitcher 63 | ft.Text("\n2. Animated Switcher"), 64 | ft.AnimatedSwitcher( 65 | ref=model.AnimatedSwitcher, 66 | transition=ft.AnimatedSwitcherTransition.SCALE, 67 | duration=500, 68 | reverse_duration=100, 69 | switch_in_curve=ft.AnimationCurve.BOUNCE_OUT, 70 | switch_out_curve=ft.AnimationCurve.BOUNCE_IN, 71 | ), 72 | ft.ElevatedButton("Animate!", on_click=controller.animate), 73 | 74 | # 3. Audio 75 | ft.Text("\n3. Audio"), 76 | ft.ElevatedButton("Play", on_click=lambda _: self.audio.play()), 77 | ft.ElevatedButton("Stop playing", on_click=lambda _: self.audio.pause()), 78 | 79 | # 4. Banner 80 | ft.Text("\n4. Banner"), 81 | ft.ElevatedButton("Show Banner", on_click=controller.open_banner), 82 | 83 | # 5. Bottom Sheet 84 | ft.Text("\n5. Bottom Sheet"), 85 | ft.ElevatedButton("Show Bottom Sheet", on_click=controller.open_bs), 86 | 87 | # 6. Card 88 | ft.Text("\n6. Card | 7. Checkbox | 10. Column | 11. Container | 26. Icon | 30. ListTile | 45. Row | 54. Text | 55. TextButton"), 89 | ft.Card( 90 | ref=model.Card 91 | ), 92 | 93 | # 8. Circle Avatar 94 | ft.Text("\n8. CircleAvatar | 51. Stack (overlap controls)"), 95 | ft.Stack( 96 | ref=model.Stack, 97 | width=40, 98 | height=40, 99 | ), 100 | 101 | # 12. DataTable 102 | ft.Text("\n12. DataTable (only DataRow and DataTable supported)"), 103 | # TODO: Create my own datatable that directly accepts a dataframe as input 104 | # which of course is compatible with this library. 105 | # TODO: Maybe create an controller function that makes that automaticalle 106 | # given a ref of datatable + dataframe. <- I like this 107 | # TODO: Create a download option given the datatable node. 108 | ft.DataTable( 109 | ref=model.DataTable, 110 | columns=[ 111 | ft.DataColumn(ft.Text("First name")), 112 | ft.DataColumn(ft.Text("Last name")), 113 | ft.DataColumn(ft.Text("Age"), numeric=True), 114 | ], 115 | ), 116 | 117 | # 13. Divider 118 | ft.Text("\n13. Divider (RefOnly)"), 119 | ft.Divider(ref=model.Divider), 120 | 121 | # 14. Draggable 122 | ft.Text("\n14. Draggable"), 123 | ft.Row( 124 | [ 125 | ft.Column( 126 | [ 127 | ft.Draggable( 128 | ref=model.Draggable, 129 | group="color", 130 | content_feedback=ft.Container( 131 | width=20, 132 | height=20, 133 | bgcolor=ft.colors.CYAN, 134 | border_radius=3, 135 | ), 136 | ), 137 | ft.Draggable( 138 | group="color", 139 | content=ft.Container( 140 | width=50, 141 | height=50, 142 | bgcolor=ft.colors.YELLOW, 143 | border_radius=5, 144 | ), 145 | ), 146 | ft.Draggable( 147 | group="color1", 148 | content=ft.Container( 149 | width=50, 150 | height=50, 151 | bgcolor=ft.colors.GREEN, 152 | border_radius=5, 153 | ), 154 | ), 155 | ] 156 | ), 157 | ft.Container(width=100), 158 | ft.Column( 159 | [ 160 | # 15. DragTarget 161 | ft.Text("\n15. DragTarget"), 162 | ft.DragTarget( 163 | ref=model.DragTarget, 164 | group="color", 165 | on_will_accept=controller.drag_will_accept, 166 | on_accept=controller.drag_accept, 167 | on_leave=controller.drag_leave, 168 | ), 169 | ] 170 | ), 171 | ] 172 | ), 173 | 174 | # 16. Dropdown | 17. ElevatedButton | 50. Snackbar 175 | ft.Text("\n16. Dropdown | 17. ElevatedButton | 50. Snackbar"), 176 | ft.Dropdown( 177 | ref=model.Dropdown, 178 | width=100, 179 | ), 180 | ft.ElevatedButton( 181 | ref=model.ElevatedButton, 182 | on_click=controller.button_clicked 183 | ), 184 | 185 | # 18. FilePicker 186 | ft.Text("\n18. FilePicker"), 187 | ft.ElevatedButton( 188 | "Choose file...", 189 | on_click=lambda _: self.file_picker.pick_files( 190 | allow_multiple=True 191 | ), 192 | ), 193 | ft.ElevatedButton( 194 | "See file on console", 195 | on_click=controller.see_file 196 | ), 197 | 198 | # 19. FilledButton 199 | ft.Text("\n19. FilledButton"), 200 | ft.FilledButton( 201 | ref=model.FilledButton, 202 | disabled=True, 203 | icon="add" 204 | ), 205 | 206 | 207 | # 20. FilledTonalButton 208 | ft.Text("\n20. FilledTonalButton"), 209 | ft.FilledTonalButton( 210 | ref=model.FilledTonalButton, 211 | disabled=True, icon="add" 212 | ), 213 | 214 | 215 | # 23. GestureDetector 216 | ft.Text("\n23. GestureDetector"), 217 | ft.Stack( 218 | [ 219 | ft.GestureDetector( 220 | ref=model.GestureDetector, 221 | mouse_cursor=ft.MouseCursor.MOVE, 222 | on_vertical_drag_update=controller.on_pan_update, 223 | left=100, 224 | top=100, 225 | ) 226 | ], 227 | height=200, 228 | ), 229 | 230 | 231 | # 24. GridView 232 | ft.Text("\n24. GridView"), 233 | ft.GridView( 234 | ref=model.GridView, 235 | expand=1, 236 | runs_count=5, 237 | max_extent=150, 238 | child_aspect_ratio=1.0, 239 | spacing=5, 240 | run_spacing=5, 241 | ), 242 | 243 | # 27. IconButton 244 | ft.Text("\n27. IconButton"), 245 | ft.Row( 246 | [ 247 | ft.IconButton( 248 | ref=model.IconButton, 249 | icon=ft.icons.PLAY_CIRCLE_FILL_OUTLINED, 250 | on_click=controller.icon_button_click, 251 | data=0 252 | ), 253 | ] 254 | ), 255 | 256 | # 29. Ignore. 257 | 258 | # 31. ListView 259 | ft.Text("\n31. ListView"), 260 | ft.Container( 261 | content=ft.ListView( 262 | ref=model.ListView, 263 | expand=True, 264 | spacing=10, 265 | ), 266 | height=100, 267 | ), 268 | 269 | # 32. Markdown 270 | ft.Text("\n32. Markdown"), 271 | ft.Markdown( 272 | ref=model.Markdown, 273 | selectable=True, 274 | extension_set=ft.MarkdownExtensionSet.GITHUB_WEB, 275 | # on_tap_link=lambda e: self.page.launch_url(e.data), 276 | code_theme="atom-one-dark", 277 | code_style=ft.TextStyle(font_family="Roboto Mono"), 278 | ), 279 | 280 | # 33. MatplotlibChart | Tested, good. 281 | ft.Text("\n33. MatplotlibChart | Tested."), 282 | 283 | # 34. NavigationBar 284 | ft.Text("\n34. NavigationBar | Tested."), 285 | 286 | # 35. NavigationRail 287 | ft.Text("\n35. NavigationRail | Tested."), 288 | 289 | # 36. OutlinedButton 290 | ft.Text("\n36. OutlinedButton"), 291 | ft.OutlinedButton(ref=model.OutlinedButton, disabled=True), 292 | 293 | # 37. PlotlyChart 294 | ft.Text("\n37. PlotlyChart | Tested."), 295 | 296 | # 40. ProgressBar 297 | ft.Text("\n40. ProgressBar"), 298 | ft.ProgressBar( 299 | ref=model.ProgressBar, 300 | width=400, 301 | color="amber", 302 | bgcolor="#eeeeee" 303 | ), 304 | 305 | # 41. ProgressRing 306 | ft.Text("\n41. ProgressRing"), 307 | ft.Row( 308 | [ 309 | ft.ProgressRing( 310 | ref=model.ProgressRing, 311 | width=16, 312 | height=16, 313 | stroke_width = 2 314 | ), 315 | ft.Text("Stuck at 75%...") 316 | ] 317 | ), 318 | 319 | # 42. Radio 320 | ft.Text("\n42. Radio | 43. RadioGroup"), 321 | ft.RadioGroup( 322 | ref=model.RadioGroup, 323 | content=ft.Column( 324 | [ 325 | ft.Radio(ref=model.Radio, label="Red"), 326 | ft.Radio(value="green", label="Green"), 327 | ft.Radio(value="blue", label="Blue") 328 | ] 329 | ) 330 | ), 331 | 332 | # 44. ResponsiveRow 333 | ft.Text("\n44. ResponsiveRow"), 334 | ft.ResponsiveRow(ref=model.ResponsiveRow), 335 | 336 | # 47. ShaderMask 337 | ft.Text("\n47. ShaderMask"), 338 | ft.Row( 339 | [ 340 | ft.ShaderMask( 341 | ref=model.ShaderMask, 342 | blend_mode=ft.BlendMode.MULTIPLY, 343 | shader=ft.RadialGradient( 344 | center=ft.alignment.center, 345 | radius=2.0, 346 | colors=[ft.colors.WHITE, ft.colors.PINK], 347 | tile_mode=ft.GradientTileMode.CLAMP, 348 | ), 349 | ) 350 | ] 351 | ), 352 | 353 | # 49. Slider 354 | ft.Text("\n49. Slider"), 355 | ft.Slider( 356 | ref=model.Slider, 357 | min=0, 358 | max=100, 359 | divisions=10, 360 | label="{value}%", 361 | # on_change=slider_changed 362 | ), 363 | 364 | # 52. Switch 365 | ft.Text("\n52. Switch"), 366 | ft.Switch(ref=model.Switch), 367 | 368 | # 53. Tabs 369 | ft.Text("\n53. Tabs"), 370 | ft.Container( 371 | ft.Tabs( 372 | ref=model.Tabs, 373 | selected_index=0, 374 | animation_duration=0, # No animation 375 | expand=1, 376 | ), 377 | width=300, 378 | height=300, 379 | ), 380 | 381 | # 56. Textfield 382 | ft.Text("\n56. Textfield"), 383 | ft.TextField(ref=model.TextField, label="TextField"), 384 | 385 | # 57. TextSpan | 57.2 TextStyle (RefOnly) 386 | ft.Text("\n57. TextSpan"), 387 | ft.Text( 388 | spans=[ 389 | ft.TextSpan( 390 | ref=model.TextSpan, 391 | style=ft.TextStyle( 392 | size=60, 393 | weight=ft.FontWeight.BOLD, 394 | foreground=ft.Paint( 395 | color=ft.colors.BLUE_700, 396 | stroke_width=6, 397 | stroke_join=ft.StrokeJoin.ROUND, 398 | style=ft.PaintingStyle.STROKE, 399 | ), 400 | ), 401 | ), 402 | ], 403 | ), 404 | 405 | # 58. Tooltip 406 | ft.Text("\n58. Tooltip"), 407 | ft.Tooltip( 408 | ref=model.Tooltip, 409 | message="This is tooltip", 410 | padding=20, 411 | border_radius=10, 412 | text_style=ft.TextStyle(size=20, color=ft.colors.WHITE), 413 | gradient=ft.LinearGradient( 414 | begin=ft.alignment.top_left, 415 | end=ft.alignment.Alignment(0.8, 1), 416 | colors=[ 417 | "0xff1f005c", 418 | "0xff5b0060", 419 | "0xff870160", 420 | "0xffac255e", 421 | "0xffca485c", 422 | "0xffe16b5c", 423 | "0xfff39060", 424 | "0xffffb56b", 425 | ], 426 | tile_mode=ft.GradientTileMode.MIRROR, 427 | rotation=math.pi / 3, 428 | ), 429 | ), 430 | 431 | # 60. VerticalDivider 432 | ft.Text("\n60. VerticalDivider | RefOnly"), 433 | ft.Column( 434 | [ 435 | ft.Row( 436 | [ 437 | ft.Container( 438 | bgcolor=ft.colors.ORANGE_300, 439 | alignment=ft.alignment.center, 440 | expand=True, 441 | ), 442 | ft.VerticalDivider(), 443 | ft.Container( 444 | bgcolor=ft.colors.BROWN_400, 445 | alignment=ft.alignment.center, 446 | expand=True, 447 | ), 448 | ft.VerticalDivider( 449 | ref=model.VerticalDivider, 450 | width=1, 451 | color="white" 452 | ), 453 | ft.Container( 454 | bgcolor=ft.colors.BLUE_300, 455 | alignment=ft.alignment.center, 456 | expand=True, 457 | ), 458 | ft.VerticalDivider(width=9, thickness=3), 459 | ft.Container( 460 | bgcolor=ft.colors.GREEN_300, 461 | alignment=ft.alignment.center, 462 | expand=True, 463 | ), 464 | ], 465 | spacing=0, 466 | expand=True, 467 | ), 468 | ], 469 | height=300, 470 | ), 471 | 472 | # 61. WindowDragArea 473 | ft.Text("\n61. WindowDragArea"), 474 | ft.WindowDragArea( 475 | ref=model.WindowDragArea, 476 | expand=True 477 | ), 478 | 479 | # 62. Canvas 480 | ft.Text("\n62. Canvas"), 481 | ft.Container( 482 | ft.canvas.Canvas( 483 | ref=model.Canvas, 484 | width=float("inf"), 485 | expand=True, 486 | ), 487 | height=150, 488 | ), 489 | 490 | # 63. LineChart 491 | ft.Text("\n63. LineChart"), 492 | 493 | 494 | # 64. BarChart 495 | ft.Text("\n64. BarChart"), 496 | 497 | 498 | # 65. PieChart 499 | ft.Text("\n65. PieChart"), 500 | 501 | 502 | ], 503 | expand=True 504 | ) 505 | ] 506 | super().__init__(model, view, controller) 507 | -------------------------------------------------------------------------------- /tests/test_datapoints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import flet as ft 3 | import os 4 | import sys 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | from flet_mvc import FletModel, data 7 | from data.component_attribute import component_value_attr_map, potential_attributes 8 | 9 | 10 | # define your model 11 | class MockModel(FletModel): 12 | @data 13 | def datapoint(self): 14 | return "" 15 | 16 | @data 17 | def datapoint_list(self): 18 | return [] 19 | 20 | @data 21 | def ref_datapoint(self): 22 | return "Datapoint initial value" 23 | 24 | @data.RefOnly 25 | def ref_only_datapoint(self): 26 | return None 27 | 28 | 29 | # Test DataPoint initialization 30 | def test_datapoint_init(): 31 | model = MockModel() 32 | assert isinstance(model.datapoint, data) 33 | 34 | 35 | # Test DataPoint set, get and reset value 36 | def test_datapoint_set_get(): 37 | model = MockModel() 38 | model.datapoint.set_value("Test Value") 39 | assert model.datapoint() == "Test Value" 40 | model.datapoint.reset() 41 | assert model.datapoint() == "" 42 | 43 | 44 | # Test DataPoint append value 45 | def test_datapoint_append(): 46 | model = MockModel() 47 | assert not model.datapoint_list.has_set_value() 48 | 49 | model.datapoint_list().append("Test Value") # normal append won't modify has_set_value attr. 50 | assert not model.datapoint_list.has_set_value() 51 | assert model.datapoint_list() == ["Test Value"] 52 | 53 | model.datapoint_list.set_value(["Test Value"]) 54 | assert model.datapoint_list.has_set_value() 55 | 56 | model.datapoint_list.reset() 57 | assert not model.datapoint_list.has_set_value() 58 | assert not model.datapoint_list() 59 | 60 | 61 | # Test DataPoint logical operations 62 | def test_datapoint_logical(): 63 | model = MockModel() 64 | model.datapoint.set_value(None) 65 | assert not model.datapoint() # Should evaluate to False 66 | model.datapoint.set_value("Test Value") 67 | assert model.datapoint() # Should evaluate to True 68 | 69 | 70 | # Test DataPoint logical operations 71 | def test_datapoint_ref_only(): 72 | model = MockModel() 73 | 74 | with pytest.raises(TypeError): 75 | model.ref_only_datapoint() 76 | 77 | with pytest.raises(TypeError): 78 | model.ref_only_datapoint.set_value() 79 | 80 | with pytest.raises(TypeError): 81 | model.ref_only_datapoint.append(1) 82 | 83 | with pytest.raises(TypeError): 84 | model.ref_only_datapoint.value 85 | 86 | with pytest.raises(TypeError): 87 | model.ref_only_datapoint.value = 1 88 | 89 | with pytest.raises(TypeError): 90 | model.ref_only_datapoint.set_default() 91 | 92 | with pytest.raises(TypeError): 93 | model.ref_only_datapoint.reset() 94 | 95 | 96 | def test_datapoint_ref_only2(): 97 | model = MockModel() 98 | assert not model.ref_only_datapoint.current 99 | ft.Text(ref=model.ref_only_datapoint) 100 | assert model.ref_only_datapoint.current 101 | 102 | 103 | # Test that we can set the initial value of a component using a Ref object 104 | @pytest.mark.parametrize("component, value_attr", component_value_attr_map.items()) 105 | def test_initial_value(component, value_attr): 106 | model = MockModel() 107 | 108 | if potential_attributes[value_attr] == list: 109 | value = ["item1", "item2"] 110 | new_default = ["default1", "default2"] 111 | elif potential_attributes[value_attr] == str: 112 | value = "Initial value" 113 | new_default = "Default" 114 | else: 115 | value = ft.Text("test") # Default case 116 | new_default = ft.Text("new_default") 117 | 118 | model.ref_datapoint.set_default(new_default) 119 | kwargs = {value_attr: value} 120 | 121 | component_instance = component(ref=model.ref_datapoint, **kwargs) 122 | 123 | assert getattr(component_instance, value_attr) == value 124 | model.ref_datapoint.reset() 125 | assert getattr(component_instance, value_attr) == new_default 126 | model.ref_datapoint.set_value(value) 127 | assert getattr(component_instance, value_attr) == value 128 | 129 | model.ref_datapoint.__hard_reset__() 130 | --------------------------------------------------------------------------------