diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..ca4a791 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,77 @@ +name: deploy-pages + +on: + push: + branches: ["master"] + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + 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: 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 + 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: + path: 'github-pages' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 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..7bb21d6 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. @@ -91,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. 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() + } } 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-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 { 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('
') }) }) 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/js_bindings.rs b/glass-easel-template-compiler/src/js_bindings.rs index ca66cfd..94a44d9 100644 --- a/glass-easel-template-compiler/src/js_bindings.rs +++ b/glass-easel-template-compiler/src/js_bindings.rs @@ -14,8 +14,8 @@ pub struct TmplGroup { fn convert_str_arr(arr: &Vec) -> js_sys::Array { let ret = js_sys::Array::new_with_length(arr.len() as u32); - for item in arr { - ret.push(&JsValue::from(item)); + for (index, item) in arr.iter().enumerate() { + ret.set(index as u32, JsValue::from(item)); } ret } diff --git a/glass-easel-template-compiler/src/tree.rs b/glass-easel-template-compiler/src/tree.rs index 39dd395..9d7cff7 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)?; } } @@ -1101,9 +1107,9 @@ impl TmplElement { var_key, var_target, var_target )?; p.value_expr(w)?; - write!(w, ",U?")?; + write!(w, ",K||(U?")?; p.lvalue_state_expr(w, scopes)?; - write!(w, ":undefined).C(C,T,E,B,F,S,J)")?; + write!(w, ":undefined)).C(C,T,E,B,F,S,J)")?; Ok(()) }) } 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, }) 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/behavior.ts b/glass-easel/src/behavior.ts index 74a010a..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 } } @@ -1605,6 +1611,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/class_list.ts b/glass-easel/src/class_list.ts index 512f515..b544f0c 100644 --- a/glass-easel/src/class_list.ts +++ b/glass-easel/src/class_list.ts @@ -2,7 +2,7 @@ import * as backend from './backend/backend_protocol' import * as composedBackend from './backend/composed_backend_protocol' import * as domlikeBackend from './backend/domlike_backend_protocol' import { BM, BackendMode } from './backend/mode' -import { Element, GeneralComponent, GeneralBackendElement } from '.' +import { Element, GeneralComponent, GeneralBackendElement, StyleSegmentIndex } from '.' import { MutationObserverTarget } from './mutation_observer' const CLASS_NAME_REG_EXP = /(~|\^+)?-?[_0-9a-z][-_0-9a-z]*/gi @@ -67,6 +67,8 @@ export class ClassList { /** @internal */ private _$rawNames: string[] = [] /** @internal */ + private _$rawNamesSegmentSep: number[] = [] + /** @internal */ private _$hasResolvedNames = false /** @internal */ private _$prefixManager: StyleScopeManager | undefined @@ -347,11 +349,33 @@ export class ClassList { } /** Set class string */ - setClassNames(names: string) { + setClassNames(names: string, index: StyleSegmentIndex = 0) { let n: string if (names === undefined || names === null) n = '' else n = String(names) - this._$rawNames = n.match(CLASS_NAME_REG_EXP) || [] + const newRawNames = n.match(CLASS_NAME_REG_EXP) || [] + while (this._$rawNamesSegmentSep.length < index) { + this._$rawNamesSegmentSep.push( + this._$rawNamesSegmentSep.length > 0 + ? this._$rawNamesSegmentSep[this._$rawNamesSegmentSep.length - 1]! + : this._$rawNames.length, + ) + } + if (this._$rawNamesSegmentSep.length === 0) { + this._$rawNames = newRawNames + } else { + const segEnd = + index < this._$rawNamesSegmentSep.length + ? this._$rawNamesSegmentSep[index]! + : this._$rawNames.length + const segStart = index > 0 ? this._$rawNamesSegmentSep[index - 1]! : 0 + const segLen = segEnd - segStart + const lenDiff = newRawNames.length - segLen + for (let i = index; i < this._$rawNamesSegmentSep.length; i += 1) { + this._$rawNamesSegmentSep[i] += lenDiff + } + this._$rawNames.splice(segStart, segLen, ...newRawNames) + } this._$updateResolvedNames() const elem = this._$elem if (elem._$mutationObserverTarget) { diff --git a/glass-easel/src/component.ts b/glass-easel/src/component.ts index 645c0e8..bde3a18 100644 --- a/glass-easel/src/component.ts +++ b/glass-easel/src/component.ts @@ -48,7 +48,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' @@ -250,6 +250,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 } @@ -375,6 +391,8 @@ export class Component< _$external: boolean shadowRoot: ShadowRoot | ExternalShadowRoot /** @internal */ + _$tmplInst: TemplateInstance | undefined + /** @internal */ _$relation: Relation | null /** @internal */ _$idPrefix: string @@ -619,16 +637,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) @@ -657,7 +680,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, @@ -754,6 +777,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 @@ -939,6 +963,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, @@ -964,21 +1007,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) } /** @@ -1254,18 +1313,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]) + } } } } @@ -1277,18 +1338,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/src/component_space.ts b/glass-easel/src/component_space.ts index 9b9bdb4..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 => { @@ -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) } @@ -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 @@ -221,11 +225,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 +246,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, @@ -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/src/element.ts b/glass-easel/src/element.ts index 698192c..ce89927 100644 --- a/glass-easel/src/element.ts +++ b/glass-easel/src/element.ts @@ -39,16 +39,16 @@ import { import { SlotMode } from './shadow_root' /** - * The "style" attributes segments + * The "style" attribute and class list segments * - * This allows different modules set the "style" attribute of an element + * This allows different modules set the "style" attribute or the class list of an element * without overriding each other. - * The final "style" attribute value is the concat of all segments. - * When calling `setNodeStyle` on an element, + * The final value is the concat of all segments. + * When calling `setNodeStyle` or `setNodeClass` on an element, * a segment can be specified. */ export const enum StyleSegmentIndex { - /** The main style segment, generally managed by the template engine */ + /** The main style segment, generally managed by the template engine (or manually set) */ MAIN = 0, /** The template style segment, preserved for template engine */ TEMPLATE_EXTRA = 1, @@ -101,7 +101,7 @@ export class Element implements NodeCast { _$placeholderHandler: (() => void) | undefined /** @internal */ _$virtual: boolean - dataset: { [name: string]: unknown } | null + dataset: { [name: string]: unknown } /** @internal */ private _$marks: { [name: string]: unknown } | null /** @internal */ @@ -153,7 +153,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 @@ -371,6 +371,11 @@ export class Element implements NodeCast { return this._$virtual } + /** Set the node class */ + setNodeClass(classNames: string, index: StyleSegmentIndex = 0) { + this.classList?.setClassNames(classNames, index) + } + /** Set the node style */ setNodeStyle(styleSegment: string, index: StyleSegmentIndex = 0) { if (this._$styleSegments[index] === styleSegment) return @@ -424,6 +429,9 @@ export class Element implements NodeCast { private static checkAndCallDetached(node: Node) { const callFunc = function callFunc(node: Node) { + if (node._$destroyOnDetach) { + node.destroyBackendElement() + } if (node instanceof Element && node._$attached) { if (node instanceof Component) { node.triggerLifetime('beforeDetach', []) @@ -452,9 +460,6 @@ export class Element implements NodeCast { node._$attached = false } } - if (node._$destroyOnDetach) { - node.destroyBackendElement() - } } callFunc(node) } @@ -880,7 +885,7 @@ export class Element implements NodeCast { (BM.DOMLIKE || (BM.DYNAMIC && context.mode === BackendMode.Domlike) ? (context as domlikeBackend.Context).document.createDocumentFragment() : (context as backend.Context | composedBackend.Context).createFragment()) - sharedFrag = frag + sharedFrag = f const recNonVirtual = (c: Node) => { // since `TextNode` also has `backendElement` private field, just make it as `Element` @@ -1760,7 +1765,7 @@ export class Element implements NodeCast { } } - private static insertChildPlaceholerReplace( + private static insertChildPlaceholderReplace( parent: Element, posIndex: number, replacer: Element, @@ -1949,7 +1954,7 @@ export class Element implements NodeCast { selfReplaceWith(replaceWith: Element) { const parent = this.parentNode if (parent) { - Element.insertChildPlaceholerReplace(parent, this.parentIndex, replaceWith) + Element.insertChildPlaceholderReplace(parent, this.parentIndex, replaceWith) } } @@ -1998,7 +2003,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]) } } @@ -2008,7 +2013,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]) } } @@ -2434,7 +2439,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 1885301..05dab73 100644 --- a/glass-easel/src/element_iterator.ts +++ b/glass-easel/src/element_iterator.ts @@ -21,7 +21,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 @@ -88,19 +88,20 @@ export class ElementIterator { const nodeTypeLimit: any = this._$nodeTypeLimit const composed = this._$composed if (this._$isAncestor) { - const rec = (node: Node): boolean => { - let cur = node - for (;;) { - if (cur instanceof nodeTypeLimit) { - if (f(cur) === false) return false - } - const next = composed ? cur.getComposedParent() : cur.parentNode - if (!next) break - cur = next + 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/src/global_options.ts b/glass-easel/src/global_options.ts index 911fae1..0b0b9b9 100644 --- a/glass-easel/src/global_options.ts +++ b/glass-easel/src/global_options.ts @@ -93,6 +93,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 */ @@ -121,6 +123,7 @@ export type NormalizedComponentOptions = { reflectToAttributes: boolean writeFieldsToNode: boolean writeIdToDOM: boolean + useMethodCallerListeners: boolean idPrefixGenerator: ((this: GeneralComponent) => string) | null pureDataPattern: RegExp | null dataDeepCopy: DeepCopyKind @@ -160,6 +163,7 @@ export const globalOptions: NormalizedComponentOptions & EnvironmentOptions = { reflectToAttributes: false, writeFieldsToNode: true, writeIdToDOM: false, + useMethodCallerListeners: false, idPrefixGenerator: null, pureDataPattern: null, dataDeepCopy: DeepCopyKind.Simple, @@ -196,6 +200,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/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..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,76 +158,47 @@ export class Relation { const oldLink = links[i]! let newLink: { target: GeneralComponent; def: RelationDefinition } | null = null const def = selfDefs[i]! - let parentBeheviorTest: - | GeneralBehavior - | TraitBehavior<{ [x: string]: unknown }, { [x: string]: unknown }> - | null - if (def.target instanceof Behavior || def.target instanceof TraitBehavior) { - parentBeheviorTest = def.target - } else { - const space = comp.getRootBehavior().ownerSpace - if (space) { - parentBeheviorTest = space._$getBehavior(def.target, def.domain) || null - } else { - parentBeheviorTest = null - } - } - if (parentBeheviorTest) { - const parentBehevior = parentBeheviorTest - 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(parentBehevior)) { - 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 (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/src/shadow_root.ts b/glass-easel/src/shadow_root.ts index 9b022b6..02a1926 100644 --- a/glass-easel/src/shadow_root.ts +++ b/glass-easel/src/shadow_root.ts @@ -251,7 +251,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, @@ -385,7 +386,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 { diff --git a/glass-easel/src/template_engine.ts b/glass-easel/src/template_engine.ts index 09afec6..485ddc0 100644 --- a/glass-easel/src/template_engine.ts +++ b/glass-easel/src/template_engine.ts @@ -5,17 +5,57 @@ import { NormalizedComponentOptions } from './global_options' import { ExternalShadowRoot } from './external_shadow_tree' import { GeneralComponent } from './component' +/** + * 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: GeneralComponent): 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 4f1a328..13d2f91 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/src/tmpl/proc_gen_wrapper.ts b/glass-easel/src/tmpl/proc_gen_wrapper.ts index 6603fff..9392ba3 100644 --- a/glass-easel/src/tmpl/proc_gen_wrapper.ts +++ b/glass-easel/src/tmpl/proc_gen_wrapper.ts @@ -894,10 +894,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/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) + }) }) diff --git a/glass-easel/tests/core/misc.test.ts b/glass-easel/tests/core/misc.test.ts index 0ae701a..3b31e5c 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, @@ -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( { @@ -338,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() 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: '' }) 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) }) }) diff --git a/glass-easel/tests/legacy/external.test.js b/glass-easel/tests/legacy/external.test.js index c0cf425..b072221 100644 --- a/glass-easel/tests/legacy/external.test.js +++ b/glass-easel/tests/legacy/external.test.js @@ -526,6 +526,32 @@ describe('Element', function () { }) }) + it('should support class segments', function () { + var e = createElem('element-a') + e.setNodeClass('c1 c2') + expect(e.$$.classList.value).toBe('c1 c2') + e.setNodeClass('e1 e2', 2) + expect(e.$$.classList.value).toBe('c1 c2 e1 e2') + e.setNodeClass('d1', 1) + expect(e.$$.classList.value).toBe('c1 c2 d1 e1 e2') + e.setNodeClass('') + expect(e.$$.classList.value).toBe('d1 e1 e2') + e.setNodeClass('c1 c2 c3') + expect(e.$$.classList.value).toBe('c1 c2 c3 d1 e1 e2') + e.setNodeClass('d1 d2', 1) + expect(e.$$.classList.value).toBe('c1 c2 c3 d1 d2 e1 e2') + e.setNodeClass('', 1) + expect(e.$$.classList.value).toBe('c1 c2 c3 e1 e2') + e.setNodeClass('e1', 2) + expect(e.$$.classList.value).toBe('c1 c2 c3 e1') + e.setNodeClass('d1 d2 d3', 1) + expect(e.$$.classList.value).toBe('c1 c2 c3 d1 d2 d3 e1') + e.setNodeClass('e1 e2', 2) + expect(e.$$.classList.value).toBe('c1 c2 c3 d1 d2 d3 e1 e2') + e.setNodeClass('', 2) + expect(e.$$.classList.value).toBe('c1 c2 c3 d1 d2 d3') + }) + it('should support style segments', function () { var e = createElem('element-a') e.setNodeStyle('color: red') diff --git a/glass-easel/tests/legacy/relation.test.js b/glass-easel/tests/legacy/relation.test.js index c5f61cb..75b5f91 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', @@ -517,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) + }) }) }) 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 7a41f2f..a85ebea 100644 --- a/glass-easel/tests/tmpl/structure.test.ts +++ b/glass-easel/tests/tmpl/structure.test.ts @@ -52,7 +52,7 @@ describe('node tree structure', () => {
a
b
c
-
d
+
d
e
f
`), @@ -272,7 +272,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) @@ -520,7 +520,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) @@ -830,12 +830,18 @@ describe('node tree structure', () => { + `, '': ` - +