Skip to content

Commit

Permalink
feat: support template update (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
LastLeaf committed Aug 1, 2023
1 parent b0539f4 commit 5f17281
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 13 deletions.
4 changes: 4 additions & 0 deletions glass-easel/src/behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
40 changes: 39 additions & 1 deletion glass-easel/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -376,6 +392,8 @@ export class Component<
_$external: boolean
shadowRoot: ShadowRoot | ExternalShadowRoot
/** @internal */
_$tmplInst: TemplateInstance | undefined
/** @internal */
_$relation: Relation | null
/** @internal */
_$idPrefix: string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions glass-easel/src/template_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 31 additions & 12 deletions glass-easel/src/tmpl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>) => ShadowedEvent<unknown>

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 = {
Expand All @@ -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
Expand All @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions glass-easel/tests/core/misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 5f17281

Please sign in to comment.