import { domBackend, execWithWarn, tmpl } from '../base/env' import * as glassEasel from '../../src' const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, writeIdToDOM: true, }) componentSpace.defineComponent({ is: '', }) const domHtml = (elem: glassEasel.Element): string => { const domElem = elem.getBackendElement() as unknown as Element return domElem.innerHTML } describe('partial update', () => { test('should be able to merge updates', () => { 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.applyDataUpdates() expect(comp.data.a).toBe(456) expect(comp.data.b).toBe('abc') comp.groupUpdates(() => { comp.updateData({ b: 'def' }) comp.updateData({ a: 789 }) }) expect(comp.data).toStrictEqual({ a: 789, b: 'def' }) }) test('should be able to set self properties or receive properties', () => { let execArr = [] as string[] const itemComp = glassEasel .registerElement({ options: { propertyEarlyInit: true, }, properties: { s: { type: String, observer() { execArr.push('A') }, }, }, observers: { s() { execArr.push('B') }, }, template: tmpl(`{{s}}`), }) .general() const def = glassEasel.registerElement({ options: { propertyEarlyInit: true, }, using: { 'x-c': itemComp, }, properties: { list: { type: Array, default: () => [ { k: 'a', v: 10 }, { k: 'b', v: 20 }, ], observer() { execArr.push('C') }, }, }, observers: { list() { execArr.push('D') }, }, template: tmpl(` `), }) const elem = glassEasel.Component.createWithContext('root', def, domBackend) glassEasel.Element.pretendAttached(elem) expect(domHtml(elem)).toBe('1020') expect(execArr).toStrictEqual(['B', 'A', 'B', 'A']) execArr = [] ;(elem.data.list[0]!.v as any) = 30 elem.setData({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment list: elem.data.list as any, }) expect(domHtml(elem)).toBe('3020') expect(execArr).toStrictEqual(['D', 'B', 'A', 'B']) execArr = [] elem.setData({ list: [] }) expect(domHtml(elem)).toBe('') expect(execArr).toStrictEqual(['D', 'C']) }) test('should be able to replace subfields', () => { const execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .lifetime('attached', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ obj: { a: 123 }, })) .methods({ setA() { this.groupUpdates(() => { this.replaceDataOnPath(['obj', 'a'], 456) }) }, }) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const child = comp.getShadowRoot()!.getElementById('c')!.asInstanceOf(childCompDef)! expect(comp.data.obj.a).toBe(123) expect(child.data.p).toBe('123') comp.setA() expect(comp.data.obj.a).toBe(456) expect(child.data.p).toBe('456') comp.replaceDataOnPath(['obj'], { a: 789 }) expect(comp.data.obj.a).toBe(456) expect(child.data.p).toBe('456') comp.applyDataUpdates() expect(comp.data.obj.a).toBe(789) expect(child.data.p).toBe('789') expect(execArr).toStrictEqual(['123']) }) test('should be able to update list fields (without key)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: ['A', 'B', 'C'], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1], 'D') }) expect(getP()).toStrictEqual(['A', 'D', 'C']) expect(execArr).toStrictEqual(['D']) }) test('should be able to update list fields (with key)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: [ { k: 1, v: 'A' }, { k: 2, v: 'B' }, { k: 2, v: 'C' }, ], })) .registerComponent() const comp = execWithWarn(1, () => glassEasel.Component.createWithContext('root', compDef, domBackend), ) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 0, 'v'], 'D') }) }) expect(getP()).toStrictEqual(['D', 'B', 'C']) expect(execArr).toStrictEqual(['D']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'v'], 'E') }) }) expect(getP()).toStrictEqual(['D', 'E', 'C']) expect(execArr).toStrictEqual(['E']) }) test('should be able to update list (full list update)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: [ { k: 1, v: 'A' }, { k: 2, v: 'B' }, { k: 3, v: 'C' }, { k: 4, v: 'D' }, ], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C', 'D']) expect(execArr).toStrictEqual(['A', 'B', 'C', 'D']) execArr = [] comp.setData({ list: [ { k: 3, v: 'C' }, { k: 5, v: 'E' }, { k: 1, v: 'A' }, { k: 6, v: 'F' }, ], }) expect(getP()).toStrictEqual(['C', 'E', 'A', 'F']) expect(execArr).toStrictEqual(['E', 'F', 'C', 'A']) }) test('should be able to update list keys', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: [ { k: 1, v: 'A' }, { k: 2, v: 'B' }, { k: 2, v: 'C' }, ], })) .registerComponent() const comp = execWithWarn(1, () => glassEasel.Component.createWithContext('root', compDef, domBackend), ) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'k'], 1) }) }) expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['B', 'A', 'C']) execArr = [] comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'k'], 3) }) expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['B', 'A']) execArr = [] comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 2, 'k'], 4) }) expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['C']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'k'], 4) }) }) expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['C', 'B']) }) test('should be able to do list-splice update (without key)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: ['A', 'B', 'C'], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 1, ['D', 'Z']) comp.spliceArrayDataOnPath(['list'], 2, 1, ['E']) }) expect(getP()).toStrictEqual(['A', 'D', 'E', 'C']) expect(execArr).toStrictEqual(['A', 'D', 'E', 'C']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 100, 0, ['F']) }) expect(getP()).toStrictEqual(['A', 'D', 'E', 'C', 'F']) expect(execArr).toStrictEqual(['A', 'D', 'E', 'C', 'F']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 3, 3, []) }) expect(getP()).toStrictEqual(['A', 'D', 'E']) expect(execArr).toStrictEqual(['A', 'D', 'E']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 0, 2, ['G']) }) expect(getP()).toStrictEqual(['G', 'E']) expect(execArr).toStrictEqual(['G', 'E']) }) test('should be able to do list-splice update (with invalid key)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: ['A', 'B', 'C'], })) .registerComponent() const comp = execWithWarn(1, () => glassEasel.Component.createWithContext('root', compDef, domBackend), ) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 1, ['D', 'Z']) comp.spliceArrayDataOnPath(['list'], 2, 1, ['E']) }) }) expect(getP()).toStrictEqual(['A', 'D', 'E', 'C']) expect(execArr).toStrictEqual(['C', 'A', 'D', 'E']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 100, 0, ['F']) }) }) expect(getP()).toStrictEqual(['A', 'D', 'E', 'C', 'F']) expect(execArr).toStrictEqual(['F', 'A', 'D', 'E', 'C']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 3, 3, []) }) }) expect(getP()).toStrictEqual(['A', 'D', 'E']) expect(execArr).toStrictEqual(['A', 'D', 'E']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 0, 2, ['G']) }) }) expect(getP()).toStrictEqual(['G', 'E']) expect(execArr).toStrictEqual(['G', 'E']) }) test('should be able to do list-splice update (with key)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: [ { k: 1, v: 'A' }, { k: 2, v: 'B' }, { k: 3, v: 'C' }, ], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 1, [ { k: 4, v: 'D' }, { k: 5, v: 'Z' }, ]) comp.spliceArrayDataOnPath(['list'], 2, 1, [{ k: 5, v: 'E' }]) }) expect(getP()).toStrictEqual(['A', 'D', 'E', 'C']) expect(execArr).toStrictEqual(['D', 'E']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 100, 0, [{ k: 6, v: 'F' }]) }) expect(getP()).toStrictEqual(['A', 'D', 'E', 'C', 'F']) expect(execArr).toStrictEqual(['F']) execArr = [] comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'v'], 'DD') comp.spliceArrayDataOnPath(['list'], 3, 3, []) }) expect(getP()).toStrictEqual(['A', 'DD', 'E']) expect(execArr).toStrictEqual(['DD']) execArr = [] comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'v'], 'D') comp.spliceArrayDataOnPath(['list'], 0, 0, [{ k: 3, v: 'C' }]) }) expect(getP()).toStrictEqual(['C', 'A', 'D', 'E']) expect(execArr).toStrictEqual(['C', 'D']) execArr = [] comp.groupUpdates(() => { comp.replaceDataOnPath(['list', 1, 'v'], 'DD') comp.spliceArrayDataOnPath(['list'], 0, 2, [{ k: 7, v: 'G' }]) }) expect(getP()).toStrictEqual(['G', 'D', 'E']) expect(execArr).toStrictEqual(['G']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], -1, 100, [{ k: 7, v: 'GG' }]) }) }) expect(getP()).toStrictEqual(['G', 'D', 'E', 'GG']) expect(execArr).toStrictEqual(['GG', 'G']) execArr = [] execWithWarn(1, () => { comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 1, []) }) }) expect(getP()).toStrictEqual(['G', 'E', 'GG']) expect(execArr).toStrictEqual(['G', 'GG']) }) test('should be able to do list-splice update (with key and using index)', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', String) .observer('p', function () { execArr.push(this.data.p) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ list: [ { k: 1, v: 'A' }, { k: 2, v: 'B' }, { k: 3, v: 'C' }, ], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asInstanceOf(childCompDef)!.data.p) }) return ret } expect(getP()).toStrictEqual(['0:A', '1:B', '2:C']) expect(execArr).toStrictEqual(['0:A', '1:B', '2:C']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 1, [{ k: 4, v: 'D' }]) }) expect(getP()).toStrictEqual(['0:A', '1:D', '2:C']) expect(execArr).toStrictEqual(['1:D']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 0, [{ k: 5, v: 'E' }]) }) expect(getP()).toStrictEqual(['0:A', '1:E', '2:D', '3:C']) expect(execArr).toStrictEqual(['1:E', '2:D', '3:C']) execArr = [] comp.groupUpdates(() => { comp.spliceArrayDataOnPath(['list'], 1, 2, []) }) expect(getP()).toStrictEqual(['0:A', '1:C']) expect(execArr).toStrictEqual(['1:C']) }) test('should be able to update list with invalid key', () => { const compDef = componentSpace .define() .template( tmpl(` {{ item.content }} `), ) .data(() => ({ list: [{ content: 'A' }, { content: 'B' }, { content: 'C' }], })) .registerComponent() const comp = execWithWarn(1, () => glassEasel.Component.createWithContext('root', compDef, domBackend), ) glassEasel.Element.pretendAttached(comp) const getP = () => { const ret = [] as string[] comp .getShadowRoot()! .childNodes[0]!.asElement()! .childNodes.forEach((child) => { ret.push(child.asElement()!.childNodes[0]!.asTextNode()!.textContent) }) return ret } expect(getP()).toStrictEqual(['A', 'B', 'C']) execWithWarn(1, () => comp.setData({ list: [{ content: 'A' }, { content: 'B' }, { content: 'C' }, { content: 'D' }], }), ) expect(getP()).toStrictEqual(['A', 'B', 'C', 'D']) execWithWarn(1, () => comp.setData({ list: [{ content: 'A' }, { content: 'B' }], }), ) expect(getP()).toStrictEqual(['A', 'B']) execWithWarn(1, () => comp.setData({ list: [{ content: 'B' }, { content: 'A' }, { content: 'C' }], }), ) expect(getP()).toStrictEqual(['B', 'A', 'C']) }) test('should update length while list partial update', () => { const compDef = componentSpace .define() .template( tmpl(`
{{ arr.length }}
`), ) .data(() => ({ arr: [{ a: 12 }, { a: 34 }, { a: 56 }] as { a: number }[], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const a = comp.getShadowRoot()!.childNodes[0]!.asElement()! expect(a.dataset.p).toBe(3) expect(a.dataset.q).toBeUndefined() expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('3') comp.setData({ 'arr[4]': { a: 78 } }) expect(a.dataset.p).toBe(5) expect(a.dataset.q).toBe(78) expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('5') comp.spliceArrayDataOnPath(['arr'], 1, 3, [{ a: 43 }, { a: 65 }]) comp.applyDataUpdates() expect(a.dataset.p).toBe(4) expect(a.dataset.q).toBeUndefined() expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('4') comp.replaceDataOnPath(['arr', 4, 'a'], 89) comp.spliceArrayDataOnPath(['arr'], 100, 0, [{ a: 123 }]) comp.applyDataUpdates() expect(a.dataset.p).toBe(6) expect(a.dataset.q).toBe(89) expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('6') comp.setData({ 'arr[9].a': 98 }) expect(a.dataset.p).toBe(10) expect(a.dataset.q).toBe(89) expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('10') }) test('should update length while list partial update (without inner data)', () => { const compDef = componentSpace .define() .options({ dataDeepCopy: glassEasel.DeepCopyKind.None, }) .template( tmpl(`
{{ arr.length }}
`), ) .data(() => ({ arr: [{ a: 12 }, { a: 34 }, { a: 56 }] as { a: number }[], })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) const a = comp.getShadowRoot()!.childNodes[0]!.asElement()! expect(a.dataset.p).toBe(3) expect(a.dataset.q).toBeUndefined() expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('3') comp.setData({ 'arr[4]': { a: 78 } }) expect(a.dataset.p).toBe(5) expect(a.dataset.q).toBe(78) expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('5') comp.spliceArrayDataOnPath(['arr'], 1, 3, [{ a: 43 }, { a: 65 }]) comp.applyDataUpdates() expect(a.dataset.p).toBe(4) expect(a.dataset.q).toBeUndefined() expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('4') comp.spliceArrayDataOnPath(['arr'], 1, 2, []) comp.replaceDataOnPath(['arr', 3, 'a'], 89) comp.spliceArrayDataOnPath(['arr'], 1, 0, [{ a: 123 }]) comp.applyDataUpdates() expect(a.dataset.p).toBe(5) expect(a.dataset.q).toBe(89) expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('5') comp.setData({ 'arr[9].a': 98 }) expect(a.dataset.p).toBe(10) expect(a.dataset.q).toBe(89) expect(a.childNodes[0]!.asTextNode()!.textContent).toBe('10') }) test('should iterate Object entries', () => { const compDef = componentSpace .define() .data(() => ({ obj: { arr: [123], }, })) .template( tmpl(` {{ item[0] }} -{{ item[0] }} `), ) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) const shadowRoot = comp.shadowRoot as glassEasel.ShadowRoot expect(shadowRoot.getComposedChildren()[2]!.asTextNode()!.textContent).toBe('123') expect(shadowRoot.getComposedChildren()[5]!.asTextNode()!.textContent).toBe('-123') comp.setData({ 'obj.arr[0]': 456, }) expect(shadowRoot.getComposedChildren()[2]!.asTextNode()!.textContent).toBe('456') expect(shadowRoot.getComposedChildren()[5]!.asTextNode()!.textContent).toBe('-456') }) test('should support custom property value comparer', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .property('p', { type: String, value: 'def', comparer(newValue, oldValue) { execArr.push('A') if (newValue === 'abc') return false if (newValue === 'ghi') return true return newValue !== oldValue }, observer() { execArr.push('C') }, }) .observer('p', () => { execArr.push('B') }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ p: 'def', })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) const child = comp.getShadowRoot()!.childNodes[0]!.asInstanceOf(childCompDef)! expect(execArr).toStrictEqual(['A', 'B']) execArr = [] glassEasel.Element.pretendAttached(comp) comp.setData({ p: '' }) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] comp.setData({ p: 'abc' }) expect(execArr).toStrictEqual(['A', 'B']) execArr = [] comp.setData({ p: '' }) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] comp.setData({ p: 'ghi' }) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] comp.setData({ p: 'ghi' }) expect(execArr).toStrictEqual(['A', 'B', 'C']) execArr = [] child.setData({ p: 'ghi' }) expect(execArr).toStrictEqual(['A', 'B', 'C']) }) test('should support global property value comparer', () => { let execArr = [] as string[] const childCompDef = componentSpace .define() .options({ propertyComparer: (newValue, oldValue) => { execArr.push('A') return newValue !== oldValue }, }) .property('p', { type: String, value: 'def', comparer: (newValue, oldValue) => { execArr.push('B') if (newValue === 'abc') return false if (newValue === 'ghi') return true return newValue !== oldValue }, observer() { execArr.push('C') }, }) .property('pp', { type: String, value: 'def', observer() { execArr.push('D') }, }) .observer('p', () => { execArr.push('E') }) .observer('pp', () => { execArr.push('F') }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ p: 'def', pp: 'def', })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) const child = comp.getShadowRoot()!.childNodes[0]!.asInstanceOf(childCompDef)! expect(execArr).toStrictEqual(['B', 'A', 'E', 'F']) execArr = [] glassEasel.Element.pretendAttached(comp) comp.setData({ p: '' }) expect(execArr).toStrictEqual(['B', 'E', 'C']) execArr = [] comp.setData({ p: 'abc' }) expect(execArr).toStrictEqual(['B', 'E']) execArr = [] comp.setData({ p: '' }) expect(execArr).toStrictEqual(['B', 'E', 'C']) execArr = [] comp.setData({ p: 'ghi' }) expect(execArr).toStrictEqual(['B', 'E', 'C']) execArr = [] comp.setData({ p: 'ghi' }) expect(execArr).toStrictEqual(['B', 'E', 'C']) execArr = [] child.setData({ p: 'ghi' }) expect(execArr).toStrictEqual(['B', 'E', 'C']) execArr = [] comp.setData({ pp: 'abc' }) expect(execArr).toStrictEqual(['A', 'F', 'D']) execArr = [] comp.setData({ pp: 'abc' }) expect(execArr).toStrictEqual(['A', 'F']) execArr = [] child.setData({ pp: 'abc' }) expect(execArr).toStrictEqual(['A', 'F']) execArr = [] comp.setData({ p: 'abc', pp: 'abc' }) expect(execArr).toStrictEqual(['B', 'A', 'E', 'F']) execArr = [] comp.setData({ p: 'ghi', pp: 'ghi' }) expect(execArr).toStrictEqual(['B', 'A', 'E', 'F', 'C', 'D']) }) test('should not allow updates before init done', () => { const compDef = componentSpace .define() .data(() => ({ a: 123, b: ['a'], })) .init(({ self }) => { let throwCount = 0 try { self.setData({ a: 456 }) } catch { throwCount += 1 } try { self.updateData({ b: ['c'] }) } catch { throwCount += 1 } try { self.replaceDataOnPath(['a'], 789) } catch { throwCount += 1 } try { self.spliceArrayDataOnPath(['b'], 0, 0, ['b']) } catch { throwCount += 1 } try { self.groupUpdates(() => undefined) } catch { throwCount += 1 } expect(throwCount).toBe(5) }) .registerComponent() 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) }) test('should warn recursive updates', () => { const childCompDef = componentSpace .define() .property('a', String) .observer('a', function () { this.triggerEvent('b') }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef, }) .template( tmpl(` `), ) .data(() => ({ cond: false, })) .methods({ ev() { this.setData({ cond: true }) }, }) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) execWithWarn(1, () => { comp.setData({ cond: true }) }) }) test('should preserve item when keys are invalid', () => { let execArr: string[] = [] const childCompDef = componentSpace .define() .property('a', String) .observer('a', function () { execArr.push(`update:${this.data.a}`) }) .lifetime('attached', function () { execArr.push(`create:${this.data.a}`) }) .template( tmpl(` {{a}} `), ) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef, }) .data(() => ({ list: [1], })) .template( tmpl(` `), ) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) expect(domHtml(comp)).toBe('1-0') expect(execArr).toEqual(['update:1-0', 'create:1-0']) execArr = [] execWithWarn(1, () => { comp.setData({ list: [2, 1], }) }) expect(domHtml(comp)).toBe('2-01-1') expect(execArr).toEqual(['update:1-1', 'update:2-0', 'create:1-1']) execArr = [] comp.spliceArrayDataOnPath(['list'], 0, 1, []) comp.applyDataUpdates() expect(domHtml(comp)).toBe('1-0') expect(execArr).toEqual(['update:1-0']) }) test('should update data in nesting components', () => { let execArr: string[] = [] const childCompDef = componentSpace .define() .options({ virtualHost: true }) .property('a', { type: Boolean, value: false, observer(a) { execArr.push(`child:property:${a}`) }, }) .observer('a', function () { execArr.push(`child:observer:${this.data.a}`) }) .template( tmpl(` {{a}} `), ) .registerComponent() const middleCompDef = componentSpace .define() .options({ virtualHost: true }) .usingComponents({ child: childCompDef, }) .property('a', { type: Boolean, value: false, observer(a) { execArr.push(`middle:property:${a}`) }, }) .observer('a', function () { execArr.push(`middle:observer:${this.data.a}`) }) .template( tmpl(` `), ) .registerComponent() const compDef = componentSpace .define() .usingComponents({ middle: middleCompDef, }) .data(() => ({ a: false, })) .template( tmpl(` `), ) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) const middle = comp.$.middle as glassEasel.GeneralComponent const child = middle.$.child as glassEasel.GeneralComponent glassEasel.Element.pretendAttached(comp) expect(domHtml(comp)).toBe('false') expect(execArr).toEqual([ 'child:observer:false', 'middle:observer:false', 'child:observer:false', ]) execArr = [] comp.setData({ a: true }) expect(domHtml(comp)).toBe('true') expect(execArr).toEqual([ 'middle:observer:true', 'child:observer:true', 'child:property:true', 'middle:property:true', ]) execArr = [] child.setData({ a: false }) expect(domHtml(comp)).toBe('false') expect(execArr).toEqual(['child:observer:false', 'child:property:false']) execArr = [] comp.setData({ a: true }) expect(domHtml(comp)).toBe('true') expect(execArr).toEqual(['middle:observer:true', 'child:observer:true', 'child:property:true']) }) test('should support unknown property handler', () => { let execArr = [] as [string, unknown][] let execReturn: boolean = true const childCompDef = componentSpace .define() .options({ unknownPropertyHandler(propName: string, value: unknown) { execArr.push([propName, value]) return execReturn }, }) .property('a', String) .property('b', String) .registerComponent() const compDef = componentSpace .define() .usingComponents({ child: childCompDef.general(), }) .template( tmpl(` `), ) .data(() => ({ p: 'p', a: 'a', dd: 'dd', style: 'style', })) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) const child = (comp.$.child as glassEasel.Element).asInstanceOf(childCompDef)! expect(execArr).toEqual([ ['style', 'style'], ['p', 'p'], ['bindff', 'ff'], ['dataDd', 'dd'], ]) expect(child.style).toBe('') expect(child.getListeners()).toEqual({}) expect(child.dataset).toEqual({}) execArr = [] execReturn = false comp.setData({ a: 'a2', dd: 'dd2', style: 'style2' }) expect(execArr).toEqual([ ['style', 'style2'], ['dataDd', 'dd2'], ['data-dd', 'dd2'], ]) expect(child.style).toBe('style2') expect(child.dataset).toEqual({ dd: 'dd2' }) }) })