Trigger on_load when router completes navigation (#984)
This commit is contained in:
parent
32f3a387ae
commit
e887dd143b
@ -441,8 +441,8 @@ async def process(
|
|||||||
state = app.state_manager.get_state(event.token)
|
state = app.state_manager.get_state(event.token)
|
||||||
|
|
||||||
# Add request data to the state.
|
# Add request data to the state.
|
||||||
state.router_data = event.router_data
|
router_data = event.router_data
|
||||||
state.router_data.update(
|
router_data.update(
|
||||||
{
|
{
|
||||||
constants.RouteVar.QUERY: format.format_query_params(event.router_data),
|
constants.RouteVar.QUERY: format.format_query_params(event.router_data),
|
||||||
constants.RouteVar.CLIENT_TOKEN: event.token,
|
constants.RouteVar.CLIENT_TOKEN: event.token,
|
||||||
@ -451,10 +451,11 @@ async def process(
|
|||||||
constants.RouteVar.CLIENT_IP: client_ip,
|
constants.RouteVar.CLIENT_IP: client_ip,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# re-assign only when the value is different
|
||||||
# Also pass router_data to all substates. (TODO: this isn't recursive currently)
|
if state.router_data != router_data:
|
||||||
for _, substate in state.substates.items():
|
# assignment will recurse into substates and force recalculation of
|
||||||
substate.router_data = state.router_data
|
# dependent ComputedVar (dynamic route variables)
|
||||||
|
state.router_data = router_data
|
||||||
|
|
||||||
# Preprocess the event.
|
# Preprocess the event.
|
||||||
update = await app.preprocess(state, event)
|
update = await app.preprocess(state, event)
|
||||||
|
@ -37,6 +37,7 @@ class PyneconeJinjaEnvironment(Environment):
|
|||||||
"color_mode": constants.COLOR_MODE,
|
"color_mode": constants.COLOR_MODE,
|
||||||
"toggle_color_mode": constants.TOGGLE_COLOR_MODE,
|
"toggle_color_mode": constants.TOGGLE_COLOR_MODE,
|
||||||
"use_color_mode": constants.USE_COLOR_MODE,
|
"use_color_mode": constants.USE_COLOR_MODE,
|
||||||
|
"hydrate": constants.HYDRATE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -267,6 +267,10 @@ class RouteArgType(SimpleNamespace):
|
|||||||
LIST = str("arg_list")
|
LIST = str("arg_list")
|
||||||
|
|
||||||
|
|
||||||
|
# the name of the backend var containing path and client information
|
||||||
|
ROUTER_DATA = "router_data"
|
||||||
|
|
||||||
|
|
||||||
class RouteVar(SimpleNamespace):
|
class RouteVar(SimpleNamespace):
|
||||||
"""Names of variables used in the router_data dict stored in State."""
|
"""Names of variables used in the router_data dict stored in State."""
|
||||||
|
|
||||||
|
@ -37,7 +37,10 @@ class HydrateMiddleware(Middleware):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Get the initial state.
|
# Get the initial state.
|
||||||
|
setattr(state, constants.IS_HYDRATED, False)
|
||||||
delta = format.format_state({state.get_name(): state.dict()})
|
delta = format.format_state({state.get_name(): state.dict()})
|
||||||
|
# since a full dict was captured, clean any dirtiness
|
||||||
|
state.clean()
|
||||||
|
|
||||||
# Get the route for on_load events.
|
# Get the route for on_load events.
|
||||||
route = event.router_data.get(constants.RouteVar.PATH, "")
|
route = event.router_data.get(constants.RouteVar.PATH, "")
|
||||||
|
@ -574,6 +574,14 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
self.dirty_vars.add(name)
|
self.dirty_vars.add(name)
|
||||||
self.mark_dirty()
|
self.mark_dirty()
|
||||||
|
|
||||||
|
# For now, handle router_data updates as a special case
|
||||||
|
if name == constants.ROUTER_DATA:
|
||||||
|
self.dirty_vars.add(name)
|
||||||
|
self.mark_dirty()
|
||||||
|
# propagate router_data updates down the state tree
|
||||||
|
for substate in self.substates.values():
|
||||||
|
setattr(substate, name, value)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Reset all the base vars to their default values."""
|
"""Reset all the base vars to their default values."""
|
||||||
# Reset the base vars.
|
# Reset the base vars.
|
||||||
|
@ -54,6 +54,13 @@ export default function Component() {
|
|||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
const change_complete = () => Event([E('{{state_name}}.{{const.hydrate}}', {})])
|
||||||
|
{{const.router}}.events.on('routeChangeComplete', change_complete)
|
||||||
|
return () => {
|
||||||
|
{{const.router}}.events.off('routeChangeComplete', change_complete)
|
||||||
|
}
|
||||||
|
}, [{{const.router}}])
|
||||||
|
|
||||||
{% for hook in hooks %}
|
{% for hook in hooks %}
|
||||||
{{ hook }}
|
{{ hook }}
|
||||||
|
@ -5,12 +5,15 @@ from typing import List, Tuple, Type
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
|
||||||
from pynecone.app import App, DefaultState, upload
|
from pynecone import constants
|
||||||
|
from pynecone.app import App, DefaultState, process, upload
|
||||||
from pynecone.components import Box
|
from pynecone.components import Box
|
||||||
from pynecone.event import Event
|
from pynecone.event import Event, get_hydrate_event
|
||||||
from pynecone.middleware import HydrateMiddleware
|
from pynecone.middleware import HydrateMiddleware
|
||||||
from pynecone.state import State, StateUpdate
|
from pynecone.state import State, StateUpdate
|
||||||
from pynecone.style import Style
|
from pynecone.style import Style
|
||||||
|
from pynecone.utils import format
|
||||||
|
from pynecone.var import ComputedVar
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -121,9 +124,9 @@ def test_add_page_set_route_dynamic(app: App, index_page, windows_platform: bool
|
|||||||
assert set(app.pages.keys()) == {"test/[dynamic]"}
|
assert set(app.pages.keys()) == {"test/[dynamic]"}
|
||||||
assert "dynamic" in app.state.computed_vars
|
assert "dynamic" in app.state.computed_vars
|
||||||
assert app.state.computed_vars["dynamic"].deps(objclass=DefaultState) == {
|
assert app.state.computed_vars["dynamic"].deps(objclass=DefaultState) == {
|
||||||
"router_data"
|
constants.ROUTER_DATA
|
||||||
}
|
}
|
||||||
assert "router_data" in app.state().computed_var_dependencies
|
assert constants.ROUTER_DATA in app.state().computed_var_dependencies
|
||||||
|
|
||||||
|
|
||||||
def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
|
def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
|
||||||
@ -547,3 +550,188 @@ async def test_upload_file_without_annotation(fixture, request):
|
|||||||
err.value.args[0]
|
err.value.args[0]
|
||||||
== "`file_upload_state.handle_upload2` handler should have a parameter annotated as List[pc.UploadFile]"
|
== "`file_upload_state.handle_upload2` handler should have a parameter annotated as List[pc.UploadFile]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicState(State):
|
||||||
|
"""State class for testing dynamic route var.
|
||||||
|
|
||||||
|
This is defined at module level because event handlers cannot be addressed
|
||||||
|
correctly when the class is defined as a local.
|
||||||
|
|
||||||
|
There are several counters:
|
||||||
|
* loaded: counts how many times `on_load` was triggered by the hydrate middleware
|
||||||
|
* counter: counts how many times `on_counter` was triggered by a non-naviagational event
|
||||||
|
-> these events should NOT trigger reload or recalculation of router_data dependent vars
|
||||||
|
* side_effect_counter: counts how many times a computed var was
|
||||||
|
recalculated when the dynamic route var was dirty
|
||||||
|
"""
|
||||||
|
|
||||||
|
loaded: int = 0
|
||||||
|
counter: int = 0
|
||||||
|
# side_effect_counter: int = 0
|
||||||
|
|
||||||
|
def on_load(self):
|
||||||
|
"""Event handler for page on_load, should trigger for all navigation events."""
|
||||||
|
self.loaded = self.loaded + 1
|
||||||
|
|
||||||
|
def on_counter(self):
|
||||||
|
"""Increment the counter var."""
|
||||||
|
self.counter = self.counter + 1
|
||||||
|
|
||||||
|
@ComputedVar
|
||||||
|
def comp_dynamic(self) -> str:
|
||||||
|
"""A computed var that depends on the dynamic var.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
same as self.dynamic
|
||||||
|
"""
|
||||||
|
# self.side_effect_counter = self.side_effect_counter + 1
|
||||||
|
return self.dynamic
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dynamic_route_var_route_change_completed_on_load(
|
||||||
|
index_page,
|
||||||
|
windows_platform: bool,
|
||||||
|
):
|
||||||
|
"""Create app with dynamic route var, and simulate navigation.
|
||||||
|
|
||||||
|
on_load should fire, allowing any additional vars to be updated before the
|
||||||
|
initial page hydrate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index_page: The index page.
|
||||||
|
windows_platform: Whether the system is windows.
|
||||||
|
"""
|
||||||
|
arg_name = "dynamic"
|
||||||
|
route = f"/test/[{arg_name}]"
|
||||||
|
if windows_platform:
|
||||||
|
route.lstrip("/").replace("/", "\\")
|
||||||
|
app = App(state=DynamicState)
|
||||||
|
assert arg_name not in app.state.vars
|
||||||
|
app.add_page(index_page, route=route, on_load=DynamicState.on_load) # type: ignore
|
||||||
|
assert arg_name in app.state.vars
|
||||||
|
assert arg_name in app.state.computed_vars
|
||||||
|
assert app.state.computed_vars[arg_name].deps(objclass=DynamicState) == {
|
||||||
|
constants.ROUTER_DATA
|
||||||
|
}
|
||||||
|
assert constants.ROUTER_DATA in app.state().computed_var_dependencies
|
||||||
|
|
||||||
|
token = "mock_token"
|
||||||
|
sid = "mock_sid"
|
||||||
|
client_ip = "127.0.0.1"
|
||||||
|
state = app.state_manager.get_state(token)
|
||||||
|
assert state.dynamic == ""
|
||||||
|
exp_vals = ["foo", "foobar", "baz"]
|
||||||
|
|
||||||
|
def _event(name, val, **kwargs):
|
||||||
|
return Event(
|
||||||
|
token=kwargs.pop("token", token),
|
||||||
|
name=name,
|
||||||
|
router_data=kwargs.pop(
|
||||||
|
"router_data", {"pathname": route, "query": {arg_name: val}}
|
||||||
|
),
|
||||||
|
payload=kwargs.pop("payload", {}),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _dynamic_state_event(name, val, **kwargs):
|
||||||
|
return _event(
|
||||||
|
name=format.format_event_handler(getattr(DynamicState, name)), # type: ignore
|
||||||
|
val=val,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
for exp_index, exp_val in enumerate(exp_vals):
|
||||||
|
update = await process(
|
||||||
|
app,
|
||||||
|
event=_event(name=get_hydrate_event(state), val=exp_val),
|
||||||
|
sid=sid,
|
||||||
|
headers={},
|
||||||
|
client_ip=client_ip,
|
||||||
|
)
|
||||||
|
# route change triggers: [full state dict, call on_load events, call set_is_hydrated(True)]
|
||||||
|
assert update == StateUpdate(
|
||||||
|
delta={
|
||||||
|
state.get_name(): {
|
||||||
|
arg_name: exp_val,
|
||||||
|
f"comp_{arg_name}": exp_val,
|
||||||
|
constants.IS_HYDRATED: False,
|
||||||
|
"loaded": exp_index,
|
||||||
|
"counter": exp_index,
|
||||||
|
# "side_effect_counter": exp_index,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
events=[
|
||||||
|
_dynamic_state_event(name="on_load", val=exp_val, router_data={}),
|
||||||
|
_dynamic_state_event(
|
||||||
|
name="set_is_hydrated",
|
||||||
|
payload={"value": "true"},
|
||||||
|
val=exp_val,
|
||||||
|
router_data={},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert state.dynamic == exp_val
|
||||||
|
on_load_update = await process(
|
||||||
|
app,
|
||||||
|
event=_dynamic_state_event(name="on_load", val=exp_val),
|
||||||
|
sid=sid,
|
||||||
|
headers={},
|
||||||
|
client_ip=client_ip,
|
||||||
|
)
|
||||||
|
assert on_load_update == StateUpdate(
|
||||||
|
delta={
|
||||||
|
state.get_name(): {
|
||||||
|
# These computed vars _shouldn't_ be here, because they didn't change
|
||||||
|
arg_name: exp_val,
|
||||||
|
f"comp_{arg_name}": exp_val,
|
||||||
|
"loaded": exp_index + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events=[],
|
||||||
|
)
|
||||||
|
on_set_is_hydrated_update = await process(
|
||||||
|
app,
|
||||||
|
event=_dynamic_state_event(
|
||||||
|
name="set_is_hydrated", payload={"value": True}, val=exp_val
|
||||||
|
),
|
||||||
|
sid=sid,
|
||||||
|
headers={},
|
||||||
|
client_ip=client_ip,
|
||||||
|
)
|
||||||
|
assert on_set_is_hydrated_update == StateUpdate(
|
||||||
|
delta={
|
||||||
|
state.get_name(): {
|
||||||
|
# These computed vars _shouldn't_ be here, because they didn't change
|
||||||
|
arg_name: exp_val,
|
||||||
|
f"comp_{arg_name}": exp_val,
|
||||||
|
"is_hydrated": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# a simple state update event should NOT trigger on_load or route var side effects
|
||||||
|
update = await process(
|
||||||
|
app,
|
||||||
|
event=_dynamic_state_event(name="on_counter", val=exp_val),
|
||||||
|
sid=sid,
|
||||||
|
headers={},
|
||||||
|
client_ip=client_ip,
|
||||||
|
)
|
||||||
|
assert update == StateUpdate(
|
||||||
|
delta={
|
||||||
|
state.get_name(): {
|
||||||
|
# These computed vars _shouldn't_ be here, because they didn't change
|
||||||
|
f"comp_{arg_name}": exp_val,
|
||||||
|
arg_name: exp_val,
|
||||||
|
"counter": exp_index + 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
events=[],
|
||||||
|
)
|
||||||
|
assert state.loaded == len(exp_vals)
|
||||||
|
assert state.counter == len(exp_vals)
|
||||||
|
# print(f"Expected {exp_vals} rendering side effects, got {state.side_effect_counter}")
|
||||||
|
# assert state.side_effect_counter == len(exp_vals)
|
||||||
|
Loading…
Reference in New Issue
Block a user