From e5dc4cf1c5e179cc710ec0d4e55f163ea14a7dc2 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 12 Nov 2024 11:11:13 -0800 Subject: [PATCH 1/3] ex --- mesop/examples/__init__.py | 1 + mesop/examples/navigation_on_load.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 mesop/examples/navigation_on_load.py diff --git a/mesop/examples/__init__.py b/mesop/examples/__init__.py index bada6c6b..362a03f6 100644 --- a/mesop/examples/__init__.py +++ b/mesop/examples/__init__.py @@ -34,6 +34,7 @@ from mesop.examples import many_checkboxes as many_checkboxes from mesop.examples import navigate_absolute as navigate_absolute from mesop.examples import navigate_advanced as navigate_advanced +from mesop.examples import navigation_on_load as navigation_on_load from mesop.examples import nested as nested from mesop.examples import on_load as on_load from mesop.examples import on_load_generator as on_load_generator diff --git a/mesop/examples/navigation_on_load.py b/mesop/examples/navigation_on_load.py new file mode 100644 index 00000000..b9213876 --- /dev/null +++ b/mesop/examples/navigation_on_load.py @@ -0,0 +1,26 @@ +import mesop as me + + +@me.stateclass +class State: + value: str + + +def on_load(e: me.LoadEvent): + me.state(State).value = "on_load ran" + + +@me.page(path="/navigation_on_load/page1", on_load=on_load) +def page1(): + me.text("page1") + me.text(me.state(State).value) + me.button("navigate", on_click=navigate) + + +@me.page(path="/navigation_on_load/page2") +def page2(): + me.text("page2") + + +def navigate(e: me.ClickEvent): + me.navigate("/navigation_on_load/page2") From 0b47c2e88e3bcb785627fe787ba32dd7938de503 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 12 Nov 2024 11:11:27 -0800 Subject: [PATCH 2/3] fix --- mesop/protos/ui.proto | 9 +++++++-- mesop/runtime/context.py | 1 + mesop/server/server.py | 32 ++++++++++++++++++++++--------- mesop/web/src/services/channel.ts | 5 ++--- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/mesop/protos/ui.proto b/mesop/protos/ui.proto index 8eb5f653..123bd7f7 100644 --- a/mesop/protos/ui.proto +++ b/mesop/protos/ui.proto @@ -24,7 +24,7 @@ message QueryParam { repeated string values = 2; } -// Next ID: 19 +// Next ID: 20 message UserEvent { optional States states = 1; @@ -43,6 +43,7 @@ message UserEvent { double double_value = 7; int32 int_value = 8; NavigationEvent navigation = 6; + HotReloadEvent hot_reload = 19; ResizeEvent resize = 10; bytes bytes_value = 9; ChangePrefersColorScheme change_prefers_color_scheme = 14; @@ -107,10 +108,14 @@ message DateRangePickerEvent { optional string end_date = 2; } -// This is a user-triggered navigation (e.g. go back/forwards) or a hot reload event. +// This is a user-triggered navigation (e.g. go back/forwards). message NavigationEvent{ } +// Fired when a hot reload is triggered. +message HotReloadEvent { +} + // Fired whenever a user resizes the viewport/browser. message ResizeEvent { } diff --git a/mesop/runtime/context.py b/mesop/runtime/context.py index d0b3c33d..323cddfd 100644 --- a/mesop/runtime/context.py +++ b/mesop/runtime/context.py @@ -280,6 +280,7 @@ def run_event_handler( ) -> Generator[None, None, None]: if ( event.HasField("navigation") + or event.HasField("hot_reload") or event.HasField("resize") or event.HasField("change_prefers_color_scheme") ): diff --git a/mesop/server/server.py b/mesop/server/server.py index 9c463c73..10a0bc5f 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -193,9 +193,18 @@ def generate_data(ui_request: pb.UiRequest) -> Generator[str, None, None]: runtime().context().reset_previous_node() runtime().context().reset_current_node() - result = runtime().context().run_event_handler(ui_request.user_event) path = ui_request.path has_run_navigate_on_load = False + + if ui_request.user_event.HasField("navigation"): + page_config = runtime().get_page_config(path=path) + if ( + page_config and page_config.on_load and not has_run_navigate_on_load + ): + has_run_navigate_on_load = True + run_page_load(path=path) + + result = runtime().context().run_event_handler(ui_request.user_event) for _ in result: navigate_commands = [ command @@ -223,14 +232,7 @@ def generate_data(ui_request: pb.UiRequest) -> Generator[str, None, None]: and not has_run_navigate_on_load ): has_run_navigate_on_load = True - result = page_config.on_load(LoadEvent(path=path)) - # on_load is a generator function then we need to iterate through - # the generator object. - if result: - for _ in result: - yield from render_loop(path=path, init_request=True) - runtime().context().set_previous_node_from_current_node() - runtime().context().reset_current_node() + run_page_load(path=path) yield from render_loop(path=path) runtime().context().set_previous_node_from_current_node() @@ -248,6 +250,18 @@ def generate_data(ui_request: pb.UiRequest) -> Generator[str, None, None]: error=pb.ServerError(exception=str(e), traceback=format_traceback()) ) + def run_page_load(*, path: str): + page_config = runtime().get_page_config(path=path) + assert page_config and page_config.on_load + result = page_config.on_load(LoadEvent(path=path)) + # on_load is a generator function then we need to iterate through + # the generator object. + if result: + for _ in result: + yield from render_loop(path=path, init_request=True) + runtime().context().set_previous_node_from_current_node() + runtime().context().reset_current_node() + @flask_app.route(UI_PATH, methods=["POST"]) def ui_stream() -> Response: # Prevent CSRF by checking the request site matches the site diff --git a/mesop/web/src/services/channel.ts b/mesop/web/src/services/channel.ts index fca2b1d5..a9c442b6 100644 --- a/mesop/web/src/services/channel.ts +++ b/mesop/web/src/services/channel.ts @@ -7,10 +7,10 @@ import { UserEvent, Component as ComponentProto, UiResponse, - NavigationEvent, ComponentConfig, Command, ChangePrefersColorScheme, + HotReloadEvent, } from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; import {Logger} from '../dev_tools/services/logger'; import {Title} from '@angular/platform-browser'; @@ -482,8 +482,7 @@ export class Channel { const request = new UiRequest(); const userEvent = new UserEvent(); userEvent.setStates(this.states); - const navigationEvent = new NavigationEvent(); - userEvent.setNavigation(navigationEvent); + userEvent.setHotReload(new HotReloadEvent()); userEvent.setViewportSize(getViewportSize()); userEvent.setThemeSettings(this.themeService.getThemeSettings()); userEvent.setQueryParamsList(getQueryParams()); From 637e82810e9ceae18941caf14770f94363ebf3e8 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 12 Nov 2024 11:29:33 -0800 Subject: [PATCH 3/3] test --- mesop/examples/__init__.py | 4 ++- mesop/examples/browser_navigation_on_load.py | 32 ++++++++++++++++++++ mesop/examples/navigation_on_load.py | 26 ---------------- mesop/server/server.py | 4 +-- mesop/tests/e2e/browser_navigation_test.ts | 25 +++++++++++++++ 5 files changed, 62 insertions(+), 29 deletions(-) create mode 100644 mesop/examples/browser_navigation_on_load.py delete mode 100644 mesop/examples/navigation_on_load.py create mode 100644 mesop/tests/e2e/browser_navigation_test.ts diff --git a/mesop/examples/__init__.py b/mesop/examples/__init__.py index 362a03f6..1909aa13 100644 --- a/mesop/examples/__init__.py +++ b/mesop/examples/__init__.py @@ -8,6 +8,9 @@ boilerplate_free_event_handlers as boilerplate_free_event_handlers, ) from mesop.examples import box as box +from mesop.examples import ( + browser_navigation_on_load as browser_navigation_on_load, +) from mesop.examples import buttons as buttons from mesop.examples import checkbox_and_radio as checkbox_and_radio from mesop.examples import composite as composite @@ -34,7 +37,6 @@ from mesop.examples import many_checkboxes as many_checkboxes from mesop.examples import navigate_absolute as navigate_absolute from mesop.examples import navigate_advanced as navigate_advanced -from mesop.examples import navigation_on_load as navigation_on_load from mesop.examples import nested as nested from mesop.examples import on_load as on_load from mesop.examples import on_load_generator as on_load_generator diff --git a/mesop/examples/browser_navigation_on_load.py b/mesop/examples/browser_navigation_on_load.py new file mode 100644 index 00000000..a4628de7 --- /dev/null +++ b/mesop/examples/browser_navigation_on_load.py @@ -0,0 +1,32 @@ +import mesop as me + + +@me.stateclass +class State: + page_1_onload_counter: int + page_2_onload_counter: int + + +def page_1_on_load(e: me.LoadEvent): + me.state(State).page_1_onload_counter += 1 + + +@me.page(path="/browser_navigation_on_load/page1", on_load=page_1_on_load) +def page1(): + me.text("page1") + me.text(f"onload ran {me.state(State).page_1_onload_counter} times") + me.button("navigate", on_click=navigate) + + +def navigate(e: me.ClickEvent): + me.navigate("/browser_navigation_on_load/page2") + + +def page_2_on_load(e: me.LoadEvent): + me.state(State).page_2_onload_counter += 1 + + +@me.page(path="/browser_navigation_on_load/page2", on_load=page_2_on_load) +def page2(): + me.text("page2") + me.text(f"onload ran {me.state(State).page_2_onload_counter} times") diff --git a/mesop/examples/navigation_on_load.py b/mesop/examples/navigation_on_load.py deleted file mode 100644 index b9213876..00000000 --- a/mesop/examples/navigation_on_load.py +++ /dev/null @@ -1,26 +0,0 @@ -import mesop as me - - -@me.stateclass -class State: - value: str - - -def on_load(e: me.LoadEvent): - me.state(State).value = "on_load ran" - - -@me.page(path="/navigation_on_load/page1", on_load=on_load) -def page1(): - me.text("page1") - me.text(me.state(State).value) - me.button("navigate", on_click=navigate) - - -@me.page(path="/navigation_on_load/page2") -def page2(): - me.text("page2") - - -def navigate(e: me.ClickEvent): - me.navigate("/navigation_on_load/page2") diff --git a/mesop/server/server.py b/mesop/server/server.py index 10a0bc5f..de629f69 100644 --- a/mesop/server/server.py +++ b/mesop/server/server.py @@ -202,7 +202,7 @@ def generate_data(ui_request: pb.UiRequest) -> Generator[str, None, None]: page_config and page_config.on_load and not has_run_navigate_on_load ): has_run_navigate_on_load = True - run_page_load(path=path) + yield from run_page_load(path=path) result = runtime().context().run_event_handler(ui_request.user_event) for _ in result: @@ -232,7 +232,7 @@ def generate_data(ui_request: pb.UiRequest) -> Generator[str, None, None]: and not has_run_navigate_on_load ): has_run_navigate_on_load = True - run_page_load(path=path) + yield from run_page_load(path=path) yield from render_loop(path=path) runtime().context().set_previous_node_from_current_node() diff --git a/mesop/tests/e2e/browser_navigation_test.ts b/mesop/tests/e2e/browser_navigation_test.ts new file mode 100644 index 00000000..828768f7 --- /dev/null +++ b/mesop/tests/e2e/browser_navigation_test.ts @@ -0,0 +1,25 @@ +import {test, expect} from '@playwright/test'; + +test('browser navigation - back and forward (triggers onload)', async ({ + page, +}) => { + await page.goto('/browser_navigation_on_load/page1'); + await expect(page.getByText('page1')).toBeVisible(); + await expect(page.getByText('onload ran 1 times')).toBeVisible(); + + // Trigger a navigation. + await page.getByRole('button', {name: 'navigate'}).click(); + + await expect(page.getByText('page2')).toBeVisible(); + await expect(page.getByText('onload ran 1 times')).toBeVisible(); + + // Go back to page 1 + await page.goBack(); + await expect(page.getByText('page1')).toBeVisible(); + await expect(page.getByText('onload ran 2 times')).toBeVisible(); + + // Go forward to page 2 + await page.goForward(); + await expect(page.getByText('page2')).toBeVisible(); + await expect(page.getByText('onload ran 2 times')).toBeVisible(); +});