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()