diff --git a/.gitignore b/.gitignore index bdf488e..69ede03 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules package-lock.json yarn.lock .vscode +.rpt2_cache +dist diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..90b3cbf --- /dev/null +++ b/global.d.ts @@ -0,0 +1,5 @@ +declare namespace jest { + interface Matchers { + toMatchDOM(value: any): CustomMatcherResult + } +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..4426715 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: "ts-jest" +} diff --git a/package.json b/package.json index df91050..723402c 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,41 @@ { "name": "superfine", - "description": "Minimal view layer for creating declarative web user interfaces.", "version": "6.0.1", - "main": "dist/superfine.js", - "module": "src/index.js", - "license": "MIT", - "repository": "jorgebucaran/superfine", - "types": "superfine.d.ts", - "files": [ - "src", - "dist", - "superfine.d.ts" - ], - "author": "Jorge Bucaran", + "description": "Minimal view layer for creating declarative web user interfaces.", "keywords": [ - "superfine", "frontend", - "virtual dom", - "vdom" + "superfine", + "vdom", + "virtual dom" + ], + "repository": "jorgebucaran/superfine", + "license": "MIT", + "author": "Jorge Bucaran", + "files": [ + "dist" ], + "main": "dist/superfine.js", + "module": "dist/superfine.mjs", + "types": "dist/superfine.d.ts", "scripts": { - "test": "jest --coverage --no-cache", - "build": "npm run bundle && npm run minify", - "bundle": "rollup -i src/index.js -o dist/superfine.js -m -f umd -n superfine", - "minify": "terser dist/superfine.js -o dist/superfine.js -mc pure_funcs=['Object.defineProperty'] --source-map includeSources,url=superfine.js.map", + "build": "rollup -c", "prepare": "npm run build", - "release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" + "release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", + "test": "jest --coverage --no-cache" }, - "babel": { - "presets": "env" + "prettier": { + "semi": false }, "devDependencies": { + "@types/jest": "^23.3.10", + "@types/node": "^10.12.18", "jest": "^23.6.0", - "rollup": "^0.62.0", - "terser": "^3.13.1", - "babel-preset-env": "^1.7.0" + "rollup": "^0.68.1", + "rollup-plugin-filesize": "^5.0.1", + "rollup-plugin-terser": "^3.0.0", + "rollup-plugin-typescript2": "^0.18.1", + "ts-jest": "^23.10.5", + "tslib": "^1.9.3", + "typescript": "^3.2.2" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..89a2596 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,30 @@ +import typescript from "rollup-plugin-typescript2" +import { terser } from "rollup-plugin-terser" +import filesize from "rollup-plugin-filesize" + +export default [ + { + input: "src/superfine.ts", + output: { + file: "dist/superfine.mjs", + format: "es" + }, + plugins: [ + typescript({ tsconfigOverride: { include: ["src"] } }), + filesize() + ] + }, + { + input: "src/superfine.ts", + output: { + file: "dist/superfine.js", + format: "umd", + name: "superfine" + }, + plugins: [ + typescript({ tsconfigOverride: { include: ["src"] } }), + terser(), + filesize() + ] + } +] diff --git a/src/index.js b/src/superfine.ts similarity index 69% rename from src/index.js rename to src/superfine.ts index 228a15e..f8ff556 100644 --- a/src/index.js +++ b/src/superfine.ts @@ -1,34 +1,52 @@ -var DEFAULT = 0 -var RECYCLED_NODE = 1 -var TEXT_NODE = 2 +/** + * The vdom representation of an element. + */ +export interface VNode { + name: string + props?: Props + children?: Array + element?: Element | null + key?: string | null + type?: number +} + +/** + * Children of a vdom node + */ +export type Children = VNode | string | number | null -var XLINK_NS = "http://www.w3.org/1999/xlink" -var SVG_NS = "http://www.w3.org/2000/svg" +const DEFAULT = 0 +const RECYCLED_NODE = 1 +const TEXT_NODE = 2 -var EMPTY_OBJECT = {} -var EMPTY_ARRAY = [] +const XLINK_NS = "http://www.w3.org/1999/xlink" +const SVG_NS = "http://www.w3.org/2000/svg" -var map = EMPTY_ARRAY.map -var isArray = Array.isArray +const EMPTY_OBJECT = {} +const EMPTY_ARRAY = [] -var merge = function(a, b) { - var target = {} +const map = EMPTY_ARRAY.map +const isArray = Array.isArray - for (var i in a) target[i] = a[i] - for (var i in b) target[i] = b[i] +const merge = (a, b) => { + let target = {} + + for (let i in a) target[i] = a[i] + for (let i in b) target[i] = b[i] return target } -var eventProxy = function(event) { +const eventProxy = event => { return event.currentTarget.events[event.type](event) } -var updateProperty = function(element, name, lastValue, nextValue, isSvg) { +const updateProperty = (element, name, lastValue, nextValue, isSvg) => { if (name === "key") { } else if (name === "style") { - for (var i in merge(lastValue, nextValue)) { - var style = nextValue == null || nextValue[i] == null ? "" : nextValue[i] + for (let i in merge(lastValue, nextValue)) { + const style = + nextValue == null || nextValue[i] == null ? "" : nextValue[i] if (i[0] === "-") { element[name].setProperty(i, style) } else { @@ -47,7 +65,7 @@ var updateProperty = function(element, name, lastValue, nextValue, isSvg) { element.addEventListener(name, eventProxy) } } else { - var nullOrFalse = nextValue == null || nextValue === false + const nullOrFalse = nextValue == null || nextValue === false if ( name in element && @@ -62,7 +80,7 @@ var updateProperty = function(element, name, lastValue, nextValue, isSvg) { element.removeAttribute(name) } } else { - var ns = isSvg && name !== (name = name.replace(/^xlink:?/, "")) + const ns = isSvg && name !== (name = name.replace(/^xlink:?/, "")) if (ns) { if (nullOrFalse) { element.removeAttributeNS(XLINK_NS, name) @@ -81,41 +99,41 @@ var updateProperty = function(element, name, lastValue, nextValue, isSvg) { } } -var createElement = function(node, lifecycle, isSvg) { - var element = +const createElement = (node, lifecycle, isSvg) => { + const element = node.type === TEXT_NODE ? document.createTextNode(node.name) : (isSvg = isSvg || node.name === "svg") - ? document.createElementNS(SVG_NS, node.name) - : document.createElement(node.name) + ? document.createElementNS(SVG_NS, node.name) + : document.createElement(node.name) - var props = node.props + const props = node.props if (props.oncreate) { - lifecycle.push(function() { + lifecycle.push(() => { props.oncreate(element) }) } - for (var i = 0, length = node.children.length; i < length; i++) { + for (let i = 0, length = node.children.length; i < length; i++) { element.appendChild(createElement(node.children[i], lifecycle, isSvg)) } - for (var name in props) { + for (let name in props) { updateProperty(element, name, null, props[name], isSvg) } return (node.element = element) } -var updateElement = function( +const updateElement = ( element, lastProps, nextProps, lifecycle, isSvg, isRecycled -) { - for (var name in merge(lastProps, nextProps)) { +) => { + for (const name in merge(lastProps, nextProps)) { if ( (name === "value" || name === "checked" ? element[name] @@ -125,20 +143,20 @@ var updateElement = function( } } - var cb = isRecycled ? nextProps.oncreate : nextProps.onupdate + const cb = isRecycled ? nextProps.oncreate : nextProps.onupdate if (cb != null) { - lifecycle.push(function() { + lifecycle.push(() => { cb(element, lastProps) }) } } -var removeChildren = function(node) { - for (var i = 0, length = node.children.length; i < length; i++) { +const removeChildren = node => { + for (let i = 0, length = node.children.length; i < length; i++) { removeChildren(node.children[i]) } - var cb = node.props.ondestroy + const cb = node.props.ondestroy if (cb != null) { cb(node.element) } @@ -146,12 +164,12 @@ var removeChildren = function(node) { return node.element } -var removeElement = function(parent, node) { - var remove = function() { +const removeElement = (parent, node) => { + const remove = () => { parent.removeChild(removeChildren(node)) } - var cb = node.props && node.props.onremove + const cb = node.props && node.props.onremove if (cb != null) { cb(node.element, remove) } else { @@ -159,14 +177,14 @@ var removeElement = function(parent, node) { } } -var getKey = function(node) { +const getKey = node => { return node == null ? null : node.key } -var createKeyMap = function(children, start, end) { - var out = {} - var key - var node +const createKeyMap = (children, start, end) => { + let out = {} + let key + let node for (; start <= end; start++) { if ((key = (node = children[start]).key) != null) { @@ -177,14 +195,14 @@ var createKeyMap = function(children, start, end) { return out } -var patchElement = function( +const patchElement = ( parent, element, lastNode, nextNode, lifecycle, - isSvg -) { + isSvg? +) => { if (nextNode === lastNode) { } else if ( lastNode != null && @@ -195,7 +213,7 @@ var patchElement = function( element.nodeValue = nextNode.name } } else if (lastNode == null || lastNode.name !== nextNode.name) { - var newElement = parent.insertBefore( + const newElement = parent.insertBefore( createElement(nextNode, lifecycle, isSvg), element ) @@ -213,18 +231,18 @@ var patchElement = function( lastNode.type === RECYCLED_NODE ) - var savedNode - var childNode + let savedNode + let childNode - var lastKey - var lastChildren = lastNode.children - var lastChStart = 0 - var lastChEnd = lastChildren.length - 1 + let lastKey + const lastChildren = lastNode.children + let lastChStart = 0 + let lastChEnd = lastChildren.length - 1 - var nextKey - var nextChildren = nextNode.children - var nextChStart = 0 - var nextChEnd = nextChildren.length - 1 + let nextKey + const nextChildren = nextNode.children + let nextChStart = 0 + let nextChEnd = nextChildren.length - 1 while (nextChStart <= nextChEnd && lastChStart <= lastChEnd) { lastKey = getKey(lastChildren[lastChStart]) @@ -276,8 +294,8 @@ var patchElement = function( removeElement(element, lastChildren[lastChStart++]) } } else { - var lastKeyed = createKeyMap(lastChildren, lastChStart, lastChEnd) - var nextKeyed = {} + const lastKeyed = createKeyMap(lastChildren, lastChStart, lastChEnd) + const nextKeyed = {} while (nextChStart <= nextChEnd) { lastKey = getKey((childNode = lastChildren[lastChStart])) @@ -354,7 +372,7 @@ var patchElement = function( } } - for (var key in lastKeyed) { + for (let key in lastKeyed) { if (nextKeyed[key] == null) { removeElement(element, lastKeyed[key]) } @@ -365,7 +383,7 @@ var patchElement = function( return (nextNode.element = element) } -var createVNode = function(name, props, children, element, key, type) { +const createVNode = (name, props, children, element, key, type) => { return { name: name, props: props, @@ -376,17 +394,17 @@ var createVNode = function(name, props, children, element, key, type) { } } -var createTextVNode = function(text, element) { +const createTextVNode = (text, element?) => { return createVNode(text, EMPTY_OBJECT, EMPTY_ARRAY, element, null, TEXT_NODE) } -var recycleChild = function(element) { +const recycleChild = element => { return element.nodeType === 3 // Node.TEXT_NODE ? createTextVNode(element.nodeValue, element) : recycleElement(element) } -var recycleElement = function(element) { +const recycleElement = element => { return createVNode( element.nodeName.toLowerCase(), EMPTY_OBJECT, @@ -397,12 +415,26 @@ var recycleElement = function(element) { ) } -export var recycle = function(container) { +/** + * Superfine SSR: Create a virtual DOM from your container to patch it on client side + * @param container a DOM element + */ +export const recycle = (container: Element): VNode => { return recycleElement(container.children[0]) } -export var patch = function(lastNode, nextNode, container) { - var lifecycle = [] +/** + * Render a vdom node in DOM element container + * @param lastNode the last vdom node + * @param nextNode the next vdom node + * @param container a DOM element where the new vdom will be rendered + */ +export const patch = function( + lastNode: VNode, + nextNode: VNode, + container: Element +): VNode { + let lifecycle = [] patchElement(container, container.children[0], lastNode, nextNode, lifecycle) @@ -411,13 +443,20 @@ export var patch = function(lastNode, nextNode, container) { return nextNode } -export var h = function(name, props) { - var node - var rest = [] - var children = [] - var length = arguments.length - - while (length-- > 2) rest.push(arguments[length]) +/** + * create a new vdom node. vdom is a description of what DOM should look like using a tree of virtual nodes. + * @param name name of an element or function that returns vdom node + * @param props HTML props, SVG props, DOM events, lifecycle events and keys + * @param rest child nodes for an element + */ +export const h = function( + name: string | Function, + props?: any, + ...rest: Array +): VNode { + let node + let children = [] + let length = arguments.length if ((props = props == null ? {} : props).children != null) { if (rest.length <= 0) { diff --git a/superfine.d.ts b/superfine.d.ts deleted file mode 100644 index d1a7c46..0000000 --- a/superfine.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -export as namespace superfine - -export type Children = VNode | string | number | null - -/** - * The virtual DOM representation of an Element. - */ -export interface VNode { - name: string, - props: Props, - children: Array, - element: Element | null, - key: string | null, - type: number -} - -/** - * Create a new virtual DOM node. A virtual DOM is a description of what a DOM should look like using a tree of virtual nodes. - * @param name The name of an Element or a function that returns a virtual DOM node. - * @param props HTML props, SVG props, DOM events, Lifecycle Events, and Keys. - * @param children The element's child nodes. - */ -export function h( - name: string, - props?: Props | null, - ...children: Array -): VNode - -/** - * Render a virtual DOM node into a DOM element container. - * - * @param {VNode} oldNode The last virtual DOM node. - * @param {VNode} nextNode The next virtual DOM node. - * @param {Element?} container A DOM element where the new virtual DOM will be rendered. - * @returns {VNode} Returns nextNode. - **/ -export function patch( - lastNode: VNode, - nextNode: VNode, - container: Element -): VNode - -declare global { - namespace JSX { - interface Element extends VNode {} - interface IntrinsicElements { - [elemName: string]: any - } - } -} diff --git a/test/test.js b/test/superfine.test.ts similarity index 95% rename from test/test.js rename to test/superfine.test.ts index 81b89cc..c07d99b 100644 --- a/test/test.js +++ b/test/superfine.test.ts @@ -1,4 +1,4 @@ -import { h, patch, recycle } from "../src/index.js" +import { h, patch, recycle } from "../src/superfine" beforeEach(() => { document.body.innerHTML = "" @@ -12,12 +12,12 @@ expect.extend({ return node }, null) - return { pass: true } + return { pass: true, message: "DOM Nodes match" } } }) const expectDeepNS = (el, ns) => - Array.from(el.childNodes).map(child => { + Array.from(el.childNodes).map((child: any) => { expect(child.namespaceURI).toBe(ns) expectDeepNS(child, ns) }) @@ -29,7 +29,7 @@ const tag = name => (props, children) => Array.isArray(props) ? props : children ) -const { div, main, input, ul, li, svg, use } = [ +const { div, main, input, ul, li, svg, use }: any = [ "div", "main", "input", @@ -240,6 +240,7 @@ test("oncreate", () => { }) test("onupdate", done => { + let node const view = state => div( { @@ -253,7 +254,7 @@ test("onupdate", done => { state ) - let node = patch(node, view("foo"), document.body) + node = patch(node, view("foo"), document.body) patch(node, view("bar"), document.body) }) @@ -303,6 +304,7 @@ test("ondestroy", done => { test("onremove/ondestroy", done => { let destroyed = false + let node const view = state => state @@ -322,14 +324,15 @@ test("onremove/ondestroy", done => { ]) : ul([li()]) - let node = patch(node, view(true), document.body) + node = patch(node, view(true), document.body) patch(node, view(false), document.body) }) test("event bubbling", done => { let count = 0 + let node - const view = state => + const view = (state?) => main( { oncreate: () => expect(count++).toBe(3), @@ -354,7 +357,7 @@ test("event bubbling", done => { ] ) - let node = patch(node, view(), document.body) + node = patch(node, view(), document.body) patch(node, view(), document.body) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c18ae27 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "module": "es2015", + "moduleResolution": "node", + "target": "es2015" + } +}