Socket
Socket
Sign inDemoInstall

lit-html

Package Overview
Dependencies
Maintainers
1
Versions
102
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.1.0 to 0.2.0

src/repeat.ts

8

package.json
{
"name": "lit-html",
"version": "0.1.0",
"version": "0.2.0",
"description": "HTML template literals in JavaScript",

@@ -22,3 +22,7 @@ "license": "BSD-3-Clause",

"typescript": "^2.4.1"
}
},
"dependencies": {
"@types/mocha": "^2.2.41"
},
"typings": "./lib/lit-html.d.ts"
}

@@ -236,4 +236,2 @@ # lit-html

Thunks are trampolined so they can return other thunks.
### Arrays/Iterables

@@ -277,3 +275,3 @@

* Event handlers: Specially named attributes can install event handlers.
* HTML values: `lit-html` sets `textContent` by default. Extensions could allow setting `innerHTML` or injecting existing DOM nodes.
* HTML values: `lit-html` creates `Text` nodes by default. Extensions could allow setting `innerHTML`.

@@ -314,8 +312,10 @@ ### Small Size

### Interface `Part`
### Abstract Class `Part`
* Property `type: string`
A `Part` is a dynamic section of a `TemplateInstance`. It's value can be set to update the section.
* Method `update(instance: TemplateInstance, node: Node, values: Iterator<any>): void`
Specially support value types are `Node`, `Function`, and `TemplateResult`.
* Method `setValue(value: any): void`
## Future Work

@@ -322,0 +322,0 @@

@@ -55,3 +55,5 @@ /**

container.__templateInstance = instance;
instance.appendTo(container, this.values);
const fragment = instance._clone();
instance.update(this.values);
container.appendChild(fragment);
} else {

@@ -82,72 +84,12 @@ instance.update(this.values);

*/
export interface TemplatePart {
type: string;
index: number;
update(instance: TemplateInstance, node: Node, values: Iterator<any>): void;
}
export class AttributePart implements TemplatePart {
type: 'attribute';
index: number;
name: string;
rawName: string;
strings: string[];
constructor(index: number, name: string, rawName: string, strings: string[]) {
this.index = index;
this.name = name;
this.rawName = rawName;
this.strings = strings;
export class TemplatePart {
constructor(
public type: string,
public index: number,
public name?: string,
public rawName?: string,
public strings?: string[]) {
}
update(_instance: TemplateInstance, node: Node, values: Iterator<any>) {
console.assert(node.nodeType === Node.ELEMENT_NODE);
const strings = this.strings;
let text = '';
for (let i = 0; i < strings.length; i++) {
text += strings[i];
if (i < strings.length - 1) {
const v = values.next().value;
if (v && typeof v !== 'string' && v[Symbol.iterator]) {
for (const t of v) {
// TODO: we need to recursively call getValue into iterables...
text += t;
}
} else {
text += v;
}
}
}
(node as Element).setAttribute(this.name, text);
}
}
export class NodePart implements TemplatePart {
type: 'node';
index: number;
constructor(index: number) {
this.index = index;
}
update(instance: TemplateInstance, node: Node, values: Iterator<any>): void {
console.assert(node.nodeType === Node.TEXT_NODE);
const value = values.next().value;
if (value && typeof value !== 'string' && value[Symbol.iterator]) {
const fragment = document.createDocumentFragment();
for (const item of value) {
const marker = new Text();
fragment.appendChild(marker);
instance.renderValue(item, marker);
}
instance.renderValue(fragment, node);
} else {
instance.renderValue(value, node);
}
}
}
export class Template {

@@ -186,3 +128,3 @@ private _strings: TemplateStringsArray;

const rawName = match![1];
this.parts.push(new AttributePart(index, attribute.name, rawName, strings));
this.parts.push(new TemplatePart('attribute', index, attribute.name, rawName, strings));
attributesToRemove.push(attribute);

@@ -194,3 +136,4 @@ }

if (strings.length > 1) {
// Generate a new text node for each literal and part
// Generate a new text node for each literal and two for each part,
// a start and end
partIndex += strings.length - 1;

@@ -203,5 +146,6 @@ for (let i = 0; i < strings.length; i++) {

if (i < strings.length - 1) {
const partNode = new Text();
node.parentNode!.insertBefore(partNode, node);
this.parts.push(new NodePart(index));
node.parentNode!.insertBefore(new Text(), node);
node.parentNode!.insertBefore(new Text(), node);
this.parts.push(new TemplatePart('node', index));
index++;
}

@@ -236,5 +180,181 @@ }

export abstract class Part {
size?: number;
abstract setValue(value: any): void;
protected _getValue(value: any) {
if (typeof value === 'function') {
try {
value = value(this);
} catch (e) {
console.error(e);
return;
}
}
if (value === null) {
// `null` as the value of Text node will render the string 'null'
return undefined;
}
return value;
}
}
export class AttributePart extends Part {
element: Element;
name: string;
strings: string[];
constructor(element: Element, name: string, strings: string[]) {
super();
console.assert(element.nodeType === Node.ELEMENT_NODE);
this.element = element;
this.name = name;
this.strings = strings;
}
setValue(values: any[]): void {
const strings = this.strings;
let text = '';
for (let i = 0; i < strings.length; i++) {
text += strings[i];
if (i < strings.length - 1) {
const v = this._getValue(values[i]);
if (v && typeof v !== 'string' && v[Symbol.iterator]) {
for (const t of v) {
// TODO: we need to recursively call getValue into iterables...
text += t;
}
} else {
text += v;
}
}
}
this.element.setAttribute(this.name, text);
}
get size(): number {
return this.strings.length - 1;
}
}
export class NodePart extends Part {
startNode: Node;
endNode: Node;
private _previousValue: any;
constructor(startNode: Node, endNode: Node) {
super();
this.startNode = startNode;
this.endNode = endNode;
}
setValue(value: any): void {
let node: Node|undefined = undefined;
value = this._getValue(value);
if (value instanceof Node) {
this.clear();
node = value;
} else if (value instanceof TemplateResult) {
let instance: TemplateInstance;
if (this._previousValue && this._previousValue._template === value.template) {
instance = this._previousValue;
} else {
this.clear();
instance = new TemplateInstance(value.template);
node = instance._clone();
}
instance.update(value.values);
this._previousValue = instance;
} else if (value && typeof value !== 'string' && value[Symbol.iterator]) {
// For an Iterable, we create a new InstancePart per item, then set its
// value to the item. This is a little bit of overhead for every item in
// an Iterable, but it lets us recurse easily and update Arrays of
// TemplateResults that will be commonly returned from expressions like:
// array.map((i) => html`${i}`)
// We reuse this parts startNode as the first part's startNode, and this
// parts endNode as the last part's endNode.
let itemStart = this.startNode;
let itemEnd;
const values = value[Symbol.iterator]() as Iterator<any>;
const previousParts = Array.isArray(this._previousValue) ? this._previousValue : undefined;
let previousPartsIndex = 0;
const itemParts = [];
let current = values.next();
let next = values.next();
while (!current.done) {
if (next.done) {
// on the last item, reuse this part's endNode
itemEnd = this.endNode;
} else {
itemEnd = new Text();
this.endNode.parentNode!.insertBefore(itemEnd, this.endNode);
}
// Reuse a part if we can, otherwise create a new one
let itemPart;
if (previousParts !== undefined && previousPartsIndex < previousParts.length) {
itemPart = previousParts[previousPartsIndex++];
} else {
itemPart = new NodePart(itemStart, itemEnd);
}
itemPart.setValue(current.value);
itemParts.push(itemPart);
current = next;
next = values.next();
itemStart = itemEnd;
}
this._previousValue = itemParts;
// If the new list is shorter than the old list, clean up:
if (previousParts !== undefined && previousPartsIndex < previousParts.length) {
const clearStart = previousParts[previousPartsIndex].startNode;
const clearEnd = previousParts[previousParts.length - 1].endNode;
const clearRange = document.createRange();
if (previousPartsIndex === 0) {
clearRange.setStartBefore(clearStart);
} else {
clearRange.setStartAfter(clearStart);
}
clearRange.setEndAfter(clearEnd);
clearRange.deleteContents();
clearRange.detach(); // is this neccessary?
}
} else {
this.clear();
node = new Text(value);
}
if (node !== undefined) {
this.endNode.parentNode!.insertBefore(node, this.endNode);
}
}
clear() {
this._previousValue = undefined;
let node: Node = this.startNode;
let next: Node|null = node.nextSibling;
while (next !== null && next !== this.endNode) {
node = next;
next = next.nextSibling;
node.parentNode!.removeChild(node);
}
}
// detach(): DocumentFragment ?
}
export class TemplateInstance {
private _template: Template;
private _parts: {part: TemplatePart, node: Node}[] = [];
_template: Template;
_parts: Part[] = [];
startNode: Node;

@@ -247,23 +367,15 @@ endNode: Node;

appendTo(container: Element|DocumentFragment, values: any[]) {
const fragment = this._clone();
this.update(values);
container.appendChild(fragment);
}
update(values: any[]) {
const valuesIterator = this._getValues(values);
for (const {part, node} of this._parts) {
part.update(this, node, valuesIterator);
let valueIndex = 0;
for (const part of this._parts) {
if (part.size === undefined) {
part.setValue(values[valueIndex++]);
} else {
part.setValue(values.slice(valueIndex, valueIndex + part.size));
valueIndex += part.size;
}
}
}
private _getFragment() {
const fragment = this._clone();
this.startNode = fragment.insertBefore(new Text(), fragment.firstChild);
this.endNode = fragment.appendChild(new Text());
return fragment;
}
private _clone(): DocumentFragment {
_clone(): DocumentFragment {
const fragment = document.importNode(this._template.element.content, true);

@@ -276,12 +388,13 @@

const parts = this._template.parts;
let index = -1;
let index = 0;
let partIndex = 0;
let part = parts[0];
while (walker.nextNode() && partIndex < parts.length) {
index++;
if (index === part.index) {
const node = walker.currentNode;
this._parts.push({part, node});
part = parts[++partIndex];
let templatePart = parts[0];
let node = walker.nextNode();
while (node != null && partIndex < parts.length) {
if (index === templatePart.index) {
this._parts.push(this._createPart(templatePart, node));
templatePart = parts[++partIndex];
} else {
index++;
node = walker.nextNode();
}

@@ -293,65 +406,12 @@ }

/**
* Converts a raw values array passed to a template tag into an iterator so
* that TemplateParts can consume it while updating.
*
* Contains a trampoline to evaluate thunks until they return a non-function value.
*/
private * _getValues(values: any[]) {
for (let value of values) {
while (typeof value === 'function') {
try {
value = value();
} catch (e) {
console.error(e);
yield;
}
}
yield value;
}
}
renderValue(value: any, node: Node) {
let templateInstance = node.__templateInstance as TemplateInstance;
if (templateInstance !== undefined && (!(value instanceof TemplateResult) || templateInstance._template !== value.template)) {
this._cleanup(node);
}
if (value instanceof DocumentFragment) {
node.__templateInstance = {
startNode: value.firstChild!,
endNode: value.lastChild!,
};
node.parentNode!.insertBefore(value, node.nextSibling);
} else if (value instanceof TemplateResult) {
if (templateInstance === undefined || value.template !== templateInstance._template) {
// We haven't stamped this template to this location, so create
// a new instance and insert it.
// TODO: Add keys and check for key equality also
node.textContent = '';
templateInstance = node.__templateInstance = new TemplateInstance(value.template);
const fragment = templateInstance._getFragment();
node.parentNode!.insertBefore(fragment, node.nextSibling);
}
templateInstance.update(value.values);
_createPart(templatePart: TemplatePart, node: Node): Part {
if (templatePart.type === 'attribute') {
return new AttributePart(node as Element, templatePart.name!, templatePart.strings!);
} else if (templatePart.type === 'node') {
return new NodePart(node, node.nextSibling!);
} else {
node.textContent = value;
throw new Error(`unknown part type: ${templatePart.type}`);
}
}
private _cleanup(node: Node) {
const instance = node.__templateInstance!;
// We had a previous template instance here, but don't now: clean up
let cleanupNode: Node|null = instance.startNode;
while (cleanupNode !== null) {
const n = cleanupNode;
cleanupNode = cleanupNode.nextSibling;
n.parentNode!.removeChild(n);
if (n === instance.endNode) {
break;
}
}
node.__templateInstance = undefined;
}
}

@@ -358,0 +418,0 @@

@@ -15,8 +15,6 @@ /**

import {html, TemplateResult, AttributePart, TemplatePart, TemplateInstance} from '../lit-html.js';
/// <reference path="../../node_modules/@types/mocha/index.d.ts" />
/// <reference path="../../node_modules/@types/chai/index.d.ts" />
declare const chai: any;
declare const mocha: any;
declare const suite: (title: string, fn: Function) => void;
declare const test: (title: string, fn: Function) => void;
import {html, TemplateResult, TemplatePart, TemplateInstance, NodePart, Part, AttributePart} from '../lit-html.js';

@@ -64,4 +62,4 @@ const assert = chai.assert;

const parts = result.template.parts;
const names = parts.map((p: AttributePart) => p.name);
const rawNames = parts.map((p: AttributePart) => p.rawName);
const names = parts.map((p: TemplatePart) => p.name);
const rawNames = parts.map((p: TemplatePart) => p.rawName);
assert.deepEqual(names, ['someprop', 'a-nother', 'multiparts', undefined, 'athing']);

@@ -71,2 +69,11 @@ assert.deepEqual(rawNames, ['someProp', 'a-nother', 'multiParts', undefined, 'aThing']);

test('parses expressions for two attributes of one element', () => {
const result = html`<div a="${1}" b="${2}"></div>`;
const parts = result.template.parts;
assert.equal(parts.length, 2);
const instance = new TemplateInstance(result.template);
instance._clone();
assert.equal(instance._parts.length, 2);
})
});

@@ -108,8 +115,2 @@

test('renders chained thunks', () => {
const container = document.createElement('div');
html`<div>${(_:any)=>(_:any)=>123}</div>`.renderTo(container);
assert.equal(container.innerHTML, '<div>123</div>');
});
test('renders thunks that throw as empty text', () => {

@@ -134,2 +135,9 @@ const container = document.createElement('div');

// test('renders multiple nested templates', () => {
// const container = document.createElement('div');
// const partial = html`<h1>${'foo'}</h1>`;
// html`${partial}${'bar'}${partial}${'baz'}qux`.renderTo(container);
// assert.equal(container.innerHTML, '<h1>foo</h1>bar<h1>foo</h1>bazqux');
// });
test('renders arrays of nested templates', () => {

@@ -141,2 +149,20 @@ const container = document.createElement('div');

test('renders an element', () => {
const container = document.createElement('div');
const child = document.createElement('p');
html`<div>${child}</div>`.renderTo(container);
assert.equal(container.innerHTML, '<div><p></p></div>');
});
test('renders an array of elements', () => {
const container = document.createElement('div');
const children = [
document.createElement('p'),
document.createElement('a'),
document.createElement('span')
];
html`<div>${children}</div>`.renderTo(container);
assert.equal(container.innerHTML, '<div><p></p><a></a><span></span></div>');
});
test('renders to an attribute', () => {

@@ -148,3 +174,3 @@ const container = document.createElement('div');

test('renders to an attribute wihtout quotes', () => {
test('renders to an attribute without quotes', () => {
const container = document.createElement('div');

@@ -173,2 +199,8 @@ html`<div foo=${'bar'}></div>`.renderTo(container);

test('renders to an attribute and node', () => {
const container = document.createElement('div');
html`<div foo="${'bar'}">${'baz'}</div>`.renderTo(container);
assert.equal(container.innerHTML, '<div foo="bar">baz</div>');
});
test('renders a combination of stuff', () => {

@@ -282,2 +314,38 @@ const container = document.createElement('div');

test('updates an element', () => {
const container = document.createElement('div');
let child: any = document.createElement('p');
const t = () => html`<div>${child}<div></div></div>`;
t().renderTo(container);
assert.equal(container.innerHTML, '<div><p></p><div></div></div>');
child = undefined;
t().renderTo(container);
assert.equal(container.innerHTML, '<div><div></div></div>');
child = new Text('foo');
t().renderTo(container);
assert.equal(container.innerHTML, '<div>foo<div></div></div>');
});
test('updates an array of elements', () => {
const container = document.createElement('div');
let children: any = [
document.createElement('p'),
document.createElement('a'),
document.createElement('span')
];
const t = () => html`<div>${children}</div>`
t().renderTo(container);
assert.equal(container.innerHTML, '<div><p></p><a></a><span></span></div>');
children = null;
t().renderTo(container);
assert.equal(container.innerHTML, '<div></div>');
children = new Text('foo');
t().renderTo(container);
assert.equal(container.innerHTML, '<div>foo</div>');
});
});

@@ -297,22 +365,20 @@

class PropertyPart implements TemplatePart {
type: 'property';
index: number;
name: string;
strings: string[];
constructor(index: number, name: string, strings: string[]) {
this.index = index;
this.name = name;
this.strings = strings;
class PropertySettingTemplateInstance extends TemplateInstance {
_createPart(templatePart: TemplatePart, node: Node): Part {
if (templatePart.type === 'attribute') {
return new PropertyPart(node as Element, templatePart.rawName!, templatePart.strings!);
}
return super._createPart(templatePart, node);
}
update(_instance: TemplateInstance, node: Node, values: Iterator<any>) {
console.assert(node.nodeType === Node.ELEMENT_NODE);
}
class PropertyPart extends AttributePart {
setValue(values: any[]): void {
const s = this.strings;
if (s[0] === '' && s[s.length - 1] === '') {
if (s.length === 2 && s[0] === '' && s[s.length - 1] === '') {
// An expression that occupies the whole attribute value will leave
// leading and trailing empty strings.
(node as any)[this.name] = values.next().value;
(this.element as any)[this.name] = values[0];
} else {

@@ -324,6 +390,6 @@ // Interpolation, so interpolate

if (i < s.length - 1) {
text += values.next().value;
text += values[i];
}
}
(node as any)[this.name] = text;
(this.element as any)[this.name] = text;
}

@@ -335,5 +401,6 @@ }

const t = html`<div someProp="${123}"></div>`;
const part = t.template.parts[0] as AttributePart;
t.template.parts[0] = new PropertyPart(part.index, part.rawName, part.strings);
t.renderTo(container);
const instance = new PropertySettingTemplateInstance(t.template);
const fragment = instance._clone();
instance.update(t.values);
container.appendChild(fragment);
assert.equal(container.innerHTML, '<div></div>');

@@ -347,5 +414,148 @@ assert.strictEqual((container.firstElementChild as any).someProp, 123);

});
suite('NodePart', () => {
let container: HTMLElement;
let startNode: Node;
let endNode: Node;
let part: NodePart;
mocha.run();
setup(() => {
container = document.createElement('div');
startNode = new Text();
endNode = new Text();
container.appendChild(startNode);
container.appendChild(endNode);
part = new NodePart(startNode, endNode);
});
suite('setValue', () => {
test('accepts a string', () => {
part.setValue('foo');
assert.equal(container.innerHTML, 'foo');
});
test('accepts a number', () => {
part.setValue(123);
assert.equal(container.innerHTML, '123');
});
test('accepts undefined', () => {
part.setValue(undefined);
assert.equal(container.innerHTML, '');
});
test('accepts null', () => {
part.setValue(null);
assert.equal(container.innerHTML, '');
});
test('accepts a thunk', () => {
part.setValue((_:any)=>123);
assert.equal(container.innerHTML, '123');
});
test('accepts thunks that throw as empty text', () => {
part.setValue((_:any)=>{throw new Error('e')});
assert.equal(container.innerHTML, '');
});
test('accepts an element', () => {
part.setValue(document.createElement('p'));
assert.equal(container.innerHTML, '<p></p>');
});
test('accepts arrays', () => {
part.setValue([1,2,3]);
assert.equal(container.innerHTML, '123');
});
test('accepts nested templates', () => {
part.setValue(html`<h1>${'foo'}</h1>`);
assert.equal(container.innerHTML, '<h1>foo</h1>');
});
test('accepts arrays of nested templates', () => {
part.setValue([1,2,3].map((i)=>html`${i}`));
assert.equal(container.innerHTML, '123');
});
test('accepts an array of elements', () => {
const children = [
document.createElement('p'),
document.createElement('a'),
document.createElement('span')
];
part.setValue(children);
assert.equal(container.innerHTML, '<p></p><a></a><span></span>');
});
test('updates when called multiple times with simple values', () => {
part.setValue('abc');
assert.equal(container.innerHTML, 'abc');
part.setValue('def');
assert.equal(container.innerHTML, 'def');
});
test('updates when called multiple times with arrays', () => {
part.setValue([1, 2, 3]);
assert.equal(container.innerHTML, '123');
part.setValue([4, 5]);
assert.equal(container.innerHTML, '45');
// check that we're not leaving orphaned marker nodes around
assert.deepEqual(['', '4', '', '5', ''], Array.from(container.childNodes).map((n) => n.nodeValue));
part.setValue([]);
assert.equal(container.innerHTML, '');
assert.deepEqual([], Array.from(container.childNodes).map((n) => n.nodeValue));
});
test('updates are stable when called multiple times with templates', () => {
let value = 'foo';
const r = () => html`<h1>${value}</h1>`;
part.setValue(r);
assert.equal(container.innerHTML, '<h1>foo</h1>');
const originalH1 = container.querySelector('h1');
value = 'bar';
part.setValue(r);
assert.equal(container.innerHTML, '<h1>bar</h1>');
const newH1 = container.querySelector('h1');
assert.isTrue(newH1 === originalH1);
});
test('updates are stable when called multiple times with arrays of templates', () => {
let items = [1, 2, 3];
const r = () => items.map((i)=>html`<li>${i}</li>`);
part.setValue(r);
assert.equal(container.innerHTML, '<li>1</li><li>2</li><li>3</li>');
const originalLIs = Array.from(container.querySelectorAll('li'));
items = [3, 2, 1];
part.setValue(r);
assert.equal(container.innerHTML, '<li>3</li><li>2</li><li>1</li>');
const newLIs = Array.from(container.querySelectorAll('li'));
assert.deepEqual(newLIs, originalLIs);
});
});
suite('clear', () => {
test('is a no-op on an already empty range', () => {
part.clear();
assert.deepEqual(Array.from(container.childNodes), [startNode, endNode]);
});
test('clears a range', () => {
container.insertBefore(new Text('foo'), endNode);
part.clear();
assert.deepEqual(Array.from(container.childNodes), [startNode, endNode]);
});
});
});
});

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc