From 3a68759711768273b8cddbf464af1c08e80a6299 Mon Sep 17 00:00:00 2001 From: SgLy <775150558@qq.com> Date: Sun, 25 Jun 2023 16:32:08 +0800 Subject: [PATCH 01/29] fix: typo --- glass-easel/src/backend/mode.ts | 4 +-- .../src/backend/suggested_backend_protocol.ts | 8 +++--- glass-easel/src/component_space.ts | 8 +++--- glass-easel/src/element.ts | 6 ++-- glass-easel/src/element_iterator.ts | 2 +- glass-easel/src/mutation_observer.ts | 28 +++++++++---------- glass-easel/src/relation.ts | 14 +++++----- glass-easel/src/shadow_root.ts | 2 +- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/glass-easel/src/backend/mode.ts b/glass-easel/src/backend/mode.ts index 27342a1..91863ea 100644 --- a/glass-easel/src/backend/mode.ts +++ b/glass-easel/src/backend/mode.ts @@ -41,12 +41,12 @@ export type CSSRule = { weightLowBits: number } -export type GetMatchedRulesResponese = { +export type GetMatchedRulesResponses = { inline: CSSProperty[] rules: CSSRule[] } -export type GetAllComputedStylesResponese = { +export type GetAllComputedStylesResponses = { properties: CSSProperty[] } diff --git a/glass-easel/src/backend/suggested_backend_protocol.ts b/glass-easel/src/backend/suggested_backend_protocol.ts index 2611b9d..0ef15d0 100644 --- a/glass-easel/src/backend/suggested_backend_protocol.ts +++ b/glass-easel/src/backend/suggested_backend_protocol.ts @@ -1,8 +1,8 @@ import { GeneralBackendContext, Node } from '../node' import { BoundingClientRect, - GetAllComputedStylesResponese, - GetMatchedRulesResponese, + GetAllComputedStylesResponses, + GetMatchedRulesResponses, ScrollOffset, } from './mode' @@ -11,9 +11,9 @@ interface GetWrapper { } export interface Element { - getAllComputedStyles(cb: (res: GetAllComputedStylesResponese) => void): void + getAllComputedStyles(cb: (res: GetAllComputedStylesResponses) => void): void getBoundingClientRect(cb: (res: BoundingClientRect) => void): void - getMatchedRules(cb: (res: GetMatchedRulesResponese) => void): void + getMatchedRules(cb: (res: GetMatchedRulesResponses) => void): void replaceStyleSheetInlineStyle(inlineStyle: string): void getScrollOffset(cb: (res: ScrollOffset) => void): void setScrollPosition(scrollLeft: number, scrollTop: number, duration: number): void diff --git a/glass-easel/src/component_space.ts b/glass-easel/src/component_space.ts index 9b9bdb4..6dc9643 100644 --- a/glass-easel/src/component_space.ts +++ b/glass-easel/src/component_space.ts @@ -97,7 +97,7 @@ export class ComponentWaitingList { remove(callback: (c: GeneralComponentDefinition) => void) { const index = this._$callbacks.indexOf(callback) - // must gurrantee order here (cannot swap-remove) + // must guarantee order here (cannot swap-remove) this._$callbacks.splice(index, 1) } @@ -221,11 +221,11 @@ export class ComponentSpace { * * The component `is` is actually treated as the "path" of the component. * In other words, the component `is` field can be a string like `path/to/the/component` . - * Other components can be used by the component with "relative path" spacified. + * Other components can be used by the component with "relative path" specified. * In this method, if the `path` is given as a relative path (not started with `/` ), * it will be converted according to the `basePath` . * If the `path` is given as a URL-like format, - * the component will be searched in imported comopnent spaces ( `importSpace()` for details). + * the component will be searched in imported component spaces ( `importSpace()` for details). */ getComponentByUrl(path: string, basePath: string): GeneralComponentDefinition { const { domain, absPath } = normalizeUrl(path, basePath) @@ -242,7 +242,7 @@ export class ComponentSpace { * Get a component by the `path` * * Similar to `getComponentByUrl()` , - * but returns `null` instead of the default component if no compnent was found. + * but returns `null` instead of the default component if no component was found. */ getComponentByUrlWithoutDefault( path: string, diff --git a/glass-easel/src/element.ts b/glass-easel/src/element.ts index 8c0783a..69ea614 100644 --- a/glass-easel/src/element.ts +++ b/glass-easel/src/element.ts @@ -1285,7 +1285,7 @@ export class Element implements NodeCast { } } - private static insertChildPlaceholerReplace( + private static insertChildPlaceholderReplace( parent: Element, posIndex: number, replacer: Element, @@ -1437,7 +1437,7 @@ export class Element implements NodeCast { selfReplaceWith(replaceWith: Element) { const parent = this.parentNode if (parent) { - Element.insertChildPlaceholerReplace(parent, parent.childNodes.indexOf(this), replaceWith) + Element.insertChildPlaceholderReplace(parent, parent.childNodes.indexOf(this), replaceWith) } } @@ -1771,7 +1771,7 @@ export class Element implements NodeCast { * Get the composed children * * This method always returns a new array. - * It is convinient but less performant. + * It is convenient but less performant. * For better performance, consider using `forEachComposedChild` . */ getComposedChildren(): Node[] { diff --git a/glass-easel/src/element_iterator.ts b/glass-easel/src/element_iterator.ts index bc27136..b2030d6 100644 --- a/glass-easel/src/element_iterator.ts +++ b/glass-easel/src/element_iterator.ts @@ -22,7 +22,7 @@ export const enum ElementIteratorType { /** * An iterator for node tree traversal * - * This iterator is convinient but seems a little slower. + * This iterator is convenient but seems a little slower. */ export class ElementIterator { private _$node: Node diff --git a/glass-easel/src/mutation_observer.ts b/glass-easel/src/mutation_observer.ts index 6940b21..2f2376c 100644 --- a/glass-easel/src/mutation_observer.ts +++ b/glass-easel/src/mutation_observer.ts @@ -51,15 +51,15 @@ export type MutationObserverEvent = export type MutationObserverListener = (this: Element, ev: T) => void export class MutationObserverTarget { - private _$bindedElement: Element + private _$boundElement: Element private _$subtreeObserversCount = 0 attrObservers: FuncArr> | null = null textObservers: FuncArr> | null = null childObservers: FuncArr> | null = null attachObservers: FuncArr> | null = null - constructor(bindedElement: Element) { - this._$bindedElement = bindedElement + constructor(boundElement: Element) { + this._$boundElement = boundElement } attachChild(child: Element) { @@ -79,7 +79,7 @@ export class MutationObserverTarget { updateSubtreeCount(diff: number) { this._$subtreeObserversCount += diff - const children = this._$bindedElement.childNodes + const children = this._$boundElement.childNodes children.forEach((child) => { if (child instanceof Element) { if (!child._$mutationObserverTarget) { @@ -146,8 +146,8 @@ export class MutationObserver { private _$listener: MutationObserverListener | null private _$normalizedListener: MutationObserverListener | null private _$subtreeListenersCount = 0 - private _$bindedFuncArrs: FuncArr>[] = [] - private _$bindedTarget: MutationObserverTarget | null = null + private _$boundFuncArrs: FuncArr>[] = [] + private _$boundTarget: MutationObserverTarget | null = null constructor(listener: (ev: MutationObserverEvent) => void) { this._$listener = listener @@ -188,11 +188,11 @@ export class MutationObserver { if (ev.target === this) listener.call(this as Element, ev) } this._$normalizedListener = cb - this._$bindedTarget = target + this._$boundTarget = target if (options.properties) { if (!target.attrObservers) target.attrObservers = new FuncArr() target.attrObservers.add(cb) - this._$bindedFuncArrs.push( + this._$boundFuncArrs.push( target.attrObservers as FuncArr>, ) this._$subtreeListenersCount += 1 @@ -200,7 +200,7 @@ export class MutationObserver { if (options.childList) { if (!target.childObservers) target.childObservers = new FuncArr() target.childObservers.add(cb) - this._$bindedFuncArrs.push( + this._$boundFuncArrs.push( target.childObservers as FuncArr>, ) this._$subtreeListenersCount += 1 @@ -208,7 +208,7 @@ export class MutationObserver { if (options.characterData) { if (!target.textObservers) target.textObservers = new FuncArr() target.textObservers.add(cb) - this._$bindedFuncArrs.push( + this._$boundFuncArrs.push( target.textObservers as FuncArr>, ) this._$subtreeListenersCount += 1 @@ -219,7 +219,7 @@ export class MutationObserver { if (options.attachStatus) { if (!target.attachObservers) target.attachObservers = new FuncArr() target.attachObservers.add(cb) - this._$bindedFuncArrs.push( + this._$boundFuncArrs.push( target.attachObservers as FuncArr>, ) } @@ -227,9 +227,9 @@ export class MutationObserver { /** End observation */ disconnect() { - this._$bindedTarget?.updateSubtreeCount(-this._$subtreeListenersCount) - const arr = this._$bindedFuncArrs - this._$bindedFuncArrs = [] + this._$boundTarget?.updateSubtreeCount(-this._$subtreeListenersCount) + const arr = this._$boundFuncArrs + this._$boundFuncArrs = [] const nl = this._$normalizedListener if (nl) { arr.forEach((funcArr) => { diff --git a/glass-easel/src/relation.ts b/glass-easel/src/relation.ts index 0fcc141..308ae98 100644 --- a/glass-easel/src/relation.ts +++ b/glass-easel/src/relation.ts @@ -162,22 +162,22 @@ export class Relation { const oldLink = links[i]! let newLink: { target: GeneralComponent; def: RelationDefinition } | null = null const def = selfDefs[i]! - let parentBeheviorTest: + let parentBehaviorTest: | GeneralBehavior | TraitBehavior<{ [x: string]: unknown }, { [x: string]: unknown }> | null if (def.target instanceof Behavior || def.target instanceof TraitBehavior) { - parentBeheviorTest = def.target + parentBehaviorTest = def.target } else { const space = comp.getRootBehavior().ownerSpace if (space) { - parentBeheviorTest = space._$getBehavior(def.target, def.domain) || null + parentBehaviorTest = space._$getBehavior(def.target, def.domain) || null } else { - parentBeheviorTest = null + parentBehaviorTest = null } } - if (parentBeheviorTest) { - const parentBehevior = parentBeheviorTest + if (parentBehaviorTest) { + const parentBehavior = parentBehaviorTest if (!isDetach) { let cur: Element = comp for (;;) { @@ -188,7 +188,7 @@ export class Relation { continue } if (cur instanceof Component) { - if (cur.hasBehavior(parentBehevior)) { + if (cur.hasBehavior(parentBehavior)) { const parentRelation = cur._$relation if (parentRelation) { let rt diff --git a/glass-easel/src/shadow_root.ts b/glass-easel/src/shadow_root.ts index 6fc96e5..d825855 100644 --- a/glass-easel/src/shadow_root.ts +++ b/glass-easel/src/shadow_root.ts @@ -383,7 +383,7 @@ export class ShadowRoot extends VirtualNode { * Get the elements that should be composed in specified slot * * This method always returns a new array (or null if the specified slot is invalid). - * It is convinient but less performant. + * It is convenient but less performant. * For better performance, consider using `forEachNodeInSpecifiedSlot` . */ getSlotContentArray(slot: Element): Node[] | null { From fa54d9d7d134daddec9416b735f408cfd84d30b2 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 19:10:16 +0800 Subject: [PATCH 02/29] build: setup pages --- .github/workflows/static.yml | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..3313cb0 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,75 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + - name: Setup node ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install + uses: pnpm/action-setup@v2 + with: + version: latest + run_install: | + - recursive: true + args: [--frozen-lockfile, --strict-peer-dependencies] + - name: Setup Rust and Cargo + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + - name: Setup wasm-pack + uses: jetli/wasm-pack-action@v0.4.0 + with: + version: 'latest' + - name: Build + run: | + pnpm -r run build + - name: Build + working-directory: glass-easel-miniprogram-adapter + run: | + npm run doc + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + # Upload entire repository + path: 'glass-easel-miniprogram-adapter/docs' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 From 492bc6bed1c75cf29ba66e945d7e1bbb715c5c3e Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 19:25:55 +0800 Subject: [PATCH 03/29] build: setup pages --- .github/workflows/static.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 3313cb0..50369be 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,28 +1,21 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages +name: deploy-pages on: - # Runs on pushes targeting the default branch push: branches: ["master"] - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: - # Single deploy job since we're just deploying deploy: environment: name: github-pages @@ -61,15 +54,23 @@ jobs: - name: Build run: | pnpm -r run build - - name: Build + - name: Generate docs for glass-easel + working-directory: glass-easel + run: | + npm run doc + - name: Generate docs for glass-easel-miniprogram-adapter working-directory: glass-easel-miniprogram-adapter run: | npm run doc + - name: Collect artifacts + run: | + mkdir github-pages + mv glass-easel/docs github-pages/glass-easel + mv glass-easel-miniprogram-adapter/docs github-pages/glass-easel-miniprogram-adapter - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: - # Upload entire repository - path: 'glass-easel-miniprogram-adapter/docs' + path: 'github-pages' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 From 4ef224968a0006b3297419d899ab4a32546dd736 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 19:26:21 +0800 Subject: [PATCH 04/29] build: setup pages --- .github/workflows/{static.yml => pages.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{static.yml => pages.yml} (100%) diff --git a/.github/workflows/static.yml b/.github/workflows/pages.yml similarity index 100% rename from .github/workflows/static.yml rename to .github/workflows/pages.yml From 12f733308707834d690b45d26d65c18ef1c06229 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 19:33:47 +0800 Subject: [PATCH 05/29] build: setup pages --- .github/workflows/pages.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 50369be..ca4a791 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -65,8 +65,9 @@ jobs: - name: Collect artifacts run: | mkdir github-pages - mv glass-easel/docs github-pages/glass-easel - mv glass-easel-miniprogram-adapter/docs github-pages/glass-easel-miniprogram-adapter + mkdir github-pages/docs + mv glass-easel/docs github-pages/docs/glass-easel + mv glass-easel-miniprogram-adapter/docs github-pages/docs/glass-easel-miniprogram-adapter - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: From ca05f306d3ad16009a6078363c2d7fe642358bda Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 19:43:06 +0800 Subject: [PATCH 06/29] build: setup pages --- .github/workflows/pages.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index ca4a791..171016c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -54,6 +54,9 @@ jobs: - name: Build run: | pnpm -r run build + - name: Generate docs for rust modules + run: | + cargo doc - name: Generate docs for glass-easel working-directory: glass-easel run: | @@ -66,6 +69,7 @@ jobs: run: | mkdir github-pages mkdir github-pages/docs + mv target/doc github-pages/cargo-doc mv glass-easel/docs github-pages/docs/glass-easel mv glass-easel-miniprogram-adapter/docs github-pages/docs/glass-easel-miniprogram-adapter - name: Upload artifact From 5f20ab6e575fdff2be45857bfcbc9149d9929cd6 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 19:55:32 +0800 Subject: [PATCH 07/29] build: setup pages --- .github/workflows/pages.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 171016c..ca4a791 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -54,9 +54,6 @@ jobs: - name: Build run: | pnpm -r run build - - name: Generate docs for rust modules - run: | - cargo doc - name: Generate docs for glass-easel working-directory: glass-easel run: | @@ -69,7 +66,6 @@ jobs: run: | mkdir github-pages mkdir github-pages/docs - mv target/doc github-pages/cargo-doc mv glass-easel/docs github-pages/docs/glass-easel mv glass-easel-miniprogram-adapter/docs github-pages/docs/glass-easel-miniprogram-adapter - name: Upload artifact From b792e73c101106e3b16d4da4685127fb990892d3 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Wed, 5 Jul 2023 20:23:06 +0800 Subject: [PATCH 08/29] docs: update TSDoc links --- README-zh_CN.md | 10 +++++++++- README.md | 10 +++++++++- glass-easel-miniprogram-adapter/src/component.ts | 2 +- glass-easel/src/shadow_root.ts | 3 ++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README-zh_CN.md b/README-zh_CN.md index 2680e5e..796d995 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -56,7 +56,15 @@ npm install --save-dev glass-easel-template-compiler glass-easel-miniprogram-adapter 接口与 [小程序自定义组件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/) 接口一致。 [glass-easel-miniprogram-template](./glass-easel-miniprogram-template) 是一个供参考的项目模板。 -此外, TypeScript 编写的子模块支持 TSDoc ,可以在子模块代码目录中通过 `npm run doc` 来生成详细的接口文档(生成好的文档位于 `docs` 子目录中); rust 编写的子模块,则可以通过 `cargo doc` 来生成详细的接口文档(与普通 rust crate 类似)。 +TypeScript 编写的子模块支持 TSDoc ,其生成文档可供参考: + +* [glass-easel TSDoc 接口文档](https://wechat-miniprogram.github.io/glass-easel/docs/glass-easel) +* [glass-easel-miniprogram-adapter TSDoc 接口文档](https://wechat-miniprogram.github.io/glass-easel/docs/glass-easel-miniprogram-adapter) + +对于 rust 编写的子模块, cargo doc 生成文档可供参考: + +* [位于 docs.rs 的 glass-easel-template-compiler 接口文档](https://docs.rs/glass-easel-template-compiler/latest/glass_easel_template_compiler/) +* [位于 docs.rs 的 glass-easel-stylesheet-compiler 接口文档](https://docs.rs/glass-easel-stylesheet-compiler/latest/glass_easel_stylesheet_compiler/) ## 常见问题 diff --git a/README.md b/README.md index 9e82cd8..605887a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,15 @@ npm install --save-dev glass-easel-template-compiler The interface of [glass-easel-miniprogram-adapter](./glass-easel-miniprogram-adapter) is the same as the custom component interface of MiniProgram. [glass-easel-miniprogram-template](./glass-easel-miniprogram-template) is a project template. -Furthermore, TypeScript modules contains TSDoc documents, which can be built with `npm run doc` (generated documents are in the `docs` directory); rust module documents can be retrived through `cargo doc` (like common rust crates). +TypeScript modules support TSDoc. The auto-generated documents are in GitHub pages. + +* [TSDoc documents for glass-easel](https://wechat-miniprogram.github.io/glass-easel/docs/glass-easel) +* [TSDoc documents for glass-easel-miniprogram-adapter](https://wechat-miniprogram.github.io/glass-easel/docs/glass-easel-miniprogram-adapter) + +For the rust interface, see the generated documents in docs.rs. + +* [glass-easel-template-compiler documents in docs.rs](https://docs.rs/glass-easel-template-compiler/latest/glass_easel_template_compiler/) +* [glass-easel-stylesheet-compiler documents in docs.rs](https://docs.rs/glass-easel-stylesheet-compiler/latest/glass_easel_stylesheet_compiler/) ## F.A.Q. diff --git a/glass-easel-miniprogram-adapter/src/component.ts b/glass-easel-miniprogram-adapter/src/component.ts index 57f6df8..331eb8c 100644 --- a/glass-easel-miniprogram-adapter/src/component.ts +++ b/glass-easel-miniprogram-adapter/src/component.ts @@ -86,7 +86,7 @@ export class ComponentCaller< TMethod extends MethodList, TComponentExport, > { - /** @internal */ + /** The corresponding `Component` */ _$!: glassEasel.Component /** @internal */ _$export?: (source: GeneralComponent | null) => TComponentExport diff --git a/glass-easel/src/shadow_root.ts b/glass-easel/src/shadow_root.ts index 6fc96e5..9539c76 100644 --- a/glass-easel/src/shadow_root.ts +++ b/glass-easel/src/shadow_root.ts @@ -249,7 +249,8 @@ export class ShadowRoot extends VirtualNode { * Create a component if the given tag name is a component in the space, or a native node if not * * The component `using` map is not used. - * The tag name is not a relative path to the host component, but an absolute path. + * Consider using `checkComponentPlaceholder` to check if the tag name is in the `using` map. + * The global using registered with `ComponentSpace.prototype.getGlobalUsingComponent` is still used. */ createComponentOrNativeNode( tagName: string, From bf364633f3a01826446142b061073b4567affa18 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Mon, 10 Jul 2023 17:44:25 +0800 Subject: [PATCH 09/29] fix: allows use the listeners in methodCaller --- glass-easel/src/component.ts | 22 +++++++++++++++++++--- glass-easel/src/global_options.ts | 8 ++++++++ glass-easel/tests/core/misc.test.ts | 21 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/glass-easel/src/component.ts b/glass-easel/src/component.ts index e71a63b..e5e95a0 100644 --- a/glass-easel/src/component.ts +++ b/glass-easel/src/component.ts @@ -961,21 +961,37 @@ export class Component< return compDef.behavior._$methodMap } - /** Get a method */ + /** + * Get a method + * + * If `useMethodCallerListeners` option is set for this component, + * this method will use the corresponding fields in the `methodCaller` . + */ static getMethod< TData extends DataList, TProperty extends PropertyList, TMethod extends MethodList, >(comp: Component, methodName: string): GeneralFuncType | undefined { + if (comp._$definition._$options.useMethodCallerListeners) { + const method = comp._$methodCaller[methodName] + return typeof method === 'function' ? method : undefined + } return comp._$methodMap[methodName] } - /** Call a method */ + /** + * Call a method + * + * If `useMethodCallerListeners` option is set for this component, + * this method will use the corresponding fields in the `methodCaller` . + * Returns `undefined` if there is no such method. + */ callMethod( methodName: T, ...args: Parameters ): ReturnType | undefined { - return this._$methodMap[methodName]?.call(this, ...args) + const func = Component.getMethod(this, methodName) + return func?.call(this, ...args) } /** diff --git a/glass-easel/src/global_options.ts b/glass-easel/src/global_options.ts index 442385b..f32ef6b 100644 --- a/glass-easel/src/global_options.ts +++ b/glass-easel/src/global_options.ts @@ -91,6 +91,8 @@ export type ComponentOptions = { writeFieldsToNode?: boolean /** Write node ID to backend node */ writeIdToDOM?: boolean + /** Use the methods in method caller as the event handlers or not */ + useMethodCallerListeners?: boolean /** Generate a prefix for ID written to backend node */ idPrefixGenerator?: ((this: GeneralComponent) => string) | null /** Filter some fields out when applying to templates */ @@ -118,6 +120,7 @@ export type NormalizedComponentOptions = { reflectToAttributes: boolean writeFieldsToNode: boolean writeIdToDOM: boolean + useMethodCallerListeners: boolean idPrefixGenerator: ((this: GeneralComponent) => string) | null pureDataPattern: RegExp | null dataDeepCopy: DeepCopyKind @@ -156,6 +159,7 @@ export const globalOptions: NormalizedComponentOptions & EnvironmentOptions = { reflectToAttributes: false, writeFieldsToNode: true, writeIdToDOM: false, + useMethodCallerListeners: false, idPrefixGenerator: null, pureDataPattern: null, dataDeepCopy: DeepCopyKind.Simple, @@ -191,6 +195,10 @@ export const normalizeComponentOptions = ( writeFieldsToNode: p.writeFieldsToNode !== undefined ? p.writeFieldsToNode : b.writeFieldsToNode, writeIdToDOM: p.writeIdToDOM !== undefined ? p.writeIdToDOM : b.writeIdToDOM, + useMethodCallerListeners: + p.useMethodCallerListeners !== undefined + ? p.useMethodCallerListeners + : b.useMethodCallerListeners, idPrefixGenerator: p.idPrefixGenerator !== undefined ? p.idPrefixGenerator : b.idPrefixGenerator, pureDataPattern: p.pureDataPattern !== undefined ? p.pureDataPattern : b.pureDataPattern, diff --git a/glass-easel/tests/core/misc.test.ts b/glass-easel/tests/core/misc.test.ts index 0ae701a..64b013e 100644 --- a/glass-easel/tests/core/misc.test.ts +++ b/glass-easel/tests/core/misc.test.ts @@ -255,6 +255,27 @@ describe('component utils', () => { expect(comp.callMethod('abc')).toBe('abc') }) + test('#getMethodsFromDef #getMethod #callMethod (when `useMethodCallerListeners` is set)', () => { + const compDef = glassEasel.Component.register( + { + options: { + useMethodCallerListeners: true, + }, + }, + componentSpace, + ) + const comp = glassEasel.createElement('root', compDef.general()) + const caller = { + abc() { + return 'abc' + }, + } + comp.setMethodCaller(caller as any) + expect(glassEasel.Component.getMethodsFromDef(compDef.general()).abc).toBeUndefined() + expect(glassEasel.Component.getMethod(comp.general(), 'abc')!()).toBe('abc') + expect(comp.callMethod('abc')).toBe('abc') + }) + test('#isInnerDataExcluded', () => { const compDef = glassEasel.Component.register( { From 57c3671fdc8aa6d7f5fc19ae32e95b34ff109cec Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Mon, 10 Jul 2023 17:45:25 +0800 Subject: [PATCH 10/29] feat(adapter): release the root component --- glass-easel-miniprogram-adapter/src/backend.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/glass-easel-miniprogram-adapter/src/backend.ts b/glass-easel-miniprogram-adapter/src/backend.ts index 0d80f1f..9eb3687 100644 --- a/glass-easel-miniprogram-adapter/src/backend.ts +++ b/glass-easel-miniprogram-adapter/src/backend.ts @@ -97,8 +97,20 @@ export class Root { * * This component, the `parent` and the `placeholder` MUST be in the same context. * The `parent` MUST be a parent node of the `placeholder` . + * This also triggers `attached` lifetimes for components. */ attach(parent: glassEasel.GeneralBackendElement, placeholder: glassEasel.GeneralBackendElement) { glassEasel.Element.replaceDocumentElement(this._$comp, parent, placeholder) } + + /** + * Release the root component + * + * Some backends require this explicit call to avoid memory leaks. + * This also triggers `detached` lifetimes for components. + */ + release() { + glassEasel.Element.pretendDetached(this._$comp) + this._$comp.destroyBackendElement() + } } From d041aa7b7e8596846ebf4f2b18135d33de7c252c Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Fri, 14 Jul 2023 17:45:34 +0800 Subject: [PATCH 11/29] optimize: make element.dataset always non-null --- glass-easel/src/element.ts | 4 +- glass-easel/src/tmpl/proc_gen_wrapper.ts | 5 +- glass-easel/tests/tmpl/binding_map.test.ts | 4 +- glass-easel/tests/tmpl/expression.test.ts | 256 ++++++++++----------- glass-easel/tests/tmpl/lvalue.test.ts | 10 +- glass-easel/tests/tmpl/structure.test.ts | 6 +- 6 files changed, 141 insertions(+), 144 deletions(-) diff --git a/glass-easel/src/element.ts b/glass-easel/src/element.ts index 69ea614..dded160 100644 --- a/glass-easel/src/element.ts +++ b/glass-easel/src/element.ts @@ -98,7 +98,7 @@ export class Element implements NodeCast { _$placeholderHandler: (() => void) | undefined /** @internal */ private _$virtual: boolean - dataset: { [name: string]: unknown } | null + dataset: { [name: string]: unknown } /** @internal */ private _$marks: { [name: string]: unknown } | null /** @internal */ @@ -144,7 +144,7 @@ export class Element implements NodeCast { this._$inheritSlots = false this._$placeholderHandler = undefined this._$virtual = virtual - this.dataset = null + this.dataset = {} this._$marks = null this._$attached = false this.classList = null diff --git a/glass-easel/src/tmpl/proc_gen_wrapper.ts b/glass-easel/src/tmpl/proc_gen_wrapper.ts index 50e4fb7..bf62013 100644 --- a/glass-easel/src/tmpl/proc_gen_wrapper.ts +++ b/glass-easel/src/tmpl/proc_gen_wrapper.ts @@ -870,10 +870,7 @@ export class ProcGenWrapper { // set dataset d(elem: Element, name: string, v: unknown) { - let dataset: { [name: string]: unknown } - if (elem.dataset) dataset = elem.dataset - else dataset = elem.dataset = {} - dataset[name] = v + elem.dataset[name] = v } // set mark diff --git a/glass-easel/tests/tmpl/binding_map.test.ts b/glass-easel/tests/tmpl/binding_map.test.ts index 2e2c288..c15a54a 100644 --- a/glass-easel/tests/tmpl/binding_map.test.ts +++ b/glass-easel/tests/tmpl/binding_map.test.ts @@ -21,10 +21,10 @@ describe('binding map update enabled', () => { const elem = glassEasel.Component.createWithContext('root', def.general(), domBackend) const child = elem.getShadowRoot()!.getElementById('b')! expect(domHtml(elem)).toBe('
abc
') - expect(child.dataset!.a).toBe('abc') + expect(child.dataset.a).toBe('abc') elem.setData({ c: 'def' }) expect(domHtml(elem)).toBe('
def
') - expect(child.dataset!.a).toBe('def') + expect(child.dataset.a).toBe('def') }) test('model data path update', () => { diff --git a/glass-easel/tests/tmpl/expression.test.ts b/glass-easel/tests/tmpl/expression.test.ts index 429d4af..77bd870 100644 --- a/glass-easel/tests/tmpl/expression.test.ts +++ b/glass-easel/tests/tmpl/expression.test.ts @@ -30,12 +30,12 @@ describe('binding expression', () => { const n4 = elem.getShadowRoot()!.getElementById('n4')! const n5 = elem.getShadowRoot()!.getElementById('n5')! const n6 = elem.getShadowRoot()!.getElementById('n6')! - expect(n1.dataset!.a).toEqual(false) - expect(n2.dataset!.a).toEqual(true) - expect(n3.dataset!.a).toEqual(null) - expect(n4.dataset!.a).toEqual(undefined) - expect(n5.dataset!.a).toEqual(123) - expect(n6.dataset!.a).toStrictEqual({ null: 456, truefalse: 123 }) + expect(n1.dataset.a).toEqual(false) + expect(n2.dataset.a).toEqual(true) + expect(n3.dataset.a).toEqual(null) + expect(n4.dataset.a).toEqual(undefined) + expect(n5.dataset.a).toEqual(123) + expect(n6.dataset.a).toStrictEqual({ null: 456, truefalse: 123 }) }) test('basic operators', () => { @@ -59,17 +59,17 @@ describe('binding expression', () => { const n3 = elem.getShadowRoot()!.getElementById('n3')! const n4 = elem.getShadowRoot()!.getElementById('n4')! const n5 = elem.getShadowRoot()!.getElementById('n5')! - expect(n1.dataset!.a).toEqual('A 1005') - expect(n2.dataset!.a).toEqual(94) - expect(n3.dataset!.a).toEqual(500) - expect(n4.dataset!.a).toEqual(10) - expect(n5.dataset!.a).toEqual(14) + expect(n1.dataset.a).toEqual('A 1005') + expect(n2.dataset.a).toEqual(94) + expect(n3.dataset.a).toEqual(500) + expect(n4.dataset.a).toEqual(10) + expect(n5.dataset.a).toEqual(14) elem.setData({ c: 10 }) - expect(n1.dataset!.a).toEqual('A 10010') - expect(n2.dataset!.a).toEqual(89) - expect(n3.dataset!.a).toEqual(1000) - expect(n4.dataset!.a).toEqual(5) - expect(n5.dataset!.a).toEqual(11) + expect(n1.dataset.a).toEqual('A 10010') + expect(n2.dataset.a).toEqual(89) + expect(n3.dataset.a).toEqual(1000) + expect(n4.dataset.a).toEqual(5) + expect(n5.dataset.a).toEqual(11) }) test('unary operators', () => { @@ -90,15 +90,15 @@ describe('binding expression', () => { const n2 = elem.getShadowRoot()!.getElementById('n2')! const n3 = elem.getShadowRoot()!.getElementById('n3')! const n4 = elem.getShadowRoot()!.getElementById('n4')! - expect(n1.dataset!.a).toEqual(false) - expect(n2.dataset!.a).toEqual(-457) - expect(n3.dataset!.a).toEqual(123) - expect(n4.dataset!.a).toEqual(-456) + expect(n1.dataset.a).toEqual(false) + expect(n2.dataset.a).toEqual(-457) + expect(n3.dataset.a).toEqual(123) + expect(n4.dataset.a).toEqual(-456) elem.setData({ a: null, b: '789' }) - expect(n1.dataset!.a).toEqual(true) - expect(n2.dataset!.a).toEqual(-790) - expect(n3.dataset!.a).toEqual(0) - expect(n4.dataset!.a).toEqual(-789) + expect(n1.dataset.a).toEqual(true) + expect(n2.dataset.a).toEqual(-790) + expect(n3.dataset.a).toEqual(0) + expect(n4.dataset.a).toEqual(-789) }) test('comparison operators', () => { @@ -127,41 +127,41 @@ describe('binding expression', () => { const n6 = elem.getShadowRoot()!.getElementById('n6')! const n7 = elem.getShadowRoot()!.getElementById('n7')! const n8 = elem.getShadowRoot()!.getElementById('n8')! - expect(n1.dataset!.a).toEqual(true) - expect(n2.dataset!.a).toEqual(false) - expect(n3.dataset!.a).toEqual(true) - expect(n4.dataset!.a).toEqual(false) - expect(n5.dataset!.a).toEqual(false) - expect(n6.dataset!.a).toEqual(true) - expect(n7.dataset!.a).toEqual(false) - expect(n8.dataset!.a).toEqual(true) + expect(n1.dataset.a).toEqual(true) + expect(n2.dataset.a).toEqual(false) + expect(n3.dataset.a).toEqual(true) + expect(n4.dataset.a).toEqual(false) + expect(n5.dataset.a).toEqual(false) + expect(n6.dataset.a).toEqual(true) + expect(n7.dataset.a).toEqual(false) + expect(n8.dataset.a).toEqual(true) elem.setData({ b: 9 }) - expect(n1.dataset!.a).toEqual(false) - expect(n2.dataset!.a).toEqual(true) - expect(n3.dataset!.a).toEqual(false) - expect(n4.dataset!.a).toEqual(true) - expect(n5.dataset!.a).toEqual(false) - expect(n6.dataset!.a).toEqual(true) - expect(n7.dataset!.a).toEqual(false) - expect(n8.dataset!.a).toEqual(true) + expect(n1.dataset.a).toEqual(false) + expect(n2.dataset.a).toEqual(true) + expect(n3.dataset.a).toEqual(false) + expect(n4.dataset.a).toEqual(true) + expect(n5.dataset.a).toEqual(false) + expect(n6.dataset.a).toEqual(true) + expect(n7.dataset.a).toEqual(false) + expect(n8.dataset.a).toEqual(true) elem.setData({ a: '9' }) - expect(n1.dataset!.a).toEqual(false) - expect(n2.dataset!.a).toEqual(false) - expect(n3.dataset!.a).toEqual(true) - expect(n4.dataset!.a).toEqual(true) - expect(n5.dataset!.a).toEqual(true) - expect(n6.dataset!.a).toEqual(false) - expect(n7.dataset!.a).toEqual(false) - expect(n8.dataset!.a).toEqual(true) + expect(n1.dataset.a).toEqual(false) + expect(n2.dataset.a).toEqual(false) + expect(n3.dataset.a).toEqual(true) + expect(n4.dataset.a).toEqual(true) + expect(n5.dataset.a).toEqual(true) + expect(n6.dataset.a).toEqual(false) + expect(n7.dataset.a).toEqual(false) + expect(n8.dataset.a).toEqual(true) elem.setData({ b: '9' }) - expect(n1.dataset!.a).toEqual(false) - expect(n2.dataset!.a).toEqual(false) - expect(n3.dataset!.a).toEqual(true) - expect(n4.dataset!.a).toEqual(true) - expect(n5.dataset!.a).toEqual(true) - expect(n6.dataset!.a).toEqual(false) - expect(n7.dataset!.a).toEqual(true) - expect(n8.dataset!.a).toEqual(false) + expect(n1.dataset.a).toEqual(false) + expect(n2.dataset.a).toEqual(false) + expect(n3.dataset.a).toEqual(true) + expect(n4.dataset.a).toEqual(true) + expect(n5.dataset.a).toEqual(true) + expect(n6.dataset.a).toEqual(false) + expect(n7.dataset.a).toEqual(true) + expect(n8.dataset.a).toEqual(false) }) test('logic and bit logic operators', () => { @@ -184,17 +184,17 @@ describe('binding expression', () => { const n3 = elem.getShadowRoot()!.getElementById('n3')! const n4 = elem.getShadowRoot()!.getElementById('n4')! const n5 = elem.getShadowRoot()!.getElementById('n5')! - expect(n1.dataset!.a).toEqual(2) - expect(n2.dataset!.a).toEqual(7) - expect(n3.dataset!.a).toEqual(5) - expect(n4.dataset!.a).toEqual(6) - expect(n5.dataset!.a).toEqual(3) + expect(n1.dataset.a).toEqual(2) + expect(n2.dataset.a).toEqual(7) + expect(n3.dataset.a).toEqual(5) + expect(n4.dataset.a).toEqual(6) + expect(n5.dataset.a).toEqual(3) elem.setData({ a: 0, b: 15 }) - expect(n1.dataset!.a).toEqual(0) - expect(n2.dataset!.a).toEqual(15) - expect(n3.dataset!.a).toEqual(15) - expect(n4.dataset!.a).toEqual(0) - expect(n5.dataset!.a).toEqual(15) + expect(n1.dataset.a).toEqual(0) + expect(n2.dataset.a).toEqual(15) + expect(n3.dataset.a).toEqual(15) + expect(n4.dataset.a).toEqual(0) + expect(n5.dataset.a).toEqual(15) }) test('conditional operator', () => { @@ -210,11 +210,11 @@ describe('binding expression', () => { }) const elem = glassEasel.createElement('root', def.general()) const n1 = elem.getShadowRoot()!.getElementById('n1')! - expect(n1.dataset!.a).toEqual(1) + expect(n1.dataset.a).toEqual(1) elem.setData({ a: 0 }) - expect(n1.dataset!.a).toEqual(2) + expect(n1.dataset.a).toEqual(2) elem.setData({ a: [], b: 3 }) - expect(n1.dataset!.a).toEqual(3) + expect(n1.dataset.a).toEqual(3) }) test('operator order', () => { @@ -235,34 +235,34 @@ describe('binding expression', () => { }) const elem = glassEasel.createElement('root', def.general()) const n0 = elem.getShadowRoot()!.getElementById('n0')! - expect(n0.dataset!.a).toEqual(3) + expect(n0.dataset.a).toEqual(3) const n1 = elem.getShadowRoot()!.getElementById('n1')! - expect(n1.dataset!.a).toEqual(1) - expect(n1.dataset!.b).toEqual(2) + expect(n1.dataset.a).toEqual(1) + expect(n1.dataset.b).toEqual(2) const n2 = elem.getShadowRoot()!.getElementById('n2')! - expect(n2.dataset!.a).toEqual(0) - expect(n2.dataset!.b).toEqual(0) + expect(n2.dataset.a).toEqual(0) + expect(n2.dataset.b).toEqual(0) const n3 = elem.getShadowRoot()!.getElementById('n3')! - expect(n3.dataset!.a).toEqual(5) - expect(n3.dataset!.b).toEqual(5) + expect(n3.dataset.a).toEqual(5) + expect(n3.dataset.b).toEqual(5) const n4 = elem.getShadowRoot()!.getElementById('n4')! - expect(n4.dataset!.a).toEqual(7) - expect(n4.dataset!.b).toEqual(7) + expect(n4.dataset.a).toEqual(7) + expect(n4.dataset.b).toEqual(7) const n5 = elem.getShadowRoot()!.getElementById('n5')! - expect(n5.dataset!.a).toEqual(0) - expect(n5.dataset!.b).toEqual(0) + expect(n5.dataset.a).toEqual(0) + expect(n5.dataset.b).toEqual(0) const n6 = elem.getShadowRoot()!.getElementById('n6')! - expect(n6.dataset!.a).toEqual(true) - expect(n6.dataset!.b).toEqual(true) + expect(n6.dataset.a).toEqual(true) + expect(n6.dataset.b).toEqual(true) const n7 = elem.getShadowRoot()!.getElementById('n7')! - expect(n7.dataset!.a).toEqual(false) - expect(n7.dataset!.b).toEqual(false) + expect(n7.dataset.a).toEqual(false) + expect(n7.dataset.b).toEqual(false) const n8 = elem.getShadowRoot()!.getElementById('n8')! - expect(n8.dataset!.a).toEqual(4) - expect(n8.dataset!.b).toEqual(4) + expect(n8.dataset.a).toEqual(4) + expect(n8.dataset.b).toEqual(4) const n10 = elem.getShadowRoot()!.getElementById('n10')! - expect(n10.dataset!.a).toEqual(-4) - expect(n10.dataset!.b).toEqual(2) + expect(n10.dataset.a).toEqual(-4) + expect(n10.dataset.b).toEqual(2) }) test('array literals', () => { @@ -273,9 +273,9 @@ describe('binding expression', () => { }) const elem = glassEasel.createElement('root', def.general()) const eA = elem.getShadowRoot()!.getElementById('a')! - expect(eA.dataset!.a).toEqual([123, undefined, 456]) + expect(eA.dataset.a).toEqual([123, undefined, 456]) elem.setData({ a: 789 }) - expect(eA.dataset!.a).toEqual([123, 789, 456]) + expect(eA.dataset.a).toEqual([123, 789, 456]) }) test('object literal path analysis', () => { @@ -353,25 +353,25 @@ describe('binding expression', () => { const eA = elem.getShadowRoot()!.getElementById('a')! const eB = elem.getShadowRoot()!.getElementById('b')! const eC = elem.getShadowRoot()!.getElementById('c')! - expect(eA.dataset!.a).toEqual({ b: 123 }) - expect(eA.dataset!.a === elem.data.a).toBe(false) - expect(eB.dataset!.a).toEqual(123) - expect(eC.dataset!.a).toBeUndefined() + expect(eA.dataset.a).toEqual({ b: 123 }) + expect(eA.dataset.a === elem.data.a).toBe(false) + expect(eB.dataset.a).toEqual(123) + expect(eC.dataset.a).toBeUndefined() elem.setData({ 'a.b': 456 }) - expect(eA.dataset!.a).toEqual({ b: 456 }) - expect(eA.dataset!.a === elem.data.a).toBe(false) - expect(eB.dataset!.a).toEqual(456) - expect(eC.dataset!.a).toBeUndefined() + expect(eA.dataset.a).toEqual({ b: 456 }) + expect(eA.dataset.a === elem.data.a).toBe(false) + expect(eB.dataset.a).toEqual(456) + expect(eC.dataset.a).toBeUndefined() elem.setData({ 'a.b.c': 789 }) - expect(eA.dataset!.a).toEqual({ b: { c: 789 } }) - expect(eA.dataset!.a === elem.data.a).toBe(false) - expect(eB.dataset!.a).toEqual({ c: 789 }) - expect(eB.dataset!.a === (elem.data.a as { [key: string]: glassEasel.DataValue }).b).toBe(false) - expect(eC.dataset!.a).toEqual(789) + expect(eA.dataset.a).toEqual({ b: { c: 789 } }) + expect(eA.dataset.a === elem.data.a).toBe(false) + expect(eB.dataset.a).toEqual({ c: 789 }) + expect(eB.dataset.a === (elem.data.a as { [key: string]: glassEasel.DataValue }).b).toBe(false) + expect(eC.dataset.a).toEqual(789) elem.setData({ a: 0 }) - expect(eA.dataset!.a).toEqual(0) - expect(eB.dataset!.a).toBeUndefined() - expect(eC.dataset!.a).toBeUndefined() + expect(eA.dataset.a).toEqual(0) + expect(eB.dataset.a).toBeUndefined() + expect(eC.dataset.a).toBeUndefined() }) test('dynamic member visit and update', () => { @@ -392,25 +392,25 @@ describe('binding expression', () => { const eA = elem.getShadowRoot()!.getElementById('a')! const eB = elem.getShadowRoot()!.getElementById('b')! const eC = elem.getShadowRoot()!.getElementById('c')! - expect(eA.dataset!.a).toEqual({ b: [123, 456] }) - expect(eA.dataset!.a === elem.data.a).toBe(false) - expect(eB.dataset!.a).toEqual(123) - expect(eC.dataset!.a).toBeUndefined() + expect(eA.dataset.a).toEqual({ b: [123, 456] }) + expect(eA.dataset.a === elem.data.a).toBe(false) + expect(eB.dataset.a).toEqual(123) + expect(eC.dataset.a).toBeUndefined() elem.setData({ d: 1 }) - expect(eA.dataset!.a).toEqual({ b: [123, 456] }) - expect(eA.dataset!.a === elem.data.a).toBe(false) - expect(eB.dataset!.a).toEqual(456) - expect(eC.dataset!.a).toBeUndefined() + expect(eA.dataset.a).toEqual({ b: [123, 456] }) + expect(eA.dataset.a === elem.data.a).toBe(false) + expect(eB.dataset.a).toEqual(456) + expect(eC.dataset.a).toBeUndefined() elem.setData({ 'a.b[1].c': 789 }) - expect(eA.dataset!.a).toEqual({ b: [123, { c: 789 }] }) - expect(eA.dataset!.a === elem.data.a).toBe(false) - expect(eB.dataset!.a).toEqual({ c: 789 }) - expect(eB.dataset!.a === (elem.data.a as { [key: string]: glassEasel.DataValue }).b).toBe(false) - expect(eC.dataset!.a).toEqual(789) + expect(eA.dataset.a).toEqual({ b: [123, { c: 789 }] }) + expect(eA.dataset.a === elem.data.a).toBe(false) + expect(eB.dataset.a).toEqual({ c: 789 }) + expect(eB.dataset.a === (elem.data.a as { [key: string]: glassEasel.DataValue }).b).toBe(false) + expect(eC.dataset.a).toEqual(789) elem.setData({ a: 0 }) - expect(eA.dataset!.a).toEqual(0) - expect(eB.dataset!.a).toBeUndefined() - expect(eC.dataset!.a).toBeUndefined() + expect(eA.dataset.a).toEqual(0) + expect(eB.dataset.a).toBeUndefined() + expect(eC.dataset.a).toBeUndefined() }) test('nested dynamic member visit and update', () => { @@ -426,16 +426,16 @@ describe('binding expression', () => { }) const elem = glassEasel.createElement('root', def.general()) const eA = elem.getShadowRoot()!.getElementById('a')! - expect(eA.dataset!.a).toEqual(20) + expect(eA.dataset.a).toEqual(20) elem.setData({ c: 1 }) - expect(eA.dataset!.a).toEqual(30) + expect(eA.dataset.a).toEqual(30) elem.setData({ 'b[1]': 0 }) - expect(eA.dataset!.a).toEqual(10) + expect(eA.dataset.a).toEqual(10) elem.setData({ b: [0, 1] }) - expect(eA.dataset!.a).toEqual(20) + expect(eA.dataset.a).toEqual(20) elem.setData({ a: [100, 200, 300] }) - expect(eA.dataset!.a).toEqual(200) + expect(eA.dataset.a).toEqual(200) elem.setData({ c: 0 }) - expect(eA.dataset!.a).toEqual(100) + expect(eA.dataset.a).toEqual(100) }) }) diff --git a/glass-easel/tests/tmpl/lvalue.test.ts b/glass-easel/tests/tmpl/lvalue.test.ts index 8b1286f..84a8a54 100644 --- a/glass-easel/tests/tmpl/lvalue.test.ts +++ b/glass-easel/tests/tmpl/lvalue.test.ts @@ -42,26 +42,26 @@ describe('model value binding', () => { const comp = elem.getShadowRoot()!.getElementById('comp')! as glassEasel.GeneralComponent expect(domHtml(elem)).toBe('
10
') expect(updateCount).toBe(1) - expect(comp.getShadowRoot()!.getElementById('a')!.dataset!.a).toBe(10) + expect(comp.getShadowRoot()!.getElementById('a')!.dataset.a).toBe(10) comp.setData({ propA: 20 }) expect(domHtml(elem)).toBe('
20
') expect(updateCount).toBe(3) - expect(comp.getShadowRoot()!.getElementById('a')!.dataset!.a).toBe(20) + expect(comp.getShadowRoot()!.getElementById('a')!.dataset.a).toBe(20) expect(elem.data.a).toEqual({ b: [20, 100] }) elem.setData({ 'a.b[0]': 30 }) expect(domHtml(elem)).toBe('
30
') expect(updateCount).toBe(4) - expect(comp.getShadowRoot()!.getElementById('a')!.dataset!.a).toBe(30) + expect(comp.getShadowRoot()!.getElementById('a')!.dataset.a).toBe(30) elem.setData({ 'a.b[1]': 200 }) expect(updateCount).toBe(4) elem.setData({ c: 1 }) expect(domHtml(elem)).toBe('
200
') expect(updateCount).toBe(5) - expect(comp.getShadowRoot()!.getElementById('a')!.dataset!.a).toBe(200) + expect(comp.getShadowRoot()!.getElementById('a')!.dataset.a).toBe(200) comp.setData({ propA: 300 }) expect(domHtml(elem)).toBe('
300
') expect(updateCount).toBe(7) - expect(comp.getShadowRoot()!.getElementById('a')!.dataset!.a).toBe(300) + expect(comp.getShadowRoot()!.getElementById('a')!.dataset.a).toBe(300) expect(elem.data.a).toEqual({ b: [30, 300] }) }) diff --git a/glass-easel/tests/tmpl/structure.test.ts b/glass-easel/tests/tmpl/structure.test.ts index ba99851..676c5e4 100644 --- a/glass-easel/tests/tmpl/structure.test.ts +++ b/glass-easel/tests/tmpl/structure.test.ts @@ -268,7 +268,7 @@ describe('node tree structure', () => { const checkIndex = () => { for (let i = 0; i < listBlock.childNodes.length; i += 1) { const itemBlock = listBlock.childNodes[i] as glassEasel.Element - expect((itemBlock.childNodes[0] as glassEasel.Element).dataset!.i).toBe(i) + expect((itemBlock.childNodes[0] as glassEasel.Element).dataset.i).toBe(i) } } glassEasel.Element.pretendAttached(elem) @@ -516,7 +516,7 @@ describe('node tree structure', () => { const keys = Object.keys(elem.data.list) for (let i = 0; i < listBlock.childNodes.length; i += 1) { const itemBlock = listBlock.childNodes[i] as glassEasel.Element - expect((itemBlock.childNodes[0] as glassEasel.Element).dataset!.i).toBe(keys[i]) + expect((itemBlock.childNodes[0] as glassEasel.Element).dataset.i).toBe(keys[i]) } } glassEasel.Element.pretendAttached(elem) @@ -1126,7 +1126,7 @@ describe('node tree structure', () => { const elem = glassEasel.Component.createWithContext('root', def, domBackend) glassEasel.Element.pretendAttached(elem) expect(domHtml(elem)).toBe('
') - expect(elem.getShadowRoot()!.childNodes[0]!.asElement()!.dataset!.camelcase).toBe(123) + expect(elem.getShadowRoot()!.childNodes[0]!.asElement()!.dataset.camelcase).toBe(123) }) test('attribute name cases', () => { From dfd52eda419625e7cb5c610ac8d3192f30d7c2b8 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Mon, 17 Jul 2023 17:34:38 +0800 Subject: [PATCH 12/29] docs: update observers guide (#10) --- .../guide/zh_CN/data_management/data_observer.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/glass-easel/guide/zh_CN/data_management/data_observer.md b/glass-easel/guide/zh_CN/data_management/data_observer.md index a58f9df..ee4ef1e 100644 --- a/glass-easel/guide/zh_CN/data_management/data_observer.md +++ b/glass-easel/guide/zh_CN/data_management/data_observer.md @@ -2,9 +2,13 @@ ## 监听数据字段变化 -通过数据监听器,可以在某些属性或数据字段被设置时触发一个函数。 +通过数据监听器,可以在某些属性或数据字段被设置时触发一个响应函数。 -例如,可以在 `a` 和 `b` 两个字段中任何一个被设置时,触发一个函数: +这个响应函数会在属性或数据字段应用到模板前被调用,可以在总体上减少模板更新次数从而提升性能。 + +在这个响应函数中,可以使用 `updateData` 来更新其他的数据字段,这些数据字段会响应函数执行完毕后生效。注意,虽然也可以使用 `setData` ,但数据监听器中的 `setData` 并不会立刻应用到模板上,而是像 `updateData` 一样、等到数据监听器执行完毕后才应用到模板上。 + +例如,可以在 `a` 和 `b` 两个字段中任何一个被设置时,触发一个响应函数: ```js // 使用 Definition API 添加数据监听器 @@ -21,8 +25,6 @@ export const addComponent = componentSpace.defineComponent({ observers: { 'a, b': function () { // 数据监听器中,最好使用 updateData 而非 setData - // (事实上,使用 setData 将与 updateData 等效) - // 数据监听器执行完后,会自动将更新应用到模板上 this.updateData({ sum: this.data.a + this.data.b, }) From 010ddd515a930da4bcb5db7b6486ad8093b239fe Mon Sep 17 00:00:00 2001 From: maniacata Date: Mon, 17 Jul 2023 21:31:46 +0800 Subject: [PATCH 13/29] fix: should apply selector on shadowRoot --- glass-easel-miniprogram-adapter/src/intersection.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/glass-easel-miniprogram-adapter/src/intersection.ts b/glass-easel-miniprogram-adapter/src/intersection.ts index 50dbd37..17deaa3 100644 --- a/glass-easel-miniprogram-adapter/src/intersection.ts +++ b/glass-easel-miniprogram-adapter/src/intersection.ts @@ -53,11 +53,14 @@ export class IntersectionObserver { targetSelector: string, listener: (status: glassEasel.backend.IntersectionStatus) => void, ) { + const shadowRoot = this._$comp._$.getShadowRoot() let targets: glassEasel.Element[] - if (this._$observeAll) { - targets = this._$comp._$.querySelectorAll(targetSelector) + if (!shadowRoot) { + targets = [] + } else if (this._$observeAll) { + targets = shadowRoot.querySelectorAll(targetSelector) } else { - const elem = this._$comp._$.querySelector(targetSelector) + const elem = shadowRoot.querySelector(targetSelector) if (elem === null) { targets = [] } else { @@ -65,10 +68,10 @@ export class IntersectionObserver { } } let relativeElement: glassEasel.Element | null - if (this._$selector === null) { + if (this._$selector === null || !shadowRoot) { relativeElement = null } else { - const rel = this._$comp._$.querySelector(this._$selector) + const rel = shadowRoot.querySelector(this._$selector) if (rel === null) { // TODO warn no matched relative element } else { From 8fc3465a768fd58717af4bc7aca7c92d2a28eae1 Mon Sep 17 00:00:00 2001 From: maniacata Date: Mon, 17 Jul 2023 21:12:51 +0800 Subject: [PATCH 14/29] fix: inconsistent between type & impl of lifetime `listenerChange` --- glass-easel/src/element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glass-easel/src/element.ts b/glass-easel/src/element.ts index dded160..f6e5579 100644 --- a/glass-easel/src/element.ts +++ b/glass-easel/src/element.ts @@ -1486,7 +1486,7 @@ export class Element implements NodeCast { if (finalChanged === FinalChanged.Init) this._$updateEventDefaultPrevented(name, false) else if (finalChanged === FinalChanged.Added) this._$updateEventDefaultPrevented(name, true) if (this instanceof Component && this._$definition._$options.listenerChangeLifetimes) { - this.triggerLifetime('listenerChanged', [true, name, func, options]) + this.triggerLifetime('listenerChange', [true, name, func, options]) } } @@ -1496,7 +1496,7 @@ export class Element implements NodeCast { if (finalChanged === FinalChanged.Failed) return if (finalChanged !== FinalChanged.NotChanged) this._$updateEventDefaultPrevented(name, false) if (this instanceof Component && this._$definition._$options.listenerChangeLifetimes) { - this.triggerLifetime('listenerChanged', [false, name, func, options]) + this.triggerLifetime('listenerChange', [false, name, func, options]) } } From 7ede0111ccf0bb09e93160a76e73dbaec0819480 Mon Sep 17 00:00:00 2001 From: maniacata Date: Mon, 17 Jul 2023 21:58:21 +0800 Subject: [PATCH 15/29] test: listenerChange --- glass-easel/tests/core/misc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glass-easel/tests/core/misc.test.ts b/glass-easel/tests/core/misc.test.ts index 64b013e..8aab4f6 100644 --- a/glass-easel/tests/core/misc.test.ts +++ b/glass-easel/tests/core/misc.test.ts @@ -175,7 +175,7 @@ describe('event', () => { listenerChangeLifetimes: true, }, lifetimes: { - listenerChanged: ( + listenerChange: ( isAdd: boolean, eventName: string, listener: any, From cca67e9add46df6c0ccaf6e4b308625c0d86759f Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Fri, 21 Jul 2023 16:19:58 +0800 Subject: [PATCH 16/29] feat: add `groupRegister` for component space (#67) --- glass-easel/src/component_space.ts | 53 ++++++++++++++++++++-- glass-easel/tests/core/placeholder.test.ts | 38 +++++++++++++++- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/glass-easel/src/component_space.ts b/glass-easel/src/component_space.ts index 6dc9643..833796a 100644 --- a/glass-easel/src/component_space.ts +++ b/glass-easel/src/component_space.ts @@ -19,7 +19,7 @@ import { NormalizedComponentOptions, } from './global_options' import { StyleScopeManager } from './class_list' -import { GeneralBackendContext } from '.' +import { GeneralBackendContext, safeCallback } from '.' import { TraitBehavior } from './trait_behaviors' const normalizePath = (path: string, basePath: string): string => { @@ -146,6 +146,10 @@ export class ComponentSpace { [path: string]: ComponentWaitingList } /** @internal */ + private _$groupingListWaiting: + | { waiting: ComponentWaitingList; comp: GeneralComponentDefinition }[] + | null = null + /** @internal */ _$componentWaitingListener: | ((isPub: boolean, alias: string, owner: GeneralComponent) => void) | null = null @@ -373,10 +377,14 @@ export class ComponentSpace { _$registerComponent(is: string, comp: GeneralComponentDefinition) { this._$list[is] = comp this._$behaviorList[is] = comp.behavior as unknown as GeneralBehavior - const arr = this._$listWaiting[is] - if (arr) { + const waiting = this._$listWaiting[is] + if (waiting) { delete this._$listWaiting[is] - arr.call(comp) + if (this._$groupingListWaiting) { + this._$groupingListWaiting.push({ waiting, comp }) + } else { + waiting.call(comp) + } } } @@ -385,6 +393,43 @@ export class ComponentSpace { this._$behaviorList[is] = beh } + /** + * Start a series of components and behaviors registration + * + * In most cases, `groupRegister` is prefered. + */ + startGroupRegister() { + this._$groupingListWaiting = [] + } + + /** + * End a series of components and behaviors registration + * + * In most cases, `groupRegister` is prefered. + */ + endGroupRegister() { + const arr = this._$groupingListWaiting + if (!arr) return + this._$groupingListWaiting = null + for (let i = 0; i < arr.length; i += 1) { + const { waiting, comp } = arr[i]! + waiting.call(comp) + } + } + + /** + * Group a series of components and behaviors registration + * + * If any placeholder should be replaced, + * the replacement will happen after the whole series of registration. + */ + groupRegister(cb: () => R): R | undefined { + this.startGroupRegister() + const ret = safeCallback('group register', cb, this, []) + this.endGroupRegister() + return ret + } + /** * Assign a public alias to a component * diff --git a/glass-easel/tests/core/placeholder.test.ts b/glass-easel/tests/core/placeholder.test.ts index e9044e1..0f3a417 100644 --- a/glass-easel/tests/core/placeholder.test.ts +++ b/glass-easel/tests/core/placeholder.test.ts @@ -79,7 +79,7 @@ describe('placeholder', () => { matchElementWithDom(elem) }) - test('using other component as placeholder', () => { + test('using another component as placeholder', () => { const componentSpace = new glassEasel.ComponentSpace() const viewDef = componentSpace.define('view').registerComponent() componentSpace.setGlobalUsingComponent('view', viewDef) @@ -101,6 +101,42 @@ describe('placeholder', () => { matchElementWithDom(elem) }) + test('group register other components as placeholders', () => { + const componentSpace = new glassEasel.ComponentSpace() + componentSpace.define('').registerComponent() + + const def = componentSpace + .define() + .placeholders({ + parent: '', + }) + .definition({ + using: { + parent: 'parent', + }, + template: tmpl(''), + }) + .registerComponent() + const elem = glassEasel.Component.createWithContext('root', def.general(), domBackend) + expect(domHtml(elem)).toBe('') + matchElementWithDom(elem) + + componentSpace.groupRegister(() => { + const parentDef = componentSpace + .define('parent') + .usingComponents({ + child: 'child', + }) + .template(tmpl('')) + .registerComponent() + componentSpace.setGlobalUsingComponent('parent', parentDef) + const childDef = componentSpace.define('child').template(tmpl('CHILD')).registerComponent() + componentSpace.setGlobalUsingComponent('child', childDef) + }) + expect(domHtml(elem)).toBe('CHILD') + matchElementWithDom(elem) + }) + test('using placeholder across component spaces and waiting', () => { const mainCs = new glassEasel.ComponentSpace() mainCs.defineComponent({ is: '' }) From bf9f7b3b4976dffc95117eb860b5cfa1d467d91d Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Mon, 24 Jul 2023 20:04:59 +0800 Subject: [PATCH 17/29] fix: composed-parent element iterator did not go out of current shadow tree --- glass-easel/src/element_iterator.ts | 35 +++++++------------ .../tests/legacy/element_iterator.test.js | 26 +++++++++++++- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/glass-easel/src/element_iterator.ts b/glass-easel/src/element_iterator.ts index b2030d6..05dab73 100644 --- a/glass-easel/src/element_iterator.ts +++ b/glass-easel/src/element_iterator.ts @@ -1,6 +1,5 @@ import { Element } from './element' import { TextNode } from './text_node' -import { Component } from './component' import { Node } from './node' /** The iterator direction and order */ @@ -89,30 +88,20 @@ export class ElementIterator { const nodeTypeLimit: any = this._$nodeTypeLimit const composed = this._$composed if (this._$isAncestor) { - const rec = (node: Node): boolean => { - let prev: Node | null = null - let cur = node - for (;;) { - if (composed && prev) { - if (cur instanceof Component && !cur._$external) { - const slot = cur.getShadowRoot()!.getContainingSlot(prev) - if (slot) { - if (rec(slot) === false) break - } else { - return false - } - } - } - if (cur instanceof nodeTypeLimit) { - if (f(cur) === false) return false - } - if (!cur.parentNode) break - prev = cur - cur = cur.parentNode + let cur = this._$node + for (;;) { + if (cur instanceof nodeTypeLimit) { + if (f(cur) === false) return } - return true + let next: Element | null + if (composed) { + next = cur.getComposedParent() + } else { + next = cur.parentNode + } + if (next) cur = next + else break } - rec(this._$node) } else { const rootFirst = this._$rootFirst const rec = (node: Node): boolean => { diff --git a/glass-easel/tests/legacy/element_iterator.test.js b/glass-easel/tests/legacy/element_iterator.test.js index f7419e0..8931cdf 100644 --- a/glass-easel/tests/legacy/element_iterator.test.js +++ b/glass-easel/tests/legacy/element_iterator.test.js @@ -56,6 +56,7 @@ describe('Element Iterator', function () { expect(e).toBe(expectResArr.shift()) }, ) + expect(expectResArr.length).toBe(0) }) it('should support composed-ancestors traversing', function () { @@ -78,6 +79,7 @@ describe('Element Iterator', function () { expect(e).toBe(expectResArr.shift()) }, ) + expect(expectResArr.length).toBe(0) }) it('should support ancestors traversing with break', function () { @@ -89,6 +91,7 @@ describe('Element Iterator', function () { expect(e).toBe(expectResArr.shift()) }, ) + expect(expectResArr.length).toBe(0) }) it('should support shadow-descendants-root-first traversing', function () { @@ -99,6 +102,7 @@ describe('Element Iterator', function () { expect(e).toBe(expectResArr.shift()) }, ) + expect(expectResArr.length).toBe(0) }) it('should support shadow-descendants-root-last traversing', function () { @@ -111,6 +115,7 @@ describe('Element Iterator', function () { ).forEach(function (e) { expect(e).toBe(expectResArr.shift()) }) + expect(expectResArr.length).toBe(0) }) it('should support composed-descendants-root-first traversing', function () { @@ -135,6 +140,22 @@ describe('Element Iterator', function () { expect(e).toBe(expectResArr.shift()) }, ) + expect(expectResArr.length).toBe(0) + var expectResArr = [ + elem.$.g.$.e, + elem.$.g.$.e.childNodes[0], + elem.$.h, + elem.$.h.childNodes[0], + elem.$.i, + elem.$.i.shadowRoot, + elem.$.i.shadowRoot.childNodes[0], + ] + glassEasel.ElementIterator.create(elem.$.g.$.e, 'composed-descendants-root-first', Object).forEach( + function (e) { + expect(e).toBe(expectResArr.shift()) + }, + ) + expect(expectResArr.length).toBe(0) }) it('should support composed-descendants-root-last traversing', function () { @@ -147,17 +168,19 @@ describe('Element Iterator', function () { ).forEach(function (e) { expect(e).toBe(expectResArr.shift()) }) + expect(expectResArr.length).toBe(0) }) it('should support root-first traversing with break', function () { var elem = createElem('element-iterator-combined') - var expectResArr = [elem.$.g, elem.$.g.shadowRoot, elem.$.g.$.d] + var expectResArr = [elem.$.g, elem.$.g.shadowRoot] glassEasel.ElementIterator.create(elem.$.g, 'composed-descendants-root-first', Object).forEach( function (e) { if (e === elem.$.g.$.d) return false expect(e).toBe(expectResArr.shift()) }, ) + expect(expectResArr.length).toBe(0) }) it('should support root-last traversing with break', function () { @@ -171,5 +194,6 @@ describe('Element Iterator', function () { if (e === elem.$.g.$.d) return false expect(e).toBe(expectResArr.shift()) }) + expect(expectResArr.length).toBe(0) }) }) From c8c936164dedc2b38c0bf51fadf947dbd7fee283 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Mon, 24 Jul 2023 21:15:17 +0800 Subject: [PATCH 18/29] fix: use closest parent/child pair in ancestor/descendant ralations (#69) --- glass-easel/src/relation.ts | 1 + glass-easel/tests/legacy/relation.test.js | 53 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/glass-easel/src/relation.ts b/glass-easel/src/relation.ts index 308ae98..1e5bd50 100644 --- a/glass-easel/src/relation.ts +++ b/glass-easel/src/relation.ts @@ -225,6 +225,7 @@ export class Relation { break } } + if (newLink) break } } } diff --git a/glass-easel/tests/legacy/relation.test.js b/glass-easel/tests/legacy/relation.test.js index c5f61cb..d6f7b8d 100644 --- a/glass-easel/tests/legacy/relation.test.js +++ b/glass-easel/tests/legacy/relation.test.js @@ -393,6 +393,59 @@ describe('Component Relations', function () { expect(c3.getRelationNodes('')).toStrictEqual([]) }) + it('should link cascade ancestors and descendants', function () { + var ancestorBeh = regBeh({}) + var descendantBeh = regBeh({}) + regElem({ + is: 'relation-ancestor-b', + behaviors: [ancestorBeh], + relations: { + 'relation-b': { + target: descendantBeh, + type: 'descendant', + linked: function (target) { + expect(this).toBe(target.parentNode) + callOrder.push(this.id) + }, + }, + }, + }) + regElem({ + is: 'relation-descendant-b', + behaviors: [descendantBeh], + relations: { + 'relation-b': { + target: ancestorBeh, + type: 'ancestor', + linked: function (target) { + expect(target).toBe(this.parentNode) + callOrder.push(this.id) + }, + }, + }, + }) + regElem({ + is: 'relation-ancestor-descendant', + template: ` + + + + + + + + `, + }) + var elem = createElem('relation-ancestor-descendant') + var callOrder = [] + glassEasel.Element.pretendAttached(elem) + expect(callOrder).toStrictEqual(['p1', 'c1', 'p2', 'c2']) + expect(elem.$.p1.getRelationNodes('relation-b')).toStrictEqual([elem.$.c1]) + expect(elem.$.c1.getRelationNodes('relation-b')).toStrictEqual([elem.$.p1]) + expect(elem.$.p2.getRelationNodes('relation-b')).toStrictEqual([elem.$.c2]) + expect(elem.$.c2.getRelationNodes('relation-b')).toStrictEqual([elem.$.p2]) + }) + it('should trigger linkFailed handler when cannot link two nodes', function () { regElem({ is: 'relation-cnt-failed', From b0539f48f88d395a4a9e4766e5d4b73924f9b4ad Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Mon, 24 Jul 2023 21:43:39 +0800 Subject: [PATCH 19/29] fix(template-compiler): missing comma when composing `wx:if` condition expression --- glass-easel-template-compiler/src/cbinding.rs | 4 ++-- glass-easel-template-compiler/src/expr.rs | 8 +++++++- glass-easel-template-compiler/src/tree.rs | 8 +++++++- glass-easel/tests/tmpl/structure.test.ts | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/glass-easel-template-compiler/src/cbinding.rs b/glass-easel-template-compiler/src/cbinding.rs index 9f54661..6a7bdc2 100644 --- a/glass-easel-template-compiler/src/cbinding.rs +++ b/glass-easel-template-compiler/src/cbinding.rs @@ -19,7 +19,7 @@ impl StrRef { impl Drop for StrRef { fn drop(&mut self) { unsafe { - drop(slice::from_raw_parts_mut(self.buf, self.len)); + let _ = Box::from_raw(slice::from_raw_parts_mut(self.buf, self.len)); } } } @@ -329,7 +329,7 @@ impl TmplGroup { impl Drop for TmplGroup { fn drop(&mut self) { unsafe { - drop(&mut *(self.inner as *mut group::TmplGroup)); + let _ = Box::from_raw(&mut *(self.inner as *mut group::TmplGroup)); } } } diff --git a/glass-easel-template-compiler/src/expr.rs b/glass-easel-template-compiler/src/expr.rs index 75a0c18..d902c36 100644 --- a/glass-easel-template-compiler/src/expr.rs +++ b/glass-easel-template-compiler/src/expr.rs @@ -1026,9 +1026,10 @@ impl TmplExpr { scopes: &Vec, ) -> Result { let mut value = String::new(); + let level = self.level(); let (pas, sub_p) = self.to_proc_gen_rec_and_combine_paths(w, scopes, TmplExprLevel::Cond, &mut value)?; - Ok(TmplExprProcGen { pas, sub_p, value }) + Ok(TmplExprProcGen { pas, sub_p, value, level }) } // this function finds which keys can be put into the binding map, @@ -1208,6 +1209,7 @@ pub(crate) struct TmplExprProcGen { pas: PathAnalysisState, sub_p: Vec, value: String, + level: TmplExprLevel, } impl TmplExprProcGen { @@ -1249,4 +1251,8 @@ impl TmplExprProcGen { write!(w, "{}", self.value)?; Ok(()) } + + pub(crate) fn above_cond_expr(&self) -> bool { + self.level >= TmplExprLevel::Cond + } } diff --git a/glass-easel-template-compiler/src/tree.rs b/glass-easel-template-compiler/src/tree.rs index 39dd395..85efc0d 100644 --- a/glass-easel-template-compiler/src/tree.rs +++ b/glass-easel-template-compiler/src/tree.rs @@ -897,7 +897,13 @@ impl TmplElement { write!(w, "{}?{}:", gen_lit_str(&v), index + 1)?; } CondItem::Dynamic(p) => { - p.value_expr(w)?; + if p.above_cond_expr() { + w.paren(|w| { + p.value_expr(w) + })?; + } else { + p.value_expr(w)?; + } write!(w, "?{}:", index + 1)?; } } diff --git a/glass-easel/tests/tmpl/structure.test.ts b/glass-easel/tests/tmpl/structure.test.ts index 676c5e4..ffc444a 100644 --- a/glass-easel/tests/tmpl/structure.test.ts +++ b/glass-easel/tests/tmpl/structure.test.ts @@ -51,7 +51,7 @@ describe('node tree structure', () => {
a
b
c
-
d
+
d
e
f
`), From 5f172811a8145429128ecd02f15b85f62b3ab041 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Tue, 1 Aug 2023 21:40:24 +0800 Subject: [PATCH 20/29] feat: support template update (#72) --- glass-easel/src/behavior.ts | 4 +++ glass-easel/src/component.ts | 40 ++++++++++++++++++++++++++- glass-easel/src/template_engine.ts | 40 +++++++++++++++++++++++++++ glass-easel/src/tmpl/index.ts | 43 +++++++++++++++++++++-------- glass-easel/tests/core/misc.test.ts | 30 ++++++++++++++++++++ 5 files changed, 144 insertions(+), 13 deletions(-) diff --git a/glass-easel/src/behavior.ts b/glass-easel/src/behavior.ts index 74a010a..04944be 100644 --- a/glass-easel/src/behavior.ts +++ b/glass-easel/src/behavior.ts @@ -1605,6 +1605,10 @@ export class Behavior< return this._$template } + _$updateTemplate(template: { [key: string]: unknown }) { + this._$template = template + } + /** Check whether the `other` behavior is a dependent behavior of this behavior */ hasBehavior(other: string | GeneralBehavior): boolean { if (this._$unprepared) this.prepare() diff --git a/glass-easel/src/component.ts b/glass-easel/src/component.ts index e5e95a0..e9f8a92 100644 --- a/glass-easel/src/component.ts +++ b/glass-easel/src/component.ts @@ -49,7 +49,7 @@ import { getDeepCopyStrategy, } from './data_proxy' import { Relation, generateRelationDefinitionGroup, RelationDefinitionGroup } from './relation' -import { Template, TemplateEngine } from './template_engine' +import { Template, TemplateEngine, TemplateInstance } from './template_engine' import { ClassList } from './class_list' import { GeneralBackendContext, GeneralBackendElement } from './node' import { DataPath, parseSinglePath, parseMultiPaths } from './data_path' @@ -251,6 +251,22 @@ export class ComponentDefinition< return this.behavior.getComponentDependencies() } + /** + * Update the template field + * + * This method has no effect if the template engine does not support template update. + */ + updateTemplate(template: { [key: string]: unknown }) { + this.behavior._$updateTemplate(template) + if (this._$detail?.template.updateTemplate) { + this._$detail.template.updateTemplate(this.behavior as unknown as GeneralBehavior) + } else { + triggerWarning( + `The template engine of component "${this.is}" does not support template update`, + ) + } + } + isPrepared(): boolean { return !!this._$detail } @@ -376,6 +392,8 @@ export class Component< _$external: boolean shadowRoot: ShadowRoot | ExternalShadowRoot /** @internal */ + _$tmplInst: TemplateInstance | undefined + /** @internal */ _$relation: Relation | null /** @internal */ _$idPrefix: string @@ -751,6 +769,7 @@ export class Component< // init template with init data if (propEarlyInit && initPropValues !== undefined) initPropValues(comp) tmplInst.initValues(dataGroup.innerData || dataGroup.data) + comp._$tmplInst = tmplInst dataGroup.setUpdateListener(tmplInst.updateValues.bind(tmplInst)) // bind behavior listeners @@ -936,6 +955,25 @@ export class Component< return this.shadowRoot as ShadowRoot } + /** + * Apply the template updates to this component instance + * + * This method has no effect if the template engine does not support template update. + */ + applyTemplateUpdates(): void { + if (this._$tmplInst?.updateTemplate) { + const dataGroup = this._$dataGroup + this._$tmplInst.updateTemplate( + this._$definition._$detail!.template, + dataGroup.innerData || dataGroup.data, + ) + } else { + triggerWarning( + `The template engine of component "${this.is}" does not support template update`, + ) + } + } + static listProperties< TData extends DataList, TProperty extends PropertyList, diff --git a/glass-easel/src/template_engine.ts b/glass-easel/src/template_engine.ts index 91a5a2d..b961b59 100644 --- a/glass-easel/src/template_engine.ts +++ b/glass-easel/src/template_engine.ts @@ -5,17 +5,57 @@ import { DataChange, DataValue } from './data_proxy' import { NormalizedComponentOptions } from './global_options' import { ExternalShadowRoot } from './external_shadow_tree' +/** + * A template engine that handles the template part of a component + */ export interface TemplateEngine { + /** + * Preprocess a behavior and generate a preprocessed template + * + * This function is called during component prepare. + * The `_$template` field of the behavior is designed to be handled by the template engine, + * and should be preprocessed in this function. + */ create(behavior: GeneralBehavior, componentOptions: NormalizedComponentOptions): Template } +/** + * A preprocessed template + */ export interface Template { + /** + * Create a template instance for a component instance + */ createInstance(elem: GeneralComponentInstance): TemplateInstance + + /** + * Update the content of the template (optional) + * + * Implement this function if template update is needed (usually used during development). + * The behavior is always the object which used when creation. + */ + updateTemplate?(behavior: GeneralBehavior): void } +/** + * A template instance that works with a component instance + */ export interface TemplateInstance { + /** + * The shadow root of the component + * + * This field should not be changed. + */ shadowRoot: ShadowRoot | ExternalShadowRoot + /** + * Apply the updated template content (optional) + * + * Implement this function if template update is needed (usually used during development). + * The template is always the object which used when creation. + */ + updateTemplate?(template: Template, data: DataValue): void + initValues(data: DataValue): void updateValues(data: DataValue, changes: DataChange[]): void diff --git a/glass-easel/src/tmpl/index.ts b/glass-easel/src/tmpl/index.ts index 35155eb..d3f5311 100644 --- a/glass-easel/src/tmpl/index.ts +++ b/glass-easel/src/tmpl/index.ts @@ -60,18 +60,28 @@ export class GlassEaselTemplateEngine implements templateEngine.TemplateEngine { } class GlassEaselTemplate implements templateEngine.Template { - genObjectGroupEnv: ProcGenEnv - updateMode: string - disallowNativeNode: boolean + genObjectGroupEnv!: ProcGenEnv + updateMode!: string + disallowNativeNode!: boolean eventObjectFilter?: (x: ShadowedEvent) => ShadowedEvent constructor(behavior: GeneralBehavior) { - if (typeof behavior._$template !== 'object' && behavior._$template !== undefined) { + this.updateTemplate(behavior) + } + + /** + * Update the underlying template content + * + * This method does not affect created instances. + */ + updateTemplate(behavior: GeneralBehavior) { + const template = behavior._$template + if (typeof template !== 'object' && template !== undefined) { throw new Error( `Component template of ${behavior.is} must be a valid compiled template (or "null" for default template).`, ) } - const c = (behavior._$template as ComponentTemplate | null | undefined) || { + const c = (template as ComponentTemplate | null | undefined) || { content: DEFAULT_PROC_GEN_GROUP, } this.genObjectGroupEnv = { @@ -89,15 +99,26 @@ class GlassEaselTemplate implements templateEngine.Template { } class GlassEaselTemplateInstance implements templateEngine.TemplateInstance { - template: GlassEaselTemplate comp: GeneralComponent shadowRoot: ShadowRoot - procGenWrapper: ProcGenWrapper + procGenWrapper!: ProcGenWrapper + forceBindingMapUpdate!: BindingMapUpdateEnabled bindingMapGen: { [field: string]: BindingMapGen[] } | undefined - forceBindingMapUpdate: BindingMapUpdateEnabled constructor(template: GlassEaselTemplate, comp: GeneralComponent) { - this.template = template + this.comp = comp + this.shadowRoot = ShadowRoot.createShadowRoot(comp) + this.shadowRoot.destroyBackendElementOnDetach() + this._$applyTemplate(template) + } + + updateTemplate(template: GlassEaselTemplate, data: DataValue) { + this._$applyTemplate(template) + this.shadowRoot.removeChildren(0, this.shadowRoot.childNodes.length) + this.bindingMapGen = this.procGenWrapper.create(data) + } + + private _$applyTemplate(template: GlassEaselTemplate) { const procGen = template.genObjectGroupEnv.group('') || DEFAULT_PROC_GEN_GROUP('') if (template.updateMode === 'bindingMap') { this.forceBindingMapUpdate = BindingMapUpdateEnabled.Forced @@ -106,15 +127,13 @@ class GlassEaselTemplateInstance implements templateEngine.TemplateInstance { } else { this.forceBindingMapUpdate = BindingMapUpdateEnabled.Enabled } - this.comp = comp - this.shadowRoot = ShadowRoot.createShadowRoot(comp) - this.shadowRoot.destroyBackendElementOnDetach() this.procGenWrapper = new ProcGenWrapper( this.shadowRoot, procGen, template.disallowNativeNode, template.eventObjectFilter, ) + this.bindingMapGen = undefined } initValues(data: DataValue): ShadowRoot | ExternalShadowRoot { diff --git a/glass-easel/tests/core/misc.test.ts b/glass-easel/tests/core/misc.test.ts index 8aab4f6..3b31e5c 100644 --- a/glass-easel/tests/core/misc.test.ts +++ b/glass-easel/tests/core/misc.test.ts @@ -359,6 +359,36 @@ describe('component utils', () => { expect(elem.getShadowRoot()!.childNodes[0]).toBeInstanceOf(glassEasel.Component) }) + test('template content update', () => { + const compDef = componentSpace + .define() + .template(tmpl('ABC-{{num}}')) + .data(() => ({ + num: 123, + })) + .registerComponent() + const elem = glassEasel.createElement('root', compDef.general()) + glassEasel.Element.pretendAttached(elem) + const shadowRoot = elem.getShadowRoot()! + expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('ABC-123') + elem.setData({ num: 456 }) + expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('ABC-456') + + compDef.updateTemplate(tmpl('DEF-{{num}}')) + expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('ABC-456') + const elem2 = glassEasel.createElement('root', compDef.general()) + const shadowRoot2 = elem2.getShadowRoot()! + expect(shadowRoot2.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-123') + + elem.applyTemplateUpdates() + expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-456') + elem.setData({ num: 789 }) + expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-789') + + elem2.applyTemplateUpdates() + expect(shadowRoot2.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-123') + }) + test('property dash name conversion', () => { const childComp = componentSpace .define() From 2d2130457a6a80b3712540c2ba36918224d8af6b Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Thu, 3 Aug 2023 12:30:21 +0800 Subject: [PATCH 21/29] test(adapter): add test cases --- .../tests/env.test.ts | 37 ++++--------------- .../tests/selector.test.ts | 4 +- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/glass-easel-miniprogram-adapter/tests/env.test.ts b/glass-easel-miniprogram-adapter/tests/env.test.ts index 1f344f3..151ec13 100644 --- a/glass-easel-miniprogram-adapter/tests/env.test.ts +++ b/glass-easel-miniprogram-adapter/tests/env.test.ts @@ -53,49 +53,28 @@ describe('env', () => { ) }) - test('add global components', () => { + test('using behaviors', () => { const env = new MiniProgramEnv() - const globalCodeSpace = env.getGlobalCodeSpace() - const def = globalCodeSpace - .getComponentSpace() - .define('external') - .options({ - virtualHost: true, - }) - .template( - tmpl(` - - `), - ) - .registerComponent() const codeSpace = env.createCodeSpace('', true) - codeSpace.getComponentSpace().setGlobalUsingComponent('external', def) - codeSpace.addComponentStaticConfig('path/to/comp', { - usingComponents: { - external: 'external', - }, - }) + codeSpace.addComponentStaticConfig('path/to/comp', {}) codeSpace.addCompiledTemplate( 'path/to/comp', tmpl(` -
- -
- `), +
{{ num }}
+ `), ) - codeSpace.componentEnv('path/to/comp', ({ Component }) => { - Component().register() + codeSpace.componentEnv('path/to/comp', ({ Behavior, Component }) => { + const beh = Behavior({ data: { num: 123 } }) + Component().behavior(beh).register() }) const backend = new glassEasel.domlikeBackend.CurrentWindowBackendContext() const ab = env.associateBackend(backend) const root = ab.createRoot('body', codeSpace, 'path/to/comp') - expect(domHtml(root.getComponent())).toBe( - '
', - ) + expect(domHtml(root.getComponent())).toBe('
123
') }) test('multiple code spaces', () => { diff --git a/glass-easel-miniprogram-adapter/tests/selector.test.ts b/glass-easel-miniprogram-adapter/tests/selector.test.ts index 4549a88..23af76e 100644 --- a/glass-easel-miniprogram-adapter/tests/selector.test.ts +++ b/glass-easel-miniprogram-adapter/tests/selector.test.ts @@ -471,10 +471,10 @@ describe('intersection observer', () => { .register() }) - const ab = env.associateBackend() + const backendContext = new glassEasel.composedBackend.EmptyComposedBackendContext() + const ab = env.associateBackend(backendContext) const root = ab.createRoot('body', codeSpace, 'path/to/comp') glassEasel.Element.pretendAttached(root.getComponent()) - expect(domHtml(root.getComponent())).toBe('
') }) }) From 76595a3593789eee43f34c74f70af37dea9fbfbf Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Fri, 4 Aug 2023 17:39:24 +0800 Subject: [PATCH 22/29] fix: relation path resolve (#75) --- glass-easel/src/behavior.ts | 22 +++-- glass-easel/src/component.ts | 15 ++- glass-easel/src/relation.ts | 106 ++++++++-------------- glass-easel/tests/legacy/relation.test.js | 47 ++++++++++ 4 files changed, 107 insertions(+), 83 deletions(-) diff --git a/glass-easel/src/behavior.ts b/glass-easel/src/behavior.ts index 04944be..2274d1f 100644 --- a/glass-easel/src/behavior.ts +++ b/glass-easel/src/behavior.ts @@ -43,7 +43,7 @@ import { RelationListener, RelationFailedListener, } from './relation' -import { normalizeUrl, ComponentSpace, ComponentWaitingList } from './component_space' +import { ComponentSpace, ComponentWaitingList } from './component_space' import { TraitBehavior } from './trait_behaviors' import { simpleDeepCopy } from './data_utils' import { EventListener } from './event' @@ -340,6 +340,7 @@ export const matchTypeWithValue = (type: NormalizedPropertyType, value: any) => } export const normalizeRelation = ( + ownerSpace: ComponentSpace, is: string, key: string, relation: RelationParams | TraitRelationParams, @@ -376,18 +377,24 @@ export const normalizeRelation = ( return null } let target: - | string | GeneralBehavior | TraitBehavior<{ [key: string]: unknown }, { [key: string]: unknown }> - let domain: string | null = null + | null = null if (relation.target instanceof ComponentDefinition) { target = relation.target.behavior as GeneralBehavior } else if (relation.target instanceof Behavior || relation.target instanceof TraitBehavior) { target = relation.target } else { - const { domain: d, absPath } = normalizeUrl(relation.target || key, is) - target = absPath - domain = d + const path = String(relation.target || key) + const usingTarget = ownerSpace.getComponentByUrlWithoutDefault(path, is) + if (usingTarget) { + target = usingTarget.behavior + } else { + const globalTarget = ownerSpace.getGlobalUsingComponent(path) + if (typeof globalTarget === 'object' && globalTarget !== null) { + target = globalTarget.behavior + } + } } if (!target) { triggerWarning( @@ -397,7 +404,6 @@ export const normalizeRelation = ( } return { target, - domain, type, linked: checkRelationFunc(relation.linked), linkChanged: checkRelationFunc(relation.linkChanged), @@ -1585,7 +1591,7 @@ export class Behavior< for (let i = 0; i < relations.length; i += 1) { const { name: key, rel: relation } = relations[i]! if (relation === undefined || relation === null) continue - const rel = normalizeRelation(is, key, relation) + const rel = normalizeRelation(space, is, key, relation) if (rel) this._$relationMap[key] = rel } } diff --git a/glass-easel/src/component.ts b/glass-easel/src/component.ts index e9f8a92..1cd5d38 100644 --- a/glass-easel/src/component.ts +++ b/glass-easel/src/component.ts @@ -634,16 +634,21 @@ export class Component< // call init functions let initDone = false - function relationInit(def: RelationParams): RelationHandler + function relationInit(relationDef: RelationParams): RelationHandler function relationInit( - def: TraitRelationParams, + relationDef: TraitRelationParams, ): RelationHandler function relationInit( - def: RelationParams | TraitRelationParams, + relationDef: RelationParams | TraitRelationParams, ): RelationHandler { if (initDone) throw new Error('Cannot execute init-time functions after initialization') - const target = def.target - const normalizedRel = normalizeRelation(behavior.is, 'undefined', def) + const target = relationDef.target + const normalizedRel = normalizeRelation( + behavior.ownerSpace, + behavior.is, + 'undefined', + relationDef, + ) let key: symbol if (normalizedRel) { key = relation.add(normalizedRel) diff --git a/glass-easel/src/relation.ts b/glass-easel/src/relation.ts index 1e5bd50..1421790 100644 --- a/glass-easel/src/relation.ts +++ b/glass-easel/src/relation.ts @@ -1,5 +1,5 @@ import { VirtualNode } from './virtual_node' -import { Behavior, GeneralBehavior } from './behavior' +import { GeneralBehavior } from './behavior' import { Component, GeneralComponent } from './component' import { Element } from './element' import { safeCallback, triggerWarning } from './func_arr' @@ -21,11 +21,7 @@ export type RelationListener = (target: unknown) => void export type RelationFailedListener = () => void export type RelationDefinition = { - target: - | string - | GeneralBehavior - | TraitBehavior<{ [x: string]: unknown }, { [x: string]: unknown }> - domain: string | null + target: GeneralBehavior | TraitBehavior<{ [x: string]: unknown }, { [x: string]: unknown }> type: RelationType linked: RelationListener | null linkChanged: RelationListener | null @@ -162,77 +158,47 @@ export class Relation { const oldLink = links[i]! let newLink: { target: GeneralComponent; def: RelationDefinition } | null = null const def = selfDefs[i]! - let parentBehaviorTest: - | GeneralBehavior - | TraitBehavior<{ [x: string]: unknown }, { [x: string]: unknown }> - | null - if (def.target instanceof Behavior || def.target instanceof TraitBehavior) { - parentBehaviorTest = def.target - } else { - const space = comp.getRootBehavior().ownerSpace - if (space) { - parentBehaviorTest = space._$getBehavior(def.target, def.domain) || null - } else { - parentBehaviorTest = null - } - } - if (parentBehaviorTest) { - const parentBehavior = parentBehaviorTest - if (!isDetach) { - let cur: Element = comp - for (;;) { - const next = cur.parentNode - if (!next) break - cur = next - if (cur instanceof VirtualNode) { - continue - } - if (cur instanceof Component) { - if (cur.hasBehavior(parentBehavior)) { - const parentRelation = cur._$relation - if (parentRelation) { - let rt - if (parentType === RelationType.ParentComponent) { - rt = RelationType.ChildComponent - } else if (parentType === RelationType.Ancestor) { - rt = RelationType.Descendant - } else { - rt = RelationType.ChildNonVirtualNode - } - const parentDefs = parentRelation._$group?.definitions[rt] - if (parentDefs) { - for (let j = 0; j < parentDefs.length; j += 1) { - const def = parentDefs[j]! - let requiredBehavior: - | GeneralBehavior - | TraitBehavior<{ [x: string]: unknown }, { [x: string]: unknown }> - | null - if (def.target instanceof Behavior || def.target instanceof TraitBehavior) { - requiredBehavior = def.target - } else { - const space = cur.getRootBehavior().ownerSpace - if (space) { - requiredBehavior = space._$getBehavior(def.target, def.domain) || null - } else { - requiredBehavior = null - } - } - if (requiredBehavior && this._$comp.hasBehavior(requiredBehavior)) { - newLink = { - target: cur as GeneralComponent, - def, - } - break + const parentBehavior = def.target + if (!isDetach) { + let cur: Element = comp + for (;;) { + const next = cur.parentNode + if (!next) break + cur = next + if (cur instanceof VirtualNode) { + continue + } + if (cur instanceof Component) { + if (cur.hasBehavior(parentBehavior)) { + const parentRelation = cur._$relation + if (parentRelation) { + let rt + if (parentType === RelationType.ParentComponent) { + rt = RelationType.ChildComponent + } else if (parentType === RelationType.Ancestor) { + rt = RelationType.Descendant + } else { + rt = RelationType.ChildNonVirtualNode + } + const parentDefs = parentRelation._$group?.definitions[rt] + if (parentDefs) { + for (let j = 0; j < parentDefs.length; j += 1) { + const def = parentDefs[j]! + if (def.target && this._$comp.hasBehavior(def.target)) { + newLink = { + target: cur as GeneralComponent, + def, } + break } - if (newLink) break } + if (newLink) break } } - if (parentType === RelationType.ParentComponent) break } - if (parentType === RelationType.ParentNonVirtualNode) break + if (parentType === RelationType.ParentComponent) break } + if (parentType === RelationType.ParentNonVirtualNode) break } } links[i] = newLink diff --git a/glass-easel/tests/legacy/relation.test.js b/glass-easel/tests/legacy/relation.test.js index d6f7b8d..75b5f91 100644 --- a/glass-easel/tests/legacy/relation.test.js +++ b/glass-easel/tests/legacy/relation.test.js @@ -570,5 +570,52 @@ describe('Component Relations', function () { expect(elem.$.a.getRelationNodes('relation-virtual-host-b')[0]).toBe(elem.$.b) expect(elem.$.b.getRelationNodes('relation-virtual-host-a')[0]).toBe(elem.$.a) }) + + it('should support relative paths', function () { + var expectOrder = [1, 2] + regElem({ + is: 'relation/path/a', + options: { + lazyRegistration: true, + }, + template: '', + relations: { + './b': { + type: 'descendant', + linked: function (target) { + expect(expectOrder.shift()).toBe(1) + }, + }, + }, + }) + regElem({ + is: 'relation/path/b', + options: { + lazyRegistration: true, + }, + relations: { + '../path/a': { + type: 'ancestor', + linked: function (target) { + expect(expectOrder.shift()).toBe(2) + }, + }, + }, + }) + var def = regElem({ + is: 'relation/path', + using: { + 'a': './path/a', + 'b': 'path/b', + }, + template: + ' ', + }) + var elem = glassEasel.Component.createWithContext('test', def, domBackend) + glassEasel.Element.pretendAttached(elem) + expect(expectOrder.length).toBe(0) + expect(elem.$.a.getRelationNodes('./b')[0]).toBe(elem.$.b) + expect(elem.$.b.getRelationNodes('../path/a')[0]).toBe(elem.$.a) + }) }) }) From 0699e002a69eaed2e27441060a332ad1cdc11972 Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Fri, 4 Aug 2023 17:51:20 +0800 Subject: [PATCH 23/29] fix: allow setData undefined (#74) --- glass-easel/src/component.ts | 38 ++++++++++++---------- glass-easel/tests/core/data_update.test.ts | 15 +++++++++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/glass-easel/src/component.ts b/glass-easel/src/component.ts index 1cd5d38..82ce87a 100644 --- a/glass-easel/src/component.ts +++ b/glass-easel/src/component.ts @@ -677,7 +677,7 @@ export class Component< const builderContext: BuilderContext = { self: methodCaller, data, - setData: comp.setData.bind(comp) as (newData: { [x: string]: unknown }) => void, + setData: comp.setData.bind(comp) as (newData?: { [x: string]: unknown }) => void, implement: ( traitBehavior: TraitBehavior, impl: TIn, @@ -1310,18 +1310,20 @@ export class Component< * All data observers will not be triggered immediately before applied. * Reads of the data will get the unchanged value before applied. */ - updateData(newData: Partial>>): void - updateData(newData: Record): void { + updateData(newData?: Partial>>): void + updateData(newData?: Record): void { const dataProxy = this._$dataGroup if (dataProxy === undefined) { throw new Error('Cannot update data before component created') } - const keys = Object.keys(newData) - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]! - const p = parseSinglePath(key) - if (p) { - dataProxy.replaceDataOnPath(p, newData[key]) + if (typeof newData === 'object' && newData !== null) { + const keys = Object.keys(newData) + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]! + const p = parseSinglePath(key) + if (p) { + dataProxy.replaceDataOnPath(p, newData[key]) + } } } } @@ -1333,18 +1335,20 @@ export class Component< * When called inside observers, the data update will not be applied to templates. * Inside observers, it is recommended to use `updateData` instead. */ - setData(newData: Partial>>): void - setData(newData: Record): void { + setData(newData?: Partial>>): void + setData(newData?: Record | undefined): void { const dataProxy = this._$dataGroup if (dataProxy === undefined) { throw new Error('Cannot update data before component created') } - const keys = Object.keys(newData) - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]! - const p = parseSinglePath(key) - if (p) { - dataProxy.replaceDataOnPath(p, newData[key]) + if (typeof newData === 'object' && newData !== null) { + const keys = Object.keys(newData) + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]! + const p = parseSinglePath(key) + if (p) { + dataProxy.replaceDataOnPath(p, newData[key]) + } } } dataProxy.applyDataUpdates() diff --git a/glass-easel/tests/core/data_update.test.ts b/glass-easel/tests/core/data_update.test.ts index 2ab11e5..620cc7d 100644 --- a/glass-easel/tests/core/data_update.test.ts +++ b/glass-easel/tests/core/data_update.test.ts @@ -617,4 +617,19 @@ describe('partial update', () => { const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) expect(comp.data).toStrictEqual({ a: 123, b: ['a'] }) }) + + test('should allow set empty data', () => { + const compDef = componentSpace + .define() + .data(() => ({ + a: 123, + b: 'abc', + })) + .registerComponent() + const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) + comp.updateData({ a: 456 }) + expect(comp.data.a).toBe(123) + comp.setData(undefined) + expect(comp.data.a).toBe(456) + }) }) From f489d5753dd386df3f14d8001efe47ab40414271 Mon Sep 17 00:00:00 2001 From: Artin Date: Mon, 7 Aug 2023 14:29:37 +0800 Subject: [PATCH 24/29] docs: fix readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 605887a..7bb21d6 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ However, no MiniProgram code can use any _glass-easel_ submodules currently, bec No. This module is the framework itself. Components like `` `` are not included. -This is because the implamentation of the components are complex and deeply coupled with WeChat environment interfaces. We cannot simply reuse the code in web environments. +This is because the implementation of the components are complex and deeply coupled with WeChat environment interfaces. We cannot simply reuse the code in web environments. We will consider do another implementation on web in future. From 9fbc52c22c63d0eb73ee87b7d8b2e76e532e148d Mon Sep 17 00:00:00 2001 From: LastLeaf Date: Thu, 17 Aug 2023 14:12:50 +0800 Subject: [PATCH 25/29] fix(template-compiler): wrong update-path-tree check on template-ref --- glass-easel/tests/tmpl/structure.test.ts | 39 +++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/glass-easel/tests/tmpl/structure.test.ts b/glass-easel/tests/tmpl/structure.test.ts index ffc444a..7390b20 100644 --- a/glass-easel/tests/tmpl/structure.test.ts +++ b/glass-easel/tests/tmpl/structure.test.ts @@ -826,12 +826,18 @@ describe('node tree structure', () => { + `, '': ` - +