From d6ef1b6567cf26a5e4758f68c580f52b613e555d Mon Sep 17 00:00:00 2001 From: weareoutman Date: Tue, 29 Aug 2023 16:58:40 +0800 Subject: [PATCH] feat(): support lifeCycle.onMount/onUnmount for control nodes --- cypress/e2e/control-nodes.spec.js | 2 ++ mock-micro-apps/e2e/storyboard.yaml | 22 +++++++++++++++ .../runtime/src/internal/Renderer.spec.ts | 27 ++++++++++++++----- packages/runtime/src/internal/Renderer.ts | 25 +++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/control-nodes.spec.js b/cypress/e2e/control-nodes.spec.js index 4ed31cd894..dbe6c34eec 100644 --- a/cypress/e2e/control-nodes.spec.js +++ b/cypress/e2e/control-nodes.spec.js @@ -180,6 +180,7 @@ for (const port of Cypress.env("ports")) { ["Hello", "prefix", "ForEach in ForEach <1>", "suffix", "Toggle"].join( "" ), + "[forEach mount] false 1", ]); cy.get("@console.error").should("not.be.called"); @@ -190,6 +191,7 @@ for (const port of Cypress.env("ports")) { ["Hello", "prefix", "ForEach in ForEach <2>", "suffix", "Toggle"].join( "" ), + "[forEach mount] false 1, [forEach unmount] 2, [forEach mount] true 2", ]); }); }); diff --git a/mock-micro-apps/e2e/storyboard.yaml b/mock-micro-apps/e2e/storyboard.yaml index 170a6e2168..f51630dde6 100644 --- a/mock-micro-apps/e2e/storyboard.yaml +++ b/mock-micro-apps/e2e/storyboard.yaml @@ -393,6 +393,8 @@ routes: - name: value value: 1 - name: constant + - name: messages + value: [] bricks: - brick: div children: @@ -407,6 +409,23 @@ routes: textContent: prefix - brick: :forEach dataSource: <%= [CTX.value] %> + lifeCycle: + onMount: + action: context.replace + args: + - messages + - | + <% + CTX.messages.concat(`[forEach mount] ${EVENT.detail.rerender} ${CTX.value}`) + %> + onUnmount: + action: context.replace + args: + - messages + - | + <% + CTX.messages.concat(`[forEach unmount] ${CTX.value}`) + %> children: - brick: p properties: @@ -423,6 +442,9 @@ routes: args: - value - <% 3 - CTX.value %> + - brick: p + properties: + textContent: <%= CTX.messages.join(", ") %> # Sub-routes incremental rendering - path: '${APP.homepage}/sub-routes/:sub' diff --git a/packages/runtime/src/internal/Renderer.spec.ts b/packages/runtime/src/internal/Renderer.spec.ts index 8a728cfb69..4631a3ee69 100644 --- a/packages/runtime/src/internal/Renderer.spec.ts +++ b/packages/runtime/src/internal/Renderer.spec.ts @@ -1002,6 +1002,16 @@ describe("renderBrick for control nodes", () => { portal: true, }, ], + lifeCycle: { + onMount: { + action: "console.info", + args: [":forEach mount", "<% EVENT.detail.rerender %>"], + }, + onUnmount: { + action: "console.info", + args: [":forEach unmount"], + }, + }, }, ], runtimeContext, @@ -1014,9 +1024,10 @@ describe("renderBrick for control nodes", () => { expect(consoleInfo).not.toBeCalled(); rendererContext.dispatchOnMount(); rendererContext.initializeScrollIntoView(); - expect(consoleInfo).toBeCalledTimes(2); + expect(consoleInfo).toBeCalledTimes(3); expect(consoleInfo).toHaveBeenNthCalledWith(1, "onMount", "mount", "a"); expect(consoleInfo).toHaveBeenNthCalledWith(2, "onMount", "mount", "b"); + expect(consoleInfo).toHaveBeenNthCalledWith(3, ":forEach mount", false); expect(container.innerHTML).toBe("
a
b
"); expect(portal.innerHTML).toBe("

portal:a

portal:b

"); @@ -1025,15 +1036,17 @@ describe("renderBrick for control nodes", () => { expect(container.innerHTML).toBe('
a
b
'); ctxStore.updateValue("list", ["a", "c"], "replace"); - expect(consoleInfo).toBeCalledTimes(2); + expect(consoleInfo).toBeCalledTimes(3); // Wait for `_.debounce()` await new Promise((resolve) => setTimeout(resolve, 0)); - expect(consoleInfo).toBeCalledTimes(6); - expect(consoleInfo).toHaveBeenNthCalledWith(3, "onUnmount", "unmount", "a"); - expect(consoleInfo).toHaveBeenNthCalledWith(4, "onUnmount", "unmount", "b"); - expect(consoleInfo).toHaveBeenNthCalledWith(5, "onMount", "mount", "a"); - expect(consoleInfo).toHaveBeenNthCalledWith(6, "onMount", "mount", "c"); + expect(consoleInfo).toBeCalledTimes(9); + expect(consoleInfo).toHaveBeenNthCalledWith(4, ":forEach unmount"); + expect(consoleInfo).toHaveBeenNthCalledWith(5, "onUnmount", "unmount", "a"); + expect(consoleInfo).toHaveBeenNthCalledWith(6, "onUnmount", "unmount", "b"); + expect(consoleInfo).toHaveBeenNthCalledWith(7, "onMount", "mount", "a"); + expect(consoleInfo).toHaveBeenNthCalledWith(8, "onMount", "mount", "c"); + expect(consoleInfo).toHaveBeenNthCalledWith(9, ":forEach mount", true); // Note: previous `title="mark"` is removed expect(container.innerHTML).toBe("
a
c
"); diff --git a/packages/runtime/src/internal/Renderer.ts b/packages/runtime/src/internal/Renderer.ts index ae83dd99d6..201e71acd1 100644 --- a/packages/runtime/src/internal/Renderer.ts +++ b/packages/runtime/src/internal/Renderer.ts @@ -66,6 +66,7 @@ import { getPreEvaluatedRaw } from "./compute/evaluate.js"; import { RuntimeBrickConfOfTplSymbols } from "./CustomTemplates/constants.js"; import { matchHomepage } from "./matchStoryboard.js"; import type { DataStore, DataStoreType } from "./data/DataStore.js"; +import { listenerFactory } from "./bindListeners.js"; export interface RenderOutput { node?: RenderBrick; @@ -413,6 +414,7 @@ export async function renderBrick( }; const controlledOutput = await renderControlNode(runtimeContext); + const { onMount, onUnmount } = brickConf.lifeCycle ?? {}; const { contextNames, stateNames } = getTracks(dataSource); if (contextNames || stateNames) { @@ -434,6 +436,13 @@ export async function renderBrick( // Ignore stale renders if (renderId === currentRenderId) { + if (onUnmount) { + listenerFactory( + onUnmount, + runtimeContext + )(new CustomEvent("unmount")); + } + rendererContext.reRender( slotId, keyPath, @@ -441,6 +450,13 @@ export async function renderBrick( returnNode ); + if (onMount) { + listenerFactory( + onMount, + scopedRuntimeContext + )(new CustomEvent("mount", { detail: { rerender: true } })); + } + for (const store of scopedStores) { store.mountAsyncData(); } @@ -464,6 +480,15 @@ export async function renderBrick( } } + if (onMount) { + rendererContext.registerArbitraryLifeCycle("onMount", () => { + listenerFactory( + onMount, + runtimeContext + )(new CustomEvent("mount", { detail: { rerender: false } })); + }); + } + return controlledOutput; }