flexxui / flexx

Write desktop and web apps in pure Python
http://flexx.readthedocs.io
BSD 2-Clause "Simplified" License
3.26k stars 257 forks source link

Dynamic creation of widgets and widgets of widgets #667

Open ceprio opened 4 years ago

ceprio commented 4 years ago

In order for this UI to grow to a full fledget application, I believe it needs to allow users to create and destroy widgets dynamically. The underlying code seems very close to be able to do that but the way widgets are structured, it seem to limit that. Here is what I would wish for:

Be able to create children widgets dynamically within a widget. This would allow for super widgets or widgets containing widgets that contaings widgets. It would also allow the whole tree structure to change in reaction to events by adding and removing widgets.

To be able to do that I see a the need for a few changes:

Any toughts?

I'll play with these ideas and see where it leads.

ceprio commented 4 years ago

Here is an example use case of the concept presented above, it think it uses most of the ideas that where put forward:

    from flexx import element as e

    class Example(e.Element):
        def init(self):  # at this point a self.container as been initialized
            tag, text, line, widget, stag = self.container.ttlws()  # syntax borrowed from yattag
            fill_my_container(self.container)  # call a sub to fill a fist part
            with tag("div") as d:
                self.button_container = d     # keep a reference on the container
                self.button1 = widget(e.Button(text='swap'))  # keep a reference on the button
                widget(e.Button(text='bar'))

        @flx.reaction('button1.pointer_click')
        def _button_clicked(self, *events):
            ev = events[-1]
            old_container = self.button_container.container
            self.button_container.container = e.Container()
            tag, text, line, widget, stag = self.button_container.container.ttlws() 
            for elem in reverse(old_container):
                self.button_container.container.append(elem)
            text("Buttons have swapped")

... At return of the reaction function, it is expected that the DOM be synchronized with the modified container. May have to do a set_container for a event to be generated? Will see...

ceprio commented 4 years ago

Ok, so first think to do in implementing this is to create a container inside the Widget class. I want to keep the Widget class so as above I've copied the Widget class into an Element class.

I added a container property (that seem the way to go for the changes to propagate). As there is already a container property for the id of the DOM container element, I've renamed it to container_id, it seem to be used only within the Widget class so it should not be a problem. I also see a children property (read only) that could be used instead of my container be I don't know its exact use so I'm going to keep it for the moment. I might rename my container to children at a later point.

So next is to fill that container with elements in different ways and see how easy it is to catch changes to eventually have _render_dom synchronize it to the DOM. We need to be able to modify the container from init(), @flx.reaction and from a pyWidget (server side). I will play with that today.

almarklein commented 4 years ago

Interesting thoughts! I'm not sure whether you are referring to widgets as flx.Widget or html components. I first thought the former, but your example that uses Element suggests the latter?

As you said it is already possible to create subwidgets dynamically. Perhaps its not documented well, and it may well be improved. Here's an example:

from flexx import flx

class Example(flx.Widget):

    def init(self):
        with flx.HBox() as self._container:
            self.but = flx.Button(text='add')
            flx.Widget(minsize=20)  # seperator

    @flx.reaction("but.pointer_click")
    def add_widgets(self, *events):
        # This would be better - create the widget exactly in the
        # context (parent widget) where you want it
        # with self._container:
        #     flx.Button("stub")
        # For sake of example, you can create a new widget under *any* context,
        # and then move it somewhere else.
        with self:
            w = flx.Button("stub")
        w.set_parent(self._container)

flx.launch(Example)
flx.run()

Further, a widget can be "orphaned" using w.set_parent(None), and destroyed using w.dispose() (important to call this in JS because garbage collections works differently there).

[...] the way widgets are structured, it seem to limit [create and destroy widgets dynamically].

Could you clarify this more, please?

ceprio commented 4 years ago

For your first question, I'm mixing Widgets with html components because ideally they should be handled the same way and used interchangeably within init:

    def init():
            tag, text, line, widget, stag = self.container.ttlws()  # syntax borrowed from yattag
            with tag("div") as d:
                with widget(flx.GroupWidget()):
                     with tag("span") as s:
                         self.span = s
                         text("My video:")
                         widget(ui.YoutubeWidget(source='RG1P8MQS1cU'))

Of course with such code widgets are hard to distinguish from html component: They just become super html components.

Your example is interesting, I saw that possibility in your code but didn't knew how to use it. It also brings additional questions like what if you wanted to add the "stub" button before the "add" button. There is probably a way but it is not very intuitive. With the code above and the existence of a container within each widget and tags, I would like to see something like:

    @flx.reaction("but.pointer_click")
    def add_widgets(self, *events):
        container = self.span.container
        container.insert(0, container.Widget(flx.Button(text="Added button"))
        self.span.sync_container() # don't know if this would be needed but the span container needs to be synced with the dom

For your last question:

[...] the way widgets are structured, it seem to limit [create and destroy widgets dynamically].

Your example just showed that it is possible to create and destroy widgets so that aspect is covered. Remains the placement of the created widget and interleaving it with tag and text to have flexibility. I do believe that if tags could be used in init(), _render_dom() would not be needed in the creation of complex Widgets and if you had a context like a container (list of children), it would allow more intuitive interactions with widgets and html elements that are child of widgets and elements. It would also allow to pass the container as context into function calls within init() or actions functions.

almarklein commented 4 years ago

I find it a bit hard to keep track of what changes you are proposing :)

To confine it a bit, some comments:

I do not think it would be a good way to mix html elements with widgets. These have different purposes and mixing them would blur these abstractions. That said, you are free to implement widgets that tightly wrap html elements in your own code.

I also see a children property (read only) that could be used instead of my container be I don't know its exact use so I'm going to keep it for the moment. I might rename my container to children at a later point.

I did not quite understand what your intention with the container was until I read this (again). I think you might be trying to replicate the children property :)

The _render_dom functionality is intended for more complex/dynamic use-cases. But indeed at that point you're working with HTML elements. So I can see how you'd want a way to deal with sub-widgets in a more dynamic way.

You make a good point about it being hard to dynamically create a subwidet and put it in a specific place. I would propose focusing on improving that. Some things that come to mind: Widget.append_child(), Widget.remove_child(), Widget.replace_child(current_child, new_child) ...

gongbudaizhe commented 2 years ago

Is there a way to dynamically create a subwidget and put it in a specific place now as in 2022 ?

almarklein commented 2 years ago

Is there a way to dynamically create a subwidget and put it in a specific place now as in 2022 ?

You should be able to set the parent of the subwidget.