зеркало из https://github.com/github/jtml.git
Коммит
59d18fe3f7
|
@ -3,15 +3,19 @@
|
|||
"plugins": ["github"],
|
||||
"extends": ["plugin:github/recommended", "plugin:github/typescript", "plugin:github/browser"],
|
||||
"rules": {
|
||||
"import/no-unresolved": "off",
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
"import/extensions": ["error", "always"]
|
||||
"import/extensions": ["error", "always"],
|
||||
"github/no-inner-html": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "test/*",
|
||||
"rules": {
|
||||
"github/unescaped-html-literal": "off"
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"github/unescaped-html-literal": "off",
|
||||
"i18n-text/no-en": "off"
|
||||
},
|
||||
"globals": {
|
||||
"chai": false,
|
||||
|
@ -24,7 +28,8 @@
|
|||
{
|
||||
"files": "*.cjs",
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"import/no-commonjs": "off"
|
||||
},
|
||||
"env": {
|
||||
"node": true
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
38
package.json
38
package.json
|
@ -13,13 +13,13 @@
|
|||
"license": "MIT",
|
||||
"author": "GitHub Inc.",
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"module": "lib/index.js",
|
||||
"main": "src/index.js",
|
||||
"module": "src/index.js",
|
||||
"files": [
|
||||
"lib"
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"build": "tsc --build ./tsconfig.build.json",
|
||||
"clean": "tsc --build --clean",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"pretest": "npm run build",
|
||||
|
@ -28,27 +28,29 @@
|
|||
},
|
||||
"prettier": "@github/prettier-config",
|
||||
"dependencies": {
|
||||
"@github/template-parts": "^0.3.0"
|
||||
"@github/template-parts": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@github/prettier-config": "0.0.4",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"chai": "^4.2.0",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-typescript": "^8.3.1",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"chai": "^4.3.6",
|
||||
"chromium": "^3.0.3",
|
||||
"eslint": "^7.12.0",
|
||||
"eslint-plugin-github": "^4.1.1",
|
||||
"karma": "^5.2.3",
|
||||
"esbuild": "^0.14.27",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-plugin-github": "^4.3.6",
|
||||
"karma": "^6.3.17",
|
||||
"karma-chai": "^0.1.0",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-chrome-launcher": "^3.1.1",
|
||||
"karma-esbuild": "^2.2.4",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-rollup-preprocessor": "^7.0.5",
|
||||
"karma-rollup-preprocessor": "^7.0.8",
|
||||
"karma-safarinative-launcher": "^1.1.0",
|
||||
"mocha": "^8.2.0",
|
||||
"rollup": "^2.32.1",
|
||||
"typescript": "^4.0.3"
|
||||
"mocha": "^9.2.2",
|
||||
"rollup": "^2.70.1",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import {NodeTemplatePart} from '@github/template-parts'
|
||||
import type {TemplatePart} from '@github/template-parts'
|
||||
|
||||
export function processDocumentFragment(part: TemplatePart, value: unknown): boolean {
|
||||
if (value instanceof DocumentFragment && part instanceof NodeTemplatePart) {
|
||||
if (value.childNodes.length) part.replace(...value.childNodes)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -29,7 +29,7 @@ class EventHandler {
|
|||
|
||||
export function processEvent(part: TemplatePart, value: unknown): boolean {
|
||||
if (part instanceof AttributeTemplatePart && part.attributeName.startsWith('on')) {
|
||||
EventHandler.for(part).set((value as unknown) as EventListener)
|
||||
EventHandler.for(part).set(value as unknown as EventListener)
|
||||
part.element.removeAttributeNS(part.attributeNamespace, part.attributeName)
|
||||
return true
|
||||
}
|
||||
|
|
111
src/html.ts
111
src/html.ts
|
@ -1,111 +1,6 @@
|
|||
import {
|
||||
TemplateInstance,
|
||||
NodeTemplatePart,
|
||||
createProcessor,
|
||||
processPropertyIdentity,
|
||||
processBooleanAttribute
|
||||
} from '@github/template-parts'
|
||||
import {processDirective} from './directive.js'
|
||||
import {processEvent} from './events.js'
|
||||
import type {TemplatePart, TemplateTypeInit} from '@github/template-parts'
|
||||
import {processor} from './processor.js'
|
||||
import {TemplateResult} from './template-result.js'
|
||||
|
||||
function processSubTemplate(part: TemplatePart, value: unknown): boolean {
|
||||
if (value instanceof TemplateResult && part instanceof NodeTemplatePart) {
|
||||
value.renderInto(part)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function processDocumentFragment(part: TemplatePart, value: unknown): boolean {
|
||||
if (value instanceof DocumentFragment && part instanceof NodeTemplatePart) {
|
||||
if (value.childNodes.length) part.replace(...value.childNodes)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isIterable(value: unknown): value is Iterable<unknown> {
|
||||
return typeof value === 'object' && Symbol.iterator in ((value as unknown) as Record<symbol, unknown>)
|
||||
}
|
||||
|
||||
function processIterable(part: TemplatePart, value: unknown): boolean {
|
||||
if (!isIterable(value)) return false
|
||||
if (part instanceof NodeTemplatePart) {
|
||||
const nodes = []
|
||||
for (const item of value) {
|
||||
if (item instanceof TemplateResult) {
|
||||
const fragment = document.createDocumentFragment()
|
||||
item.renderInto(fragment)
|
||||
nodes.push(...fragment.childNodes)
|
||||
} else if (item instanceof DocumentFragment) {
|
||||
nodes.push(...item.childNodes)
|
||||
} else {
|
||||
nodes.push(String(item))
|
||||
}
|
||||
}
|
||||
if (nodes.length) part.replace(...nodes)
|
||||
return true
|
||||
} else {
|
||||
part.value = Array.from(value).join(' ')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function processPart(part: TemplatePart, value: unknown): void {
|
||||
processDirective(part, value) ||
|
||||
processBooleanAttribute(part, value) ||
|
||||
processEvent(part, value) ||
|
||||
processSubTemplate(part, value) ||
|
||||
processDocumentFragment(part, value) ||
|
||||
processIterable(part, value) ||
|
||||
processPropertyIdentity(part, value)
|
||||
}
|
||||
|
||||
const templates = new WeakMap<TemplateStringsArray, HTMLTemplateElement>()
|
||||
const renderedTemplates = new WeakMap<Node | NodeTemplatePart, HTMLTemplateElement>()
|
||||
const renderedTemplateInstances = new WeakMap<Node | NodeTemplatePart, TemplateInstance>()
|
||||
export class TemplateResult {
|
||||
constructor(
|
||||
public readonly strings: TemplateStringsArray,
|
||||
public readonly values: unknown[],
|
||||
public readonly processor: TemplateTypeInit
|
||||
) {}
|
||||
|
||||
get template(): HTMLTemplateElement {
|
||||
if (templates.has(this.strings)) {
|
||||
return templates.get(this.strings)!
|
||||
} else {
|
||||
const template = document.createElement('template')
|
||||
const end = this.strings.length - 1
|
||||
template.innerHTML = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '')
|
||||
templates.set(this.strings, template)
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
renderInto(element: Node | NodeTemplatePart): void {
|
||||
const template = this.template
|
||||
if (renderedTemplates.get(element) !== template) {
|
||||
renderedTemplates.set(element, template)
|
||||
const instance = new TemplateInstance(template, this.values, this.processor)
|
||||
renderedTemplateInstances.set(element, instance)
|
||||
if (element instanceof NodeTemplatePart) {
|
||||
element.replace(...instance.children)
|
||||
} else {
|
||||
element.appendChild(instance)
|
||||
}
|
||||
return
|
||||
}
|
||||
renderedTemplateInstances.get(element)!.update((this.values as unknown) as Record<string, unknown>)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultProcessor = createProcessor(processPart)
|
||||
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult {
|
||||
return new TemplateResult(strings, values, defaultProcessor)
|
||||
}
|
||||
|
||||
export function render(result: TemplateResult, element: Node | NodeTemplatePart): void {
|
||||
result.renderInto(element)
|
||||
return new TemplateResult(strings, values, processor)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
export {TemplateResult, html, render} from './html.js'
|
||||
export {TemplateResult} from './template-result.js'
|
||||
export {render} from './render.js'
|
||||
export {processor} from './processor.js'
|
||||
export {html} from './html.js'
|
||||
export {isDirective, directive} from './directive.js'
|
||||
export {until} from './until.js'
|
||||
export {unsafeHTML} from './unsafe-html.js'
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import {TemplateResult} from './template-result.js'
|
||||
import {NodeTemplatePart} from '@github/template-parts'
|
||||
import type {TemplatePart} from '@github/template-parts'
|
||||
|
||||
function isIterable(value: unknown): value is Iterable<unknown> {
|
||||
return typeof value === 'object' && Symbol.iterator in (value as unknown as Record<symbol, unknown>)
|
||||
}
|
||||
|
||||
export function processIterable(part: TemplatePart, value: unknown): boolean {
|
||||
if (!isIterable(value)) return false
|
||||
if (part instanceof NodeTemplatePart) {
|
||||
const nodes = []
|
||||
for (const item of value) {
|
||||
if (item instanceof TemplateResult) {
|
||||
const fragment = document.createDocumentFragment()
|
||||
item.renderInto(fragment)
|
||||
nodes.push(...fragment.childNodes)
|
||||
} else if (item instanceof DocumentFragment) {
|
||||
nodes.push(...item.childNodes)
|
||||
} else {
|
||||
nodes.push(String(item))
|
||||
}
|
||||
}
|
||||
if (nodes.length) part.replace(...nodes)
|
||||
return true
|
||||
} else {
|
||||
part.value = Array.from(value).join(' ')
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import {createProcessor, processPropertyIdentity, processBooleanAttribute} from '@github/template-parts'
|
||||
import type {TemplatePart} from '@github/template-parts'
|
||||
|
||||
import {processDirective} from './directive.js'
|
||||
import {processEvent} from './events.js'
|
||||
import {processIterable} from './iterable.js'
|
||||
import {processDocumentFragment} from './document-fragment.js'
|
||||
import {processSubTemplate} from './sub-template.js'
|
||||
|
||||
export function processPart(part: TemplatePart, value: unknown): void {
|
||||
processDirective(part, value) ||
|
||||
processBooleanAttribute(part, value) ||
|
||||
processEvent(part, value) ||
|
||||
processSubTemplate(part, value) ||
|
||||
processDocumentFragment(part, value) ||
|
||||
processIterable(part, value) ||
|
||||
processPropertyIdentity(part, value)
|
||||
}
|
||||
|
||||
export const processor = createProcessor(processPart)
|
|
@ -0,0 +1,7 @@
|
|||
import type {NodeTemplatePart} from '@github/template-parts'
|
||||
|
||||
import {TemplateResult} from './template-result.js'
|
||||
|
||||
export function render(result: TemplateResult, element: Node | NodeTemplatePart): void {
|
||||
result.renderInto(element)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import {NodeTemplatePart} from '@github/template-parts'
|
||||
import type {TemplatePart} from '@github/template-parts'
|
||||
|
||||
import {TemplateResult} from './template-result.js'
|
||||
|
||||
export function processSubTemplate(part: TemplatePart, value: unknown): boolean {
|
||||
if (value instanceof TemplateResult && part instanceof NodeTemplatePart) {
|
||||
value.renderInto(part)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import {TemplateInstance, NodeTemplatePart} from '@github/template-parts'
|
||||
import type {TemplateTypeInit} from '@github/template-parts'
|
||||
|
||||
const templates = new WeakMap<TemplateStringsArray, HTMLTemplateElement>()
|
||||
const renderedTemplates = new WeakMap<Node | NodeTemplatePart, HTMLTemplateElement>()
|
||||
const renderedTemplateInstances = new WeakMap<Node | NodeTemplatePart, TemplateInstance>()
|
||||
export class TemplateResult {
|
||||
constructor(
|
||||
public readonly strings: TemplateStringsArray,
|
||||
public readonly values: unknown[],
|
||||
public processor: TemplateTypeInit
|
||||
) {}
|
||||
|
||||
get template(): HTMLTemplateElement {
|
||||
if (templates.has(this.strings)) {
|
||||
return templates.get(this.strings)!
|
||||
} else {
|
||||
const template = document.createElement('template')
|
||||
const end = this.strings.length - 1
|
||||
template.innerHTML = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '')
|
||||
templates.set(this.strings, template)
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
renderInto(element: Node | NodeTemplatePart): void {
|
||||
const template = this.template
|
||||
if (renderedTemplates.get(element) !== template) {
|
||||
renderedTemplates.set(element, template)
|
||||
const instance = new TemplateInstance(template, this.values, this.processor)
|
||||
renderedTemplateInstances.set(element, instance)
|
||||
if (element instanceof NodeTemplatePart) {
|
||||
element.replace(...instance.children)
|
||||
} else {
|
||||
element.appendChild(instance)
|
||||
}
|
||||
return
|
||||
}
|
||||
renderedTemplateInstances.get(element)!.update(this.values as unknown as Record<string, unknown>)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import {processPart} from './html.js'
|
||||
import {directive} from './directive.js'
|
||||
import type {TemplatePart} from '@github/template-parts'
|
||||
|
||||
import {processPart} from './processor.js'
|
||||
import {directive} from './directive.js'
|
||||
|
||||
const untils: WeakMap<TemplatePart, {i: number}> = new WeakMap()
|
||||
export const until = directive((...promises: unknown[]) => (part: TemplatePart) => {
|
||||
if (!untils.has(part)) untils.set(part, {i: promises.length})
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import {expect} from 'chai'
|
||||
import {html, render, directive} from '../lib/index.js'
|
||||
|
||||
describe('directives', () => {
|
||||
let surface: HTMLElement
|
||||
beforeEach(() => {
|
||||
surface = document.createElement('section')
|
||||
})
|
||||
|
||||
it('handles directives differently', () => {
|
||||
const setAsFoo = directive(() => part => {
|
||||
part.value = 'foo'
|
||||
})
|
||||
const main = () => html`<div class="${setAsFoo()}"></div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="foo"></div>')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,85 @@
|
|||
import {expect} from 'chai'
|
||||
import {html, render} from '../lib/index.js'
|
||||
|
||||
describe('events', () => {
|
||||
let surface: HTMLElement
|
||||
beforeEach(() => {
|
||||
surface = document.createElement('section')
|
||||
})
|
||||
|
||||
describe('event listeners', () => {
|
||||
it('handles event listeners properly', () => {
|
||||
let i = 0
|
||||
const main = () => html`<div onclick="${() => (i += 1)}"></div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.querySelector('div')?.click()
|
||||
expect(i).to.equal(1)
|
||||
surface.querySelector('div')?.dispatchEvent(new CustomEvent('click'))
|
||||
expect(i).to.equal(2)
|
||||
})
|
||||
|
||||
it('does not rebind event listeners multiple times', () => {
|
||||
let i = 0
|
||||
const main = () => html`<div onclick="${() => (i += 1)}"></div>`
|
||||
render(main(), surface)
|
||||
render(main(), surface)
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.querySelector('div')?.click()
|
||||
expect(i).to.equal(1)
|
||||
surface.querySelector('div')?.dispatchEvent(new CustomEvent('click'))
|
||||
expect(i).to.equal(2)
|
||||
})
|
||||
|
||||
it('allows events to be driven by params', () => {
|
||||
let i = 0
|
||||
const main = (amt: number) => html`<div onclick="${() => (i += amt)}"></div>`
|
||||
render(main(1), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.querySelector('div')?.click()
|
||||
expect(i).to.equal(1)
|
||||
render(main(4), surface)
|
||||
surface.querySelector('div')?.dispatchEvent(new CustomEvent('click'))
|
||||
expect(i).to.equal(5)
|
||||
})
|
||||
|
||||
it('will unbind event listeners by passing null', () => {
|
||||
let i = 0
|
||||
const main = (listener: Function | null) => html`<div onclick="${listener}"></div>`
|
||||
render(
|
||||
main(() => (i += 1)),
|
||||
surface
|
||||
)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.querySelector('div')?.click()
|
||||
expect(i).to.equal(1)
|
||||
render(main(null), surface)
|
||||
surface.querySelector('div')?.click()
|
||||
surface.querySelector('div')?.click()
|
||||
surface.querySelector('div')?.click()
|
||||
expect(i).to.equal(1)
|
||||
})
|
||||
|
||||
it('binds event handler objects', () => {
|
||||
const handler = {
|
||||
i: 0,
|
||||
handleEvent() {
|
||||
this.i += 1
|
||||
}
|
||||
}
|
||||
const main = () => html`<div onclick="${handler}"></div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(handler.i).to.equal(0)
|
||||
surface.querySelector('div')?.click()
|
||||
expect(handler.i).to.equal(1)
|
||||
surface.querySelector('div')?.dispatchEvent(new CustomEvent('click'))
|
||||
expect(handler.i).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
252
test/html.ts
252
test/html.ts
|
@ -1,4 +1,5 @@
|
|||
import {html, render, directive} from '../lib/index.js'
|
||||
import {expect} from 'chai'
|
||||
import {html} from '../lib/index.js'
|
||||
|
||||
describe('html', () => {
|
||||
it('creates new TemplateResults with each call', () => {
|
||||
|
@ -9,252 +10,3 @@ describe('html', () => {
|
|||
expect(other()).to.not.equal(other())
|
||||
})
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
let surface
|
||||
beforeEach(() => {
|
||||
surface = document.createElement('section')
|
||||
})
|
||||
|
||||
it('calls `createCallback` on first render', () => {
|
||||
const main = (x = null) => html`<div class="${x}"></div>`
|
||||
let called = false
|
||||
const instance = main()
|
||||
instance.processor = {
|
||||
processCallback() {
|
||||
throw new Error('Expected processCallback to not be called')
|
||||
},
|
||||
createCallback() {
|
||||
called = true
|
||||
}
|
||||
}
|
||||
instance.renderInto(surface)
|
||||
expect(called).to.equal(true)
|
||||
})
|
||||
|
||||
it('memoizes by TemplateResult#template, updating old templates with new values', () => {
|
||||
const main = (x = null) => html`<div class="${x}"></div>`
|
||||
render(main('foo'), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="foo"></div>')
|
||||
render(main('bar'), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="bar"></div>')
|
||||
})
|
||||
|
||||
describe('nesting', () => {
|
||||
it('supports nested html calls', () => {
|
||||
const main = child => html`<div>${child}</div>`
|
||||
const child = message => html`<span>${message}</span>`
|
||||
render(main(child('Hello')), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Hello</span></div>')
|
||||
})
|
||||
|
||||
it('updates nodes from repeat calls', () => {
|
||||
const main = child => html`<div>${child}</div>`
|
||||
const child = message => html`<span>${message}</span>`
|
||||
render(main(child('Hello')), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Hello</span></div>')
|
||||
render(main(child('Goodbye')), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Goodbye</span></div>')
|
||||
})
|
||||
|
||||
it('can nest document fragments and text nodes', () => {
|
||||
const main = frag => html`<span>${frag}</span>`
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(new Text('Hello World'))
|
||||
render(main(fragment), surface)
|
||||
expect(surface.innerHTML).to.equal('<span>Hello World</span>')
|
||||
fragment.append(document.createTextNode('Hello Universe!'))
|
||||
render(main(fragment), surface)
|
||||
expect(surface.innerHTML).to.equal('<span>Hello Universe!</span>')
|
||||
})
|
||||
|
||||
it('renders DocumentFragments nested in sub templates nested in arrays', () => {
|
||||
const sub = () => {
|
||||
const frag = document.createDocumentFragment()
|
||||
frag.appendChild(document.createElement('div'))
|
||||
return html`<span>${frag}</span>`
|
||||
}
|
||||
const main = () => html`<div>${[sub(), sub()]}</div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.contain('<div><span><div></div></span><span><div></div></span></div>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('iterables', () => {
|
||||
it('supports arrays of strings in nodes', () => {
|
||||
const main = list => html`<div>${list}</div>`
|
||||
render(main(['one', 'two', 'three']), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
render(main(['four', 'five', 'six']), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports iterables of Sub Templates with text nodes', () => {
|
||||
const main = list => html`<div>${list}</div>`
|
||||
let fragments = ['one', 'two', 'three'].map(text => html`${text}`)
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
fragments = ['four', 'five', 'six'].map(text => html`${text}`)
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports iterables of fragments with text nodes', () => {
|
||||
const main = list => html`<div>${list}</div>`
|
||||
let fragments = ['one', 'two', 'three'].map(text => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(new Text(text))
|
||||
return fragment
|
||||
})
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
fragments = ['four', 'five', 'six'].map(text => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(new Text(text))
|
||||
return fragment
|
||||
})
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports other strings iterables in nodes', () => {
|
||||
const main = list => html`<div>${list}</div>`
|
||||
render(main(new Set(['one', 'two', 'three'])), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
render(
|
||||
main(
|
||||
new Map([
|
||||
[4, 'four'],
|
||||
[5, 'five'],
|
||||
[6, 'six']
|
||||
]).values()
|
||||
),
|
||||
surface
|
||||
)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports iterables of strings in attributes', () => {
|
||||
const main = list => html`<div class="${list}"></div>`
|
||||
render(main(['one', 'two', 'three']), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="one two three"></div>')
|
||||
render(main(new Set(['four', 'five', 'six'])), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="four five six"></div>')
|
||||
})
|
||||
|
||||
it('supports nested iterables of document fragments', () => {
|
||||
// prettier-ignore
|
||||
const main = list => html`<ul>${list}</ul>`
|
||||
render(
|
||||
main(
|
||||
['One', 'Two'].map(text => {
|
||||
const f = document.createDocumentFragment()
|
||||
const li = document.createElement('li')
|
||||
li.textContent = text
|
||||
f.append(li)
|
||||
return f
|
||||
})
|
||||
),
|
||||
surface
|
||||
)
|
||||
expect(surface.innerHTML).to.equal('<ul><li>One</li><li>Two</li></ul>')
|
||||
})
|
||||
|
||||
it('supports nested iterables of templates', () => {
|
||||
const child = item => html`<li>${item.name}</li>`
|
||||
// prettier-ignore
|
||||
const main = list => html`<ul>${list.map(child)}</ul>`
|
||||
render(main([{name: 'One'}, {name: 'Two'}, {name: 'Three'}]), surface)
|
||||
expect(surface.innerHTML).to.equal('<ul><li>One</li><li>Two</li><li>Three</li></ul>')
|
||||
render(main([{name: 'Two'}, {name: 'Three'}, {name: 'Four'}]), surface)
|
||||
expect(surface.innerHTML).to.equal('<ul><li>Two</li><li>Three</li><li>Four</li></ul>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('directives', () => {
|
||||
it('handles directives differently', () => {
|
||||
const setAsFoo = directive(() => part => {
|
||||
part.value = 'foo'
|
||||
})
|
||||
const main = () => html`<div class="${setAsFoo()}"></div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="foo"></div>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('event listeners', () => {
|
||||
it('handles event listeners properly', () => {
|
||||
let i = 0
|
||||
const main = () => html`<div onclick="${() => (i += 1)}"></div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.children[0].click()
|
||||
expect(i).to.equal(1)
|
||||
surface.children[0].dispatchEvent(new CustomEvent('click'))
|
||||
expect(i).to.equal(2)
|
||||
})
|
||||
|
||||
it('does not rebind event listeners multiple times', () => {
|
||||
let i = 0
|
||||
const main = () => html`<div onclick="${() => (i += 1)}"></div>`
|
||||
render(main(), surface)
|
||||
render(main(), surface)
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.children[0].click()
|
||||
expect(i).to.equal(1)
|
||||
surface.children[0].dispatchEvent(new CustomEvent('click'))
|
||||
expect(i).to.equal(2)
|
||||
})
|
||||
|
||||
it('allows events to be driven by params', () => {
|
||||
let i = 0
|
||||
const main = amt => html`<div onclick="${() => (i += amt)}"></div>`
|
||||
render(main(1), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.children[0].click()
|
||||
expect(i).to.equal(1)
|
||||
render(main(4), surface)
|
||||
surface.children[0].dispatchEvent(new CustomEvent('click'))
|
||||
expect(i).to.equal(5)
|
||||
})
|
||||
|
||||
it('will unbind event listeners by passing null', () => {
|
||||
let i = 0
|
||||
const main = listener => html`<div onclick="${listener}"></div>`
|
||||
render(
|
||||
main(() => (i += 1)),
|
||||
surface
|
||||
)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(i).to.equal(0)
|
||||
surface.children[0].click()
|
||||
expect(i).to.equal(1)
|
||||
render(main(null), surface)
|
||||
surface.children[0].click()
|
||||
surface.children[0].click()
|
||||
surface.children[0].click()
|
||||
expect(i).to.equal(1)
|
||||
})
|
||||
|
||||
it('binds event handler objects', () => {
|
||||
const handler = {
|
||||
i: 0,
|
||||
handleEvent() {
|
||||
this.i += 1
|
||||
}
|
||||
}
|
||||
const main = () => html`<div onclick="${handler}"></div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
expect(handler.i).to.equal(0)
|
||||
surface.children[0].click()
|
||||
expect(handler.i).to.equal(1)
|
||||
surface.children[0].dispatchEvent(new CustomEvent('click'))
|
||||
expect(handler.i).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
import {expect} from 'chai'
|
||||
import {html, render, TemplateResult} from '../lib/index.js'
|
||||
|
||||
describe('iterables', () => {
|
||||
let surface: HTMLElement
|
||||
beforeEach(() => {
|
||||
surface = document.createElement('section')
|
||||
})
|
||||
|
||||
it('supports arrays of strings in nodes', () => {
|
||||
const main = (list: string[]) => html`<div>${list}</div>`
|
||||
render(main(['one', 'two', 'three']), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
render(main(['four', 'five', 'six']), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports iterables of Sub Templates with text nodes', () => {
|
||||
const main = (list: Iterable<TemplateResult>) => html`<div>${list}</div>`
|
||||
let fragments = ['one', 'two', 'three'].map(text => html`${text}`)
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
fragments = ['four', 'five', 'six'].map(text => html`${text}`)
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports iterables of fragments with text nodes', () => {
|
||||
const main = (list: Iterable<DocumentFragment>) => html`<div>${list}</div>`
|
||||
let fragments = ['one', 'two', 'three'].map(text => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(new Text(text))
|
||||
return fragment
|
||||
})
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
fragments = ['four', 'five', 'six'].map(text => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(new Text(text))
|
||||
return fragment
|
||||
})
|
||||
render(main(fragments), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports other strings iterables in nodes', () => {
|
||||
const main = (list: Iterable<string>) => html`<div>${list}</div>`
|
||||
render(main(new Set(['one', 'two', 'three'])), surface)
|
||||
expect(surface.innerHTML).to.equal('<div>onetwothree</div>')
|
||||
render(
|
||||
main(
|
||||
new Map([
|
||||
[4, 'four'],
|
||||
[5, 'five'],
|
||||
[6, 'six']
|
||||
]).values()
|
||||
),
|
||||
surface
|
||||
)
|
||||
expect(surface.innerHTML).to.equal('<div>fourfivesix</div>')
|
||||
})
|
||||
|
||||
it('supports iterables of strings in attributes', () => {
|
||||
const main = (list: Iterable<string>) => html`<div class="${list}"></div>`
|
||||
render(main(['one', 'two', 'three']), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="one two three"></div>')
|
||||
render(main(new Set(['four', 'five', 'six'])), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="four five six"></div>')
|
||||
})
|
||||
|
||||
it('supports nested iterables of document fragments', () => {
|
||||
// prettier-ignore
|
||||
const main = (list: DocumentFragment[]) => html`<ul>${list}</ul>`
|
||||
render(
|
||||
main(
|
||||
['One', 'Two'].map(text => {
|
||||
const f = document.createDocumentFragment()
|
||||
const li = document.createElement('li')
|
||||
li.textContent = text
|
||||
f.append(li)
|
||||
return f
|
||||
})
|
||||
),
|
||||
surface
|
||||
)
|
||||
expect(surface.innerHTML).to.equal('<ul><li>One</li><li>Two</li></ul>')
|
||||
})
|
||||
|
||||
it('supports nested iterables of templates', () => {
|
||||
const child = (item: Record<string, unknown>) => html`<li>${item.name}</li>`
|
||||
// prettier-ignore
|
||||
const main = (list: Array<Record<string, unknown>>) => html`<ul>${list.map(child)}</ul>`
|
||||
render(main([{name: 'One'}, {name: 'Two'}, {name: 'Three'}]), surface)
|
||||
expect(surface.innerHTML).to.equal('<ul><li>One</li><li>Two</li><li>Three</li></ul>')
|
||||
render(main([{name: 'Two'}, {name: 'Three'}, {name: 'Four'}]), surface)
|
||||
expect(surface.innerHTML).to.equal('<ul><li>Two</li><li>Three</li><li>Four</li></ul>')
|
||||
})
|
||||
})
|
|
@ -1,5 +1,3 @@
|
|||
const resolve = require('@rollup/plugin-node-resolve').default
|
||||
|
||||
process.env.CHROME_BIN = require('chromium').path
|
||||
|
||||
module.exports = function (config) {
|
||||
|
@ -9,18 +7,13 @@ module.exports = function (config) {
|
|||
files: [
|
||||
{pattern: 'lib/*.js', type: 'module', included: false},
|
||||
{pattern: 'node_modules/**', type: 'module', included: false},
|
||||
{pattern: 'test/*', type: 'module', included: true, watched: false}
|
||||
{pattern: 'test/*.ts', type: 'module', included: true, watched: false}
|
||||
],
|
||||
preprocessors: {
|
||||
'test/*.ts': ['rollup']
|
||||
'test/*.ts': ['esbuild']
|
||||
},
|
||||
rollupPreprocessor: {
|
||||
plugins: [resolve()],
|
||||
output: {
|
||||
format: 'iife',
|
||||
name: 'test',
|
||||
sourcemap: 'inline'
|
||||
}
|
||||
esbuild: {
|
||||
target: 'es2019'
|
||||
},
|
||||
reporters: ['mocha'],
|
||||
port: 9876,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import {expect} from 'chai'
|
||||
import {html, render} from '../lib/index.js'
|
||||
import type {TemplateResult} from '../lib/index.js'
|
||||
|
||||
describe('render', () => {
|
||||
let surface: HTMLElement
|
||||
beforeEach(() => {
|
||||
surface = document.createElement('section')
|
||||
})
|
||||
|
||||
it('memoizes by TemplateResult#template, updating old templates with new values', () => {
|
||||
const main = (x: string | null = null) => html`<div class="${x}"></div>`
|
||||
render(main('foo'), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="foo"></div>')
|
||||
render(main('bar'), surface)
|
||||
expect(surface.innerHTML).to.equal('<div class="bar"></div>')
|
||||
})
|
||||
|
||||
describe('nesting', () => {
|
||||
it('supports nested html calls', () => {
|
||||
const main = (child: TemplateResult) => html`<div>${child}</div>`
|
||||
const child = (message: string) => html`<span>${message}</span>`
|
||||
render(main(child('Hello')), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Hello</span></div>')
|
||||
})
|
||||
|
||||
it('updates nodes from repeat calls', () => {
|
||||
const main = (child: TemplateResult) => html`<div>${child}</div>`
|
||||
const child = (message: string) => html`<span>${message}</span>`
|
||||
render(main(child('Hello')), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Hello</span></div>')
|
||||
render(main(child('Goodbye')), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Goodbye</span></div>')
|
||||
})
|
||||
|
||||
it('can nest document fragments and text nodes', () => {
|
||||
const main = (frag: DocumentFragment) => html`<span>${frag}</span>`
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.append(new Text('Hello World'))
|
||||
render(main(fragment), surface)
|
||||
expect(surface.innerHTML).to.equal('<span>Hello World</span>')
|
||||
fragment.append(document.createTextNode('Hello Universe!'))
|
||||
render(main(fragment), surface)
|
||||
expect(surface.innerHTML).to.equal('<span>Hello Universe!</span>')
|
||||
})
|
||||
|
||||
it('renders DocumentFragments nested in sub templates nested in arrays', () => {
|
||||
const sub = () => {
|
||||
const frag = document.createDocumentFragment()
|
||||
frag.appendChild(document.createElement('div'))
|
||||
return html`<span>${frag}</span>`
|
||||
}
|
||||
const main = () => html`<div>${[sub(), sub()]}</div>`
|
||||
render(main(), surface)
|
||||
expect(surface.innerHTML).to.contain('<div><span><div></div></span><span><div></div></span></div>')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,3 +1,4 @@
|
|||
import {expect} from 'chai'
|
||||
import {html, render, unsafeHTML} from '../lib/index.js'
|
||||
|
||||
describe('unsafeHTML', () => {
|
||||
|
@ -23,7 +24,7 @@ describe('unsafeHTML', () => {
|
|||
})
|
||||
it('updates correctly', async () => {
|
||||
const surface = document.createElement('section')
|
||||
const fn = name => html`<div>${unsafeHTML(`<span>Hello</span><span>${name}</span>`)}</div>`
|
||||
const fn = (name: string) => html`<div>${unsafeHTML(`<span>Hello</span><span>${name}</span>`)}</div>`
|
||||
render(fn('World'), surface)
|
||||
expect(surface.innerHTML).to.equal('<div><span>Hello</span><span>World</span></div>')
|
||||
render(fn('Universe'), surface)
|
||||
|
|
|
@ -1,36 +1,37 @@
|
|||
import {expect} from 'chai'
|
||||
import {html, render, until} from '../lib/index.js'
|
||||
|
||||
describe('until', () => {
|
||||
it('renders a Promise when it resolves', async () => {
|
||||
let resolve
|
||||
let resolve: Function
|
||||
const promise = new Promise(res => (resolve = res))
|
||||
const surface = document.createElement('section')
|
||||
render(html`<div>${until(promise)}</div>`, surface)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
resolve('foo')
|
||||
resolve!('foo')
|
||||
await promise
|
||||
expect(surface.innerHTML).to.equal('<div>foo</div>')
|
||||
})
|
||||
|
||||
it('renders non-promise values until promises have resolved', async () => {
|
||||
let resolve
|
||||
let resolve: Function
|
||||
const promise = new Promise(res => (resolve = res))
|
||||
const surface = document.createElement('section')
|
||||
render(html`<div>${until(promise, 'loading...')}</div>`, surface)
|
||||
expect(surface.innerHTML).to.equal('<div>loading...</div>')
|
||||
resolve('foo')
|
||||
resolve!('foo')
|
||||
await promise
|
||||
expect(surface.innerHTML).to.equal('<div>foo</div>')
|
||||
})
|
||||
|
||||
it('renders values only once', async () => {
|
||||
let resolve
|
||||
let resolve: Function
|
||||
const promise = new Promise(res => (resolve = res))
|
||||
const surface = document.createElement('section')
|
||||
const exec = () => render(html`<div>${until(promise, 'loading...')}</div>`, surface)
|
||||
exec()
|
||||
expect(surface.innerHTML).to.equal('<div>loading...</div>')
|
||||
resolve('foo')
|
||||
resolve!('foo')
|
||||
await promise
|
||||
exec()
|
||||
expect(surface.innerHTML).to.equal('<div>foo</div>')
|
||||
|
@ -39,52 +40,53 @@ describe('until', () => {
|
|||
})
|
||||
|
||||
it('can re-render content as it changes', async () => {
|
||||
let resolve
|
||||
let resolve: Function
|
||||
const promise = new Promise(res => (resolve = res))
|
||||
const surface = document.createElement('section')
|
||||
const exec = a => render(html`<div>${until(promise, a)}</div>`, surface)
|
||||
const exec = (a: string) => render(html`<div>${until(promise, a)}</div>`, surface)
|
||||
exec('loading...')
|
||||
expect(surface.innerHTML).to.equal('<div>loading...</div>')
|
||||
exec('still loading...')
|
||||
expect(surface.innerHTML).to.equal('<div>still loading...</div>')
|
||||
exec('taking forever...')
|
||||
expect(surface.innerHTML).to.equal('<div>taking forever...</div>')
|
||||
resolve('foo')
|
||||
resolve!('foo')
|
||||
await promise
|
||||
expect(surface.innerHTML).to.equal('<div>foo</div>')
|
||||
})
|
||||
|
||||
it('will not render promises behind already resolved ones', async () => {
|
||||
let resolveFoo, resolveBar
|
||||
let resolveFoo: Function
|
||||
let resolveBar: Function
|
||||
const promiseFoo = new Promise(res => (resolveFoo = res))
|
||||
const promiseBar = new Promise(res => (resolveBar = res))
|
||||
const surface = document.createElement('section')
|
||||
const exec = () => render(html`<div>${until(promiseFoo, promiseBar)}</div>`, surface)
|
||||
exec()
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
resolveFoo('foo')
|
||||
resolveFoo!('foo')
|
||||
await promiseFoo
|
||||
expect(surface.innerHTML).to.equal('<div>foo</div>')
|
||||
resolveBar('bar')
|
||||
resolveBar!('bar')
|
||||
await promiseBar
|
||||
expect(surface.innerHTML).to.equal('<div>foo</div>')
|
||||
})
|
||||
|
||||
it('supports boolean attributes', async () => {
|
||||
let resolve
|
||||
let resolve: Function
|
||||
const promise = new Promise(res => (resolve = res))
|
||||
const surface = document.createElement('section')
|
||||
const exec = a => render(html`<div hidden="${until(promise, a)}"></div>`, surface)
|
||||
const exec = (a: boolean) => render(html`<div hidden="${until(promise, a)}"></div>`, surface)
|
||||
exec(false)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
exec(true)
|
||||
expect(surface.innerHTML).to.equal('<div hidden=""></div>')
|
||||
exec(false)
|
||||
expect(surface.innerHTML).to.equal('<div></div>')
|
||||
resolve(true)
|
||||
resolve!(true)
|
||||
await promise
|
||||
expect(surface.innerHTML).to.equal('<div hidden=""></div>')
|
||||
resolve(false)
|
||||
resolve!(false)
|
||||
expect(surface.innerHTML).to.equal('<div hidden=""></div>')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"exclude": ["test"],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./lib",
|
||||
"noEmit": false
|
||||
}
|
||||
}
|
|
@ -1,18 +1,15 @@
|
|||
{
|
||||
"include": ["src"],
|
||||
"include": ["src", "test"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": false,
|
||||
"outDir": "./lib",
|
||||
"noEmit": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "ES2017"
|
||||
"target": "ES2019"
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче