import { tmpl, composedBackend, domBackend, shadowBackend } from '../base/env' import * as glassEasel from '../../src' import { virtual as matchElementWithDom } from '../base/match' const domHtml = (elem: glassEasel.Element): string => { const domElem = elem.getBackendElement() as unknown as Element return domElem.innerHTML } type FirstArgument = T extends (...args: infer R) => any ? R[0] : never type ComponentWaitingListener = Exclude< FirstArgument, null > const testCases = (testBackend: glassEasel.GeneralBackendContext) => { const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, writeIdToDOM: true, }) componentSpace.defineComponent({ is: '', }) test('using simple placeholder and waiting', () => { const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, writeIdToDOM: true, }) componentSpace.setGlobalUsingComponent('', componentSpace.defineComponent({ is: '' })) // eslint-disable-next-line @typescript-eslint/no-empty-function const listener = jest.fn((() => {}) as ComponentWaitingListener) componentSpace.setComponentWaitingListener(listener) const def = componentSpace .define() .placeholders({ child: '', }) .definition({ using: { child: 'placeholder/simple/child', 'child-another': 'placeholder/simple/child', }, template: tmpl(`
`), }) .registerComponent() const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) expect(listener).toHaveBeenCalledTimes(1) expect(listener).toHaveBeenNthCalledWith(1, false, 'placeholder/simple/child', elem) expect(domHtml(elem)).toBe('
') matchElementWithDom(elem) componentSpace.defineComponent({ is: 'placeholder/simple/child', template: tmpl('child
'), }) expect(domHtml(elem)).toBe('
child
') matchElementWithDom(elem) elem.setData({ b: true, }) expect(domHtml(elem)).toBe( '
child
', ) matchElementWithDom(elem) }) test('using another component as placeholder', () => { const componentSpace = new glassEasel.ComponentSpace() const viewDef = componentSpace.define('view').registerComponent() componentSpace.setGlobalUsingComponent('view', viewDef) const def = componentSpace .define() .placeholders({ child: 'view', }) .definition({ using: { child: 'placeholder/simple/child', }, template: tmpl('test'), }) .registerComponent() const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) expect(domHtml(elem)).toBe('test') matchElementWithDom(elem) }) test('group register other components as placeholders', () => { const componentSpace = new glassEasel.ComponentSpace() componentSpace.setGlobalUsingComponent('', componentSpace.defineComponent({ is: '' })) const def = componentSpace .define() .placeholders({ parent: '', }) .definition({ using: { parent: 'parent', }, template: tmpl(''), }) .registerComponent() const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) 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.setGlobalUsingComponent('', mainCs.defineComponent({ is: '' })) const extraCs = new glassEasel.ComponentSpace() mainCs.importSpace('space://extra', extraCs, false) mainCs.importSpace('space-private://extra', extraCs, true) // eslint-disable-next-line @typescript-eslint/no-empty-function const listener = jest.fn((() => {}) as ComponentWaitingListener) extraCs.setComponentWaitingListener(listener) const def = mainCs.defineComponent({ using: { child: 'space://extra/child-pub', 'child-private': 'space-private://extra/child', }, placeholders: { child: '', 'child-private': '', }, template: tmpl(` `), }) const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) expect(listener).toHaveBeenCalledTimes(2) expect(listener).toHaveBeenNthCalledWith(1, true, 'child-pub', elem) expect(listener).toHaveBeenNthCalledWith(2, false, 'child', elem) expect(domHtml(elem)).toBe('') matchElementWithDom(elem) extraCs.defineComponent({ is: 'child', template: tmpl('A'), }) expect(domHtml(elem)).toBe('A') matchElementWithDom(elem) extraCs.exportComponent('child-pub', 'child') expect(domHtml(elem)).toBe('AA') matchElementWithDom(elem) }) test('using native node as placeholder', () => { const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, writeIdToDOM: true, }) componentSpace.defineComponent({ is: '', }) componentSpace.setGlobalUsingComponent('span', 'span') // eslint-disable-next-line @typescript-eslint/no-empty-function const listener = jest.fn((() => {}) as ComponentWaitingListener) componentSpace.setComponentWaitingListener(listener) const def = componentSpace .define() .placeholders({ child: 'span', }) .definition({ using: { child: 'placeholder/simple/child', }, template: tmpl(` A `), }) .registerComponent() const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) expect(listener).toHaveBeenCalledTimes(1) expect(listener).toHaveBeenNthCalledWith(1, false, 'placeholder/simple/child', elem) expect(domHtml(elem)).toBe('A') matchElementWithDom(elem) componentSpace.defineComponent({ is: 'placeholder/simple/child', template: tmpl('B'), }) expect(domHtml(elem)).toBe('AB') matchElementWithDom(elem) }) test('using component with placeholder as generic', () => { const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, writeIdToDOM: true, }) componentSpace.defineComponent({ is: '', }) componentSpace.setGlobalUsingComponent('span', 'span') const def = componentSpace .define() .placeholders({ child: 'span', 'child-of-child': 'span', }) .definition({ using: { child: 'placeholder/simple/child', 'child-of-child': 'placeholder/simple/child-of-child', }, template: tmpl(` A `), }) .registerComponent() const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) expect(domHtml(elem)).toBe('A') matchElementWithDom(elem) componentSpace.defineComponent({ is: 'placeholder/simple/child', generics: { g: true, }, template: tmpl('B'), }) expect(domHtml(elem)).toBe('AB') matchElementWithDom(elem) componentSpace.defineComponent({ is: 'placeholder/simple/child-of-child', template: tmpl('C'), }) expect(domHtml(elem)).toBe('ABC') matchElementWithDom(elem) }) test('component property should update when replaced', () => { const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, }) componentSpace.defineComponent({ is: '', }) componentSpace.setGlobalUsingComponent('span', 'span') const def = componentSpace .define() .placeholders({ child: 'span', c: 'span', }) .definition({ using: { child: 'placeholder/simple/child', c: 'placeholder/simple/c', }, data: { prop: 'old', arr: ['1'], }, template: tmpl(` A {{index}} `), }) .registerComponent() const elem = glassEasel.Component.createWithContext('root', def, testBackend) expect(domHtml(elem)).toBe('A0') matchElementWithDom(elem) elem.setData({ prop: 'new', }) expect(domHtml(elem)).toBe('A0') matchElementWithDom(elem) const childDef = componentSpace.defineComponent({ is: 'placeholder/simple/child', properties: { prop: glassEasel.NormalizedPropertyType.String, }, template: tmpl('{{ prop }}'), }) const child = (elem.$.child as glassEasel.GeneralComponent).asInstanceOf(childDef)! expect(domHtml(elem)).toBe('new0') expect(child.data.prop).toBe('new') elem.setData({ arr: ['1', '2'], }) expect(domHtml(elem)).toBe('new01') matchElementWithDom(elem) const cDef = componentSpace.defineComponent({ is: 'placeholder/simple/c', properties: { prop: glassEasel.NormalizedPropertyType.String, }, template: tmpl('{{ prop }}'), }) const c0 = (elem.$['c-0'] as glassEasel.GeneralComponent).asInstanceOf(cDef)! const c1 = (elem.$['c-1'] as glassEasel.GeneralComponent).asInstanceOf(cDef)! expect(domHtml(elem)).toBe('new12') expect(c0.data.prop).toBe('1') expect(c0.dataset.index).toBe(0) expect(c1.data.prop).toBe('2') expect(c1.dataset.index).toBe(1) matchElementWithDom(elem) }) test('trigger lifetimes when replacing', () => { const callOrder: number[] = [] const placeholder = componentSpace.defineComponent({ properties: { n: Number, }, template: tmpl('{{n + 1}}'), lifetimes: { created() { callOrder.push(1) }, attached() { callOrder.push(2) }, detached() { callOrder.push(3) }, moved() { callOrder.push(0) }, }, }) componentSpace.defineComponent({ is: 'placeholder/lifetime/a', lifetimes: { attached() { callOrder.push(7) }, detached() { callOrder.push(0) }, moved() { callOrder.push(8) }, }, }) const def = componentSpace.defineComponent({ is: 'placeholder/lifetime/parent', using: { child: 'child', a: '../lifetime/a', placeholder: placeholder.general(), }, placeholders: { child: 'placeholder', }, template: tmpl(` `), }) const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) expect(callOrder).toStrictEqual([1]) glassEasel.Element.pretendAttached(elem) expect(callOrder).toStrictEqual([1, 2, 7]) expect(domHtml(elem)).toBe('3') matchElementWithDom(elem) callOrder.splice(0, 99) componentSpace.defineComponent({ is: 'placeholder/lifetime/child', properties: { n: String, }, template: tmpl('
{{n + 1}}
'), lifetimes: { created() { callOrder.push(4) }, attached() { callOrder.push(5) }, detached() { callOrder.push(0) }, moved() { callOrder.push(6) }, }, }) expect(callOrder).toStrictEqual([4, 3, 5, 8]) expect(domHtml(elem)).toBe('
21
') matchElementWithDom(elem) }) test('replacing virtual host component', () => { const placeholder = componentSpace.defineComponent({ options: { virtualHost: true, }, template: tmpl('
placeholder
'), }) const card = componentSpace.defineComponent({ template: tmpl(`
`), }) const def = componentSpace.defineComponent({ is: 'placeholder/ttt/parent', using: { child: 'child', placeholder: placeholder.general(), card, }, placeholders: { child: 'placeholder', }, template: tmpl(` content `), }) const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend) matchElementWithDom(elem) componentSpace.defineComponent({ is: 'placeholder/ttt/child', options: { virtualHost: true, }, template: tmpl('
actual
'), }) matchElementWithDom(elem) }) test('replacing after data changed', () => { const cs = new glassEasel.ComponentSpace() const Root = cs.defineComponent({ using: { child: 'child', }, placeholders: { child: 'div', }, template: tmpl(` `), data: { list: [] as number[], }, }) const elem = glassEasel.Component.createWithContext('root', Root, testBackend) glassEasel.Element.pretendAttached(elem) elem.setData({ list: [0, 0] }) expect(domHtml(elem)).toBe('
') matchElementWithDom(elem) elem.setData({ list: [1, 2, 3, 4] }) expect(domHtml(elem)).toBe( '
', ) matchElementWithDom(elem) cs.defineComponent({ is: 'child', options: { virtualHost: true, }, template: tmpl(`
{{item}}
`), properties: { item: Number, }, }) expect(domHtml(elem)).toBe('
1
2
3
4
') matchElementWithDom(elem) }) test('replacing dynamic slotting component after data changed', () => { const cs = new glassEasel.ComponentSpace() const Tmp = cs.defineComponent({ options: { virtualHost: true, dynamicSlots: true, }, properties: { item: Number, }, template: tmpl(`
{{item}}
`), }) const Root = cs.defineComponent({ using: { child: 'child', tmp: Tmp, }, placeholders: { child: 'tmp', }, template: tmpl(` {{item}}-{{i}} `), data: { list: [] as number[], }, }) const elem = glassEasel.Component.createWithContext('root', Root, testBackend) glassEasel.Element.pretendAttached(elem) elem.setData({ list: [0, 0] }) expect(domHtml(elem)).toBe('
0
0
') matchElementWithDom(elem) elem.setData({ list: [1, 2, 3, 4] }) expect(domHtml(elem)).toBe( '
1
2
3
4
', ) matchElementWithDom(elem) cs.defineComponent({ is: 'child', options: { virtualHost: true, dynamicSlots: true, }, template: tmpl(`
{{item}}-
`), properties: { item: Number, }, }) expect(domHtml(elem)).toBe('
1-1-1
2-2-2
3-3-3
4-4-4
') matchElementWithDom(elem) }) test('binding map update after replacing', () => { const cs = new glassEasel.ComponentSpace() const Root = cs.defineComponent({ using: { child: 'child', }, placeholders: { child: 'div', }, template: tmpl(` `), data: { visible: false, }, }) const elem = glassEasel.Component.createWithContext('root', Root, testBackend) glassEasel.Element.pretendAttached(elem) expect(domHtml(elem)).toBe('
') matchElementWithDom(elem) // update something elem.setData({ a: 1, b: 2 } as any) const callOrder: boolean[] = [] cs.defineComponent({ is: 'child', template: tmpl(`
{{visible}}
`), properties: { visible: { type: Boolean, observer: (val: boolean) => { callOrder.push(val) }, }, }, }) expect(callOrder).toStrictEqual([]) expect(domHtml(elem)).toBe('
false
') matchElementWithDom(elem) elem.setData({ visible: true }) expect(callOrder).toStrictEqual([true]) expect(domHtml(elem)).toBe('
true
') matchElementWithDom(elem) }) } describe('placeholder (DOM backend)', () => testCases(domBackend)) describe('placeholder (shadow backend)', () => testCases(shadowBackend)) describe('placeholder (composed backend)', () => testCases(composedBackend))