lit-html
Version:
HTML template literals in JavaScript
178 lines (158 loc) • 6.64 kB
text/typescript
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {removeNodes} from './dom.js';
import {insertNodeIntoTemplate, removeNodesFromTemplate} from './modify-template.js';
import {templateInstances} from './render.js';
import {templateCaches} from './template-factory.js';
import {TemplateInstance} from './template-instance.js';
import {TemplateResult} from './template-result.js';
import {Template} from './template.js';
export {html, svg, TemplateResult} from '../lit-html.js';
declare global {
interface Window {
ShadyCSS: any;
}
class ShadowRoot {}
}
// Get a key to lookup in `templateCaches`.
const getTemplateCacheKey = (type: string, scopeName: string) =>
`${type}--${scopeName}`;
let compatibleShadyCSSVersion = true;
if (typeof window.ShadyCSS === 'undefined') {
compatibleShadyCSSVersion = false;
} else if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') {
console.warn(
`Incompatible ShadyCSS version detected.` +
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` +
`@webcomponents/shadycss@1.3.1.`);
compatibleShadyCSSVersion = false;
}
/**
* Template factory which scopes template DOM using ShadyCSS.
* @param scopeName {string}
*/
const shadyTemplateFactory = (scopeName: string) =>
(result: TemplateResult) => {
const cacheKey = getTemplateCacheKey(result.type, scopeName);
let templateCache = templateCaches.get(cacheKey);
if (templateCache === undefined) {
templateCache = new Map<TemplateStringsArray, Template>();
templateCaches.set(cacheKey, templateCache);
}
let template = templateCache.get(result.strings);
if (template === undefined) {
const element = result.getTemplateElement();
if (compatibleShadyCSSVersion) {
window.ShadyCSS.prepareTemplateDom(element, scopeName);
}
template = new Template(result, element);
templateCache.set(result.strings, template);
}
return template;
};
const TEMPLATE_TYPES = ['html', 'svg'];
/**
* Removes all style elements from Templates for the given scopeName.
*/
function removeStylesFromLitTemplates(scopeName: string) {
TEMPLATE_TYPES.forEach((type) => {
const templates = templateCaches.get(getTemplateCacheKey(type, scopeName));
if (templates !== undefined) {
templates.forEach((template) => {
const {element: {content}} = template;
// IE 11 doesn't support the iterable param Set constructor
const styles = new Set<Element>();
Array.from(content.querySelectorAll('style')).forEach((s: Element) => {
styles.add(s);
});
removeNodesFromTemplate(template, styles);
});
}
});
}
const shadyRenderSet = new Set<string>();
/**
* For the given scope name, ensures that ShadyCSS style scoping is performed.
* This is done just once per scope name so the fragment and template cannot
* be modified.
* (1) extracts styles from the rendered fragment and hands them to ShadyCSS
* to be scoped and appended to the document
* (2) removes style elements from all lit-html Templates for this scope name.
*
* Note, <style> elements can only be placed into templates for the
* initial rendering of the scope. If <style> elements are included in templates
* dynamically rendered to the scope (after the first scope render), they will
* not be scoped and the <style> will be left in the template and rendered
* output.
*/
const ensureStylesScoped =
(fragment: DocumentFragment, template: Template, scopeName: string) => {
// only scope element template once per scope name
if (!shadyRenderSet.has(scopeName)) {
shadyRenderSet.add(scopeName);
const styleTemplate = document.createElement('template');
Array.from(fragment.querySelectorAll('style')).forEach((s: Element) => {
styleTemplate.content.appendChild(s);
});
window.ShadyCSS.prepareTemplateStyles(styleTemplate, scopeName);
// Fix templates: note the expectation here is that the given `fragment`
// has been generated from the given `template` which contains
// the set of templates rendered into this scope.
// It is only from this set of initial templates from which styles
// will be scoped and removed.
removeStylesFromLitTemplates(scopeName);
// ApplyShim case
if (window.ShadyCSS.nativeShadow) {
const style = styleTemplate.content.querySelector('style');
if (style !== null) {
// Insert style into rendered fragment
fragment.insertBefore(style, fragment.firstChild);
// Insert into lit-template (for subsequent renders)
insertNodeIntoTemplate(
template,
style.cloneNode(true),
template.element.content.firstChild);
}
}
}
};
// NOTE: We're copying code from lit-html's `render` method here.
// We're doing this explicitly because the API for rendering templates is likely
// to change in the near term.
export function render(
result: TemplateResult,
container: Element|DocumentFragment,
scopeName: string) {
const templateFactory = shadyTemplateFactory(scopeName);
const template = templateFactory(result);
let instance = templateInstances.get(container);
// Repeat render, just call update()
if (instance !== undefined && instance.template === template &&
instance.processor === result.processor) {
instance.update(result.values);
return;
}
// First render, create a new TemplateInstance and append it
instance = new TemplateInstance(template, result.processor, templateFactory);
templateInstances.set(container, instance);
const fragment = instance._clone();
instance.update(result.values);
// If there's a shadow host, do ShadyCSS scoping...
if (container instanceof ShadowRoot && compatibleShadyCSSVersion) {
ensureStylesScoped(fragment, template, scopeName);
window.ShadyCSS.styleElement(container.host);
}
removeNodes(container, container.firstChild);
container.appendChild(fragment);
}