zauberzeug / nicegui

Create web-based user interfaces with Python. The nice way.
https://nicegui.io
MIT License
10.22k stars 609 forks source link

Vertical stepper's animation #3881

Open StarDustEins opened 1 month ago

StarDustEins commented 1 month ago

Description

Vertical stepper's animation seems not good, in Quasor's official demo it works smoothly. I use the latest version of Nicegui 2.3.0

https://github.com/user-attachments/assets/7600407d-e6a0-4cd2-bdf6-8218c39a1dc3

falkoschindler commented 1 month ago

Thanks for reporting this issue, @StarDustEins!

Minimum reproducible example:

with ui.stepper().props('vertical animated') as stepper:
    with ui.step('A'):
        with ui.stepper_navigation():
            ui.button('Next', on_click=stepper.next)
    with ui.step('B'):
        with ui.stepper_navigation():
            ui.button('Next', on_click=stepper.next)
            ui.button('Back', on_click=stepper.previous)
    with ui.step('C'):
        with ui.stepper_navigation():
            ui.button('Back', on_click=stepper.previous)
falkoschindler commented 1 month ago

So far I failed to reproduce it with plain Quasar/Vue:

<html>
  <head>
    <link href="https://fonts.googleapis.com/css?family=Roboto:400|Material+Icons" rel="stylesheet" type="text/css" />
    <link href="https://cdn.jsdelivr.net/npm/quasar@2.17.0/dist/quasar.prod.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="q-app">
      <q-stepper ref="stepper" :model-value="step" @update:model-value="onStepChange" vertical animated>
        <q-step :name="1" title="Step 1">
          <q-stepper-navigation>
            <q-btn @click="next" label="Next"></q-btn>
          </q-stepper-navigation>
        </q-step>
        <q-step :name="2" title="Step 2">
          <q-stepper-navigation>
            <q-btn @click="next" label="Next"></q-btn>
            <q-btn @click="previous" label="Previous"></q-btn>
          </q-stepper-navigation>
        </q-step>
        <q-step :name="3" title="Step 3">
          <q-stepper-navigation>
            <q-btn @click="previous" label="Previous"></q-btn>
          </q-stepper-navigation>
        </q-step>
      </q-stepper>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@2.17.0/dist/quasar.umd.prod.js"></script>
    <script>
      const app = Vue.createApp({
        setup() {
          const step = Vue.ref(1);
          const stepper = Vue.ref(null);
          return {
            step,
            stepper,
            onStepChange: (newStep) => (step.value = newStep),
            next: () => stepper.value.next(),
            previous: () => stepper.value.previous(),
          };
        },
      });
      app.use(Quasar);
      app.mount("#q-app");
    </script>
  </body>
</html>

This animates as expected. But what's the difference to a NiceGUI app? Does anyone have an idea?

StarDustEins commented 1 month ago

I have no idea about Vue and js

CrystalWindSnake commented 1 month ago

@falkoschindler I suspect the reason for the issue is that the stepper rebuilds all its child elements every time it updates

falkoschindler commented 2 weeks ago

I looked into this issue once again, but still don't understand why NiceGUI behaves differently than plain Quasar. @CrystalWindSnake might be right and Vue is recreating the QStep elements. But I don't see why, because NiceGUI is only calling next() and previous(), just like in the Quasar snippet I posted above.

CrystalWindSnake commented 2 weeks ago

I looked into this issue once again, but still don't understand why NiceGUI behaves differently than plain Quasar. @CrystalWindSnake might be right and Vue is recreating the QStep elements. But I don't see why, because NiceGUI is only calling next() and previous(), just like in the Quasar snippet I posted above.

NiceGUI not only calls next(), but also calls the update method of the stepper itself. This is causing the entire component to refresh, including all its child elements.

In the following example, when you click next, the backend will print updating stepper.

from nicegui import ui

class MyStepper(ui.stepper):
    def update(self) -> None:
        print("updating stepper")
        return super().update()

with MyStepper().props("vertical animated") as stepper:
    with ui.step("A"):
        with ui.stepper_navigation():
            ui.button("Next", on_click=stepper.next)
    with ui.step("B"):
        with ui.stepper_navigation():
            ui.button("Next", on_click=stepper.next)
            ui.button("Back", on_click=stepper.previous)
    with ui.step("C"):
        with ui.stepper_navigation():
            ui.button("Back", on_click=stepper.previous)

ui.run()
falkoschindler commented 2 weeks ago

Ah, I wasn't aware of update() being called. This is because ui.stepper.LOOPBACK = True which causes update being called when the value changes. So I'd expect the problem being fixed by setting ui.stepper.LOOPBACK = False (the value is updated by setting the "model-value" directly on the client) or ui.stepper.LOOPBACK = None (the value is updated automatically by the Vue element). But none of them work.

It seems like calling next is not enough to change the step:

with ui.element('q-stepper').props('model-value=A vertical animated') as stepper:
    with ui.element('q-step').props('name=A title=A'):
        ui.button('next', on_click=lambda: stepper.run_method('next'))
    with ui.element('q-step').props('name=B title=B'):
        ui.button('back', on_click=lambda: stepper.run_method('previous'))

Only when subscribing to the update:model-value event and setting the new model-value prop, the step changes:

with ui.element('q-stepper').props('model-value=A vertical animated') \
        .on('update:model-value', lambda e: stepper.props(f'model-value={e.args}')) as stepper:
    with ui.element('q-step').props('name=A title=A'):
        ui.button('next', on_click=lambda: stepper.run_method('next'))
    with ui.element('q-step').props('name=B title=B'):
        ui.button('back', on_click=lambda: stepper.run_method('previous'))

Now the question remains: What does ui.stepper do differently? Calling .props() also leads to an update(), so this can't be the problem. And ui.stepper overwrites _handle_value_change, but removing this code doesn't help.