├── docs ├── CNAME ├── _config.yml ├── examples │ ├── observable.html │ ├── templates.html │ ├── basic.html │ └── binding.html └── index.md ├── .gitignore ├── src ├── index.ts ├── util │ ├── html.ts │ ├── html.test.ts │ ├── resolvable.test.ts │ ├── resolvable.ts │ └── dom-wrapper.ts ├── utilities.ts ├── utilities.test.ts ├── compose.ts ├── render │ ├── string-render.ts │ ├── render.ts │ └── dom-render.ts ├── elements.test.ts ├── elements.ts └── component.ts ├── tsconfig.json ├── tsconfig.test.json ├── .github └── workflows │ └── npm.yml ├── Makefile ├── webpack.config.js ├── package.json ├── README.md └── LICENSE /docs/CNAME: -------------------------------------------------------------------------------- 1 | declarativ.js.org 2 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | show_downloads: "true" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | dist/ 5 | package-install.lock 6 | pnpm-lock.yaml 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { compose, wrapCompose } from './compose'; 2 | export { observe } from './util/resolvable'; 3 | export * as el from './elements'; 4 | export * as util from './utilities'; 5 | -------------------------------------------------------------------------------- /src/util/html.ts: -------------------------------------------------------------------------------- 1 | 2 | export function escapeHtml(str: string) : string { 3 | return str 4 | .replace(/&/g, "&") 5 | .replace(//g, ">") 7 | .replace(/"/g, """) 8 | .replace(/'/g, "'"); 9 | } 10 | -------------------------------------------------------------------------------- /src/util/html.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { escapeHtml } from './html'; 3 | import { DataResolvable, PendingTasks } from './resolvable'; 4 | 5 | import * as el from '../elements'; 6 | import * as util from '../utilities'; 7 | 8 | describe("util/html.ts", () => { 9 | it("should escape HTML chars", () => { 10 | let htmlStr = ""; 11 | expect(escapeHtml(htmlStr)).to.not.equal(htmlStr); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | import { node, Component, ResolvableNode } from './component'; 2 | import { ResolvableValue, resolve } from "./util/resolvable"; 3 | 4 | export function html(str: string) : Component { 5 | return new Component(() => str); 6 | } 7 | 8 | export function forEach(items: ResolvableValue, ...components: ResolvableNode[]) : Component { 9 | return node(resolve(items).then(array => array.map((item) => node(components)?.bind(item))))!!; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "declaration": true, 6 | "noImplicitAny": true, 7 | "module": "es6", 8 | "target": "es5", 9 | "lib": ["DOM", "ES2015", "ES2016.Array.Include", "ES2019.Array"], 10 | "allowJs": false, 11 | "allowSyntheticDefaultImports": true, 12 | "noImplicitThis": true, 13 | "strictNullChecks": true, 14 | "typeRoots": [ 15 | "src/@types", 16 | "node_modules/@types" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utilities.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import * as el from './elements'; 4 | import * as util from './utilities'; 5 | 6 | describe("utilities.ts", () => { 7 | it("iterates over arrays with forEach()", async function() { 8 | expect( 9 | await el.p( 10 | util.forEach( 11 | Promise.resolve(["a", "b", "c"]), 12 | el.span((str: any) => str) 13 | ) 14 | ).renderString() 15 | ).to.be.equal("

abc

"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "declaration": false, 6 | "inlineSourceMap": true, 7 | "noImplicitAny": true, 8 | "module": "commonjs", 9 | "target": "es5", 10 | "lib": ["DOM", "ES2015", "ES2016.Array.Include", "ES2019.Array"], 11 | "allowJs": false, 12 | "allowSyntheticDefaultImports": true, 13 | "noImplicitThis": true, 14 | "strictNullChecks": true, 15 | "typeRoots": [ 16 | "src/@types", 17 | "node_modules/@types" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS Package 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/*' 7 | - '*.js' 8 | - '*.json' 9 | pull_request: 10 | paths: 11 | - 'src/*' 12 | - '*.js' 13 | - '*.json' 14 | 15 | jobs: 16 | test: 17 | name: "Run tests" 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | node: [8, 10] 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node }} 27 | - name: Install 28 | run: npm install 29 | - name: Test 30 | run: npm test 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all install build-dev build test serve clean 2 | 3 | NPM := pnpm 4 | ifeq (, $(shell which pnpm)) 5 | NPM = npm 6 | endif 7 | 8 | all: test build-dev 9 | 10 | install: package-install.lock 11 | 12 | package-install.lock: package.json 13 | ${NPM} install 14 | touch package-install.lock 15 | 16 | build-dev: install 17 | webpack-cli --mode=development --config webpack.config.js 18 | 19 | build: install 20 | webpack-cli --mode=production --config webpack.config.js 21 | 22 | test: install 23 | ${NPM} test 24 | 25 | serve: install build-dev 26 | http-server 27 | 28 | clean: 29 | rm -rf dist/ 30 | rm -rf node_modules/ 31 | rm -f package-install.lock 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const DeclarationBundlerPlugin = require('bundle-dts-webpack-plugin'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | const pkg = require('./package.json'); 5 | 6 | module.exports = (env, argv) => [ 7 | { name: "declarativ.min.js", target: "umd", bundleDependencies: true }, 8 | { name: "index.js", target: "commonjs2" } 9 | ].map(opts => ({ 10 | entry: './src/index.ts', 11 | output: { 12 | path: path.resolve(__dirname, 'dist'), 13 | filename: opts.name, 14 | library: 'declarativ', 15 | libraryTarget: opts.target 16 | }, 17 | externals: opts.bundleDependencies ? [] : [nodeExternals()], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.tsx?$/, 22 | include: path.resolve(__dirname, 'src'), 23 | loader: 'ts-loader' 24 | } 25 | ] 26 | }, 27 | resolve: { 28 | extensions: [ '.tsx', '.ts', '.js' ] 29 | }, 30 | plugins: [ 31 | new DeclarationBundlerPlugin({ 32 | moduleName: "'declarativ'", 33 | out: "./types.d.ts" 34 | }) 35 | ] 36 | })); 37 | -------------------------------------------------------------------------------- /src/compose.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a simple declarative component-based data binding 3 | * functionality for small HTML documents. 4 | * 5 | * @module compose/index 6 | */ 7 | 8 | import { Component, ResolvableNode } from './component'; 9 | 10 | /** 11 | * Wrap a component in a composition function. 12 | * 13 | * @param {Component} component - the component to wrap 14 | * @returns {function(...[ResolvableNode]): Component} - the composed function 15 | */ 16 | export function wrapCompose(component: Component) : (children: ResolvableNode[]) => Component { 17 | return function(...children: ResolvableNode[]) { 18 | return component.withChildrenArray(children); 19 | }; 20 | } 21 | 22 | /** 23 | * Shorthand for creating a new Component instance. 24 | * 25 | * @param {function(string, any): string} template - the HTML function to template with 26 | * @return {function(...[ResolvableNode]): Component} - a Component function 27 | */ 28 | export function compose(template: (inner: string, data: any) => string) : (...children: ResolvableNode[]) => Component { 29 | return function(...children: ResolvableNode[]) { 30 | return new Component(template, children); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "declarativ", 3 | "version": "0.1.7", 4 | "description": "This is definitely not JSX.", 5 | "module": "dist/index.js", 6 | "main": "dist/declarativ.min.js", 7 | "types": "dist/types.d.ts", 8 | "files": [ 9 | "README.md", 10 | "LICENSE", 11 | "package.json", 12 | "dist/*" 13 | ], 14 | "scripts": { 15 | "test": "env TS_NODE_PROJECT=\"tsconfig.test.json\" mocha -r ts-node/register src/*.test.ts src/**/*.test.ts" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/fennifith/declarativ.git" 20 | }, 21 | "author": "James Fenn (https://jfenn.me/)", 22 | "license": "MPL-2.0", 23 | "bugs": { 24 | "url": "https://github.com/fennifith/declarativ/issues" 25 | }, 26 | "homepage": "https://declarativ.js.org/", 27 | "dependencies": { 28 | "ts-polyfill": "^3.8.2" 29 | }, 30 | "devDependencies": { 31 | "@types/chai": "^4.2.11", 32 | "@types/mocha": "^7.0.2", 33 | "bundle-dts-webpack-plugin": "^1.1.0", 34 | "chai": "^4.2.0", 35 | "mocha": "^6.2.0", 36 | "ts-loader": "^7.0.5", 37 | "ts-mocha": "^7.0.0", 38 | "ts-node": "^8.10.2", 39 | "typescript": "^3.9.5", 40 | "webpack": "^4.43.0", 41 | "webpack-cli": "^3.3.7", 42 | "webpack-node-externals": "^1.7.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/examples/observable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic - Declarativ Examples 5 | 6 | 7 |
8 | 9 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/examples/templates.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic - Declarativ Examples 5 | 6 | 7 |
8 | 9 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/render/string-render.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../component'; 2 | import { Render, RenderOpts } from './render'; 3 | import { forEachAsync } from '../util/resolvable'; 4 | import { element } from '../util/dom-wrapper'; 5 | 6 | export class StringRender extends Render { 7 | 8 | constructor(opts?: RenderOpts) { 9 | super(opts); 10 | } 11 | 12 | /** 13 | * Perform a recursive render... thing... 14 | * 15 | * @param {*} parentData - The current data object to bind to components. 16 | * @param {HTMLElement|ElementImpl?} tempElement - The element/object that components should replace. 17 | * @param {Component} component - The component to start the render at. 18 | * @return {String} The rendered string. 19 | */ 20 | async doRender(data: any, tempElement: string | null, component: Component) : Promise { 21 | // create basic html 22 | let innerHtml = ""; 23 | await forEachAsync(await component.resolveChildren(data), async (child) => { 24 | if (typeof child === "string") 25 | innerHtml += child; 26 | else innerHtml += await this.render(data, null, child); 27 | }); 28 | 29 | this.opts.debugLogger?.(" Resolved child elements:", innerHtml); 30 | 31 | // TODO: support attribute values / this.tasks.call() on string returns 32 | 33 | // render HTML structure 34 | let str = component.template(innerHtml, data); 35 | let strImpl = element(str); 36 | await component.tasks.call(strImpl, data); 37 | 38 | if (strImpl) 39 | return strImpl.get(); 40 | else throw "Error occurred: null string"; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /docs/examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic - Declarativ Examples 5 | 6 | 7 |
8 | 9 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/util/resolvable.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { DataResolvable, PendingTasks } from './resolvable'; 3 | 4 | describe("util/resolvable.ts", () => { 5 | describe("DataResolvable", () => { 6 | it("should resolve promises", async function() { 7 | let data = new DataResolvable(Promise.resolve(3)); 8 | expect(await data.resolve()).to.equal(3); 9 | }); 10 | 11 | it("should resolve functions", async function() { 12 | let data = new DataResolvable(() => 15); 13 | expect(await data.resolve()).to.equal(15); 14 | }); 15 | 16 | it("should resolve raw values", async function() { 17 | let data = new DataResolvable("hello!"); 18 | expect(await data.resolve()).to.equal("hello!"); 19 | }); 20 | 21 | it("should pass resolved arguments through to functions", async function() { 22 | let data = new DataResolvable((d: any) => d); 23 | expect(await data.resolve(250)).to.equal(250); 24 | }); 25 | }); 26 | 27 | describe("PendingTasks", () => { 28 | let functions = [ 29 | (): any => null, 30 | (): any => null, 31 | (): any => null, 32 | (): any => null, 33 | (): any => null, 34 | ]; 35 | 36 | let tasks = new PendingTasks([]); 37 | functions.forEach((task) => tasks.push(task)); 38 | 39 | it("should have the correct length attribute", () => { 40 | expect(tasks.length).to.equal(functions.length); 41 | }); 42 | 43 | it("can be constructed from an array", () => { 44 | let newTasks = new PendingTasks(functions); 45 | expect(newTasks).to.have.property('length'); 46 | expect(newTasks.length).to.be.equal(functions.length); 47 | }); 48 | 49 | it("resolves all tasks when called", async function() { 50 | let i = 0; 51 | let pending = new PendingTasks([() => i = 1]); 52 | await pending.call(); 53 | 54 | expect(i).to.equal(1); 55 | }); 56 | 57 | it("passes arguments to called functions", async function() { 58 | let val = 0; 59 | let pending = new PendingTasks([(arg) => val = arg]); 60 | await pending.call(20); 61 | 62 | expect(val).to.equal(20); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /docs/examples/binding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic - Declarativ Examples 5 | 6 | 7 |
8 | 9 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/render/render.ts: -------------------------------------------------------------------------------- 1 | import { Component, Element } from "../component"; 2 | import { DataObservable, resolve } from '../util/resolvable'; 3 | 4 | export interface RenderOpts { 5 | debugLogger?: (...data: any[]) => void 6 | strict?: boolean 7 | } 8 | 9 | export abstract class Render { 10 | 11 | opts: RenderOpts 12 | 13 | constructor(opts?: RenderOpts) { 14 | this.opts = opts || {} 15 | } 16 | 17 | /** 18 | * Base/wrapped render function (calls doRender with conditions) 19 | * 20 | * @param {*} parentData 21 | * @param {*} tempElement 22 | * @param {*} component 23 | */ 24 | async render(parentData: any, tempElement: E | null, component: Component) : Promise { 25 | try { 26 | this.opts.debugLogger?.(`Rendering component ${component.template}`) 27 | 28 | // render loading state first... (if present) 29 | if (component.loadingState) { 30 | tempElement = await this.render(null, tempElement, await resolve(component.loadingState)); 31 | } 32 | 33 | // resolve critical data first 34 | let data = component.data ? await resolve(component.data, parentData) : parentData; 35 | this.opts.debugLogger?.(' Resolved data:', data); 36 | 37 | // perform actual render 38 | let element = await this.doRender(data, tempElement, component); 39 | this.opts.debugLogger?.(' Finished render:', element); 40 | 41 | if (component.data instanceof DataObservable) { 42 | // subscribe to observable changes 43 | component.data.subscribe(async (newData) => { 44 | element = await this.doRender(newData, element, component); 45 | }); 46 | } 47 | 48 | return element; 49 | } catch (e) { 50 | // fallback component (if present) 51 | if (component.fallbackState) 52 | return await this.render(e, tempElement, component.fallbackState); 53 | else throw e; 54 | } 55 | } 56 | 57 | /** 58 | * Perform a recursive render... thing... 59 | * 60 | * @param {*} parentData - The current data object to bind to components. 61 | * @param {HTMLElement|ElementImpl?} tempElement - The element/object that components should replace. 62 | * @param {Component} component - The component to start the render at. 63 | * @return {*} The rendered item. 64 | */ 65 | abstract async doRender(parentData: any, tempElement: E | null, component: Component) : Promise 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/elements.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { escapeHtml } from './util/html'; 3 | import { DataResolvable, PendingTasks } from './util/resolvable'; 4 | 5 | import * as el from './elements'; 6 | import * as util from './utilities'; 7 | 8 | describe("elements.ts", () => { 9 | describe("Basic Rendering", () => { 10 | it("should render an empty tag", async function() { 11 | expect(await el.p().renderString()).to.be.equal("

"); 12 | }); 13 | 14 | it("should render text inside a tag", async function() { 15 | expect(await el.p("Hello!").renderString()).to.be.equal("

Hello!

"); 16 | }); 17 | 18 | it("should render elements inside a tag", async function() { 19 | expect( 20 | await el.p( 21 | el.span(), 22 | el.a() 23 | ).renderString() 24 | ).to.be.equal("

") 25 | }); 26 | 27 | it("should render functions inside a tag", async function() { 28 | expect( 29 | await el.p(() => "Hello!").renderString() 30 | ).to.be.equal("

Hello!

"); 31 | }); 32 | 33 | it("should render promises inside a tag", async function() { 34 | expect( 35 | await el.p(Promise.resolve("Hello!")).renderString() 36 | ).to.be.equal("

Hello!

"); 37 | }); 38 | 39 | it("should render tag attributes", async function() { 40 | expect( 41 | await el.p().attr("id", "render").renderString() 42 | ).to.be.equal('

') 43 | }); 44 | }); 45 | 46 | describe("Data States", () => { 47 | it("should fallback to error components", async function() { 48 | expect( 49 | await el.p( 50 | () => { throw "aaa"; } 51 | ).whenError( 52 | el.span("Oh no.") 53 | ).renderString() 54 | ).to.be.equal("Oh no."); 55 | }); 56 | }); 57 | 58 | describe("Data Binding", () => { 59 | it("should bind data to child functions", async function() { 60 | expect( 61 | await el.p( 62 | (data: any) => data 63 | ).bind("Hello!").renderString() 64 | ).to.equal("

Hello!

"); 65 | }); 66 | 67 | it("should bind data to child elements", async function() { 68 | expect( 69 | await el.p( 70 | el.span( 71 | el.span( 72 | (data: any) => data 73 | ) 74 | ) 75 | ).bind("Hello!").renderString() 76 | ).to.equal("

Hello!

"); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/render/dom-render.ts: -------------------------------------------------------------------------------- 1 | import { Render, RenderOpts } from './render'; 2 | import { forEachAsync } from '../util/resolvable'; 3 | import * as dom from '../util/dom-wrapper'; 4 | import { Component } from '../component'; 5 | 6 | let nodeCount = 0; 7 | 8 | export class DOMRender extends Render { 9 | 10 | constructor(opts?: RenderOpts) { 11 | super(opts); 12 | } 13 | 14 | /** 15 | * Perform a recursive render... thing... 16 | * 17 | * @param {*} data - The current data object to bind to components. 18 | * @param {Node|ElementImpl?} tempElement - The element/object that components should replace. 19 | * @param {Component} component - The component to start the render at. 20 | * @return {*} The rendered item. 21 | */ 22 | async doRender(data: any, tempElement: Node | null, component: Component) : Promise { 23 | // create basic html 24 | let innerHtml = ""; 25 | let components: {[id: string]: Component} = {}; 26 | await forEachAsync(await component.resolveChildren(data), async (child, index) => { 27 | if (typeof child === "string") { 28 | innerHtml += child; 29 | } else { 30 | let id = `decl-${nodeCount++}-${index}`; 31 | innerHtml += ``; 32 | components[id] = child; 33 | } 34 | }); 35 | 36 | this.opts.debugLogger?.(" Resolved child elements:", innerHtml); 37 | 38 | // render HTML structure 39 | let elements = dom.createHtml(component.template(innerHtml, data)); 40 | let elementImpl = dom.element(elements[0]); 41 | 42 | // call immediate tasks (attributes, etc.) 43 | await component.tasks.call(elementImpl, data); 44 | 45 | let tempElementImpl = dom.element(tempElement); 46 | if (tempElementImpl) { // replace tempElement on dom 47 | tempElementImpl.replaceWith(elements[0]); 48 | if (!elements[0].parentNode) 49 | throw "No parent node on element " + elements[0]; 50 | 51 | for (let i = 1; i < elements.length; i++) { 52 | // insert any additional nodes into the DOM (in case the template is weird) 53 | dom.element(elements[0].parentNode)?.insertAfter(elements[i], elements[i-1]); 54 | } 55 | } 56 | 57 | // render / await child nodes 58 | await Promise.all(Object.keys(components).map(async (id) => { 59 | let temp = document.querySelector(`#${id}`); 60 | if (!temp || !(temp instanceof HTMLElement)) 61 | throw `couldn't find child ${id}`; 62 | 63 | let result = await this.render(data, temp, components[id]); 64 | for (let i = 0; i < elements.length; i++) { 65 | // account for stray elements in template (see large comment block below) 66 | let element = elements[i]; 67 | if (element instanceof HTMLElement && element.id === id) 68 | elements[i] = result; 69 | } 70 | })); 71 | 72 | await Promise.all(elements.map((element) => component.tasksAfter.call(element, data))); 73 | 74 | // Only returning the first element makes it possible for some to 75 | // potentially "leak" from the tree. As such, multi-element nodes should 76 | // be discouraged (for now...) 77 | // 78 | // This could be worked around in the future by writing a `dom-wrapper` 79 | // implementation for arrays that modifies each element in the array; 80 | // `element.replace(...)` would replace the first element and remove all 81 | // others, etc. 82 | return elements[0]; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/util/resolvable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Useful data-handling classes and functions. 3 | * 4 | * @module util/resolvable 5 | */ 6 | 7 | export type ResolvableValue = T | ((data: any) => ResolvableValue) | Promise 8 | 9 | /** 10 | * A wrapper class for data-based promises and/or arbitrary 11 | * values that enter a component. 12 | * 13 | * @property {Object|function(Object): Object|Promise} 14 | * @class DataResolvable 15 | */ 16 | export class DataResolvable { 17 | 18 | value: ResolvableValue; 19 | 20 | constructor(value: ResolvableValue) { 21 | if (value instanceof DataResolvable) 22 | this.value = value.value; 23 | else this.value = value; 24 | } 25 | 26 | isBlocking() { 27 | return this.value instanceof Promise || typeof this.value === 'function'; 28 | } 29 | 30 | async resolve(data?: any) : Promise { 31 | // TODO: ideally, Promises/functions should resolve recursively (e.g. Promises that return a function), but this breaks the Component's forEach functionality. 32 | // I'm not entirely sure why this happens. Everything seems to work fine as it is, though, so I'll just leave it alone. 33 | 34 | const isFn = (arg: any): arg is ((arg: any) => T) => { 35 | return typeof arg === 'function' 36 | } 37 | 38 | if (this.value instanceof Promise) { 39 | return await this.value; 40 | } else if (isFn(this.value)) { 41 | return await this.value(data); 42 | } else return this.value; 43 | } 44 | } 45 | 46 | /** 47 | * A wrapper for data-based event streams providing continuous 48 | * updates to the declarativ tree. 49 | * 50 | * @class DataObservable 51 | */ 52 | export class DataObservable extends DataResolvable { 53 | 54 | listeners: ((value: T) => void)[] 55 | 56 | constructor(value: T) { 57 | super(value); 58 | this.listeners = []; 59 | } 60 | 61 | update(value: T) { 62 | this.value = value; 63 | this.listeners.forEach((listener) => listener(value)); 64 | } 65 | 66 | subscribe(listener: (value: T) => void) { 67 | this.listeners.push(listener); 68 | } 69 | 70 | unsubscribe(listener: (value: T) => void) { 71 | if (this.listeners.includes(listener)) 72 | this.listeners.splice(this.listeners.indexOf(listener), 1); 73 | } 74 | 75 | } 76 | 77 | /** 78 | * A wrapper for data-based event streams providing continuous 79 | * updates to the declarativ tree using the js Proxy API. 80 | * 81 | * @class ProxyDataObservable 82 | */ 83 | export class ProxyDataObservable extends DataObservable<{[key: string]: any}> { 84 | 85 | proxy: any; 86 | 87 | constructor(value: {[key: string]: any}) { 88 | super(value); 89 | this.proxy = new Proxy(value || {}, { 90 | set: (obj, prop, val) => { 91 | value[String(prop)] = val; 92 | this.update(this.value); 93 | return true; 94 | }, 95 | deleteProperty: (obj, prop) => { 96 | delete value[String(prop)]; 97 | this.update(this.value); 98 | return true; 99 | } 100 | }); 101 | } 102 | 103 | } 104 | 105 | export function observe(data: any) { 106 | return new ProxyDataObservable(data); 107 | } 108 | 109 | export function resolvable(value: ResolvableValue) : DataResolvable | T { 110 | // TODO: rx support? 111 | if (value instanceof Promise || typeof value === 'function') 112 | return new DataResolvable(value); 113 | else return value; 114 | } 115 | 116 | export async function resolve(value: ResolvableValue, data?: any) : Promise { 117 | let obj = resolvable(value); 118 | if (obj instanceof DataResolvable) 119 | return await obj.resolve(data); 120 | else return obj; 121 | } 122 | 123 | /** 124 | * A set of pending functions to execute at a later 125 | * point in time. 126 | * 127 | * @class PendingTasks 128 | */ 129 | export class PendingTasks { 130 | 131 | tasks: ((...args: any[]) => void)[]; 132 | 133 | constructor(tasks?: PendingTasks | ((...args: any[]) => void)[]) { 134 | if (tasks instanceof PendingTasks) 135 | this.tasks = [...tasks.tasks]; 136 | else if (tasks instanceof Array) 137 | this.tasks = [...tasks]; 138 | else this.tasks = []; 139 | } 140 | 141 | get length() { 142 | return this.tasks.length; 143 | } 144 | 145 | push(fun: (...args: any[]) => void) { 146 | this.tasks.push(fun); 147 | return this; 148 | } 149 | 150 | async call(...args: any[]) { 151 | return Promise.all( 152 | this.tasks.map(function(fun) { 153 | let ret = fun.apply(null, args); 154 | return ret instanceof Promise ? ret : Promise.resolve(); 155 | }) 156 | ); 157 | } 158 | } 159 | 160 | export async function forEachAsync(iterable: T[], fun: (item: T, index: number) => void) { 161 | await iterable.reduce((promise, item, index) => { 162 | return promise.then(() => fun(item, index)); 163 | }, Promise.resolve()); 164 | } 165 | -------------------------------------------------------------------------------- /src/elements.ts: -------------------------------------------------------------------------------- 1 | import { compose } from './compose'; 2 | 3 | export const address = compose((inner) => `
${inner}
`); 4 | export const article = compose((inner) => `
${inner}
`); 5 | export const aside = compose((inner) => ``); 6 | export const footer = compose((inner) => `
${inner}
`); 7 | export const header = compose((inner) => `
${inner}
`); 8 | export const h1 = compose((inner) => `

${inner}

`); 9 | export const h2 = compose((inner) => `

${inner}

`); 10 | export const h3 = compose((inner) => `

${inner}

`); 11 | export const h4 = compose((inner) => `

${inner}

`); 12 | export const h5 = compose((inner) => `
${inner}
`); 13 | export const main = compose((inner) => `
${inner}
`); 14 | export const nav = compose((inner) => ``); 15 | export const section = compose((inner) => `
${inner}
`); 16 | export const blockquote = compose((inner) => `
${inner}
`); 17 | export const div = compose((inner) => `
${inner}
`); 18 | export const hr = compose((inner) => `
${inner}`); 19 | export const li = compose((inner) => `
  • ${inner}
  • `); 20 | export const ol = compose((inner) => `
      ${inner}
    `); 21 | export const p = compose((inner) => `

    ${inner}

    `); 22 | export const pre = compose((inner) => `
    ${inner}
    `); 23 | export const ul = compose((inner) => `
      ${inner}
    `); 24 | export const a = compose((inner) => `${inner}`); 25 | export const br = compose((inner) => `
    `); 26 | export const code = compose((inner) => `${inner}`); 27 | export const em = compose((inner) => `${inner}`); 28 | export const i = compose((inner) => `${inner}`); 29 | export const b = compose((inner) => `${inner}`); 30 | export const small = compose((inner) => `${inner}`); 31 | export const span = compose((inner) => `${inner}`); 32 | export const strong = compose((inner) => `${inner}`); 33 | export const sub = compose((inner) => `${inner}`); 34 | export const sup = compose((inner) => `${inner}`); 35 | export const time = compose((inner) => ``); 36 | export const area = compose((inner) => `${inner}`); 37 | export const audio = compose((inner) => ``); 38 | export const img = compose((inner) => `${inner}`); 39 | export const map = compose((inner) => `${inner}`); 40 | export const track = compose((inner) => `${inner}`); 41 | export const video = compose((inner) => ``); 42 | export const embed = compose((inner) => `${inner}`); 43 | export const iframe = compose((inner) => ``); 44 | export const noembed = compose((inner) => `${inner}`); 45 | export const object = compose((inner) => `${inner}`); 46 | export const picture = compose((inner) => `${inner}`); 47 | export const source = compose((inner) => `${inner}`); 48 | export const canvas = compose((inner) => `${inner}`); 49 | export const caption = compose((inner) => `${inner}`); 50 | export const col = compose((inner) => `${inner}`); 51 | export const colgroup = compose((inner) => `${inner}`); 52 | export const table = compose((inner) => `${inner}
    `); 53 | export const tbody = compose((inner) => `${inner}`); 54 | export const td = compose((inner) => `${inner}`); 55 | export const tfoot = compose((inner) => `${inner}`); 56 | export const th = compose((inner) => `${inner}`); 57 | export const thead = compose((inner) => `${inner}`); 58 | export const tr = compose((inner) => `${inner}`); 59 | export const button = compose((inner) => ``); 60 | export const datalist = compose((inner) => `${inner}`); 61 | export const fieldset = compose((inner) => `
    ${inner}
    `); 62 | export const form = compose((inner) => `
    ${inner}
    `); 63 | export const input = compose((inner) => `${inner}`); 64 | export const label = compose((inner) => ``); 65 | export const legend = compose((inner) => `${inner}`); 66 | export const meter = compose((inner) => `${inner}`); 67 | export const optgroup = compose((inner) => `${inner}`); 68 | export const option = compose((inner) => ``); 69 | export const select = compose((inner) => ``); 70 | export const textarea = compose((inner) => ``); 71 | export const details = compose((inner) => `
    ${inner}
    `); 72 | export const summary = compose((inner) => `${inner}`); 73 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Declarativ 2 | 3 | "Declarativ" is a lightweight and asynchronous HTML templating library for JavaScript. It definitely isn't my own reinvention of React's [JSX](https://reactjs.org/docs/introducing-jsx.html). Okay, it kind of is, but whatever, it's still cool. 4 | 5 | Declarativ allows you to write a document tree using a series of nested function calls, much like how Declarative UI works inside [Flutter](https://flutter.dev/docs/get-started/flutter-for/declarative#how-to-change-ui-in-a-declarative-framework) or in [Jetpack Compose](https://developer.android.com/jetpack/compose). Here's an example: 6 | 7 | ```js 8 | container( 9 | jumbotron( 10 | h1("This is a big header."), 11 | button("Do something").on("click", () => alert("Hello!")), 12 | p($.get("https://loripsum.net/api/plaintext")) 13 | ) 14 | ) 15 | ``` 16 | 17 | ## Installation 18 | 19 | #### Script Tag 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | (the module will be included in the global scope as the `declarativ` variable) 26 | 27 | #### NPM/Webpack 28 | 29 | ```sh 30 | npm install declarativ 31 | ``` 32 | 33 | #### From Source 34 | 35 | ```sh 36 | git clone https://github.com/fennifith/declarativ.git 37 | cd declarativ && make install 38 | ``` 39 | 40 | ## Usage 41 | 42 | Most component trees can be built using the standard functions defined in `declarativ.el`. I often use [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) to move them to the current scope when using more than one or two of them, which makes it a bit easier to work with. Here's an example: 43 | 44 | ```js 45 | const { div, h1, p, a } = declarativ.el; 46 | 47 | let components = div( 48 | h1("This is a big header."), 49 | p( 50 | "Here, have a bit of text", 51 | a("and a link").attr("href", "https://example.com/"), 52 | "." 53 | ) 54 | ); 55 | ``` 56 | 57 | After defining your component tree, it can be placed on the DOM by either calling the `render` or `renderString` functions. Calling `render` will place them inside whatever element you pass as its argument, while `renderString` simply returns their HTML representation. 58 | 59 | ```js 60 | components.render("#content").then(() => { 61 | console.log("Elements rendered!"); 62 | }); 63 | ``` 64 | 65 | Working examples can be found in the [examples](https://github.com/fennifith/declarativ/tree/master/docs/examples/) folder. 66 | 67 | ### Promises 68 | 69 | Promises can be mixed in or bound to components to pass data to them, and the component will wait for them to resolve before rendering. Because inner components depend on their parent nodes to render, higher components will render first, and only the bound component and inner nodes will wait for the Promise. 70 | 71 | ```js 72 | div( 73 | p("This will render first."), 74 | p(new Promise((resolve) => { 75 | setTimeout(() => resolve("This will render second."), 1000); 76 | })), 77 | p( 78 | new Promise((resolve) => { 79 | setTimeout(() => resolve("This will render last..."), 2000); 80 | }), 81 | " but not this!" 82 | ) 83 | ) 84 | ``` 85 | 86 | ### Handling Data 87 | 88 | Nodes can exist in various forms inside of a component. In the last example, I specified a Promise and a string as the contents of a paragraph element. However, not all of the promises you use will return a string. Often times, you will handle data structures that need to be bound to multiple elements. This is where the `.bind()` function comes in useful. 89 | 90 | ```js 91 | div( 92 | p("This will render first"), 93 | div( 94 | p((data) => data.first), 95 | p((data) => data.second) 96 | ).bind(Promise.resolve({ 97 | first: "This is a string.", 98 | second: "This is another string." 99 | })) 100 | ) 101 | ``` 102 | 103 | Okay, a lot is happening here. I'll slow down and explain. 104 | 105 | The `bind` function allows you to specify a set of data to be passed to other parts of a component - and extends upon the types of nodes that can be placed inside it. Because the paragraph elements inside the div are not bound to any data, they inherit the Promise that is bound to their parent. The nodes inside of the paragraph elements are then specified as a function of the resolved data, returning the text to render. 106 | 107 | A more complex data binding situation based off the GitHub API can be found in [examples/binding.html](./examples/binding.html). 108 | 109 | ### Templates 110 | 111 | Templating functionality is crucial for projects that involve a large number of elements or repeat a common set of element structures in multiple places. There are a few different ways to create them: 112 | 113 | #### Functions 114 | 115 | The easiest is to just create a function that returns another component, like so: 116 | 117 | ```js 118 | function myComponent(title, description) { 119 | return div( 120 | h3(title), 121 | p(description) 122 | ); 123 | } 124 | ``` 125 | 126 | Because you're just passing the arguments directly into the structure, this allows you to pass your function a string, another component, a function(data), or a Promise, and have it resolve during the render. 127 | 128 | #### Wrapped Components 129 | 130 | If you want to make a component that just slightly extends upon an existing instance of one, it can be wrapped in a function that will act like other components during use. This isn't useful very often, as any child components will be lost in the process, but it is useful if you just want to add a class name or attribute to a component without defining a structure. 131 | 132 | ```js 133 | const myComponent = declarativ.wrapCompose( 134 | div().className("fancypants") 135 | ); 136 | ``` 137 | 138 | #### Custom Elements 139 | 140 | This is possibly the least useful kind of template, but I'll leave it here anyway. Most elements are specified inside `declarativ.elements`, but in the event that you want to use one that isn't, you can create an element template by calling `declarativ.compose()` with a template function. 141 | 142 | By "template function", it must be a function that accepts a string and returns that string inside of the element's HTML tag. For example, here I implement the deprecated `
    ` tag. 143 | 144 | ```js 145 | const myComponent = declarativ.compose((inner) => `
    ${inner}
    `); 146 | ``` 147 | 148 | Working examples of all of these templates can be found in [examples/templates.html](./examples/templates.html). 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Declarativ 2 | [![Build Status](https://github.com/fennifith/declarativ/workflows/NodeJS%20Package/badge.svg)](https://github.com/fennifith/declarativ/actions) 3 | [![NPM Package](https://img.shields.io/npm/v/declarativ?color=red&logo=npm)](https://www.npmjs.com/package/declarativ) 4 | [![Discord](https://img.shields.io/discord/514625116706177035.svg?logo=discord&colorB=7289da)](https://discord.jfenn.me/) 5 | [![Liberapay](https://img.shields.io/badge/liberapay-donate-yellow.svg?logo=liberapay)](https://jfenn.me/links/liberapay) 6 | ======= 7 | 8 | "Declarativ" is a lightweight and asynchronous HTML templating library for JavaScript. It definitely isn't my own reinvention of React's [JSX](https://reactjs.org/docs/introducing-jsx.html). Okay, it kind of is, but whatever, it's still cool. 9 | 10 | Declarativ allows you to write a document tree using a series of nested function calls, much like how Declarative UI works inside [Flutter](https://flutter.dev/docs/get-started/flutter-for/declarative#how-to-change-ui-in-a-declarative-framework) or in [Jetpack Compose](https://developer.android.com/jetpack/compose). Here's an example: 11 | 12 | ```js 13 | container( 14 | jumbotron( 15 | h1("This is a big header."), 16 | button("Do something").on("click", () => alert("Hello!")), 17 | p($.get("https://loripsum.net/api/plaintext")) 18 | ) 19 | ) 20 | ``` 21 | 22 | ## Installation 23 | 24 | #### Script Tag 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | (the module will be included in the global scope as the `declarativ` variable) 31 | 32 | #### NPM/Webpack 33 | 34 | ```sh 35 | npm install declarativ 36 | ``` 37 | 38 | #### From Source 39 | 40 | ```sh 41 | git clone https://github.com/fennifith/declarativ.git 42 | cd declarativ && make install 43 | ``` 44 | 45 | ## Usage 46 | 47 | Most component trees can be built using the standard functions defined in `declarativ.el`. I often use [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) to move them to the current scope when using more than one or two of them, which makes it a bit easier to work with (provided you don't use any variables that conflict with the element names). Here's an example: 48 | 49 | ```js 50 | const { div, h1, p, a } = declarativ.el; 51 | 52 | let components = div( 53 | h1("This is a big header."), 54 | p( 55 | "Here, have a bit of text", 56 | a("and a link").attr("href", "https://example.com/"), 57 | "." 58 | ) 59 | ); 60 | ``` 61 | 62 | After defining your component tree, it can be placed on the DOM by either calling the `render` or `renderString` functions. Calling `render` will place them inside whatever element (or selector) you pass as its argument, while `renderString` simply returns their HTML representation. 63 | 64 | ```js 65 | components.render("#content").then(() => { 66 | console.log("Elements rendered!"); 67 | }); 68 | ``` 69 | 70 | Working examples can be found in the [examples](../../tree/master/docs/examples/) folder. 71 | 72 | ### Promises & Asynchronicity 73 | 74 | Promises can be mixed in with components, and declarativ will wait for them to resolve before processing the result. 75 | 76 | ```js 77 | p( 78 | "Everything in this example ", 79 | new Promise((resolve) => { 80 | setTimeout(() => resolve("will all render "), 1000); 81 | }), 82 | new Promise((resolve) => { 83 | setTimeout(() => resolve("at the exact same "), 2000); 84 | }), 85 | "time!" 86 | ) 87 | ``` 88 | 89 | This happens a bit differently when using the `.bind` method; components that are unbound will render first, and any children within a bound component will wait for its promise to resolve before being processed. 90 | 91 | ```js 92 | div( 93 | p("This will render first."), 94 | p("This will render second.").bind(new Promise((resolve) => { 95 | setTimeout(() => resolve(), 1000); 96 | })), 97 | p("This will render last.").bind(new Promise((resolve) => { 98 | setTimeout(() => resolve("at the exact same"), 2000); 99 | })) 100 | ) 101 | ``` 102 | 103 | ### Handling Data 104 | 105 | Nodes can exist in various forms inside of a component. In the last example, I specified a Promise and a string as the contents of a paragraph element. However, not all of the promises you use will return a string. Often times, you will handle data structures that need to be bound to multiple elements. This is where the `.bind()` function comes in useful. 106 | 107 | ```js 108 | div( 109 | p("This will render first"), 110 | div( 111 | p((data) => data.first), 112 | p((data) => data.second) 113 | ).bind(Promise.resolve({ 114 | first: "This is a string.", 115 | second: "This is another string." 116 | })) 117 | ) 118 | ``` 119 | 120 | Okay, a lot is happening here. I'll slow down and explain. 121 | 122 | The `bind` function _also_ allows you to specify a set of data to be passed to other parts of a component - and extends upon the types of nodes that can be placed inside it. Because the paragraph elements inside the div are not bound to any data, they inherit the Promise that is bound to their parent. The nodes inside of the paragraph elements are then specified as a function of the resolved data, returning the text to render. 123 | 124 | A more complex data binding situation based off the GitHub API can be found in [examples/binding.html](./docs/examples/binding.html). 125 | 126 | ### Templates 127 | 128 | Templating functionality is crucial for projects that involve a large number of elements or repeat a common set of element structures in multiple places. There are a few different ways to create them: 129 | 130 | #### Functions 131 | 132 | The easiest is to just create a function that returns another component, like so: 133 | 134 | ```js 135 | function myComponent(title, description) { 136 | return div( 137 | h3(title), 138 | p(description) 139 | ); 140 | } 141 | ``` 142 | 143 | Because you're just passing the arguments directly into the structure, this allows you to pass your function a string, another component, a function(data), or a Promise, and have it resolve during the render. 144 | 145 | #### Wrapped Components 146 | 147 | If you want to make a component that just slightly extends upon an existing instance of one, it can be wrapped in a function that will act like other components during use. This isn't useful very often, as any child components will be lost in the process, but it is useful if you just want to add a class name or attribute to a component without defining a structure. 148 | 149 | ```js 150 | const myComponent = declarativ.wrapCompose( 151 | div().className("fancypants") 152 | ); 153 | ``` 154 | 155 | #### Custom Elements 156 | 157 | This is possibly the least useful kind of template, but I'll leave it here anyway. Most elements are specified inside `declarativ.elements`, but in the event that you want to use one that isn't, you can create an element template by calling `declarativ.compose()` with a template function. 158 | 159 | By "template function", it must be a function that accepts a string and returns that string inside of the element's HTML tag. For example, here I implement the deprecated `
    ` tag. 160 | 161 | ```js 162 | const myComponent = declarativ.compose((inner) => `
    ${inner}
    `); 163 | ``` 164 | 165 | Working examples of all of these templates can be found in [examples/templates.html](./docs/examples/templates.html). 166 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import 'ts-polyfill/lib/es2019-array'; 2 | 3 | import { ResolvableValue, DataResolvable, PendingTasks, resolvable, resolve } from './util/resolvable'; 4 | import { ElementImpl } from './util/dom-wrapper'; 5 | import { escapeHtml } from './util/html'; 6 | import { DOMRender } from './render/dom-render'; 7 | import { StringRender } from './render/string-render'; 8 | import { RenderOpts } from './render/render'; 9 | 10 | export type ComponentNode = Component | string 11 | export type ResolvableNode = ResolvableValue 12 | 13 | export type Element = HTMLElement | string 14 | 15 | export function node(variable: any) : Component | null { 16 | if (variable instanceof Component) 17 | return variable; 18 | else if (typeof variable === "string") // text string 19 | return new Component(() => escapeHtml(variable)); 20 | else if (variable instanceof Array) // wrap array of component children 21 | return new Component((s: any) => s, variable) 22 | else if (typeof variable === "function" || variable instanceof Promise || variable instanceof DataResolvable) // wrap promised component 23 | return new Component((s: any) => s, [variable]) 24 | else if (typeof variable === "undefined" || variable === null) { // null component (throw error on use) 25 | console.error("declarativ: Null component: ", variable); 26 | return null; 27 | } else { 28 | console.error("declarativ: Cannot resolve passed node: ", variable); 29 | return null; 30 | } 31 | } 32 | 33 | /** 34 | * A representative Component constructed from a template 35 | * function and a set of data. 36 | * 37 | * @param fun {function(string, Object): string} the HTML function to template with 38 | * @param data {Object|Promise} the data (or promise) to resolve 39 | * @param children {Array|Promise} inner components to template inside of this one 40 | * @class Component 41 | */ 42 | export class Component { 43 | 44 | children: ResolvableNode[] 45 | fallbackState: Component | null 46 | loadingState: Component | null 47 | 48 | data: ResolvableValue | null 49 | template: (inner: string, data?: any) => string 50 | 51 | tasks: PendingTasks 52 | tasksAfter: PendingTasks 53 | 54 | observing: boolean 55 | rerender: () => void 56 | 57 | constructor(template: ((inner: string, data?: any) => string), children?: ResolvableNode[]) { 58 | this.children = children || []; 59 | this.fallbackState = null; 60 | this.loadingState = null; 61 | this.data = null; 62 | this.template = template; 63 | this.tasks = new PendingTasks(); 64 | this.tasksAfter = new PendingTasks(); 65 | this.observing = false; 66 | this.rerender = () => {}; 67 | } 68 | 69 | withChildren(...children: ResolvableNode[]) : Component { 70 | return this.withChildrenArray(children); 71 | } 72 | 73 | withChildrenArray(children: ResolvableNode[]) : Component { 74 | let node = this.clone(); 75 | node.children = children.flat(Infinity).map((child) => resolvable(child)); 76 | return node; 77 | } 78 | 79 | bind(data: any) : Component { 80 | let node = this.clone(); 81 | node.data = resolvable(data); 82 | return node; 83 | } 84 | 85 | whenError(...nodes: ResolvableNode[]) : Component { 86 | let n = this.clone(); 87 | n.fallbackState = node(nodes); 88 | return n; 89 | } 90 | 91 | whenLoading(...nodes: ResolvableNode[]) : Component { 92 | let n = this.clone(); 93 | n.loadingState = node(nodes); 94 | return n; 95 | } 96 | 97 | otherwise(...nodes: ResolvableNode[]) : Component { 98 | return this.whenError(nodes); 99 | } 100 | 101 | clone() : Component { 102 | return Object.assign(Object.create(Object.getPrototypeOf(this)), this); 103 | } 104 | 105 | isBlocking() : boolean { 106 | return true; // TODO: allow non-blocking simple components 107 | } 108 | 109 | /** 110 | * Iterate over a set of children for all data items passed. 111 | * 112 | * @param {Array} children 113 | * @returns {Component} 114 | */ 115 | forEach(...children: ResolvableNode[]) : Component { 116 | return this.withChildren(function(data): ResolvableNode { // set children to a function of the passed data 117 | return Object.keys(data).map((key): ResolvableNode => children.map(function(child) { // for each item, return all of the passed elements 118 | if (child instanceof Component) 119 | return child.bind(data[key]); // if the child is a component, bind its data directly 120 | else return new DataResolvable(child).resolve(data[key]); // if the child is another function/promise, resolve it as usual 121 | })) 122 | }); 123 | } 124 | 125 | /** 126 | * Wait for the child elements of a specified component to resolve. 127 | * 128 | * @param {Object} data The resolved data. 129 | * @returns {Promise} 130 | */ 131 | async resolveChildren(data: any) : Promise { 132 | let children: ComponentNode[] = []; 133 | 134 | const pushItem = (item: ComponentNode) => { 135 | if (typeof item === 'string') 136 | children.push(item); 137 | else { 138 | let n = node(item); 139 | if (n) children.push(n); 140 | } 141 | }; 142 | 143 | let arr = this.children.flat(Infinity); 144 | for (let i = 0; i < arr.length; i++) { 145 | let value = await resolve(arr[i], data); 146 | 147 | if (value instanceof Array) { 148 | // flatten inner arrays (avoid creating unnecessary nodes) 149 | value.flat(Infinity).forEach((item) => { 150 | pushItem(item); 151 | }); 152 | } else { 153 | pushItem(value); 154 | } 155 | } 156 | 157 | return children; 158 | } 159 | 160 | isEmptyTemplate() : boolean { 161 | // this isn't a perfect check, but it's probably close enough... 162 | return this.template("") === "" && this.template.toString().length <= 6; 163 | } 164 | 165 | /** 166 | * Calls the passed function on the rendered element after 167 | * it is added to the page/DOM. 168 | * 169 | * @param {function(HTMLElement|jQuery|string, Object)} fun 170 | * @returns {Component} 171 | */ 172 | runAfter(fun: (e: Element, data: any) => void) : Component { 173 | let node = this.clone(); 174 | node.tasksAfter = new PendingTasks(this.tasksAfter).push(fun); 175 | return node; 176 | } 177 | 178 | /** 179 | * 180 | * @param {function(ElementImpl, Object)} fun 181 | * @returns {Component} 182 | */ 183 | runWrapped(fun: (e: ElementImpl, data: any) => void) : Component { 184 | let node = this.clone(); 185 | node.tasks = new PendingTasks(this.tasks).push(fun); 186 | return node; 187 | } 188 | 189 | /** 190 | * Calls the passed function on the rendered element. 191 | * 192 | * @param {function(HTMLElement|jQuery|string, Object)} fun 193 | * @returns {Component} 194 | */ 195 | run(fun: (e: Element, data: any) => void) : Component { 196 | return this.runWrapped((e, data) => fun(e.get(), data)); 197 | } 198 | 199 | runWrappedWithValue(value: ResolvableValue, fun: (e: ElementImpl, data: T) => void) : Component { 200 | return this.runWrapped(async function(element, data): Promise { 201 | return fun(element, await (new DataResolvable(value)).resolve(data)); 202 | }) 203 | } 204 | 205 | runWithValue(value: ResolvableValue, fun: (e: Element, data: T) => void) : Component { 206 | return this.run(async function(element, data): Promise { 207 | return fun(element, await (new DataResolvable(value)).resolve(data)); 208 | }) 209 | } 210 | 211 | id(value: ResolvableValue) : Component { 212 | return this.attr("id", value); 213 | } 214 | 215 | attr(name: string, value: ResolvableValue) : Component { 216 | return this.runWrappedWithValue(value, (element, resolvedValue) => { 217 | element.attr(name, resolvedValue); 218 | }); 219 | } 220 | 221 | attrs(values: { [name: string]: ResolvableValue }) : Component { 222 | return Object.keys(values).reduce((component: Component, key: string) => { 223 | return component.attr(key, values[key]); 224 | }, this); 225 | } 226 | 227 | className(value: ResolvableValue) : Component { 228 | return this.attr("class", value); 229 | } 230 | 231 | on(event: string, callback: (e: Event) => void) : Component { 232 | return this.runWrapped((element) => { 233 | element.on(event, callback); 234 | }); 235 | } 236 | 237 | async renderString(opts?: RenderOpts) : Promise { 238 | return await (new StringRender(opts)).render(null, null, this); 239 | } 240 | 241 | /** 242 | * Render the component and its child elements on the DOM. 243 | * 244 | * @param {?Node|string} tempElement - The temporary element to replace upon render. 245 | * @param {RenderOpts} opts - Render options. 246 | * @returns {Promise} 247 | */ 248 | async render(tempElement: Node | string, opts?: RenderOpts) : Promise { 249 | let element; 250 | if (typeof tempElement === 'string') 251 | element = document.querySelector(tempElement); 252 | else element = tempElement; 253 | 254 | return await (new DOMRender(opts)).render(null, element, this); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/util/dom-wrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps DOM functions to abstract them 3 | * from the rest of the library. 4 | * 5 | * @module dom/dom-wrapper 6 | */ 7 | 8 | /** 9 | * Base element implementation class. 10 | * 11 | * @class ElementImpl 12 | */ 13 | export abstract class ElementImpl { 14 | 15 | element: E 16 | 17 | constructor(element: E) { 18 | this.element = element; 19 | } 20 | 21 | attr(name: string, value?: string) : string | null { 22 | if (value) { 23 | this.setAttr(name, value); 24 | return value; 25 | } else return this.getAttr(name); 26 | } 27 | 28 | abstract getAttr(name: string) : string | null 29 | 30 | abstract setAttr(name: string, value: string) : void 31 | 32 | get className() : string | null { 33 | return this.getClassName(); 34 | } 35 | 36 | set className(value: string | null) { 37 | this.setClassName(value || ""); 38 | } 39 | 40 | getClassName() : string | null { 41 | return this.getAttr("class"); 42 | } 43 | 44 | setClassName(value: string) { 45 | this.setAttr("class", value); 46 | } 47 | 48 | abstract on(event: string, callback: (event: Event) => void) : void 49 | 50 | /** 51 | * Inserts one element before another inside 52 | * of the element that it is called upon. 53 | * 54 | * @param {HTMLElement} other The element to insert 55 | * @param {HTMLElement} ref The reference/index element to insert 56 | * `other` before. 57 | */ 58 | abstract insertBefore(other: E, ref: E) : void 59 | 60 | /** 61 | * Inserts one element after another inside 62 | * of the element that it is called upon. 63 | * 64 | * @param {HTMLElement} other The element to insert 65 | * @param {HTMLElement} ref The reference/index element to insert 66 | * `other` after. 67 | */ 68 | abstract insertAfter(other: E, ref: E) : void 69 | 70 | /** 71 | * Remove the element from the DOM. 72 | */ 73 | abstract remove() : void 74 | 75 | /** 76 | * Replace the element with another. 77 | * 78 | * @param {HTMLElement} other The element to replace it with. 79 | * @param {HTMLElement?} parent The parent element to replace inside. 80 | */ 81 | abstract replaceWith(other: E, parent?: E) : void 82 | 83 | /** 84 | * Find a specific element inside of another. 85 | * 86 | * @param {string} selector The selector string to query. 87 | */ 88 | abstract find(selector: string) : E|null 89 | 90 | /** 91 | * Append a child node to the current 92 | * 93 | * @param {HTMLElement} child The node to append. 94 | */ 95 | abstract appendChild(child: E) : void 96 | 97 | /** 98 | * Clear all child nodes from the element. 99 | */ 100 | abstract empty() : void 101 | 102 | get() : E { 103 | return this.element; 104 | } 105 | } 106 | 107 | /** 108 | * The most hacky and basic possible implementation 109 | * for string HTML parsing / manipulation. 110 | * 111 | * @class StringElementImpl 112 | */ 113 | class StringElementImpl extends ElementImpl { 114 | 115 | attrs: { [key: string]: string } 116 | 117 | constructor(element: string) { 118 | super(element); 119 | this.attrs = {}; 120 | } 121 | 122 | setAttr(name: string, value: string) { 123 | this.attrs[name] = value; 124 | } 125 | 126 | getAttr(name: string) : string { 127 | return this.attrs[name]; 128 | } 129 | 130 | on(event: string, callback: (event: Event) => void) { 131 | throw "No .on implementation!"; 132 | } 133 | 134 | find(selector: string): string { 135 | throw "No .find implementation!"; 136 | } 137 | 138 | appendChild(element: string) { 139 | throw "No .appendChild implementation!"; 140 | } 141 | 142 | insertBefore(other: string, ref: string) : void { 143 | throw "No .insertBefore implementation!"; 144 | } 145 | 146 | insertAfter(other: string, ref: string) : void { 147 | throw "No .insertAfter implementation!"; 148 | } 149 | 150 | remove() { 151 | throw "No .remove implementation!"; 152 | } 153 | 154 | replaceWith(other: string, parent?: string) : void { 155 | throw "No .replaceWith implementation!"; 156 | } 157 | 158 | empty() { 159 | throw "No .empty implementation!"; 160 | } 161 | 162 | get() : string { 163 | let index = this.element.indexOf(">"); 164 | return this.element.slice(0, index) 165 | + Object.keys(this.attrs).map((key) => ` ${key}="${this.attrs[key]}"`) 166 | + this.element.slice(index); 167 | } 168 | } 169 | 170 | /** 171 | * Implementation for HTML nodes. 172 | * 173 | * @class HTMLNodeImpl 174 | */ 175 | class HTMLNodeImpl extends ElementImpl { 176 | 177 | constructor(element: Node) { 178 | super(element); 179 | } 180 | 181 | setAttr(name: string, value: string) { 182 | throw "No .setAttr implementation!"; 183 | } 184 | 185 | getAttr(name: string) : string | null { 186 | throw "No .getAttr implementation!"; 187 | } 188 | 189 | on(event: string, callback: (e: Event) => void) { 190 | this.element.addEventListener(event, callback); 191 | } 192 | 193 | insertBefore(other: Node, ref: Node) { 194 | this.element.insertBefore(other, ref); 195 | } 196 | 197 | insertAfter(other: Node, ref: Node) { 198 | if (ref.nextSibling) 199 | this.insertBefore(other, ref.nextSibling) 200 | else this.appendChild(other); 201 | } 202 | 203 | remove() { 204 | throw "No .remove implementation!"; 205 | } 206 | 207 | replaceWith(other: Node, parent: Node) { 208 | if (parent) 209 | parent.replaceChild(other, this.element); 210 | else throw "Cannot replace element; no parent defined."; 211 | } 212 | 213 | find(selector: string): Node | null { 214 | return null; 215 | } 216 | 217 | appendChild(child: Node) { 218 | this.element.appendChild(child); 219 | } 220 | 221 | empty() { 222 | while (this.element.firstChild) 223 | this.element.removeChild(this.element.firstChild); 224 | } 225 | } 226 | 227 | /** 228 | * Implementation for HTML elements. 229 | * 230 | * @class HTMLElementImpl 231 | */ 232 | class HTMLElementImpl extends ElementImpl { 233 | 234 | constructor(element: HTMLElement) { 235 | super(element); 236 | } 237 | 238 | setAttr(name: string, value: string) { 239 | this.element.setAttribute(name, value); 240 | } 241 | 242 | getAttr(name: string) : string | null { 243 | return this.element.getAttribute(name); 244 | } 245 | 246 | on(event: string, callback: (e: Event) => void) { 247 | this.element.addEventListener(event, callback); 248 | } 249 | 250 | insertBefore(other: HTMLElement, ref: Node) { 251 | this.element.insertBefore(other, ref); 252 | } 253 | 254 | insertAfter(other: HTMLElement, ref: Node) { 255 | if (ref.nextSibling) 256 | this.insertBefore(other, ref.nextSibling) 257 | else this.appendChild(other); 258 | } 259 | 260 | remove() { 261 | this.element.remove(); 262 | } 263 | 264 | replaceWith(other: HTMLElement, parent: HTMLElement) { 265 | if (this.element.replaceWith) 266 | this.element.replaceWith(other); 267 | else if (parent) 268 | parent.replaceChild(other, this.element); 269 | else throw "Cannot replace element; no parent defined."; 270 | } 271 | 272 | find(selector: string): HTMLElement | null { 273 | return this.element.querySelector(selector); 274 | } 275 | 276 | appendChild(child: HTMLElement) { 277 | this.element.appendChild(child); 278 | } 279 | 280 | empty() { 281 | while (this.element.firstChild) 282 | this.element.removeChild(this.element.firstChild); 283 | } 284 | } 285 | 286 | export async function getAnimationFrame() { 287 | await new Promise((resolve, reject) => { 288 | window.requestAnimationFrame(() => 289 | window.requestAnimationFrame(() => resolve())); 290 | }); 291 | } 292 | 293 | /** 294 | * Creates a new HTML element. 295 | * 296 | * @param html The HTML string to parse. 297 | * @returns {Node[]} The root elements of the created HTML. 298 | */ 299 | export function createHtml(html: string) : Node[] { 300 | let template = document.createElement('template'); 301 | template.innerHTML = html.trim ? html.trim() : html; 302 | 303 | let children = template.content.childNodes; 304 | let ret = []; // copy children into new array 305 | for (let i = 0; i < children.length; i++) { 306 | ret.push(children[i]); 307 | } 308 | 309 | if (ret.length > 0) 310 | return ret; 311 | else return [template]; 312 | } 313 | 314 | /** 315 | * Creates a new text element. 316 | * 317 | * @param str The string to create. 318 | * @returns {Text} A created DOM node. 319 | */ 320 | export function createText(str: string): Text { 321 | return document.createTextNode(str); 322 | } 323 | 324 | /** 325 | * Provides an implementation of basic DOM functions for a 326 | * specified element. 327 | * 328 | * @param {HTMLElementImpl|HTMLElement|string} e The element to provide an implementation for. 329 | * @returns {ElementImpl} 330 | */ 331 | export function element(e: T) : ElementImpl | null { 332 | if (e instanceof ElementImpl) 333 | return e; 334 | else if (typeof e === "string") 335 | return new StringElementImpl(`${e}`); 336 | else if (e instanceof HTMLElement) 337 | return new HTMLElementImpl(e); 338 | else if (e instanceof Node) 339 | return new HTMLNodeImpl(e); 340 | else if (e === null || typeof e === 'undefined') 341 | return null; 342 | else { 343 | console.log(e); 344 | throw `dom-wrapper: Cannot implement element "${Object.getPrototypeOf(e).constructor.name}" ${e}.`; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------