Kitware / trame-router

trame-router brings Vue Router capabilities into trame widgets and ui
MIT License
1 stars 1 forks source link

Conditionally Rendering PyVista Visualizations Based on Route Parameters in Trame Router #7

Open MouseAndKeyboard opened 1 month ago

MouseAndKeyboard commented 1 month ago

Hello,

I'm developing an application using Trame and have run into a challenge that I hope you can help me with.

I want to define a dynamic route in my application:

/models/:slug

Depending on the :slug parameter in the route, I aim to conditionally render different 3D visualizations provided by PyVista. My initial approach is to check the value of $route.params.slug and then display the corresponding PyVista plotter UI.

Here's a simplified version of what I'm trying to implement:

with RouterViewLayout(server, "/models/:slug") as layout:
    # Intend to access $route.params.slug here
    viewer = get_viewer_conditional_on_slug(slug_value)
    ctrl.add("view_update")(view.update)
    ctrl.view_update_image = view.update_image
    ctrl.reset_camera = view.reset_camera

My intended workflow is:

  1. When the RouterViewLayout component loads or mounts, I want to retrieve the $route.params.slug value and assign it to a state variable.

  2. Updating this state variable should trigger functions decorated with @state.change, allowing me to react to the new route parameter.

  3. These state change functions would create a new PyVista plotter based on the slug value, rendering the appropriate 3D visualization on the page.

TL;DR: I need to render different PyVista visualizations based on the dynamic :slug route parameter. What would be the best way to access $route.params.slug within the RouterViewLayout and trigger the necessary updates in Trame?

Any guidance or suggestions would be greatly appreciated!

Thank you.

jourdain commented 1 month ago

Your RouterViewLayout needs to be static Python wise. It will be up to the JS to report back to the server which slug you are so you can display what you aim to display.

What I'm trying to say, is that the python code only execute once. After that, it is up to the JavaScript side trigger and render what ever has been described.

HTH

MouseAndKeyboard commented 4 weeks ago

Okay, I come from using React. -> What I almost want it some sort of "on-component-mounted" callback. -> Within this callback I want to access the $route.params.slug parameter and pass that to a trigger.

My challenges are:

  1. I'm not exactly sure how I trigger something onMounted within trame. Either "manually" using the server.js_call or somehow binding a trame function/trigger directly to the event.
  2. How do I pass the slug as a parameter into the function.

Fundamentally I want to do something like the code below:

import pyvista as pv
from pyvista.trame.ui import plotter_ui
from trame.widgets import vuetify3, router
from trame.ui.vuetify3 import SinglePageWithDrawerLayout
from trame.ui.router import RouterViewLayout
from trame.app import get_server

server = get_server(client_type='vue3')
state, ctrl = server.state, server.controller

# Initialize the plotter
plotter = pv.Plotter(off_screen=True)
plotter.add_mesh(pv.Sphere(radius=1), color='red', point_size=10)

# Set up the main layout
with SinglePageWithDrawerLayout(server) as layout:
    with layout.toolbar:
        vuetify3.VBtn("Home", to="/")

    with layout.content:
        router.RouterView()

# Define the controller function
@ctrl.trigger("on_route_load")
def on_route_load(id, **kwargs):
    state.ball_index = id

# Update the plotter when ball_index changes
@state.change('ball_index')
def update_plotter(ball_index=0, **kwargs):
    # Clear the plotter
    plotter.clear()

    # Select the actor based on ball_index
    if state.ball_index == '0':
        actor = pv.Sphere(radius=1)
    elif state.ball_index == '1':
        actor = pv.Cone()
    elif state.ball_index == '2':
        actor = pv.Cylinder()
    elif state.ball_index == '3':
        actor = pv.Cube()
    else:
        actor = pv.Sphere(radius=1)

    # Add the new actor
    plotter.add_mesh(actor, color='red', point_size=10)

# Set up the router view layout
with RouterViewLayout(server, "/:id") as layout:
    with vuetify3.VCard():
        vuetify3.VCardTitle("Dynamic Shape Viewer")
        vuetify3.VCardText("Current Shape Index: {{ $route.params.id }}")

        # Include the script to trigger the controller on route change
        vuetify3.VCardText(
            """
            <script>
                export default {
                    mounted() {
                        this.$trame.trigger('on_route_load', { id: this.$route.params.id });
                    },
                    watch: {
                        '$route.params.id': function(newVal, oldVal) {
                            this.$trame.trigger('on_route_load', { id: newVal });
                        }
                    }
                }
            </script>
            """
        )

# Initialize the ball_index
state.ball_index = '0'

if __name__ == "__main__":
    server.start()

I'm not sure how to actually invoke the trigger and pass the id parameter in this case.

jourdain commented 4 weeks ago

Here is an example with the client trigger you want.

jourdain commented 4 weeks ago

That is also another good base. But also on the arg side, you can get your hand on the router to extract the :slug from it. You can trigger a console.log($router) and figure out the path to get the piece you want using the browser dev tool.

You can also monitor the $router change and trigger a method on the python side from that like below.

client.ClientStateChange(
     trigger_on_create=True,
     value="$router.currentRoute.value.path",
     change=(self.page_changed, "[$event]"),
)