├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------