simple
", + Doc([], refs, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(HTMLParagraphElement); + expect((res.childNodes[0] as HTMLParagraphElement).outerHTML).toBe("simple
"); + expect(dynamicValueCollector).not.toHaveBeenCalled(); + }); + + it("should parse comment", () => { + const res = parse( + "", + Doc([], refs, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(Comment); + expect((res.childNodes[0] as Comment).nodeValue).toBe("simple
"); + expect(dynamicValueCollector).not.toHaveBeenCalled(); + }); + + it("should parse plain html with static injected value", () => { + const res = parse( + "$val0
", + Doc(["simple"], refs, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(HTMLParagraphElement); + expect((res.childNodes[0] as HTMLParagraphElement).outerHTML).toBe("simple
"); + expect(dynamicValueCollector).not.toHaveBeenCalled(); + }); + + it("should parse plain html with dynamic injected value", () => { + const fn = () => "simple"; + const res = parse( + "$val0
", + Doc([fn], refs, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(HTMLParagraphElement); + expect((res.childNodes[0] as HTMLParagraphElement).outerHTML).toBe("$val0
"); + expect(dynamicValueCollector).toHaveBeenCalledWith({ + data: null, + name: "nodeValue", + value: fn, + rawValue: "$val0", + prop: null, + }); + }); + + it("should parse event attribute", () => { + const handler = () => { + }; + const res = parse( + "", + Doc([handler], {}, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(HTMLButtonElement); + expect((res.childNodes[0] as HTMLButtonElement).outerHTML).toBe(""); + expect(dynamicValueCollector).toHaveBeenCalledTimes(1); + expect(dynamicValueCollector).toHaveBeenCalledWith({ + "data": null, + "name": "onclick", + "prop": "click", + "rawValue": "$val0", + "value": [handler] + }); + }); + + it("should parse event attribute with option", () => { + const handler = () => { + }; + const res = parse( + '', + Doc([handler, {once: true}], {}, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(HTMLButtonElement); + expect((res.childNodes[0] as HTMLButtonElement).outerHTML).toBe(""); + expect(dynamicValueCollector).toHaveBeenCalledTimes(1); + expect(dynamicValueCollector).toHaveBeenCalledWith({ + "data": null, + "name": "onclick", + "prop": "click", + "rawValue": "$val0, $val1", + "value": [handler, ", ", {once: true}] + }); + }); + + it("should parse and ignore unknown event attribute", () => { + const handler = () => { + }; + const res = parse( + "", + Doc([handler], {}, dynamicValueCollector) as unknown as DocumentLike + ); + + expect(res.childNodes.length).toBe(1); + expect(res.childNodes[0]).toBeInstanceOf(HTMLButtonElement); + expect((res.childNodes[0] as HTMLButtonElement).outerHTML).toBe(""); + expect(dynamicValueCollector).toHaveBeenCalledTimes(1); + expect(dynamicValueCollector).toHaveBeenCalledWith({ + "data": null, + "name": "onval", + "prop": null, + "rawValue": "$val0", + "value": [handler] + }); + }); + + it("should parse event attribute for webComponent", () => { + class MyButton extends HTMLElement { + } + + customElements.define("my-button", MyButton); + + const handler = () => { + }; + const res = parse( + "more than 10
` + const b = html`less than 10
` + const {dynamicValue, update} = renderContent( + () => x > 10 ? html`>${a}` : html`<${b}` + ) + + expect(a.nodes).toHaveLength(1); + expect(a.mounted).toBeTruthy(); + expect(b.nodes).toHaveLength(0); + expect(b.mounted).toBeFalsy(); + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('>'); + expect((dynamicValue.renderedNodes[1] as Element).outerHTML).toBe('more than 10
'); + expect(document.body.innerHTML).toBe('>more than 10
') + + x = 5; + + update(); + + expect(a.nodes).toHaveLength(0); + expect(a.mounted).toBeFalsy(); + expect(b.nodes).toHaveLength(1); + expect(b.mounted).toBeTruthy(); + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('<'); + expect((dynamicValue.renderedNodes[1] as Element).outerHTML).toBe('less than 10
'); + expect(document.body.innerHTML).toBe('<less than 10
') + }) + + it('should render and update a template', () => { + let txt = 'sample'; + const temp = html`${() => txt}`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('sample'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('sample') + + txt = 'changed'; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('changed'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('changed') + }) + + it('should render and update nested templates', () => { + let txt = 'sample'; + const temp = html`${html`${() => txt}`}`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('sample'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('sample') + + txt = 'changed'; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('changed'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('changed') + }) + + it('should render and update deeply nested templates', () => { + let txt = 'sample'; + const temp = html`${html`${html`${html`${() => txt}`}`}`}`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('sample'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('sample') + + txt = 'changed'; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('changed'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('changed') + }) + + it('should render and update when helper with templates', () => { + let condition = true; + const temp = html`${when(() => condition, 'truthy', 'falsy')}`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('truthy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('truthy') + + condition = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('falsy') + + condition = true; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('truthy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('truthy') + }) + + it('should render and update nested when helper with templates', () => { + let condition1 = true; + let condition2 = true; + const temp = html`${ + when(() => condition1, + html`${ + when(() => condition2, + 'deep-truthy', + 'deep-falsy' + )}after` + , + 'falsy') + }`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-truthy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-truthyafter') + + condition1 = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('falsy') + + condition1 = true; + condition2 = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-falsyafter') + + condition1 = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('falsy') + + condition1 = true; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-falsyafter') + }) + + it('should render and update deeply nested when helper with templates', () => { + let condition1 = true; + let condition2 = true; + let condition3 = true; + const temp = html`${ + when(() => condition1, + html`${ + when(() => condition2, + 'deep-truthy', + 'deep-falsy' + )}${when(() => condition3, + 'after-truthy', + 'after-falsy' + )}` + , + 'falsy') + }`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-truthy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-truthyafter-truthy') + + condition3 = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-truthy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-truthyafter-falsy') + + condition2 = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-falsyafter-falsy') + + condition1 = false; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('falsy') + + condition1 = true; + condition3 = true; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-falsy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-falsyafter-truthy') + + condition2 = true; + + update() + + expect(dynamicValue.renderedNodes).toHaveLength(2); + expect(dynamicValue.renderedNodes[0]).toBeInstanceOf(Text); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('deep-truthy'); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('deep-truthyafter-truthy') + }) + + it("should render nested 'when' helper with 'is' helper for condition and 'state' for data update", () => { + const [currentPlayer, setCurrentPlayer] = state("x") + const [ended, setEnded] = state(false); + + const temp = html`${when(ended, html` + ${when(is(currentPlayer, 'xy'), + html`tie`, + html`${currentPlayer} won` + )} + `)}`; + const {dynamicValue, update} = renderContent(temp) + + expect(dynamicValue.renderedNodes).toHaveLength(1); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe(''); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('') + + setEnded(true); + + expect(document.body.innerHTML).toBe('x won') + + setCurrentPlayer('xy') + + expect(document.body.innerHTML).toBe('tie') + + setEnded(false) + + expect(document.body.innerHTML).toBe('') + + setCurrentPlayer('x') + + expect(document.body.innerHTML).toBe('') + + setEnded(true) + + expect(dynamicValue.renderedNodes).toHaveLength(3); + expect(dynamicValue.renderedNodes[0].nodeValue).toBe('x'); + expect(dynamicValue.renderedNodes[1].nodeValue).toBe(' won'); + expect((dynamicValue.renderedNodes[2] as Element).outerHTML).toBe(''); + expect(dynamicValue.data).toBe(temp); + expect(document.body.innerHTML).toBe('x won') + }); + + // todo: html.spec.ts has these tests but moving them here makes it easier to debug + it.todo('should render and update repeat helper with primitives') + it.todo('should render and update nested repeat helper with primitives') + it.todo('should render and update deeply nested repeat helper with primitives') + it.todo('should render and update repeat helper with objects') + it.todo('should render and update nested repeat helper with objects') + it.todo('should render and update deeply nested repeat helper with objects') + it.todo('should render and update repeat helper with templates') + it.todo('should render and update nested repeat helper with templates') + it.todo('should render and update deeply nested repeat helper with templates') +}) diff --git a/src/dynamic-value/ContentDynamicValueResolver.ts b/src/dynamic-value/ContentDynamicValueResolver.ts new file mode 100644 index 00000000..43dc4f9a --- /dev/null +++ b/src/dynamic-value/ContentDynamicValueResolver.ts @@ -0,0 +1,90 @@ +import { DynamicValueResolver } from './DynamicValueResolver' +import { shallowCopyArrayOrObjectLiteral, val } from '../utils' +import { HtmlTemplate } from '../html' +import { resolveContentDynamicValueToNodes } from './resolve-content-dynamic-value-to-nodes' +import { syncNodes } from './sync-nodes' + +const getDataAsList = (data: unknown): unknown[] => { + if (!data) return [] + + if (Array.isArray(data)) { + return data.flat(Infinity) + } + + return [data] +} + +export class ContentDynamicValueResolver extends DynamicValueResolver { + get renderedNodes() { + return this.data instanceof HtmlTemplate + ? this.data.nodes + : super.renderedNodes + } + + set renderedNodes(newNodes: Node[]) { + super.renderedNodes = Array.from(new Set(newNodes)) + } + + resolve(refs: Recordsample
` - - const e: Executable = { - ...defaultExec, - content: [ - { - name: 'nodeValue', - rawValue: '{{val0}}', - value: '', - renderedNodes: [txt], - parts: [p], - }, - ], - } - - handleExecutable(txt, e, refs) - - expect(div.outerHTML).toBe('sample
sample
'], - }, - ], - } - - handleExecutable(txt, e, refs) - - expect(div.outerHTML).toBe('one
two
') - expect(execVal.renderedNodes).toEqual(nodes) - }) - - it('should replace some nodes with new ones', () => { - const div = document.createElement('div') - - const p1 = document.createElement('p') - const p2 = document.createElement('p') - const p3 = document.createElement('p') - const p4 = document.createElement('p') - - p1.innerHTML = 'one' - p2.innerHTML = 'two' - p3.innerHTML = 'three' - p4.innerHTML = 'four' - - const nodes = [p1, p3] - - div.appendChild(p2) - div.appendChild(p4) - - expect(div.innerHTML).toBe('two
four
') - - const execVal: ExecutableValue = { - name: 'nodeValue', - rawValue: '', - value: '', - renderedNodes: Array.from(div.childNodes), - parts: [], - } - - handleTextExecutable(execVal, nodes, div) - - expect(div.innerHTML).toBe('one
three
') - expect(execVal.renderedNodes).toEqual(nodes) - }) - - it('should replace one node with new ones', () => { - const div = document.createElement('div') - - const p1 = document.createElement('p') - const p2 = document.createElement('p') - const p3 = document.createElement('p') - - p1.innerHTML = 'one' - p2.innerHTML = 'two' - p3.innerHTML = 'three' - - div.appendChild(p1) - - expect(div.innerHTML).toBe('one
') - - const execVal: ExecutableValue = { - name: 'nodeValue', - rawValue: '', - value: '', - renderedNodes: [p1], - parts: [], - } - - handleTextExecutable(execVal, [p2, p3], div) - - expect(div.innerHTML).toBe('two
three
') - expect(execVal.renderedNodes).toEqual([p2, p3]) - }) -}) diff --git a/src/executable/handle-text-executable.ts b/src/executable/handle-text-executable.ts deleted file mode 100644 index 918ff989..00000000 --- a/src/executable/handle-text-executable.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ExecutableValue } from '../types' -import { changeCurrentIntoNewItems } from './change-current-into-new-items' - -export const handleTextExecutable = ( - executableValue: ExecutableValue, - nodes: Arraymore than 10
` const b = html`less than 10
` - const temp = html`${() => (x > 10 ? html`${a}` : html`${b}`)}` + const temp = html`total: ${() => (x > 10 ? html`>${a}` : html`<${b}`)}` temp.render(document.body) - expect(document.body.innerHTML).toBe('more than 10
') + expect(temp.nodes).toHaveLength(3) + expect(temp.nodes.map(n => (n as Element).outerHTML || n.nodeValue)).toEqual([ + "total: ", + ">", + "more than 10
" + ]) + expect(a.mounted).toBeTruthy() + expect(a.nodes).toHaveLength(1) + expect((a.nodes[0] as Element).outerHTML).toBe('more than 10
') - x = 5 + expect(b.mounted).toBeFalsy() + expect(b.nodes).toHaveLength(0) - temp.update() + expect(document.body.innerHTML).toBe('total: >more than 10
') - expect(document.body.innerHTML).toBe('less than 10
') + x = 5 + + temp.update() + + expect(temp.nodes).toHaveLength(3) + expect(temp.nodes.map(n => (n as Element).outerHTML || n.nodeValue)).toEqual([ + "total: ", + "<", + "less than 10
" + ]) + + expect(document.body.innerHTML).toBe('total: <less than 10
') }) it('should handle deeply nested HTML', () => { @@ -306,12 +328,8 @@ describe('html', () => { }) it('should render nested from values', () => { - const button = html` - ` - const app = html` -no items
') }); + + it('when nested with dependent logic', () => { + const [currentPlayer, setCurrentPlayer] = state("x") + const [ended, setEnded] = state(false) + + html` + ${when(ended, html` + ${when(is(currentPlayer, 'xy'), + html`tie`, + html`${currentPlayer} won` + )} + + `)} + `.render(document.body) + + expect(document.body.innerHTML).toBe('') + + setEnded(true) + expect(document.body.innerHTML).toBe('x won\n' + + '\t\t\t\t\t') + + setCurrentPlayer('xy') + expect(document.body.innerHTML).toBe('tie\n' + + '\t\t\t\t\t') + + setEnded(false) + expect(document.body.innerHTML).toBe('') + + setCurrentPlayer('x') + expect(document.body.innerHTML).toBe('') + + setEnded(true) + expect(document.body.innerHTML).toBe('x won\n' + + '\t\t\t\t\t') + + setCurrentPlayer('x') + setEnded(false) + expect(document.body.innerHTML).toBe('') + }) }) it('should work with state helper', () => { @@ -1804,6 +1886,7 @@ describe('html', () => { }); it('unmount and resume on mount', () => { + jest.useFakeTimers() const updateMock = jest.fn(); const [x, setX] = state(5); @@ -1825,9 +1908,13 @@ describe('html', () => { setX(20); + jest.advanceTimersByTime(1000) + expect(updateMock).toHaveBeenCalled() expect(document.body.innerHTML).toBe("item 20") + + jest.useRealTimers() }) }) @@ -1859,4 +1946,5 @@ describe('html', () => { expect(document.body.innerHTML).toBe('<button>click me</button>') }); + }) diff --git a/src/html.ts b/src/html.ts index bb325b40..2bb2bbd8 100644 --- a/src/html.ts +++ b/src/html.ts @@ -1,13 +1,12 @@ import { - Executable, StateGetter, StateSetter, StateSubscriber, StateUnSubscriber, } from './types' -import { Doc } from './executable/Doc' -import { handleExecutable } from './executable/handle-executable' -import { parse } from '@beforesemicolon/html-parser' +import type { DynamicValueResolver } from './dynamic-value/DynamicValueResolver' +import { Doc } from './Doc' +import { parse } from '@beforesemicolon/html-parser/dist/parse' import { Helper } from './Helper' // prevents others from creating functions that can be subscribed to @@ -16,38 +15,28 @@ const id = 'S' + Math.floor(Math.random() * 10000000) export class HtmlTemplate { #htmlTemplate: string - #nodes: Node[] = [] - #renderTarget: ShadowRoot | Element | null = null + #nodes: Array