Getting Started
73 |Installation
74 |npm
75 |lit-htm is distributed on npm, in the lit-html package.
76 |77 | npm install lit-html 78 |
unpkg.com
79 |lit-html is also loadable directly from the unpkg.com CDN:
80 |81 | import {html, render} from 'https://unpkg.com/lit-html/lib/lit-extended.js?module'; 82 |
Importing
83 |lit-html is written in and distributed as standard JavaScript modules. 84 | Modules are increasingly supported in JavaScript environments and are shipping in Chrome, Opera and Safari, and soon will be in Firefox and Edge.
85 |To use lit-html, import it via a path:
86 |87 | import {html, render} from './node_modules/lit-html/lib/lit-extended.js'; 88 |
The path to use depends on where you've installed lit-html to. Browsers only support importing other modules by path, not by package name, so without other tools involved, you'll have to use paths.
89 |If you use some tool than can convert package names into paths, then you can import by path:
90 |91 | import {html, render} from 'lit-html/lib/lit-extended.js'; 92 |
Why is lit-html distributed as JavaScript modules, not as XXX?
93 |Until modules arrived, browsers have not had a standard way to import code from code, and user-land module loaders or bundlers were required. Since there was no standard, competing formats multiplied. Often libraries will publish in a number of formats to support users of different tools, but this causes problems when a common library is depended on by many other intermediate libraries: If some of those intermediate libraries load format A, and others load format B, and yet others load format C, etc., then multiple copies are loaded cause bloat, performance slowdowns, and sometimes hard-to-find bugs.
94 |The only true solution is to have one canonical version of a library that all other libraries import. Since modules support is rolling out to browsers now, and moduels are very well supported by tools, it makes for that format to be modules.
95 | 96 |How it Works
75 |lit-html
utilizes some unique properties of JavaScript template literals and HTML <template>
elements to function and achieve fast performance. So it's helpful to understand them first.
Tagged Template Literals
77 |A JavaScript template literal is a string literal that can have JavaScript expressions embedded in it:
78 |
79 | `My name is ${name}.`
80 |
The literal uses backticks instead of quotes, and can span multiple lines. The part inside the ${}
can be any JavaScript expression.
A tagged template literal is prefixed with a special template tag function:
82 |83 | let name = 'Monica'; 84 | tag`My name is ${name}.` 85 |
Tags are functions that take the literal strings of the template and values of the embedded expressions, and return a new value. This can be any kind of value, not just strings. lit-html returns an object representing the template, called a TemplateResult
.
The key features of template tags that lit-html utilizes to make updates fast is that the object holding the literals strings of the template is exactly the same for every call to the tag for a particular template.
87 |This means that the strings can be used as a key into a cache so that lit-html can do the template preparation just once, the first time it renders a template, and updates skip that work.
88 |HTML <template>
Elements
89 | A <template>
element is an inert fragment of DOM. Inside a <template>
, script don't run, images don't load, custom elements aren't upgraded, etc. <template>
s can be efficiently cloned. They're usually used to tell the HTML parser that a section of the document must not be instantiated when parsed, and will be managed by code at a later time, but it can also be created imperatively with createElement
and innerHTML
.
lit-html creates HTML <template>
elements from the tagged template literals, and then clone's them to create new DOM.
Template Creation
92 |The first time a particular lit-html template is rendered anywhere in the application, lit-html does one-time setup work to create the HTML template behind the scenes. It joins all the literal parts with a special placeholder, similar to "{{}}"
, then creates a <template>
and sets its innerHTML
to the result.
If we start with a template like this:
94 |95 | let header = (title) => html`<h1>${title}</h1>`; 96 |
lit-html will generate the following HTML:
97 |98 | <h1>{{}}</h1> 99 |
And create a <template>
from that.
Then lit-html walks the template's DOM and extracts the placeholder and remembers their location. The final template doesn't contain the placeholders:
101 |102 | <h1></h1> 103 |
And there's an auxillary table of where the expressions were:
104 |[{type: 'node', index: 1}]
Template Rendering
106 |render()
takes a TemplateResult
and renders it to a DOM container. On the initial render it clones the template, then walks it using the remembered placeholder positions, to create Part
objects.
A Part
is a "hole" in the DOM where values can be injected. lit-html includes two type of parts by default: NodePart
and AttributePart
, which let you set text content and attribute values respectively. The Part
s, container, and template they were created from are grouped together in an object called a TemplateInstance
.
Introduction
75 |What is lit-html?
76 |lit-html is a simple, modern, safe, small and fast HTML templating library for JavaScript.
77 |lit-html lets you write HTML templates in JavaScript using template literals with embedded JavaScript expressions. Behind the scenes lit-html creates HTML <template>
elements from your JavaScript templates and processes them so that it knows exactly where to insert and update the values from expressions.
lit-html Templates
79 |lit-html templates are tagged template literals - they look like JavaScript strings but are enclosed in backticks (`
) instead of quotes - and tagged with lit-html's html
tag:
81 | html`<h1>Hello ${name}</h1>`
82 |
Since lit-html templates almost always need to merge in data from JavaScript values, and be able to update DOM when that data changes, they'll most often be written within functions that take some data and return a lit-html template, so that the function can be called multiple times:
83 |84 | let myTemplate = (data) => html` 85 | <h1>${data.title}</h1> 86 | <p>${data.body}</p>`; 87 |
lit-html is lazily rendered. Calling this function will evaluate the template literal using lit-html html
tag, and return a TemplateResult
- a record of the template to render and data to render it with. TemplateResults
are very cheap to produce and no real work actually happens until they are rendered to the DOM.
Rendering
89 |To render a TempalteResult
, call the render()
function with a result and DOM container to render to:
91 | const result = myTemplate({title: 'Hello', body: 'lit-html is cool'}); 92 | render(result, document.body); 93 |
Template Dialects: lit-html vs lit-extended
94 |lit-html allows extensive customization of template features and syntax through what are called "part callbacks". lit-html includes a core and very un-opinionated template dialect in the lit-html.js
module which only supports the basic features of HTML: attributes and text content.
96 | import {html} from 'lit-html'; 97 | 98 | let result = html`<p>This template only supports attributes and text</p>`; 99 |
lit-html also includes a module at lib/lit-extended.js
which implements a more opinionated, feature-rich dialect called inspired by Polymer's template syntax. It sets properties instead of attributes by default and allows for declarative event handlers, attributes and boolean attributes.
101 | import {html} from 'lit-html/lib/lit-extended.js'; 102 | 103 | let result = html` 104 | <p> 105 | This template sets properties by default, which is great for custom elements: 106 | 107 | <my-element items=${[1, 2, 3]}></my-element> 108 | 109 | Attributes can be set with a $ suffix on the attribute name: 110 | 111 | <p class$="important">I have class</p> 112 | 113 | Events can be added with on- prefixed attribute names: 114 | 115 | <button on-click=${(e) => window.alert('clicked')}>Click Me</button> 116 | 117 | Boolean attributes can be toggled by adding a ? suffix: 118 | 119 | <span hidden?=${hide}>I'm not hidden</span> 120 | </p>`; 121 |
In lit-html the type of template you write is determined by the html
tag you use. If you import html
from lit-html.js
, you're using the basic core library. If you import html
from lib/lit-extended.js
, you're using lit-extended.
You can mix and match templates using different dialects and they will behave as intended.
123 | 124 |Next-generation HTML Templates in JavaScript
24 |lit-html: An efficient, expressive, extensible HTML templating library for JavaScript.
25 |lit-html lets you write HTML templates in JavaScript, then efficiently render and re-render those templates together with data to create and update DOM:
31 |32 | import {html, render} from 'lit-html'; 33 | 34 | // A lit-html template uses the `html` template tag: 35 | let sayHello = (name) => html`<h1>Hello ${name}</h1>`; 36 | 37 | // It's rendered with the `render()` function: 38 | render(sayHello('World'), document.body); 39 | 40 | // And re-renders only update the data that changed, without 41 | // VDOM diffing! 42 | render(sayHello('Everyone'), document.body); 43 |
Why use lit-html?
44 |Efficient
47 |
48 | lit-html is extremely fast. It uses fast platform features like HTML <template>
elements with native cloning.
49 |
51 | Unlike VDOM libraries, lit-html only ever updates the parts of templates that actually change - it doesn't ever re-render the entire view. 52 |
53 |Expressive
57 |58 | lit-html gives you the full power of JavaScript and functional programming patterns. 59 |
60 |61 | Templates are values that can be computed, passed to and from functions and nested. Expressions are real JavaScript and can include anything you need at all. 62 |
63 |64 | lit-html support many kind of values natively: strings, DOM nodes, heterogeneous lists, Promises, nested templates and more. 65 |
66 |
Extensible
70 |71 | lit-html is extremely customizable and extensible. 72 |
73 |74 | Different dialects of templates can be created with additional features for setting element properties, declarative event handlers and more. 75 |
76 |77 | Directives customize how values are handled, allowing for asynchronous values, efficient keyed-repeats, error boundaries, and more. lit-html is like your very own a template construction kit. 78 |
79 |lit-html is not a framework, nor does it include a component model. If focuses on one thing and one thing only: efficiently creating and updating DOM. It can be used standalone for simple tasks, or combined with a framework or component model, like Web Components, for a full-featured UI development platform.
83 |Announcement at Polymer Summit 2017
84 |elements 29 | */ 30 | class CustomRenderer extends marked.Renderer { 31 | code(code: string, language: string, _isEscaped: boolean) { 32 | const prismLanguage = Prism.languages[language]; 33 | const highlighted = (prismLanguage === undefined) 34 | ? code 35 | : Prism.highlight(code, prismLanguage); 36 | return html`37 | ${highlighted} 38 |`; 39 | } 40 | } 41 | marked.setOptions({ 42 | renderer: new CustomRenderer(), 43 | }); 44 | 45 | const docsOutDir = path.resolve(__dirname, '../../docs'); 46 | const docsSrcDir = path.resolve(__dirname, '../../docs-src'); 47 | const root = 'lit-html'; 48 | 49 | type FileData = { 50 | path: path.ParsedPath; 51 | outPath: string; 52 | attributes: any; 53 | body: string; 54 | } 55 | 56 | async function generateDocs() { 57 | await fs.emptyDir(docsOutDir); 58 | const fileNames = await glob('**/*.md', { cwd: docsSrcDir }); 59 | 60 | // Read in all file data 61 | const files = new Map(await Promise.all(fileNames.map(async (fileName) => { 62 | const filePath = path.parse(fileName); 63 | const content = await fs.readFile(path.join(docsSrcDir, fileName), 'utf-8'); 64 | const pageData = frontMatter(content); 65 | const outPath = `${filePath.dir}/${stripOrdering(filePath.name)}.html`; 66 | 67 | return [fileName, { 68 | path: filePath, 69 | outPath, 70 | attributes: pageData.attributes, 71 | body: pageData.body, 72 | }] as [string, FileData]; 73 | }))); 74 | 75 | for (const fileData of files.values()) { 76 | const outDir = path.join(docsOutDir, fileData.path.dir); 77 | await fs.mkdirs(outDir); 78 | const body = marked(fileData.body); 79 | // const section = fileData.path.dir.split(path.sep)[0] || 'home'; 80 | const outContent = page(fileData.outPath, body, files); 81 | const outPath = path.join(docsOutDir, fileData.outPath); 82 | fs.writeFile(outPath, outContent); 83 | } 84 | 85 | fs.copyFileSync(path.join(docsSrcDir, 'index.css'), path.join(docsOutDir, 'index.css')); 86 | fs.copyFileSync(path.resolve(__dirname, '../node_modules/prismjs/themes/prism-okaidia.css'), path.join(docsOutDir, 'prism.css')); 87 | await fs.writeFile(path.join(docsOutDir, '.nojekyll'), ''); 88 | await exec('npm run gen-docs', {cwd: '../'}); 89 | } 90 | 91 | /** 92 | * The main page template 93 | */ 94 | const page = (pagePath: string, content: string, files: Map ) => html` 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ${sideNav(pagePath, files)} 104 | ${topNav(pagePath.split('/')[0])} 105 | 106 | ${content} 107 | 108 | 111 | 112 | 113 | `; 114 | 115 | const topNav = (section: string) => html` 116 | 125 | `; 126 | 127 | interface Outline extends Map {} 128 | type OutlineData = FileData|Outline; 129 | 130 | /** 131 | * Generates an outline of all the files. 132 | * 133 | * The outline is a set of nested maps of filenames to file data. 134 | * The output is sorted with index first, then alpha-by-name 135 | */ 136 | const getOutline = (pagePath: string, files: Map ) => { 137 | 138 | const outline: Outline = new Map(); 139 | 140 | for (const fileData of files.values()) { 141 | 142 | const parts = fileData.path.dir.split(path.sep); 143 | let parent = outline; 144 | 145 | for (const part of parts) { 146 | let child = parent.get(part); 147 | if (child === undefined) { 148 | child = new Map() as Outline; 149 | parent.set(part, child); 150 | } 151 | if (child instanceof Map) { 152 | parent = child; 153 | } else { 154 | console.error(child); 155 | throw new Error('oops!'); 156 | } 157 | } 158 | parent.set(fileData.path.name, fileData); 159 | } 160 | 161 | function sortOutline(unsorted: Outline, sorted: Outline) { 162 | // re-insert index first 163 | sorted.set('index', unsorted.get('index')!); 164 | unsorted.delete('index'); 165 | // re-insert other entries in alpha-order 166 | unsorted.forEach((value, key) => { 167 | if (value instanceof Map) { 168 | value = sortOutline(value, new Map()); 169 | } 170 | sorted.set(key, value); 171 | }); 172 | return sorted; 173 | } 174 | 175 | return sortOutline(outline, new Map()); 176 | } 177 | 178 | const sideNav = (pagePath: string, files: Map ) => { 179 | // Side nav is only rendered for the guide 180 | if (!pagePath.startsWith('guide')) { 181 | return ''; 182 | } 183 | 184 | // Renders the outline, using the frontmatter from the pages 185 | const renderOutline = (outline: Outline): string => { 186 | return html` 187 | 188 | ${Array.from(outline.entries()).map(([name, data]) => { 189 | // if (name === 'index') { 190 | // return ''; 191 | // } 192 | const fileData = (data instanceof Map ? data.get('index') : data) as FileData; 193 | const isFile = !(data instanceof Map); 194 | let url = `/${root}/${fileData.path.dir}/`; 195 | if (isFile) { 196 | url = url + `${stripOrdering(fileData.path.name)}.html`; 197 | } 198 | return html` 199 |
209 | `; 210 | }; 211 | 212 | const renderPageOutline = (data: FileData) => { 213 | const tokens = marked.lexer(data.body); 214 | const headers = tokens.filter((t) => t.type === 'heading' && t.depth === 2) as marked.Tokens.Heading[]; 215 | 216 | return html` 217 |- 200 | 201 | ${isFile ? fileData.attributes['title'] : name} 202 | 203 | ${isFile && pagePath === fileData.outPath ? renderPageOutline(data as FileData) : ''} 204 | ${isFile ? '' : renderOutline(data as Outline)} 205 |
206 | `; 207 | })} 208 |218 | ${headers.map((header) => { 219 | return html`
222 | `; 223 | } 224 | 225 | return html` 226 | 230 | `; 231 | } 232 | 233 | // Nearly no-op template tag to get syntax highlighting and support Arrays. 234 | const html = (strings: TemplateStringsArray, ...values: any[]) => 235 | values.reduce((acc, v, i) => acc + (Array.isArray(v) ? v.join('\n') : String(v)) + strings[i + 1], strings[0]); 236 | 237 | const stripOrdering = (filename: string) => filename.replace(/^\d+-/, ''); 238 | 239 | const getId = (s: string) => s.toLowerCase().replace(/[^\w]+/g, '-'); 240 | 241 | generateDocs(); 242 | -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./lib", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 28 | 29 | /* Additional Checks */ 30 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 31 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 32 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 33 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 34 | 35 | /* Module Resolution Options */ 36 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 37 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 38 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 39 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 40 | "typeRoots": [ 41 | "./custom_typings" 42 | ] /* List of folders to include type definitions from. */ 43 | // "types": [], /* Type declaration files to be included in compilation. */ 44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | 47 | /* Source Map Options */ 48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 52 | 53 | /* Experimental Options */ 54 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "lib": ["es2017", "esnext.asynciterable", "dom"], 6 | "declaration": true, 7 | "sourceMap": false, 8 | "inlineSources": false, 9 | "outDir": "./", 10 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true 16 | }, 17 | "include": [ 18 | "src/**/*.ts" 19 | ], 20 | "exclude": [] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "class-name": true, 5 | "indent": [ 6 | true, 7 | "spaces", 8 | 2 9 | ], 10 | "prefer-const": true, 11 | "no-duplicate-variable": true, 12 | "no-eval": true, 13 | "no-internal-module": true, 14 | "no-trailing-whitespace": true, 15 | "no-var-keyword": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "single", 24 | "avoid-escape" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always" 29 | ], 30 | "trailing-comma": [ 31 | true, 32 | "multiline" 33 | ], 34 | "triple-equals": [ 35 | true, 36 | "allow-null-check" 37 | ], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "variable-name": [ 49 | true, 50 | "ban-keywords" 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-decl", 56 | "check-operator", 57 | "check-separator", 58 | "check-type" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /wct.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserOptions": { 3 | "chrome": [ 4 | "window-size=1920,1080", 5 | "headless", 6 | "disable-gpu" 7 | ] 8 | } 9 | } 10 | --------------------------------------------------------------------------------- ${header.text.replace('<', '<')}
`; 220 | })} 221 |
Renders the result as HTML, rather than text.
98 |Note, this is unsafe to use with any user-provided input that hasn't been 100 | sanitized or escaped, as it may lead to cross-site-scripting 101 | vulnerabilities.
102 |