More tests, more bugs, more fixes
This commit is contained in:
parent
a8fd5aa2fe
commit
626194fc99
|
@ -58,10 +58,31 @@ function followPath(topNodes: ChildNode[], path: number[]): Node {
|
|||
return n;
|
||||
}
|
||||
|
||||
type PlaceholderAction = (variableParts: HtmlFragment[], node: Node) => void;
|
||||
type PlaceholderState = { [key: string]: any };
|
||||
|
||||
function nodeInserter(n: number): PlaceholderAction {
|
||||
return (vs, node) => {
|
||||
interface PlaceholderAction {
|
||||
reset(state: PlaceholderState, node: Node): void;
|
||||
act(state: PlaceholderState, variableParts: HtmlFragment[], node: Node): void;
|
||||
}
|
||||
|
||||
class NodeInserter implements PlaceholderAction {
|
||||
constructor(public variablePartIndex: number) {}
|
||||
|
||||
reset(state: PlaceholderState, node: Node) {
|
||||
if ('nodeCount' in state) {
|
||||
const previouslyInsertedNodes = state.nodeCount as number;
|
||||
node.parentNode?.insertBefore(makePlaceholder(), node);
|
||||
for (let n = 0; n < previouslyInsertedNodes; n++) {
|
||||
const nextNode = node.nextSibling;
|
||||
node.parentNode?.removeChild(node);
|
||||
if (nextNode === null) break;
|
||||
node = nextNode;
|
||||
}
|
||||
}
|
||||
state.nodeCount = 0;
|
||||
}
|
||||
|
||||
act(state: PlaceholderState, vs: HtmlFragment[], node: Node): void {
|
||||
function walk(f: HtmlFragment) {
|
||||
if (Array.isArray(f)) {
|
||||
f.forEach(walk);
|
||||
|
@ -88,35 +109,80 @@ function nodeInserter(n: number): PlaceholderAction {
|
|||
}
|
||||
}
|
||||
node.parentNode?.insertBefore(newNode, node);
|
||||
state.nodeCount++;
|
||||
}
|
||||
}
|
||||
walk(vs[n]);
|
||||
walk(vs[this.variablePartIndex]);
|
||||
node.parentNode?.removeChild(node);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function attributesInserter(n: number): PlaceholderAction {
|
||||
return (vs, node) => {
|
||||
class AttributesInserter implements PlaceholderAction {
|
||||
constructor(public variablePartIndex: number) {}
|
||||
|
||||
reset(state: PlaceholderState, node: Node): void {
|
||||
const attrNames: string[] = (state.attrNames ?? []);
|
||||
attrNames.forEach(n => (node as Element).removeAttribute(n));
|
||||
state.attrNames = [];
|
||||
}
|
||||
|
||||
act(state: PlaceholderState, vs: HtmlFragment[], node: Node): void {
|
||||
const e = document.createElement('template');
|
||||
const n = this.variablePartIndex;
|
||||
e.innerHTML = `<x-dummy ${renderFragment(vs[n], false).join('')}></x-dummy>`;
|
||||
Array.from(e.content.firstElementChild!.attributes).forEach(a =>
|
||||
(node as Element).setAttribute(a.name, a.value));
|
||||
};
|
||||
Array.from(e.content.firstElementChild!.attributes).forEach(a => {
|
||||
state.attrNames.push(a.name);
|
||||
(node as Element).setAttribute(a.name, a.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function attributeValueInserter(
|
||||
attrName: string,
|
||||
constantParts: string[],
|
||||
placeholders: number[],
|
||||
): PlaceholderAction {
|
||||
return (vs, node) => {
|
||||
const pieces = [constantParts[0]];
|
||||
placeholders.forEach((n, i) => {
|
||||
class AttributeValueInserter implements PlaceholderAction {
|
||||
constructor(
|
||||
public attrName: string,
|
||||
public constantParts: string[],
|
||||
public placeholders: number[],
|
||||
) {}
|
||||
|
||||
reset(_state: PlaceholderState, _node: Node): void {}
|
||||
|
||||
act(_state: PlaceholderState, vs: HtmlFragment[], node: Node): void {
|
||||
const pieces = [this.constantParts[0]];
|
||||
this.placeholders.forEach((n, i) => {
|
||||
pieces.push(...renderFragment(vs[n], false));
|
||||
pieces.push(constantParts[i + 1]);
|
||||
pieces.push(this.constantParts[i + 1]);
|
||||
});
|
||||
(node as Element).setAttribute(attrName, pieces.join(''));
|
||||
};
|
||||
(node as Element).setAttribute(this.attrName, pieces.join(''));
|
||||
}
|
||||
}
|
||||
|
||||
class CompoundAction implements PlaceholderAction {
|
||||
constructor(public actions: PlaceholderAction[]) {}
|
||||
|
||||
reset(states: PlaceholderState, n: Node) {
|
||||
this.actions.forEach((a, i) => {
|
||||
const state = states[i] ??= {};
|
||||
a.reset(state, n);
|
||||
});
|
||||
}
|
||||
|
||||
act(states: PlaceholderState, vs: HtmlFragment[], n: Node) {
|
||||
this.actions.forEach((a, i) => {
|
||||
const state = states[i] ??= {};
|
||||
a.act(state, vs, n);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class HtmlFragmentInstance {
|
||||
state: { [path: string]: PlaceholderState } = {};
|
||||
constructor(
|
||||
public container: ChildNode[],
|
||||
) {}
|
||||
}
|
||||
|
||||
function makePlaceholder(): Element {
|
||||
return document.createElement('x-placeholder');
|
||||
}
|
||||
|
||||
export class HtmlFragmentBuilder {
|
||||
|
@ -138,8 +204,7 @@ export class HtmlFragmentBuilder {
|
|||
splitByPlaceholders(n.textContent ?? '');
|
||||
constantParts.forEach((c, i) => {
|
||||
if (i > 0) {
|
||||
const placeholder = document.createElement('x-placeholder');
|
||||
n.parentNode?.insertBefore(placeholder, n);
|
||||
n.parentNode?.insertBefore(makePlaceholder(), n);
|
||||
}
|
||||
n.parentNode?.insertBefore(document.createTextNode(c), n);
|
||||
});
|
||||
|
@ -147,10 +212,10 @@ export class HtmlFragmentBuilder {
|
|||
n.parentNode?.removeChild(n);
|
||||
placeholders.forEach((n, i) => {
|
||||
const currentPath = path.slice();
|
||||
currentPath[currentPath.length - 1] = i * 2 + 1;
|
||||
currentPath[currentPath.length - 1] += i * 2 + 1;
|
||||
this.placeholderActions.push({
|
||||
path: currentPath,
|
||||
action: nodeInserter(n),
|
||||
action: new NodeInserter(n),
|
||||
});
|
||||
});
|
||||
path[path.length - 1] += constantParts.length + placeholders.length;
|
||||
|
@ -167,12 +232,12 @@ export class HtmlFragmentBuilder {
|
|||
e.removeAttributeNode(attr);
|
||||
i--;
|
||||
const n = parseInt(nameIsPlaceholder[1], 10);
|
||||
actions.push(attributesInserter(n));
|
||||
actions.push(new AttributesInserter(n));
|
||||
} else {
|
||||
const { constantParts, placeholders } =
|
||||
splitByPlaceholders(attr.value);
|
||||
if (constantParts.length !== 1) {
|
||||
actions.push(attributeValueInserter(
|
||||
actions.push(new AttributeValueInserter(
|
||||
attrName,
|
||||
constantParts,
|
||||
placeholders));
|
||||
|
@ -182,7 +247,7 @@ export class HtmlFragmentBuilder {
|
|||
if (actions.length) {
|
||||
this.placeholderActions.push({
|
||||
path: path.slice(),
|
||||
action: (vs, n) => actions.forEach(a => a(vs, n)),
|
||||
action: actions.length === 1 ? actions[0] : new CompoundAction(actions),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -217,14 +282,19 @@ export class HtmlFragmentBuilder {
|
|||
walk(this.template.content);
|
||||
}
|
||||
|
||||
clone(): ChildNode[] {
|
||||
return Array.from(
|
||||
(this.template.cloneNode(true) as HTMLTemplateElement).content.childNodes);
|
||||
clone(): HtmlFragmentInstance {
|
||||
return new HtmlFragmentInstance(Array.from(
|
||||
(this.template.cloneNode(true) as HTMLTemplateElement).content.childNodes));
|
||||
}
|
||||
|
||||
update(template: ChildNode[], variableParts: Array<HtmlFragment>) {
|
||||
update(template: HtmlFragmentInstance, variableParts: Array<HtmlFragment>) {
|
||||
this.placeholderActions.forEach(({ path, action }) => {
|
||||
action(variableParts, followPath(template, path));
|
||||
const state = template.state[path.map(n => '' + n).join('.')] ??= {};
|
||||
action.reset(state, followPath(template.container, path));
|
||||
});
|
||||
this.placeholderActions.forEach(({ path, action }) => {
|
||||
const state = template.state[path.map(n => '' + n).join('.')] ??= {};
|
||||
action.act(state, variableParts, followPath(template.container, path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -238,17 +308,17 @@ export type HtmlTemplater =
|
|||
=> ChildNode[];
|
||||
|
||||
export function template(): HtmlTemplater {
|
||||
let container: ChildNode[] | null = null;
|
||||
let instance: HtmlFragmentInstance | null = null;
|
||||
return (constantParts, ... variableParts) => {
|
||||
let b = templateCache.get(constantParts);
|
||||
if (b === void 0) {
|
||||
b = new HtmlFragmentBuilder(constantParts);
|
||||
templateCache.set(constantParts, b);
|
||||
}
|
||||
if (container === null) {
|
||||
container = b.clone();
|
||||
if (instance === null) {
|
||||
instance = b.clone();
|
||||
}
|
||||
b.update(container, variableParts);
|
||||
return container;
|
||||
b.update(instance, variableParts);
|
||||
return instance.container;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ describe('basic templating', () => {
|
|||
compareHTML(template()`<x>${y}</x>`, '<x>abc</x>');
|
||||
});
|
||||
|
||||
it('should substitute multiple strings', () => {
|
||||
const y = 'abc';
|
||||
compareHTML(template()`<x>${y} ${y} ${y}</x>`, '<x>abc abc abc</x>');
|
||||
});
|
||||
|
||||
it('should substitute a node', () => {
|
||||
const y = template()`<z>q</z>`;
|
||||
compareHTML(template()`<x>${y}</x>`, '<x><z>q</z></x>');
|
||||
|
@ -41,4 +46,57 @@ describe('basic templating', () => {
|
|||
compareHTML(template()`<q><x t="${v()}">${ps.map(p => p())}</x></q>`,
|
||||
'<q><x t="aaa">123234</x></q>');
|
||||
});
|
||||
|
||||
it('should substitute into attributes, with children', () => {
|
||||
const v = () => 'aaa';
|
||||
const ps = [() => '123', () => '234'];
|
||||
const f = 'z';
|
||||
compareHTML(template()`<q><x t="${v()}">${f}</x>${ps.map(p => p())}</q>`,
|
||||
'<q><x t="aaa">z</x>123234</q>');
|
||||
});
|
||||
|
||||
it('should substitute into attributes, with children and whitespace', () => {
|
||||
const v = () => 'aaa';
|
||||
const ps = [() => '123', () => '234'];
|
||||
const f = 'z';
|
||||
compareHTML(template()`<q><x ttt="${v()}">${f}</x>
|
||||
${ps.map(p => p())}
|
||||
</q>`,
|
||||
`<q><x ttt="aaa">z</x>
|
||||
123234
|
||||
</q>`);
|
||||
});
|
||||
|
||||
it('example from paradise.js', () => {
|
||||
const pieces = ['C', 'D'];
|
||||
compareHTML(template()`<p class="event">
|
||||
<span class="timestamp" title="${'A'}">${'B'}</span>
|
||||
${pieces.map(p => p)}
|
||||
</p>`,
|
||||
`<p class="event">
|
||||
<span class="timestamp" title="A">B</span>
|
||||
CD
|
||||
</p>`);
|
||||
});
|
||||
|
||||
it('should be callable multiple times', () => {
|
||||
const v = () => 'aaa';
|
||||
const ps = [() => '123', () => '234', () => '345'];
|
||||
const expected = '<q><x t="aaa">123234345</x></q>';
|
||||
const t = template();
|
||||
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, expected);
|
||||
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, expected);
|
||||
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, expected);
|
||||
});
|
||||
|
||||
it('should be callable multiple times with extra items', () => {
|
||||
const v = () => 'aaa';
|
||||
const t = template();
|
||||
const ps = [() => '1'];
|
||||
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, '<q><x t="aaa">1</x></q>');
|
||||
ps.push(() => '2');
|
||||
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, '<q><x t="aaa">12</x></q>');
|
||||
ps.push(() => '3');
|
||||
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, '<q><x t="aaa">123</x></q>');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue