chore(tools): new tooling (#948)

pull/953/head
Jan van Brügge 4 years ago
commit f3a088596e
No known key found for this signature in database
GPG Key ID: 88E0BF7B7A546481

39
.ecrc

@ -1,39 +0,0 @@
{
"ignoreDefaults": true,
"exclude": [
"^coverage/",
"^node_modules/",
"^es/",
"^helpers/",
"^modules/",
"^test/",
"^h.d.ts",
"^h.js",
"^h.js.map",
"^hooks.d.ts",
"^hooks.js",
"^hooks.js.map",
"^htmldomapi.d.ts",
"^htmldomapi.js",
"^htmldomapi.js.map",
"^is.d.ts",
"^is.js",
"^is.js.map",
"^jsx.d.ts",
"^jsx-global.d.ts",
"^jsx.js",
"^jsx.js.map",
"^snabbdom.d.ts",
"^snabbdom.js",
"^snabbdom.js.map",
"^thunk.d.ts",
"^thunk.js",
"^thunk.js.map",
"^tovnode.d.ts",
"^tovnode.js",
"^tovnode.js.map",
"^vnode.d.ts",
"^vnode.js",
"^vnode.js.map"
]
}

@ -1,45 +0,0 @@
module.exports = {
extends: 'standard-with-typescript',
parserOptions: {
project: [
'./src/package/tsconfig.json',
'./src/test/tsconfig.json',
]
},
plugins: [
'markdown',
],
overrides: [
{
files: ['**/*.md'],
processor: 'markdown/markdown'
},
{
files: ['**/*.md/*.@(mjs|ts)'],
rules: {
'no-undef': 'off',
'no-unused-expressions': 'off',
'no-unused-vars': 'off',
},
}
],
rules: {
'import/newline-after-import': 'error',
'max-statements-per-line': 'error',
'no-var': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/prefer-includes': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/method-signature-style': 'off',
'comma-dangle': ['error', 'only-multiline'],
'no-mixed-operators': 'off',
}
}

@ -0,0 +1,63 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:markdown/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
],
env: {
browser: true,
node: false,
},
overrides: [
{
files: ["*.ts", "*.tsx"],
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json", "test/tsconfig.json"],
},
extends: [
"plugin:@typescript-eslint/recommended-requiring-type-checking",
],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
{
files: ["test/**/*.ts", "test/unit/*.tsx"],
env: {
browser: true,
node: true,
},
},
{
files: ["*.js"],
excludedFiles: ["examples/**"],
extends: ["plugin:node/recommended"],
env: {
node: true,
browser: false,
},
rules: {
"@typescript-eslint/no-var-requires": "off",
},
},
],
rules: {
"max-statements-per-line": "error",
"no-var": "error",
"import/newline-after-import": "error",
"import/no-default-export": "error",
},
};

@ -1,26 +1,21 @@
name: CI
on: pull_request
on:
pull_request:
push:
branches:
- main
jobs:
ci:
runs-on: ubuntu-latest
environment: ci
steps:
- uses: actions/checkout@v2
- name: Turnstyle
uses: softprops/turnstyle@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Read .nvmrc
run: echo ::set-output name=node-version::$(cat .nvmrc)
id: nvm
- uses: actions/setup-node@v2
with:
node-version: ${{ steps.nvm.outputs.node-version }}
- run: npm ci
- run: npx run-s test docs check-clean
- run: npm run test:ci
env:
BROWSER_STACK_USERNAME: ${{ secrets.BROWSER_STACK_USERNAME }}
BROWSER_STACK_ACCESS_KEY: ${{ secrets. BROWSER_STACK_ACCESS_KEY }}

83
.gitignore vendored

@ -9,88 +9,7 @@
## Package artifacts
/build/package/tsconfig.tsbuildinfo
/build/package/h.d.ts
/build/package/h.js
/build/package/h.js.map
/build/package/helpers/attachto.d.ts
/build/package/helpers/attachto.js
/build/package/helpers/attachto.js.map
/build/package/hooks.d.ts
/build/package/hooks.js
/build/package/hooks.js.map
/build/package/htmldomapi.d.ts
/build/package/htmldomapi.js
/build/package/htmldomapi.js.map
/build/package/init.d.ts
/build/package/init.js
/build/package/init.js.map
/build/package/is.d.ts
/build/package/is.js
/build/package/is.js.map
/build/package/jsx-global.d.ts
/build/package/jsx-global.js
/build/package/jsx-global.js.map
/build/package/jsx.d.ts
/build/package/jsx.js
/build/package/jsx.js.map
/build/package/modules/attributes.d.ts
/build/package/modules/attributes.js
/build/package/modules/attributes.js.map
/build/package/modules/class.d.ts
/build/package/modules/class.js
/build/package/modules/class.js.map
/build/package/modules/dataset.d.ts
/build/package/modules/dataset.js
/build/package/modules/dataset.js.map
/build/package/modules/eventlisteners.d.ts
/build/package/modules/eventlisteners.js
/build/package/modules/eventlisteners.js.map
/build/package/modules/hero.d.ts
/build/package/modules/hero.js
/build/package/modules/hero.js.map
/build/package/modules/module.d.ts
/build/package/modules/module.js
/build/package/modules/module.js.map
/build/package/modules/props.d.ts
/build/package/modules/props.js
/build/package/modules/props.js.map
/build/package/modules/style.d.ts
/build/package/modules/style.js
/build/package/modules/style.js.map
/build/package/thunk.d.ts
/build/package/thunk.js
/build/package/thunk.js.map
/build/package/tovnode.d.ts
/build/package/tovnode.js
/build/package/tovnode.js.map
/build/package/vnode.d.ts
/build/package/vnode.js
/build/package/vnode.js.map
# Test artifacts
/build/test/benchmark/core.js
/build/test/benchmark/core.js.map
/build/test/unit/attachto.js
/build/test/unit/attachto.js.map
/build/test/unit/attributes.js
/build/test/unit/attributes.js.map
/build/test/unit/core.js
/build/test/unit/core.js.map
/build/test/unit/dataset.js
/build/test/unit/dataset.js.map
/build/test/unit/eventlisteners.js
/build/test/unit/eventlisteners.js.map
/build/test/unit/htmldomapi.js
/build/test/unit/htmldomapi.js.map
/build/test/unit/jsx.js
/build/test/unit/jsx.js.map
/build/test/unit/style.js
/build/test/unit/style.js.map
/build/test/unit/thunk.js
/build/test/unit/thunk.js.map
/build/
## Test bundle artifacts

2
.husky/.gitignore vendored

@ -1 +1 @@
_
_

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --config commitlint.config.json --edit $1
npm exec commithelper -- check --file $1 --fix

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm exec -- npm-run-all -s docs check-clean test
npm exec -- lint-staged

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec < /dev/tty && npm exec commithelper -- prompt --file $1

@ -0,0 +1 @@
/coverage/

@ -0,0 +1,3 @@
build/
node_modules/
coverage/

@ -1 +0,0 @@
/CHANGELOG.md

@ -1,5 +0,0 @@
{
"skip": {
"tag": true
}
}

@ -5,7 +5,7 @@
"javascriptreact",
"markdown",
"typescript",
"typescriptreact",
"typescriptreact"
],
"eslint.options.ignorePath": ".gitignore"
}

@ -4,116 +4,109 @@ All notable changes to this project will be documented in this file. See [standa
## [2.1.0](https://github.com/snabbdom/snabbdom/compare/v2.0.0...v2.1.0) (2020-09-14)
### Features
* **eventlisteners:** add types for VNode in listener ([63b1b6c](https://github.com/snabbdom/snabbdom/commit/63b1b6c22e49d06b1fb509a14d321ec19f324bb5)), closes [#796](https://github.com/snabbdom/snabbdom/issues/796)
* **eventlisteners:** relax custom event listener type ([15ce059](https://github.com/snabbdom/snabbdom/commit/15ce059e2b5e80d1975168fff2d2a44f71bd5cbb)), closes [#850](https://github.com/snabbdom/snabbdom/issues/850)
- **eventlisteners:** add types for VNode in listener ([63b1b6c](https://github.com/snabbdom/snabbdom/commit/63b1b6c22e49d06b1fb509a14d321ec19f324bb5)), closes [#796](https://github.com/snabbdom/snabbdom/issues/796)
- **eventlisteners:** relax custom event listener type ([15ce059](https://github.com/snabbdom/snabbdom/commit/15ce059e2b5e80d1975168fff2d2a44f71bd5cbb)), closes [#850](https://github.com/snabbdom/snabbdom/issues/850)
## [2.0.0](https://github.com/snabbdom/snabbdom/compare/v1.0.1...v2.0.0) (2020-09-10)
### ⚠ BREAKING CHANGES
* **eventlisteners:** loaded/carrying event listeners are no longer supported.
- **eventlisteners:** loaded/carrying event listeners are no longer supported.
### Features
* **eventlisteners:** add missing mult. listeners type ([5a89efe](https://github.com/snabbdom/snabbdom/commit/5a89efe01580d50f15649c19a444745867c5c0d4)), closes [#794](https://github.com/snabbdom/snabbdom/issues/794)
* **eventlisteners:** remove loaded listeners feature ([6e0ff8e](https://github.com/snabbdom/snabbdom/commit/6e0ff8e8141c70891e55e41a3107d6d4de0bc754)), closes [#802](https://github.com/snabbdom/snabbdom/issues/802) [#802](https://github.com/snabbdom/snabbdom/issues/802)
- **eventlisteners:** add missing mult. listeners type ([5a89efe](https://github.com/snabbdom/snabbdom/commit/5a89efe01580d50f15649c19a444745867c5c0d4)), closes [#794](https://github.com/snabbdom/snabbdom/issues/794)
- **eventlisteners:** remove loaded listeners feature ([6e0ff8e](https://github.com/snabbdom/snabbdom/commit/6e0ff8e8141c70891e55e41a3107d6d4de0bc754)), closes [#802](https://github.com/snabbdom/snabbdom/issues/802) [#802](https://github.com/snabbdom/snabbdom/issues/802)
### Bug Fixes
* **deps:** add regenertor-runtime to devDeps ([2a2964c](https://github.com/snabbdom/snabbdom/commit/2a2964c3eb47cd2f5a7ae88f49b2afe9ea299d7e)), closes [#813](https://github.com/snabbdom/snabbdom/issues/813)
* **docs:** gitter badge url ([7e19849](https://github.com/snabbdom/snabbdom/commit/7e198493c11f6d4afa8b03d727083d661e85ec0e))
* **examples:** example import paths ([8111f62](https://github.com/snabbdom/snabbdom/commit/8111f6234a70840673412da6cd37a726a7c839f8)), closes [#761](https://github.com/snabbdom/snabbdom/issues/761)
* **examples:** totalHeight 0 on remove last element reorder animation ([afa77c0](https://github.com/snabbdom/snabbdom/commit/afa77c04d4ab959a5f2bb5853e5dd821c744843f))
* **package:** remove directories field ([c7a2a93](https://github.com/snabbdom/snabbdom/commit/c7a2a93f5a2ed63bd76130e5e3d3769a9f1c1c58))
* **package:** update urls paldepind -> snabbdom ([f94185a](https://github.com/snabbdom/snabbdom/commit/f94185a5bbb31018af48b77449e74f58339fe404)), closes [#775](https://github.com/snabbdom/snabbdom/issues/775)
- **deps:** add regenertor-runtime to devDeps ([2a2964c](https://github.com/snabbdom/snabbdom/commit/2a2964c3eb47cd2f5a7ae88f49b2afe9ea299d7e)), closes [#813](https://github.com/snabbdom/snabbdom/issues/813)
- **docs:** gitter badge url ([7e19849](https://github.com/snabbdom/snabbdom/commit/7e198493c11f6d4afa8b03d727083d661e85ec0e))
- **examples:** example import paths ([8111f62](https://github.com/snabbdom/snabbdom/commit/8111f6234a70840673412da6cd37a726a7c839f8)), closes [#761](https://github.com/snabbdom/snabbdom/issues/761)
- **examples:** totalHeight 0 on remove last element reorder animation ([afa77c0](https://github.com/snabbdom/snabbdom/commit/afa77c04d4ab959a5f2bb5853e5dd821c744843f))
- **package:** remove directories field ([c7a2a93](https://github.com/snabbdom/snabbdom/commit/c7a2a93f5a2ed63bd76130e5e3d3769a9f1c1c58))
- **package:** update urls paldepind -> snabbdom ([f94185a](https://github.com/snabbdom/snabbdom/commit/f94185a5bbb31018af48b77449e74f58339fe404)), closes [#775](https://github.com/snabbdom/snabbdom/issues/775)
### [1.0.1](https://github.com/paldepind/snabbdom/compare/v1.0.0...v1.0.1) (2020-06-18)
### User facing changes
* **package:** fix ./snabbdom related files and exports fields errors ([89b917b](https://github.com/paldepind/snabbdom/commit/89b917bb3f3f8986390e3e400327a9087533d928))
- **package:** fix ./snabbdom related files and exports fields errors ([89b917b](https://github.com/paldepind/snabbdom/commit/89b917bb3f3f8986390e3e400327a9087533d928))
## [1.0.0](https://github.com/paldepind/snabbdom/compare/v0.7.4...v1.0.0) (2020-06-18)
### ⚠ BREAKING CHANGES
* **exports:** The main export path, 'snabbdom' was replaced with
the export path 'snabbdom/init'. This new export path includes only
the named export `init`.
* **exports:** No default exports exist. All exports are named.
* **exports:** the import path `snabbdom/snabbdom.bundle` is removed.
* **typescript:** Types exported by this package have re-declared
the global `Element.setAttribute` and `Element.setAttributeNS` to
accept `number` and `boolean` for the `value` parameter. This
change removes that re-declaration and thus the only valid value is
`string`. If your code provides `number` and/or `boolean`, then it
may now fail to compile.
* **props:** props module does not attempt to delete node
properties. This may affect you if you are using the props module
to add non-native (custom) properties to DOM nodes. Instead, it is
recommended to use _data-* attributes_.
https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
* CommonJS modules are no longer provided.
* import paths in ES modules include file name
extensions.
* Compiled to ES2015 (was ES5).
* UMD bundles are no longer provided.
- **exports:** The main export path, 'snabbdom' was replaced with
the export path 'snabbdom/init'. This new export path includes only
the named export `init`.
- **exports:** No default exports exist. All exports are named.
- **exports:** the import path `snabbdom/snabbdom.bundle` is removed.
- **typescript:** Types exported by this package have re-declared
the global `Element.setAttribute` and `Element.setAttributeNS` to
accept `number` and `boolean` for the `value` parameter. This
change removes that re-declaration and thus the only valid value is
`string`. If your code provides `number` and/or `boolean`, then it
may now fail to compile.
- **props:** props module does not attempt to delete node
properties. This may affect you if you are using the props module
to add non-native (custom) properties to DOM nodes. Instead, it is
recommended to use _data-\* attributes_.
https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
- CommonJS modules are no longer provided.
- import paths in ES modules include file name
extensions.
- Compiled to ES2015 (was ES5).
- UMD bundles are no longer provided.
### Internal changes
* **commitlint:** add type auto and scope deps for renovate ([b56a0ac](https://github.com/paldepind/snabbdom/commit/b56a0ac796a3c27644f8332278a7cbb9d24a95c6))
* **commitlint:** fix and enable in CI ([f8cf5cc](https://github.com/paldepind/snabbdom/commit/f8cf5ccba402cbbf6982da681db8707fd12fc8d4)), closes [#662](https://github.com/paldepind/snabbdom/issues/662)
* **deps:** update dependency @typescript-eslint/eslint-plugin to v3.3.0 ([9448e42](https://github.com/paldepind/snabbdom/commit/9448e4267cf077890deb0deb32c3200e4d19a213))
* **deps:** update dependency tsconfigs to v5 ([eb1ec8c](https://github.com/paldepind/snabbdom/commit/eb1ec8c280544a322fc9844c255b8ec5c8e004c6))
* **deps:** update dependency typescript to v3.9.5 ([5e24b20](https://github.com/paldepind/snabbdom/commit/5e24b20a52a8c20ed82e3fc7d20977262016731a))
* **docs:** lint code examples ([41cb359](https://github.com/paldepind/snabbdom/commit/41cb3596e8898399545f02ff8205a1d45f62f391))
* **eslint:** lint cjs files ([d581217](https://github.com/paldepind/snabbdom/commit/d58121755f4e2da50ad82e52818b66333ae10a37))
* **format:** sort file lists ([e77615b](https://github.com/paldepind/snabbdom/commit/e77615b16bd60fbd1963528a2c46e7dfbbb77e0e)), closes [#673](https://github.com/paldepind/snabbdom/issues/673)
* **git:** ignore each test artifact specifically ([b34e9a9](https://github.com/paldepind/snabbdom/commit/b34e9a9d3a8096c2c5cd7eebeafba0e52ed08a75))
* **package:** consistent values in files field ([6fe56f8](https://github.com/paldepind/snabbdom/commit/6fe56f8538f6e0073d458d7a8e21c8d469c0a9df)), closes [#672](https://github.com/paldepind/snabbdom/issues/672)
* **relic:** remove @types/assert ([2846189](https://github.com/paldepind/snabbdom/commit/28461899bdce0c2134dca92298e40c1ecf7be363))
* **typescript:** package and tests are two projects ([8a71211](https://github.com/paldepind/snabbdom/commit/8a71211b4a38616c9d90bc9214e116d1b3e869b5))
* **vscode:** eslint.validate short forms ([ba3e85b](https://github.com/paldepind/snabbdom/commit/ba3e85bf90f77254fad08435d484e19f836c6783))
* **vscode:** use workspace typescript ([eabbd2f](https://github.com/paldepind/snabbdom/commit/eabbd2f056b40e5e9376ccf94c9a6f9177bd020a))
- **commitlint:** add type auto and scope deps for renovate ([b56a0ac](https://github.com/paldepind/snabbdom/commit/b56a0ac796a3c27644f8332278a7cbb9d24a95c6))
- **commitlint:** fix and enable in CI ([f8cf5cc](https://github.com/paldepind/snabbdom/commit/f8cf5ccba402cbbf6982da681db8707fd12fc8d4)), closes [#662](https://github.com/paldepind/snabbdom/issues/662)
- **deps:** update dependency @typescript-eslint/eslint-plugin to v3.3.0 ([9448e42](https://github.com/paldepind/snabbdom/commit/9448e4267cf077890deb0deb32c3200e4d19a213))
- **deps:** update dependency tsconfigs to v5 ([eb1ec8c](https://github.com/paldepind/snabbdom/commit/eb1ec8c280544a322fc9844c255b8ec5c8e004c6))
- **deps:** update dependency typescript to v3.9.5 ([5e24b20](https://github.com/paldepind/snabbdom/commit/5e24b20a52a8c20ed82e3fc7d20977262016731a))
- **docs:** lint code examples ([41cb359](https://github.com/paldepind/snabbdom/commit/41cb3596e8898399545f02ff8205a1d45f62f391))
- **eslint:** lint cjs files ([d581217](https://github.com/paldepind/snabbdom/commit/d58121755f4e2da50ad82e52818b66333ae10a37))
- **format:** sort file lists ([e77615b](https://github.com/paldepind/snabbdom/commit/e77615b16bd60fbd1963528a2c46e7dfbbb77e0e)), closes [#673](https://github.com/paldepind/snabbdom/issues/673)
- **git:** ignore each test artifact specifically ([b34e9a9](https://github.com/paldepind/snabbdom/commit/b34e9a9d3a8096c2c5cd7eebeafba0e52ed08a75))
- **package:** consistent values in files field ([6fe56f8](https://github.com/paldepind/snabbdom/commit/6fe56f8538f6e0073d458d7a8e21c8d469c0a9df)), closes [#672](https://github.com/paldepind/snabbdom/issues/672)
- **relic:** remove @types/assert ([2846189](https://github.com/paldepind/snabbdom/commit/28461899bdce0c2134dca92298e40c1ecf7be363))
- **typescript:** package and tests are two projects ([8a71211](https://github.com/paldepind/snabbdom/commit/8a71211b4a38616c9d90bc9214e116d1b3e869b5))
- **vscode:** eslint.validate short forms ([ba3e85b](https://github.com/paldepind/snabbdom/commit/ba3e85bf90f77254fad08435d484e19f836c6783))
- **vscode:** use workspace typescript ([eabbd2f](https://github.com/paldepind/snabbdom/commit/eabbd2f056b40e5e9376ccf94c9a6f9177bd020a))
### User facing changes
* **docs:** enable eslint rule array-bracket-spacing ([77e54e9](https://github.com/paldepind/snabbdom/commit/77e54e9105394d4e6d21647a38adfb80ea567ee2))
* **docs:** enable eslint rule import/first ([17cf7ae](https://github.com/paldepind/snabbdom/commit/17cf7ae931185d8ab1ac1e4f8b7042677e03db8d))
* **docs:** enable eslint rule import/newline-after-import ([cd3a5cf](https://github.com/paldepind/snabbdom/commit/cd3a5cf17ee33c738d17adcb78a8c254e96653b9))
* **docs:** enable eslint rule indent ([e2861bb](https://github.com/paldepind/snabbdom/commit/e2861bb1bd68c63c99c30907bb331c21f27cb248))
* **docs:** enable eslint rule key-spacing ([349b686](https://github.com/paldepind/snabbdom/commit/349b686bd8cfc24a2845fbab62081a15e5b42d0f))
* **docs:** enable eslint rule max-statements-per-line ([a128a23](https://github.com/paldepind/snabbdom/commit/a128a23ac3182677d10d1979213c92dcf04294ea))
* **docs:** enable eslint rule no-multi-spaces ([8179381](https://github.com/paldepind/snabbdom/commit/8179381a775acea73d0064c77d643944cf684947)), closes [#692](https://github.com/paldepind/snabbdom/issues/692)
* **docs:** enable eslint rule object-curly-spacing ([8b8fbd5](https://github.com/paldepind/snabbdom/commit/8b8fbd5e34fdc3a99ce67635cc44f5ef9e3cf30c))
* **docs:** enable eslint rule quote-props ([37512fe](https://github.com/paldepind/snabbdom/commit/37512fe8ee02cf374cf3d9a62c0f6e3203be2f28))
* **docs:** enable eslint rule quotes ([2d455b5](https://github.com/paldepind/snabbdom/commit/2d455b52dcabc2f8e52082536e635a66e98eb650))
* **docs:** enable eslint rule semi ([f4e7885](https://github.com/paldepind/snabbdom/commit/f4e7885663e645aeb728470d5e1f159365f94f1b))
* **docs:** enable eslint rule space-before-blocks ([9f2d2d7](https://github.com/paldepind/snabbdom/commit/9f2d2d7a1687c7169dff775724a811e1dd7ccd8b))
* **docs:** enable eslint rule space-before-function-paren ([23e7b87](https://github.com/paldepind/snabbdom/commit/23e7b87c64b5d587acae5ce7bd899ec0d071958e))
* **docs:** enable eslint rules object-*-newline ([9a45b5b](https://github.com/paldepind/snabbdom/commit/9a45b5b22aba0fbed97431e99268a173995de4a1))
* **docs:** fix wrong module import paths ([3b6baee](https://github.com/paldepind/snabbdom/commit/3b6baee049f44cbc55cc2b0131c2c551e9c1b452)), closes [#691](https://github.com/paldepind/snabbdom/issues/691)
* **docs:** provide a release changelog ([616df35](https://github.com/paldepind/snabbdom/commit/616df35909f1d639d562418ea32122625104b00c)), closes [#670](https://github.com/paldepind/snabbdom/issues/670)
* **exports:** main export provided ([3becd84](https://github.com/paldepind/snabbdom/commit/3becd84cc1dcfb84e2ee292eab18aae36e415040)), closes [#682](https://github.com/paldepind/snabbdom/issues/682)
* **exports:** only named exports ([fefd141](https://github.com/paldepind/snabbdom/commit/fefd141f5f3567bb6dccbd6c43ce81f285f985bd)), closes [#522](https://github.com/paldepind/snabbdom/issues/522) [#523](https://github.com/paldepind/snabbdom/issues/523)
* **exports:** relative values in exports field ([187088e](https://github.com/paldepind/snabbdom/commit/187088ee0ebfaed2e84a992bdde50d207131ec29)), closes [#674](https://github.com/paldepind/snabbdom/issues/674)
* **exports:** remove package.json main field ([3122eec](https://github.com/paldepind/snabbdom/commit/3122eec9b98ffdf52fd31ceb2ced17c219e25042)), closes [#680](https://github.com/paldepind/snabbdom/issues/680)
* **exports:** remove the /snabbdom.bundle path ([c862993](https://github.com/paldepind/snabbdom/commit/c8629933599b3fdf3ea774f3ce67517908b79d8d))
* **exports:** replaced main export path with init ([09f2d1c](https://github.com/paldepind/snabbdom/commit/09f2d1ca5a16fd0b402209d90e58b97998efacef)), closes [#522](https://github.com/paldepind/snabbdom/issues/522)
* **package:** no module field ([2b30e25](https://github.com/paldepind/snabbdom/commit/2b30e25f0d261d2d7f37127bda0c235b0a5acf57)), closes [#681](https://github.com/paldepind/snabbdom/issues/681)
* **props:** do not attempt to delete node properties ([6f316c1](https://github.com/paldepind/snabbdom/commit/6f316c141b43ccb1c2c355ab8d0c499984154ef1)), closes [#623](https://github.com/paldepind/snabbdom/issues/623) [#283](https://github.com/paldepind/snabbdom/issues/283) [#415](https://github.com/paldepind/snabbdom/issues/415) [#307](https://github.com/paldepind/snabbdom/issues/307) [#151](https://github.com/paldepind/snabbdom/issues/151) [#416](https://github.com/paldepind/snabbdom/issues/416)
* **typescript:** do not redeclare Element.setAttribute(NS) ([0620b5e](https://github.com/paldepind/snabbdom/commit/0620b5eda03cd124d4bd743660cb376b0d75a0a3)), closes [#615](https://github.com/paldepind/snabbdom/issues/615)
* do not provide UMD bundles ([8e24bbf](https://github.com/paldepind/snabbdom/commit/8e24bbf016ff5cc0afb2759ec2e4b745921ee453)), closes [#498](https://github.com/paldepind/snabbdom/issues/498) [#514](https://github.com/paldepind/snabbdom/issues/514) [#481](https://github.com/paldepind/snabbdom/issues/481)
* only esm and correct import paths ([dad44f0](https://github.com/paldepind/snabbdom/commit/dad44f0d632d344ca13ee8430d941c26a53d5c2a)), closes [#516](https://github.com/paldepind/snabbdom/issues/516) [#437](https://github.com/paldepind/snabbdom/issues/437) [#263](https://github.com/paldepind/snabbdom/issues/263)
- **docs:** enable eslint rule array-bracket-spacing ([77e54e9](https://github.com/paldepind/snabbdom/commit/77e54e9105394d4e6d21647a38adfb80ea567ee2))
- **docs:** enable eslint rule import/first ([17cf7ae](https://github.com/paldepind/snabbdom/commit/17cf7ae931185d8ab1ac1e4f8b7042677e03db8d))
- **docs:** enable eslint rule import/newline-after-import ([cd3a5cf](https://github.com/paldepind/snabbdom/commit/cd3a5cf17ee33c738d17adcb78a8c254e96653b9))
- **docs:** enable eslint rule indent ([e2861bb](https://github.com/paldepind/snabbdom/commit/e2861bb1bd68c63c99c30907bb331c21f27cb248))
- **docs:** enable eslint rule key-spacing ([349b686](https://github.com/paldepind/snabbdom/commit/349b686bd8cfc24a2845fbab62081a15e5b42d0f))
- **docs:** enable eslint rule max-statements-per-line ([a128a23](https://github.com/paldepind/snabbdom/commit/a128a23ac3182677d10d1979213c92dcf04294ea))
- **docs:** enable eslint rule no-multi-spaces ([8179381](https://github.com/paldepind/snabbdom/commit/8179381a775acea73d0064c77d643944cf684947)), closes [#692](https://github.com/paldepind/snabbdom/issues/692)
- **docs:** enable eslint rule object-curly-spacing ([8b8fbd5](https://github.com/paldepind/snabbdom/commit/8b8fbd5e34fdc3a99ce67635cc44f5ef9e3cf30c))
- **docs:** enable eslint rule quote-props ([37512fe](https://github.com/paldepind/snabbdom/commit/37512fe8ee02cf374cf3d9a62c0f6e3203be2f28))
- **docs:** enable eslint rule quotes ([2d455b5](https://github.com/paldepind/snabbdom/commit/2d455b52dcabc2f8e52082536e635a66e98eb650))
- **docs:** enable eslint rule semi ([f4e7885](https://github.com/paldepind/snabbdom/commit/f4e7885663e645aeb728470d5e1f159365f94f1b))
- **docs:** enable eslint rule space-before-blocks ([9f2d2d7](https://github.com/paldepind/snabbdom/commit/9f2d2d7a1687c7169dff775724a811e1dd7ccd8b))
- **docs:** enable eslint rule space-before-function-paren ([23e7b87](https://github.com/paldepind/snabbdom/commit/23e7b87c64b5d587acae5ce7bd899ec0d071958e))
- **docs:** enable eslint rules object-\*-newline ([9a45b5b](https://github.com/paldepind/snabbdom/commit/9a45b5b22aba0fbed97431e99268a173995de4a1))
- **docs:** fix wrong module import paths ([3b6baee](https://github.com/paldepind/snabbdom/commit/3b6baee049f44cbc55cc2b0131c2c551e9c1b452)), closes [#691](https://github.com/paldepind/snabbdom/issues/691)
- **docs:** provide a release changelog ([616df35](https://github.com/paldepind/snabbdom/commit/616df35909f1d639d562418ea32122625104b00c)), closes [#670](https://github.com/paldepind/snabbdom/issues/670)
- **exports:** main export provided ([3becd84](https://github.com/paldepind/snabbdom/commit/3becd84cc1dcfb84e2ee292eab18aae36e415040)), closes [#682](https://github.com/paldepind/snabbdom/issues/682)
- **exports:** only named exports ([fefd141](https://github.com/paldepind/snabbdom/commit/fefd141f5f3567bb6dccbd6c43ce81f285f985bd)), closes [#522](https://github.com/paldepind/snabbdom/issues/522) [#523](https://github.com/paldepind/snabbdom/issues/523)
- **exports:** relative values in exports field ([187088e](https://github.com/paldepind/snabbdom/commit/187088ee0ebfaed2e84a992bdde50d207131ec29)), closes [#674](https://github.com/paldepind/snabbdom/issues/674)
- **exports:** remove package.json main field ([3122eec](https://github.com/paldepind/snabbdom/commit/3122eec9b98ffdf52fd31ceb2ced17c219e25042)), closes [#680](https://github.com/paldepind/snabbdom/issues/680)
- **exports:** remove the /snabbdom.bundle path ([c862993](https://github.com/paldepind/snabbdom/commit/c8629933599b3fdf3ea774f3ce67517908b79d8d))
- **exports:** replaced main export path with init ([09f2d1c](https://github.com/paldepind/snabbdom/commit/09f2d1ca5a16fd0b402209d90e58b97998efacef)), closes [#522](https://github.com/paldepind/snabbdom/issues/522)
- **package:** no module field ([2b30e25](https://github.com/paldepind/snabbdom/commit/2b30e25f0d261d2d7f37127bda0c235b0a5acf57)), closes [#681](https://github.com/paldepind/snabbdom/issues/681)
- **props:** do not attempt to delete node properties ([6f316c1](https://github.com/paldepind/snabbdom/commit/6f316c141b43ccb1c2c355ab8d0c499984154ef1)), closes [#623](https://github.com/paldepind/snabbdom/issues/623) [#283](https://github.com/paldepind/snabbdom/issues/283) [#415](https://github.com/paldepind/snabbdom/issues/415) [#307](https://github.com/paldepind/snabbdom/issues/307) [#151](https://github.com/paldepind/snabbdom/issues/151) [#416](https://github.com/paldepind/snabbdom/issues/416)
- **typescript:** do not redeclare Element.setAttribute(NS) ([0620b5e](https://github.com/paldepind/snabbdom/commit/0620b5eda03cd124d4bd743660cb376b0d75a0a3)), closes [#615](https://github.com/paldepind/snabbdom/issues/615)
- do not provide UMD bundles ([8e24bbf](https://github.com/paldepind/snabbdom/commit/8e24bbf016ff5cc0afb2759ec2e4b745921ee453)), closes [#498](https://github.com/paldepind/snabbdom/issues/498) [#514](https://github.com/paldepind/snabbdom/issues/514) [#481](https://github.com/paldepind/snabbdom/issues/481)
- only esm and correct import paths ([dad44f0](https://github.com/paldepind/snabbdom/commit/dad44f0d632d344ca13ee8430d941c26a53d5c2a)), closes [#516](https://github.com/paldepind/snabbdom/issues/516) [#437](https://github.com/paldepind/snabbdom/issues/437) [#263](https://github.com/paldepind/snabbdom/issues/263)
## [v0.7.2] - 2018-09-02
@ -121,7 +114,7 @@ extensions.
- Improvements to TypeScript types #364. Thanks to @gfmio.
- In some cases and browsers the style module would cause elements to not be removed correctly #367. Thanks to @jvanbruegge for fixing this tricky bug.
## [v0.7.0] - 2017-07-27
## Breaking change
@ -144,50 +137,52 @@ The example above will result in the HTML: `<div foo bar="baz" />`. Even if `bar
Previously `h("input", { attrs: { required: 0 } })` would result in the HTML `<input>` since `required` was a know boolean attribute and `0` is falsey. Per the new behavior the HTML will be `<input required="0">`. To accomidate for the change always give boolean values for boolean attributes.
## Bugfixes
- `toVNode` now handles `DocumentFragment` which makes it possible to patch into a fragment. #320. Thanks to @staltz.
- Custom boolean attributes are handled correctly. #314. Thanks to @caridy.
- Type improvement. `VNode` key property can be `undefined` #290. Thanks to @yarom82.
- Type improvement. `VNode` key property can be `undefined` #290. Thanks to @yarom82.
- Data attributes are checked for existence before deleting. Old behavior caused error in Safari. #313. Thanks to @FeliciousX.
## Performance improvements
- New handling of boolean attributes. #314. Thanks to @caridy.
## [v0.6.9] - 2017-05-19
## Bug fixes
- Fix style delayed and remove to be optional in TypeScript, https://github.com/snabbdom/snabbdom/issues/295
## [v0.6.8] - 2017-05-16
## Bug fixes
- Fix error when class is set by vdom selector in SVG, https://github.com/snabbdom/snabbdom/issues/217. Thanks to @caesarsol
- Fix hyperscript to support undefined or null children in TypeScript, https://github.com/snabbdom/snabbdom/issues/226. Thanks to @ornicar
- Fix thunk function so it is not called redundantly, https://github.com/snabbdom/snabbdom/pull/273. Thanks to @caesarsol
- Improve TypeScript types of VNode props, https://github.com/snabbdom/snabbdom/issues/264 and https://github.com/snabbdom/snabbdom/issues/264. Thanks to @mightyiam
- Fix error when class is set by vdom selector in SVG, https://github.com/snabbdom/snabbdom/issues/217. Thanks to @caesarsol
- Fix hyperscript to support undefined or null children in TypeScript, https://github.com/snabbdom/snabbdom/issues/226. Thanks to @ornicar
- Fix thunk function so it is not called redundantly, https://github.com/snabbdom/snabbdom/pull/273. Thanks to @caesarsol
- Improve TypeScript types of VNode props, https://github.com/snabbdom/snabbdom/issues/264 and https://github.com/snabbdom/snabbdom/issues/264. Thanks to @mightyiam
- Fix toVNode() for comment nodes, lacking some fields, https://github.com/snabbdom/snabbdom/pull/266. Thanks to @staltz
## Performance improvements
- Improvement for attribute patching, https://github.com/snabbdom/snabbdom/issues/257. Thanks to @diervo
- Improvement for attribute patching, https://github.com/snabbdom/snabbdom/issues/257. Thanks to @diervo
## [v0.6.6] - 2017-03-07
## Bug fixes
- The attributes module sets boolean attributes correctly according to the specificaiton. https://github.com/snabbdom/snabbdom/issues/254. Thanks to @PerWiklander for reporting the bug.
## [v0.6.5] - 2017-02-25
This is a patch version with a few bug fixes.
## Bug fixes
- Fix `toVNode()` to handle text nodes correctly, https://github.com/snabbdom/snabbdom/issues/252. Thanks to @Steelfish
- Fix `toVNode()` to handle text nodes correctly, https://github.com/snabbdom/snabbdom/issues/252. Thanks to @Steelfish
- Fix dataset module to support old browsers, such as IE10. Thanks @staltz
- Fix "create element" workflow to align with "update element" workflow, https://github.com/snabbdom/snabbdom/pull/234. Thanks @caridy
- Fix "create element" workflow to align with "update element" workflow, https://github.com/snabbdom/snabbdom/pull/234. Thanks @caridy
## [v0.6.4] - 2017-02-09
This version adds some features such as support for comment nodes and better server-side/client-side rendering collaboration, besides some bug fixes.
@ -198,13 +193,13 @@ This version adds some features such as support for comment nodes and better ser
Example:
``` js
h('!', 'Will show as a comment')
```js
h("!", "Will show as a comment");
```
will be rendered on the DOM as
``` html
```html
<!-- Will show as a comment -->
```
@ -214,31 +209,31 @@ Useful for client-side rendering over existing HTML that was rendered server-sid
Example:
``` js
import {toVNode} from 'snabbdom/tovnode'
```js
import { toVNode } from "snabbdom/tovnode";
// ...
patch(toVNode(element), vnode)
patch(toVNode(element), vnode);
```
Will deep-convert the `element` to a VNode, this way allowing existing HTML to not be ignored by the patch process.
## Bug fixes
- Fix compatibility issue of String.prototype.startsWith in the Style Module. https://github.com/snabbdom/snabbdom/pull/228 Thanks to @zhulongzheng
- Support for `null`/`undefined` children without crashing. https://github.com/snabbdom/snabbdom/issues/226 Thanks to @nunocastromartins
- Fix compatibility issue of String.prototype.startsWith in the Style Module. https://github.com/snabbdom/snabbdom/pull/228 Thanks to @zhulongzheng
- Support for `null`/`undefined` children without crashing. https://github.com/snabbdom/snabbdom/issues/226 Thanks to @nunocastromartins
## [v0.6.3] - 2017-01-16
## Bugfixes
- Fix the export of the `Module` interface for TypeScript projects depending on snabbdom.
- Fix the export of the `Module` interface for TypeScript projects depending on snabbdom.
## [v0.6.2] - 2017-01-16
## Bugfixes
- Fix the export of the `Hooks` interface for TypeScript projects depending on snabbdom.
- Fix the export of the `Hooks` interface for TypeScript projects depending on snabbdom.
## [v0.6.1] - 2017-01-05
The biggest change in this release is that the Snabbdom source code has been ported to TypeScript. The work has been primarily done by @staltz. This brings much improved support for using Snabbdom in TypeScript projects.
@ -246,11 +241,13 @@ The biggest change in this release is that the Snabbdom source code has been por
**Note**: This release contains breaking changes. See below.
## New features
- Complete TypeScript support. Thanks to @staltz.
- Support for CSS variables. #195. Thanks to @jlesquembre.
- Allow `h(sel, data, node)` and `h(sel, node)` shortcut notations in the `h` function. #196. That is, instead of `h('div', [child])` one can now do `h('div', child)`. Thanks to @AlexGalays.
## Bugfixes
- Fix custom element creation when tag name begins with 'svg'. #213. Thanks to @tdumitrescu.
- Fix bug related to updating one child with same key but different selector. #188. Thanks to @zhulongzheng.
- Strings can be used as children inside SVG elements. #208. Thanks to @jbucaran and @jbucaran.
@ -260,18 +257,16 @@ The biggest change in this release is that the Snabbdom source code has been por
The TypeScript rewrite uses the `import` and `export` features introduced in ECMAScript 2015. Unfortunately the ES imports have no analogy to the CommonJS pattern of setting `module.exports`. This means that the Snabbdom modules that previously used this feature now have to be imported in a slightly different way.
``` js
```js
var h = require("snabbdom/h"); // The old way
var h = require("snabbdom/h").h; // The new way
var h = require("snabbdom/h").default; // Alternative new way
var {h} = require("snabbdom/h"); // Using destructuring
var { h } = require("snabbdom/h"); // Using destructuring
```
## [v0.6.0] - 2017-01-05
Deprecated. Use [version 0.6.1](https://github.com/snabbdom/snabbdom/releases/tag/v0.6.1) instead.
Deprecated. Use [version 0.6.1](https://github.com/snabbdom/snabbdom/releases/tag/v0.6.1) instead.
## v0.5.0 - 2016-05-16
@ -279,8 +274,7 @@ Deprecated. Use [version 0.6.1](https://github.com/snabbdom/snabbdom/releases/ta
This release contains a new thunk implementation that solves many issues with the old thunk implementation. The thunk API has changed slightly. Please see the [thunks](https://github.com/paldepind/snabbdom#thunks) section in the readme.
[Unreleased]: https://github.com/snabbdom/snabbdom/compare/v0.7.2...HEAD
[unreleased]: https://github.com/snabbdom/snabbdom/compare/v0.7.2...HEAD
[v0.7.2]: https://github.com/snabbdom/snabbdom/compare/v0.7.0...v0.7.2
[v0.7.0]: https://github.com/snabbdom/snabbdom/compare/v0.6.9...v0.7.0
[v0.6.9]: https://github.com/snabbdom/snabbdom/compare/v0.6.8...v0.6.9

@ -2,32 +2,14 @@
## Making a release
Make sure you have permission to publish, by running
You will need a personal GitHub API token (this is used to create the release on GitHub). You can obtain one [here](https://github.com/settings/tokens/new?scopes=repo&description=release-it) (it only needs "repo" access, not "admin" or other scopes).
npm access ls-collaborators
Make sure the token is available with an environment variable. It's best to put this in `~/.profile`:
While on the `master` branch, switch to a new branch, possibly called `release`:
```sh
export GITHUB_TOKEN="<token from step 1>"
```
git switch --create release
npm run make-release-commit
If you saved the token to `~/.profile` you will not have to repeat this in the future.
Create a new pull request from this branch. The name of the pull request possibly identical to the commit message.
"Rebase and merge" the pull request.
git switch master
git pull
Where `$VERSION` is the new version, run
git tag v$VERSION
For example:
git tag v5.2.4
And then
git push --tags
npm compile
npm publish
Then simply run `npm run release`. This will automatically determine the new version number based on the commit messages, create the commit, tag, github release and publish to npm.

@ -3,7 +3,7 @@
A virtual DOM library with focus on simplicity, modularity, powerful features
and performance.
* * *
---
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
[![Build Status](https://travis-ci.org/snabbdom/snabbdom.svg?branch=master)](https://travis-ci.org/snabbdom/snabbdom)
@ -16,7 +16,7 @@ and performance.
Thanks to [Browserstack](https://www.browserstack.com/) for providing access to
their great cross-browser testing tools.
* * *
---
## Introduction
@ -37,112 +37,126 @@ performance, small size and all the features listed below.
## Features
* Core features
* About 200 SLOC you could easily read through the entire core and fully
- Core features
- About 200 SLOC you could easily read through the entire core and fully
understand how it works.
* Extendable through modules.
* A rich set of hooks available, both per vnode and globally for modules,
- Extendable through modules.
- A rich set of hooks available, both per vnode and globally for modules,
to hook into any part of the diff and patch process.
* Splendid performance. Snabbdom is among the fastest virtual DOM libraries.
* Patch function with a function signature equivalent to a reduce/scan
function. Allows for easier integration with a FRP library.
* Features in modules
* `h` function for easily creating virtual DOM nodes.
* [SVG _just works_ with the `h` helper](#svg).
* Features for doing complex CSS animations.
* Powerful event listener functionality.
* [Thunks](#thunks) to optimize the diff and patch process even further.
* Third party features
* JSX support thanks to [snabbdom-pragma](https://github.com/Swizz/snabbdom-pragma).
* Server-side HTML output provided by [snabbdom-to-html](https://github.com/acstll/snabbdom-to-html).
* Compact virtual DOM creation with [snabbdom-helpers](https://github.com/krainboltgreene/snabbdom-helpers).
* Template string support using [snabby](https://github.com/jamen/snabby).
* Virtual DOM assertion with [snabbdom-looks-like](https://github.com/jvanbruegge/snabbdom-looks-like)
- Splendid performance. Snabbdom is among the fastest virtual DOM libraries.
- Patch function with a function signature equivalent to a reduce/scan
function. Allows for easier integration with a FRP library.
- Features in modules
- `h` function for easily creating virtual DOM nodes.
- [SVG _just works_ with the `h` helper](#svg).
- Features for doing complex CSS animations.
- Powerful event listener functionality.
- [Thunks](#thunks) to optimize the diff and patch process even further.
- [JSX support, including TypeScript types](#jsx)
- Third party features
- Server-side HTML output provided by [snabbdom-to-html](https://github.com/acstll/snabbdom-to-html).
- Compact virtual DOM creation with [snabbdom-helpers](https://github.com/krainboltgreene/snabbdom-helpers).
- Template string support using [snabby](https://github.com/jamen/snabby).
- Virtual DOM assertion with [snabbdom-looks-like](https://github.com/jvanbruegge/snabbdom-looks-like)
## Example
```mjs
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h' // helper function for creating vnodes
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([ // Init patch function with chosen modules
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
])
]);
const container = document.getElementById('container')
const container = document.getElementById("container");
const vnode = h('div#container.two.classes', { on: { click: someFn } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
' and this is just normal text',
h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element this modifies the DOM as a side effect
patch(container, vnode)
const newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
h('span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type'),
' and this is still just normal text',
h('a', { props: { href: '/bar' } }, 'I\'ll take you places!')
])
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode) // Snabbdom efficiently updates the old view to the new state
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
```
## More examples
* [Animated reordering of elements](http://snabbdom.github.io/snabbdom/examples/reorder-animation/)
* [Hero transitions](http://snabbdom.github.io/snabbdom/examples/hero/)
* [SVG Carousel](http://snabbdom.github.io/snabbdom/examples/carousel-svg/)
- [Animated reordering of elements](http://snabbdom.github.io/snabbdom/examples/reorder-animation/)
- [Hero transitions](http://snabbdom.github.io/snabbdom/examples/hero/)
- [SVG Carousel](http://snabbdom.github.io/snabbdom/examples/carousel-svg/)
* * *
---
## Table of contents
* [Core documentation](#core-documentation)
* [`init`](#init)
* [`patch`](#patch)
* [Unmounting](#unmounting)
* [`snabbdom/h`](#snabbdomh)
* [`snabbdom/tovnode`](#snabbdomtovnode)
* [Hooks](#hooks)
* [Overview](#overview)
* [Usage](#usage)
* [The `init` hook](#the-init-hook)
* [The `insert` hook](#the-insert-hook)
* [The `remove` hook](#the-remove-hook)
* [The `destroy` hook](#the-destroy-hook)
* [Creating modules](#creating-modules)
* [Modules documentation](#modules-documentation)
* [The class module](#the-class-module)
* [The props module](#the-props-module)
* [The attributes module](#the-attributes-module)
* [The dataset module](#the-dataset-module)
* [The style module](#the-style-module)
* [Custom properties (CSS variables)](#custom-properties-css-variables)
* [Delayed properties](#delayed-properties)
* [Set properties on `remove`](#set-properties-on-remove)
* [Set properties on `destroy`](#set-properties-on-destroy)
* [The eventlisteners module](#the-eventlisteners-module)
* [SVG](#svg)
* [Classes in SVG Elements](#classes-in-svg-elements)
* [Thunks](#thunks)
* [Virtual Node](#virtual-node)
* [sel : String](#sel--string)
* [data : Object](#data--object)
* [children : Array<vnode>](#children--arrayvnode)
* [text : string](#text--string)
* [elm : Element](#elm--element)
* [key : string | number](#key--string--number)
* [Structuring applications](#structuring-applications)
* [Common errors](#common-errors)
* [Opportunity for community feedback](#opportunity-for-community-feedback)
- [Core documentation](#core-documentation)
- [`init`](#init)
- [`patch`](#patch)
- [Unmounting](#unmounting)
- [`h`](#h)
- [`tovnode`](#tovnode)
- [Hooks](#hooks)
- [Overview](#overview)
- [Usage](#usage)
- [The `init` hook](#the-init-hook)
- [The `insert` hook](#the-insert-hook)
- [The `remove` hook](#the-remove-hook)
- [The `destroy` hook](#the-destroy-hook)
- [Creating modules](#creating-modules)
- [Modules documentation](#modules-documentation)
- [The class module](#the-class-module)
- [The props module](#the-props-module)
- [The attributes module](#the-attributes-module)
- [The dataset module](#the-dataset-module)
- [The style module](#the-style-module)
- [Custom properties (CSS variables)](#custom-properties-css-variables)
- [Delayed properties](#delayed-properties)
- [Set properties on `remove`](#set-properties-on-remove)
- [Set properties on `destroy`](#set-properties-on-destroy)
- [The eventlisteners module](#the-eventlisteners-module)
- [SVG](#svg)
- [Classes in SVG Elements](#classes-in-svg-elements)
- [Thunks](#thunks)
- [JSX](#jsx)
- [TypeScript](#typescript)
- [Babel](#babel)
- [Virtual Node](#virtual-node)
- [sel : String](#sel--string)
- [data : Object](#data--object)
- [children : Array<vnode>](#children--arrayvnode)
- [text : string](#text--string)
- [elm : Element](#elm--element)
- [key : string | number](#key--string--number)
- [Structuring applications](#structuring-applications)
- [Common errors](#common-errors)
- [Opportunity for community feedback](#opportunity-for-community-feedback)
## Core documentation
@ -157,10 +171,9 @@ takes a list of modules and returns a `patch` function that uses the
specified set of modules.
```mjs
import { classModule } from 'snabbdom/modules/class'
import { styleModule } from 'snabbdom/modules/style'
import { classModule, styleModule } from "snabbdom";
const patch = init([classModule, styleModule])
const patch = init([classModule, styleModule]);
```
### `patch`
@ -181,7 +194,7 @@ performant architecture. This also avoids the creation of a new old
vnode tree.
```mjs
patch(oldVnode, newVnode)
patch(oldVnode, newVnode);
```
#### Unmounting
@ -189,53 +202,65 @@ patch(oldVnode, newVnode)
While there is no API specifically for removing a VNode tree from its mount point element, one way of almost achieving this is providing a comment VNode as the second argument to `patch`, such as:
```mjs
patch(oldVnode, h('!', { hooks: { post: () => { /* patch complete */ } } }))
patch(
oldVnode,
h("!", {
hooks: {
post: () => {
/* patch complete */
},
},
})
);
```
Of course, then there is still a single comment node at the mount point.
### `snabbdom/h`
### `h`
It is recommended that you use `snabbdom/h` to create vnodes. `h` accepts a
It is recommended that you use `h` to create vnodes. It accepts a
tag/selector as a string, an optional data object and an optional string or
array of children.
```mjs
import { h } from 'snabbdom/h'
import { h } from "snabbdom";
const vnode = h('div', { style: { color: '#000' } }, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
])
const vnode = h("div", { style: { color: "#000" } }, [
h("h1", "Headline"),
h("p", "A paragraph"),
]);
```
### `snabbdom/tovnode`
### `tovnode`
Converts a DOM node into a virtual node. Especially good for patching over an pre-existing,
Converts a DOM node into a virtual node. Especially good for patching over an pre-existing,
server-side generated content.
```mjs
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h' // helper function for creating vnodes
import { toVNode } from 'snabbdom/tovnode'
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
toVNode,
} from "snabbdom";
const patch = init([ // Init patch function with chosen modules
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
])
]);
const newVNode = h('div', { style: { color: '#000' } }, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
])
const newVNode = h("div", { style: { color: "#000" } }, [
h("h1", "Headline"),
h("p", "A paragraph"),
]);
patch(toVNode(document.querySelector('.container')), newVNode)
patch(toVNode(document.querySelector(".container")), newVNode);
```
### Hooks
@ -247,18 +272,18 @@ desired points in the life of a virtual node.
#### Overview
| Name | Triggered when | Arguments to callback |
| - | - | - |
| `pre` | the patch process begins | none |
| `init` | a vnode has been added | `vnode` |
| `create` | a DOM element has been created based on a vnode | `emptyVnode, vnode` |
| `insert` | an element has been inserted into the DOM | `vnode` |
| `prepatch` | an element is about to be patched | `oldVnode, vnode` |
| `update` | an element is being updated | `oldVnode, vnode` |
| `postpatch` | an element has been patched | `oldVnode, vnode` |
| `destroy` | an element is directly or indirectly being removed | `vnode` |
| `remove` | an element is directly being removed from the DOM | `vnode, removeCallback` |
| `post` | the patch process is done | none |
| Name | Triggered when | Arguments to callback |
| ----------- | -------------------------------------------------- | ----------------------- |
| `pre` | the patch process begins | none |
| `init` | a vnode has been added | `vnode` |
| `create` | a DOM element has been created based on a vnode | `emptyVnode, vnode` |
| `insert` | an element has been inserted into the DOM | `vnode` |
| `prepatch` | an element is about to be patched | `oldVnode, vnode` |
| `update` | an element is being updated | `oldVnode, vnode` |
| `postpatch` | an element has been patched | `oldVnode, vnode` |
| `destroy` | an element is directly or indirectly being removed | `vnode` |
| `remove` | an element is directly being removed from the DOM | `vnode, removeCallback` |
| `post` | the patch process is done | none |
The following hooks are available for modules: `pre`, `create`,
`update`, `destroy`, `remove`, `post`.
@ -273,12 +298,14 @@ To use hooks, pass them as an object to `hook` field of the data
object argument.
```mjs
h('div.row', {
h("div.row", {
key: movie.rank,
hook: {
insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight }
}
})
insert: (vnode) => {
movie.elmHeight = vnode.elm.offsetHeight;
},
},
});
```
#### The `init` hook
@ -319,10 +346,10 @@ To see the difference between this hook and the `remove` hook,
consider an example.
```mjs
const vnode1 = h('div', [h('div', [h('span', 'Hello')])])
const vnode2 = h('div', [])
patch(container, vnode1)
patch(vnode1, vnode2)
const vnode1 = h("div", [h("div", [h("span", "Hello")])]);
const vnode2 = h("div", []);
patch(container, vnode1);
patch(vnode1, vnode2);
```
Here `destroy` is triggered for both the inner `div` element _and_ the
@ -345,8 +372,8 @@ const myModule = {
},
update: function (oldVnode, vnode) {
// invoked whenever a virtual node is updated
}
}
},
};
```
With this mechanism you can easily augment the behaviour of Snabbdom.
@ -365,7 +392,7 @@ object should map class names to booleans that indicates whether or
not the class should stay or go on the vnode.
```mjs
h('a', { class: { active: true, selected: false } }, 'Toggle')
h("a", { class: { active: true, selected: false } }, "Toggle");
```
### The props module
@ -373,7 +400,7 @@ h('a', { class: { active: true, selected: false } }, 'Toggle')
Allows you to set properties on DOM elements.
```mjs
h('a', { props: { href: '/foo' } }, 'Go to Foo')
h("a", { props: { href: "/foo" } }, "Go to Foo");
```
Properties can only be set. Not removed. Even though browsers allow addition and
@ -389,7 +416,7 @@ instead. Perhaps via [the dataset module](#the-dataset-module).
Same as props, but set attributes instead of properties on DOM elements.
```mjs
h('a', { attrs: { href: '/foo' } }, 'Go to Foo')
h("a", { attrs: { href: "/foo" } }, "Go to Foo");
```
Attributes are added and updated using `setAttribute`. In case of an
@ -412,7 +439,7 @@ the DOM element.
Allows you to set custom data attributes (`data-*`) on DOM elements. These can then be accessed with the [HTMLElement.dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) property.
```mjs
h('button', { dataset: { action: 'reset' } }, 'Reset')
h("button", { dataset: { action: "reset" } }, "Reset");
```
### The style module
@ -421,9 +448,17 @@ The style module is for making your HTML look slick and animate smoothly. At
its core it allows you to set CSS properties on elements.
```mjs
h('span', {
style: { border: '1px solid #bada55', color: '#c0ffee', fontWeight: 'bold' }
}, 'Say my name, and every colour illuminates')
h(
"span",
{
style: {
border: "1px solid #bada55",
color: "#c0ffee",
fontWeight: "bold",
},
},
"Say my name, and every colour illuminates"
);
```
Note that the style module does not remove style attributes if they
@ -431,9 +466,13 @@ are removed as properties from the style object. To remove a style,
you should instead set it to the empty string.
```mjs
h('div', {
style: { position: shouldFollow ? 'fixed' : '' }
}, 'I, I follow, I follow you')
h(
"div",
{
style: { position: shouldFollow ? "fixed" : "" },
},
"I, I follow, I follow you"
);
```
#### Custom properties (CSS variables)
@ -442,9 +481,13 @@ CSS custom properties (aka CSS variables) are supported, they must be prefixed
with `--`
```mjs
h('div', {
style: { '--warnColor': 'yellow' }
}, 'Warning')
h(
"div",
{
style: { "--warnColor": "yellow" },
},
"Warning"
);
```
#### Delayed properties
@ -453,9 +496,17 @@ You can specify properties as being delayed. Whenever these properties
change, the change is not applied until after the next frame.
```mjs
h('span', {
style: { opacity: '0', transition: 'opacity 1s', delayed: { opacity: '1' } }
}, 'Imma fade right in!')
h(
"span",
{
style: {
opacity: "0",
transition: "opacity 1s",
delayed: { opacity: "1" },
},
},
"Imma fade right in!"
);
```
This makes it easy to declaratively animate the entry of elements.
@ -470,13 +521,17 @@ animated with CSS transitions. Only once all the styles are done
animating will the element be removed from the DOM.
```mjs
h('span', {
style: {
opacity: '1',
transition: 'opacity 1s',
remove: { opacity: '0' }
}
}, 'It\'s better to fade out than to burn away')
h(
"span",
{
style: {
opacity: "1",
transition: "opacity 1s",
remove: { opacity: "0" },
},
},
"It's better to fade out than to burn away"
);
```
This makes it easy to declaratively animate the removal of elements.
@ -486,13 +541,17 @@ The `all` value of `transition-property` is not supported.
#### Set properties on `destroy`
```mjs
h('span', {
style: {
opacity: '1',
transition: 'opacity 1s',
destroy: { opacity: '0' }
}
}, 'It\'s better to fade out than to burn away')
h(
"span",
{
style: {
opacity: "1",
transition: "opacity 1s",
destroy: { opacity: "0" },
},
},
"It's better to fade out than to burn away"
);
```
The `all` value of `transition-property` is not supported.
@ -508,10 +567,10 @@ you want to listen to. The function will be called when the event
happens and will be passed the event object that belongs to it.
```mjs
function clickHandler (ev) {
console.log('got clicked')
function clickHandler(ev) {
console.log("got clicked");
}
h('div', { on: { click: clickHandler } })
h("div", { on: { click: clickHandler } });
```
Very often, however, you're not really interested in the event object
@ -528,27 +587,27 @@ first element in the array should be a function that will be invoked
with the value in the second element once the event occurs.
```mjs
function clickHandler (number) {
console.log('button ' + number + ' was clicked!')
function clickHandler(number) {
console.log("button " + number + " was clicked!");
}
h('div', [
h('a', { on: { click: [clickHandler, 1] } }),
h('a', { on: { click: [clickHandler, 2] } }),
h('a', { on: { click: [clickHandler, 3] } }),
])
h("div", [
h("a", { on: { click: [clickHandler, 1] } }),
h("a", { on: { click: [clickHandler, 2] } }),
h("a", { on: { click: [clickHandler, 3] } }),
]);
```
Each handler is called not only with the given arguments but also with the current event and vnode appended to the argument list. It also supports using multiple listeners per event by specifying an array of handlers:
```mjs
stopPropagation = function (ev) {
ev.stopPropagation()
}
ev.stopPropagation();
};
sendValue = function (func, ev, vnode) {
func(vnode.elm.value)
}
func(vnode.elm.value);
};
h('a', { on: { click: [[sendValue, console.log], stopPropagation] } })
h("a", { on: { click: [[sendValue, console.log], stopPropagation] } });
```
Snabbdom allows swapping event handlers between renders. This happens without
@ -565,22 +624,24 @@ In particular, you should **not** do something like this:
```mjs
// Does not work
const sharedHandler = {
change: function (e) { console.log('you chose: ' + e.target.value) }
}
h('div', [
h('input', {
props: { type: 'radio', name: 'test', value: '0' },
on: sharedHandler
change: function (e) {
console.log("you chose: " + e.target.value);
},
};
h("div", [
h("input", {
props: { type: "radio", name: "test", value: "0" },
on: sharedHandler,
}),
h('input', {
props: { type: 'radio', name: 'test', value: '1' },
on: sharedHandler
h("input", {
props: { type: "radio", name: "test", value: "1" },
on: sharedHandler,
}),
h('input', {
props: { type: 'radio', name: 'test', value: '2' },
on: sharedHandler
})
])
h("input", {
props: { type: "radio", name: "test", value: "2" },
on: sharedHandler,
}),
]);
```
For many such cases, you can use array-based handlers instead (described above).
@ -589,22 +650,22 @@ Alternatively, simply make sure each node is passed unique `on` values:
```mjs
// Works
const sharedHandler = function (e) {
console.log('you chose: ' + e.target.value)
}
h('div', [
h('input', {
props: { type: 'radio', name: 'test', value: '0' },
on: { change: sharedHandler }
console.log("you chose: " + e.target.value);
};
h("div", [
h("input", {
props: { type: "radio", name: "test", value: "0" },
on: { change: sharedHandler },
}),
h('input', {
props: { type: 'radio', name: 'test', value: '1' },
on: { change: sharedHandler }
h("input", {
props: { type: "radio", name: "test", value: "1" },
on: { change: sharedHandler },
}),
h('input', {
props: { type: 'radio', name: 'test', value: '2' },
on: { change: sharedHandler }
})
])
h("input", {
props: { type: "radio", name: "test", value: "2" },
on: { change: sharedHandler },
}),
]);
```
## SVG
@ -614,11 +675,20 @@ nodes. SVG elements are automatically created with the appropriate
namespaces.
```mjs
const vnode = h('div', [
h('svg', { attrs: { width: 100, height: 100 } }, [
h('circle', { attrs: { cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow' } })
])
])
const vnode = h("div", [
h("svg", { attrs: { width: 100, height: 100 } }, [
h("circle", {
attrs: {
cx: 50,
cy: 50,
r: 40,
stroke: "green",
"stroke-width": 4,
fill: "yellow",
},
}),
]),
]);
```
See also the [SVG example](./examples/svg) and the [SVG Carousel example](./examples/carousel-svg/).
@ -650,8 +720,8 @@ dealing with immutable data.
Consider a simple function for creating a virtual node based on a number.
```mjs
function numberView (n) {
return h('div', 'Number is: ' + n)
function numberView(n) {
return h("div", "Number is: " + n);
}
```
@ -661,8 +731,8 @@ vnode is wasteful. To avoid the overhead we can use the `thunk` helper
function.
```mjs
function render (state) {
return thunk('num', numberView, [state.number])
function render(state) {
return thunk("num", numberView, [state.number]);
}
```
@ -676,22 +746,77 @@ The view function here is only an example. In practice thunks are only
relevant if you are rendering a complicated view that takes
significant computational time to generate.
## JSX
### TypeScript
Add the following options to your `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "jsx"
}
}
```
Then make sure that you use the `.tsx` file extension and import the `jsx` function at the top of the file:
```tsx
import { jsx, VNode } from "snabbdom";
const node: VNode = (
<div>
<span>I was created with JSX</span>
</div>
);
```
### Babel
Add the following options to your babel configuration:
```json
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "jsx"
}
]
]
}
```
Then make sure that you use the `.jsx` file extension and import the `jsx` function at the top of the file:
```jsx
import { jsx } from "snabbdom";
const node = (
<div>
<span>I was created with JSX</span>
</div>
);
```
## Virtual Node
**Properties**
* [sel](#sel--string)
* [data](#data--object)
* [children](#children--array)
* [text](#text--string)
* [elm](#elm--element)
* [key](#key--string--number)
- [sel](#sel--string)
- [data](#data--object)
- [children](#children--array)
- [text](#text--string)
- [elm](#elm--element)
- [key](#key--string--number)
### sel : String
The `.sel` property of a virtual node is the CSS selector passed to
[`h()`](#snabbdomh) during creation. For example: `h('div#container',
{}, [...])` will create a a virtual node which has `div#container` as
[`h()`](#snabbdomh) during creation. For example: `h('div#container', {}, [...])` will create a a virtual node which has `div#container` as
its `.sel` property.
### data : Object
@ -708,9 +833,9 @@ For example `h('div', {props: {className: 'container'}}, [...])` will produce a
```mjs
({
props: {
className: 'container'
}
})
className: "container",
},
});
```
as its `.data` object.
@ -728,14 +853,14 @@ create a virtual node with
```mjs
[
{
sel: 'h1',
sel: "h1",
data: {},
children: undefined,
text: 'Hello, World',
text: "Hello, World",
elm: Element,
key: undefined,
}
]
},
];
```
as its `.children` property.
@ -779,33 +904,33 @@ regards to how you should structure your application.
Here are some approaches to building applications with Snabbdom.
* [functional-frontend-architecture](https://github.com/paldepind/functional-frontend-architecture)
- [functional-frontend-architecture](https://github.com/paldepind/functional-frontend-architecture)
a repository containing several example applications that
demonstrates an architecture that uses Snabbdom.
* [Cycle.js](https://cycle.js.org/)
"A functional and reactive JavaScript framework for cleaner code"
uses Snabbdom
* [Vue.js](http://vuejs.org/) use a fork of snabbdom.
* [scheme-todomvc](https://github.com/amirouche/scheme-todomvc/) build
redux-like architecture on top of snabbdom bindings.
* [kaiju](https://github.com/AlexGalays/kaiju) -
- [Cycle.js](https://cycle.js.org/)
"A functional and reactive JavaScript framework for cleaner code"
uses Snabbdom
- [Vue.js](http://vuejs.org/) use a fork of snabbdom.
- [scheme-todomvc](https://github.com/amirouche/scheme-todomvc/) build
redux-like architecture on top of snabbdom bindings.
- [kaiju](https://github.com/AlexGalays/kaiju) -
Stateful components and observables on top of snabbdom
* [Tweed](https://tweedjs.github.io)
- [Tweed](https://tweedjs.github.io)
An Object Oriented approach to reactive interfaces.
* [Cyclow](http://cyclow.js.org) -
- [Cyclow](http://cyclow.js.org) -
"A reactive frontend framework for JavaScript"
uses Snabbdom
* [Tung](https://github.com/Reon90/tung)
- [Tung](https://github.com/Reon90/tung)
A JavaScript library for rendering html. Tung helps to divide html and JavaScript development.
* [sprotty](https://github.com/theia-ide/sprotty) - "A web-based diagramming framework" uses Snabbdom.
* [Mark Text](https://github.com/marktext/marktext) - "Realtime preview Markdown Editor" build on Snabbdom.
* [puddles](https://github.com/flintinatux/puddles) -
- [sprotty](https://github.com/theia-ide/sprotty) - "A web-based diagramming framework" uses Snabbdom.
- [Mark Text](https://github.com/marktext/marktext) - "Realtime preview Markdown Editor" build on Snabbdom.
- [puddles](https://github.com/flintinatux/puddles) -
"Tiny vdom app framework. Pure Redux. No boilerplate." - Built with :heart: on Snabbdom.
* [Backbone.VDOMView](https://github.com/jcbrand/backbone.vdomview) - A [Backbone](http://backbonejs.org/) View with VirtualDOM capability via Snabbdom.
* [Rosmaro Snabbdom starter](https://github.com/lukaszmakuch/rosmaro-snabbdom-starter) - Building user interfaces with state machines and Snabbdom.
* [Pureact](https://github.com/irony/pureact) - "65 lines implementation of React incl Redux and hooks with only one dependency - Snabbdom"
* [Snabberb](https://github.com/tobymao/snabberb) - A minimalistic Ruby framework using [Opal](https://github.com/opal/opal) and Snabbdom for building reactive views.
* [WebCell](https://github.com/EasyWebApp/WebCell) - Web Components engine based on JSX & TypeScript
- [Backbone.VDOMView](https://github.com/jcbrand/backbone.vdomview) - A [Backbone](http://backbonejs.org/) View with VirtualDOM capability via Snabbdom.
- [Rosmaro Snabbdom starter](https://github.com/lukaszmakuch/rosmaro-snabbdom-starter) - Building user interfaces with state machines and Snabbdom.
- [Pureact](https://github.com/irony/pureact) - "65 lines implementation of React incl Redux and hooks with only one dependency - Snabbdom"
- [Snabberb](https://github.com/tobymao/snabberb) - A minimalistic Ruby framework using [Opal](https://github.com/opal/opal) and Snabbdom for building reactive views.
- [WebCell](https://github.com/EasyWebApp/WebCell) - Web Components engine based on JSX & TypeScript
Be sure to share it if you're building an application in another way
using Snabbdom.
@ -820,40 +945,40 @@ Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node':
The reason for this error is reusing of vnodes between patches (see code example), snabbdom stores actual dom nodes inside the virtual dom nodes passed to it as performance improvement, so reusing nodes between patches is not supported.
```mjs
const sharedNode = h('div', {}, 'Selected')
const vnode1 = h('div', [
h('div', {}, ['One']),
h('div', {}, ['Two']),
h('div', {}, [sharedNode]),
])
const vnode2 = h('div', [
h('div', {}, ['One']),
h('div', {}, [sharedNode]),
h('div', {}, ['Three']),
])
patch(container, vnode1)
patch(vnode1, vnode2)
const sharedNode = h("div", {}, "Selected");
const vnode1 = h("div", [
h("div", {}, ["One"]),
h("div", {}, ["Two"]),
h("div", {}, [sharedNode]),
]);
const vnode2 = h("div", [
h("div", {}, ["One"]),
h("div", {}, [sharedNode]),
h("div", {}, ["Three"]),
]);
patch(container, vnode1);
patch(vnode1, vnode2);
```
You can fix this issue by creating a shallow copy of the object (here with object spread syntax):
```mjs
const vnode2 = h('div', [
h('div', {}, ['One']),
h('div', {}, [{ ...sharedNode }]),
h('div', {}, ['Three']),
])
const vnode2 = h("div", [
h("div", {}, ["One"]),
h("div", {}, [{ ...sharedNode }]),
h("div", {}, ["Three"]),
]);
```
Another solution would be to wrap shared vnodes in a factory function:
```mjs
const sharedNode = () => h('div', {}, 'Selected')
const vnode1 = h('div', [
h('div', {}, ['One']),
h('div', {}, ['Two']),
h('div', {}, [sharedNode()]),
])
const sharedNode = () => h("div", {}, "Selected");
const vnode1 = h("div", [
h("div", {}, ["One"]),
h("div", {}, ["Two"]),
h("div", {}, [sharedNode()]),
]);
```
## Opportunity for community feedback

@ -1,101 +0,0 @@
module.exports = {
// Latest mainstream
BS_Chrome_Current: {
base: 'BrowserStack',
browser: 'chrome',
browser_version: 'latest',
os: 'Windows',
os_version: '10',
},
BS_Firefox_Current: {
base: 'BrowserStack',
browser: 'firefox',
browser_version: 'latest',
os: 'Windows',
os_version: '10',
},
BS_Safari_Current: {
base: 'BrowserStack',
browser: 'safari',
browser_version: 'latest',
os: 'OS X',
os_version: 'High Sierra',
},
BS_Android_8: {
base: 'BrowserStack',
browser: 'Android',
device: 'Google Pixel 2',
os: 'Android',
os_version: '8.0',
real_mobile: true,
},
// Older mainstream
/* https://github.com/snabbdom/snabbdom/issues/468
BS_Chrome_49: {
base: 'BrowserStack',
browser: 'chrome',
browser_version: '49',
os: 'Windows',
os_version: '10',
},
*/
BS_Firefox_52: {
base: 'BrowserStack',
browser: 'firefox',
browser_version: '52',
os: 'Windows',
os_version: '10',
},
/* https://github.com/snabbdom/snabbdom/issues/469
BS_Safari_9: {
base: 'BrowserStack',
browser: 'safari',
browser_version: '9.1',
os: 'OS X',
os_version: 'El Capitan',
},
*/
// Misc
BS_Android_4_4: {
base: 'BrowserStack',
device_browser: 'ucbrowser',
device: 'Google Nexus 5',
os: 'Android',
os_version: '4.4',
real_mobile: true,
},
/* https://github.com/snabbdom/snabbdom/issues/470
BS_iphone_10: {
base: 'BrowserStack',
browser: 'Mobile Safari',
browser_version: null,
device: 'iPhone 7',
real_mobile: true,
os: 'ios',
os_version: '10.3',
},
*/
BS_MS_Edge: {
base: 'BrowserStack',
browser: 'edge',
browser_version: 'latest',
os: 'Windows',
os_version: '10',
},
BS_IE_11: {
base: 'BrowserStack',
browser: 'ie',
browser_version: '11.0',
os: 'Windows',
os_version: '7',
},
BS_IE_10: {
base: 'BrowserStack',
browser: 'ie',
browser_version: '10.0',
os: 'Windows',
os_version: '7',
},
}

@ -0,0 +1,89 @@
module.exports = {
// Latest mainstream
BS_Chrome_Current: {
base: "BrowserStack",
browser: "chrome",
browser_version: "latest",
os: "Windows",
os_version: "10",
},
BS_Firefox_Current: {
base: "BrowserStack",
browser: "firefox",
browser_version: "latest",
os: "Windows",
os_version: "10",
},
BS_Safari_Current: {
base: "BrowserStack",
browser: "safari",
browser_version: "latest",
os: "OS X",
os_version: "Big Sur",
},
BS_Android_8: {
base: "BrowserStack",
browser: "Android",
device: "Google Pixel 2",
os: "Android",
os_version: "8.0",
real_mobile: true,
},
// Older mainstream
BS_Chrome_50: {
base: "BrowserStack",
browser: "chrome",
browser_version: "50",
os: "Windows",
os_version: "10",
},
BS_Firefox_52: {
base: "BrowserStack",
browser: "firefox",
browser_version: "52",
os: "Windows",
os_version: "10",
},
BS_Safari_10: {
base: "BrowserStack",
browser: "safari",
browser_version: "10.1",
os: "OS X",
os_version: "Sierra",
},
// Misc
BS_Android_4_4: {
base: "BrowserStack",
device_browser: "ucbrowser",
device: "Google Nexus 5",
os: "Android",
os_version: "4.4",
real_mobile: true,
},
BS_iphone_10: {
base: "BrowserStack",
browser: "Mobile Safari",
browser_version: null,
device: "iPhone 7",
real_mobile: true,
os: "ios",
os_version: "10.3",
},
BS_MS_Edge: {
base: "BrowserStack",
browser: "edge",
browser_version: "latest",
os: "Windows",
os_version: "10",
},
BS_IE_11: {
base: "BrowserStack",
browser: "ie",
browser_version: "11.0",
os: "Windows",
os_version: "7",
es5: true,
},
};

@ -1,29 +0,0 @@
{
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"scope-empty": [2, "never"],
"scope-enum": [2, "always", [
"ci",
"commitlint",
"deps",
"docs",
"eslint",
"eventlisteners",
"examples",
"exports",
"format",
"git",
"github",
"karma",
"npm",
"package",
"relic",
"style",
"ttypescript",
"typescript",
"vscode"
]]
}
}

@ -1,74 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta charset="utf-8">
<title>Carousel</title>
<script type="module" src="./index.js"></script>
<style type="text/css">
div.view {
margin: 10px;
}
h1 {
font-size: 24px;
color: #505000;
}
svg {
display: block;
margin-bottom: 10px;
border: 1px solid gray;
}
g#carousel {
-webkit-transition: -webkit-transform 1s ease;
transition: transform 1s ease;
}
polygon {
stroke: #808000;
transition: fill 0.5s linear;
}
polygon#yellow {
fill: rgba(255,255,0,0.4);
}
polygon#yellow:hover, polygon#yellow:active {
fill: yellow;
}
polygon#green {
fill: rgba(0,128,0,0.4);
}
polygon#green:hover, polygon#green:active {
fill: green;
}
polygon#magenta {
fill: rgba(255,0,255,0.4);
}
polygon#magenta:hover, polygon#magenta:active {
fill: magenta;
}
polygon#red {
fill: rgba(255,0,0,0.4);
}
polygon#red:hover, polygon#red:active {
fill: red;
}
polygon#cyan {
fill: rgba(0,255,255,0.4);
}
polygon#cyan:hover, polygon#cyan:active {
fill: cyan;
}
polygon#blue {
fill: rgba(0,0,255,0.4);
}
polygon#blue:hover, polygon#blue:active {
fill: blue;
}
button {
font-size: 15px;
margin: 0 0.7em 0.7em 0;
}
</style>
</head>
<body>
<div id="container"></div>
</body>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta charset="utf-8" />
<title>Carousel</title>
<script type="module" src="./index.js"></script>
<style type="text/css">
div.view {
margin: 10px;
}
h1 {
font-size: 24px;
color: #505000;
}
svg {
display: block;
margin-bottom: 10px;
border: 1px solid gray;
}
g#carousel {
-webkit-transition: -webkit-transform 1s ease;
transition: transform 1s ease;
}
polygon {
stroke: #808000;
transition: fill 0.5s linear;
}
polygon#yellow {
fill: rgba(255, 255, 0, 0.4);
}
polygon#yellow:hover,
polygon#yellow:active {
fill: yellow;
}
polygon#green {
fill: rgba(0, 128, 0, 0.4);
}
polygon#green:hover,
polygon#green:active {
fill: green;
}
polygon#magenta {
fill: rgba(255, 0, 255, 0.4);
}
polygon#magenta:hover,
polygon#magenta:active {
fill: magenta;
}
polygon#red {
fill: rgba(255, 0, 0, 0.4);
}
polygon#red:hover,
polygon#red:active {
fill: red;
}
polygon#cyan {
fill: rgba(0, 255, 255, 0.4);
}
polygon#cyan:hover,
polygon#cyan:active {
fill: cyan;
}
polygon#blue {
fill: rgba(0, 0, 255, 0.4);
}
polygon#blue:hover,
polygon#blue:active {
fill: blue;
}
button {
font-size: 15px;
margin: 0 0.7em 0.7em 0;
}
</style>
</head>
<body>
<div id="container"></div>
</body>
</html>

@ -1,74 +1,119 @@
import { init } from '../../build/package/init.js'
import { attributesModule } from '../../build/package/modules/attributes.js'
import { styleModule } from '../../build/package/modules/style.js'
import { eventListenersModule } from '../../build/package/modules/eventlisteners.js'
import { h } from '../../build/package/h.js'
import {
init,
attributesModule,
styleModule,
eventListenersModule,
h,
} from "../../build/index.js";
var patch = init([attributesModule, styleModule, eventListenersModule])
const patch = init([attributesModule, styleModule, eventListenersModule]);
var vnode
let vnode;
var data = {
degRotation: 0
}
let data = {
degRotation: 0,
};
function gRotation () {
function gRotation() {
// console.log("gRotation: %s", data.degRotation);
return 'rotate(' + data.degRotation + 'deg)'
return "rotate(" + data.degRotation + "deg)";
}
function triangleClick (id) {
console.log('triangleClick: %s', id)
render()
function triangleClick(id) {
console.log("triangleClick: %s", id);
render();
}
function handleRotate (degs) {
data.degRotation += degs
console.log('handleRotate: %s, %s', degs, data.degRotation)
render()
function handleRotate(degs) {
data.degRotation += degs;
console.log("handleRotate: %s, %s", degs, data.degRotation);
render();
}
function handleReset (degs) {
data.degRotation = degs
console.log('handleReset: %s', degs)
render()
function handleReset(degs) {
data.degRotation = degs;
console.log("handleReset: %s", degs);
render();
}
function render () {
vnode = patch(vnode, view(data))
function render() {
vnode = patch(vnode, view(data));
}
const hTriangle = (id, degRotation) =>
h('polygon#' + id, {
h("polygon#" + id, {
attrs: {
points: '-50,-88 0,-175 50,-88',
transform: 'rotate(' + degRotation + ')',
'stroke-width': 3
points: "-50,-88 0,-175 50,-88",
transform: "rotate(" + degRotation + ")",
"stroke-width": 3,
},
on: {
click: () => {
triangleClick(id);
},
},
on: { click: () => { triangleClick(id) } }
})
});
const view = (data) =>
h('div.view', [
h('h1', 'Snabbdom SVG Carousel'),
h('svg', { attrs: { width: 380, height: 380, viewBox: [-190, -190, 380, 380] } }, [
h('g#carousel',
{ style: { '-webkit-transform': gRotation(), transform: gRotation() } }, [
hTriangle('yellow', 0),
hTriangle('green', 60),
hTriangle('magenta', 120),
hTriangle('red', 180),
hTriangle('cyan', 240),
hTriangle('blue', 300)
])
]),
h('button', { on: { click: () => { handleRotate(60) } } }, 'Rotate Clockwise'),
h('button', { on: { click: () => { handleRotate(-60) } } }, 'Rotate Anticlockwise'),
h('button', { on: { click: () => { handleReset(0) } } }, 'Reset')
])
const view = () =>
h("div.view", [
h("h1", "Snabbdom SVG Carousel"),
h(
"svg",
{ attrs: { width: 380, height: 380, viewBox: [-190, -190, 380, 380] } },
[
h(
"g#carousel",
{
style: { "-webkit-transform": gRotation(), transform: gRotation() },
},
[
hTriangle("yellow", 0),
hTriangle("green", 60),
hTriangle("magenta", 120),
hTriangle("red", 180),
hTriangle("cyan", 240),
hTriangle("blue", 300),
]
),
]
),
h(
"button",
{
on: {
click: () => {
handleRotate(60);
},
},
},
"Rotate Clockwise"
),
h(
"button",
{
on: {
click: () => {
handleRotate(-60);
},
},
},
"Rotate Anticlockwise"
),
h(
"button",
{
on: {
click: () => {
handleReset(0);
},
},
},
"Reset"
),
]);
window.addEventListener('DOMContentLoaded', () => {
var container = document.getElementById('container')
vnode = patch(container, view(data))
render()
})
window.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("container");
vnode = patch(container, view(data));
render();
});

@ -2,14 +2,18 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>Hero animation</title>
<script type="module" src="./index.js"></script>
<style>
{
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
{
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html, body {
html,
body {
height: 100%;
margin: 0;
}
@ -22,8 +26,7 @@
position: relative;
background: #fff;
}
@media (min-width: 28em),
@media (min-height: 38em) {
@media (min-width: 28em), @media (min-height: 38em) {
body {
display: flex;
flex-direction: column;
@ -33,7 +36,7 @@
position: relative;
}
.page-container {
box-shadow: 0 0 1em rgba(0, 0, 0, .5);
box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
width: 28em;
min-height: 38em;
height: 38em;
@ -41,14 +44,13 @@
}
.page {
background: #fff;
transition: opacity 0.4s ease-in-out,
transform 0.4s ease-in-out;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
width: 100%;
height: 100%;
}
h2 {
font-size: 1.1em;
margin: .2em 0;
margin: 0.2em 0;
}
.header {
height: 3.5em;
@ -58,13 +60,12 @@
.header-content {
width: 100%;
box-sizing: border-box;
padding: .4em .8em;
padding: 0.4em 0.8em;
color: #fff;
display: flex;
align-items: center;
position: absolute;
transition: opacity 0.4s ease-in-out,
transform 0.4s ease-in-out;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
}
.header h1 {
font-weight: normal;
@ -88,7 +89,7 @@
justify-content: center;
width: 2.7em;
height: 2.7em;
margin-right: .5em;
margin-right: 0.5em;
position: relative;
}
.header .rank-circle {
@ -97,7 +98,7 @@
width: 2.7em;
height: 2.7em;
border-radius: 1.35em;
margin-right: .5em;
margin-right: 0.5em;
transition: transform 0.4s ease-in-out;
top: 0;
left: 0;
@ -113,13 +114,12 @@
text-align: center;
width: 1.8em;
height: 1.8em;
border-radius: .9em;
background: rgba(0, 0, 0, .5);
border-radius: 0.9em;
background: rgba(0, 0, 0, 0.5);
transition: transform 0.4s ease-in-out;
}
.hero {
transition: transform 0.4s ease-in-out,
opacity 0.4s ease-in-out;
transition: transform 0.4s ease-in-out, opacity 0.4s ease-in-out;
}
.page-content {
position: relative;
@ -130,13 +130,11 @@
.list {
position: absolute;
width: 100%;
transition: transform 0.4s ease-in-out,
opacity 0.4s ease-in-out;
transition: transform 0.4s ease-in-out, opacity 0.4s ease-in-out;
}
.desc {
position: absolute;
transition: transform 0.4s ease-in-out,
opacity 0.4s ease-in-out;
transition: transform 0.4s ease-in-out, opacity 0.4s ease-in-out;
padding: 1em;
}
.spacer {

@ -1,136 +1,239 @@
/* jshint esnext: true */
import { init } from '../../build/package/init.js'
import { classModule } from '../../build/package/modules/class.js'
import { heroModule } from '../../build/package/modules/hero.js'
import { styleModule } from '../../build/package/modules/style.js'
import { eventListenersModule } from '../../build/package/modules/eventlisteners.js'
import { h } from '../../build/package/h.js'
import {
init,
classModule,
heroModule,
styleModule,
eventListenersModule,
h,
} from "../../build/index.js";
var patch = init([classModule, heroModule, styleModule, eventListenersModule])
const patch = init([
classModule,
heroModule,
styleModule,
eventListenersModule,
]);
var vnode
let vnode;
var data = {
const data = {
selected: undefined,
movies: [
{ rank: 1, title: 'This is an', desc: 'Lorem ipsum dolor sit amet, sed pede integer vitae bibendum, accumsan sit, vulputate aenean tempora ipsum. Lorem sed id et metus, eros posuere suspendisse nec nunc justo, fusce augue placerat nibh purus suspendisse. Aliquam aliquam, ut eget. Mollis a eget sed nibh tincidunt nec, mi integer, proin magna lacus iaculis tortor. Aliquam vel arcu arcu, vivamus a urna fames felis vel wisi, cursus tortor nec erat dignissim cras sem, mauris ac venenatis tellus elit.' },
{ rank: 2, title: 'example of', desc: 'Consequuntur ipsum nulla, consequat curabitur in magnis risus. Taciti mattis bibendum tellus nibh, at dui neque eget, odio pede ut, sapien pede, ipsum ut. Sagittis dui, sodales sem, praesent ipsum conubia eget lorem lobortis wisi.' },
{ rank: 3, title: 'Snabbdom', desc: 'Quam lorem aliquam fusce wisi, urna purus ipsum pharetra sed, at cras sodales enim vestibulum odio cras, luctus integer phasellus.' },
{ rank: 4, title: 'doing hero transitions', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.' },
{ rank: 5, title: 'using the', desc: 'Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.' },
{ rank: 6, title: 'module for hero transitions', desc: 'Sapien laoreet, ligula elit tortor nulla pellentesque, maecenas enim turpis, quae duis venenatis vivamus ultricies, nunc imperdiet sollicitudin ipsum malesuada. Ut sem. Wisi fusce nullam nibh enim. Nisl hymenaeos id sed sed in. Proin leo et, pulvinar nunc pede laoreet.' },
{ rank: 7, title: 'click on ar element in', desc: 'Accumsan quia, id nascetur dui et congue erat, id excepteur, primis ratione nec. At nulla et. Suspendisse lobortis, lobortis in tortor fringilla, duis adipiscing vestibulum voluptates sociosqu auctor.' },
{ rank: 8, title: 'the list', desc: 'Ante tellus egestas vel hymenaeos, ut viverra nibh ut, ipsum nibh donec donec dolor. Eros ridiculus vel egestas convallis ipsum, commodo ut venenatis nullam porta iaculis, suspendisse ante proin leo, felis risus etiam.' },
{ rank: 9, title: 'to witness', desc: 'Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst.' },
{ rank: 10, title: 'the effect', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris' },
]
}
{
rank: 1,
title: "This is an",
desc:
"Lorem ipsum dolor sit amet, sed pede integer vitae bibendum, accumsan sit, vulputate aenean tempora ipsum. Lorem sed id et metus, eros posuere suspendisse nec nunc justo, fusce augue placerat nibh purus suspendisse. Aliquam aliquam, ut eget. Mollis a eget sed nibh tincidunt nec, mi integer, proin magna lacus iaculis tortor. Aliquam vel arcu arcu, vivamus a urna fames felis vel wisi, cursus tortor nec erat dignissim cras sem, mauris ac venenatis tellus elit.",
},
{
rank: 2,
title: "example of",
desc:
"Consequuntur ipsum nulla, consequat curabitur in magnis risus. Taciti mattis bibendum tellus nibh, at dui neque eget, odio pede ut, sapien pede, ipsum ut. Sagittis dui, sodales sem, praesent ipsum conubia eget lorem lobortis wisi.",
},
{
rank: 3,
title: "Snabbdom",
desc:
"Quam lorem aliquam fusce wisi, urna purus ipsum pharetra sed, at cras sodales enim vestibulum odio cras, luctus integer phasellus.",
},
{
rank: 4,
title: "doing hero transitions",
desc:
"Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.",
},
{
rank: 5,
title: "using the",
desc:
"Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.",
},
{
rank: 6,
title: "module for hero transitions",
desc:
"Sapien laoreet, ligula elit tortor nulla pellentesque, maecenas enim turpis, quae duis venenatis vivamus ultricies, nunc imperdiet sollicitudin ipsum malesuada. Ut sem. Wisi fusce nullam nibh enim. Nisl hymenaeos id sed sed in. Proin leo et, pulvinar nunc pede laoreet.",
},
{
rank: 7,
title: "click on ar element in",
desc:
"Accumsan quia, id nascetur dui et congue erat, id excepteur, primis ratione nec. At nulla et. Suspendisse lobortis, lobortis in tortor fringilla, duis adipiscing vestibulum voluptates sociosqu auctor.",
},
{
rank: 8,
title: "the list",
desc:
"Ante tellus egestas vel hymenaeos, ut viverra nibh ut, ipsum nibh donec donec dolor. Eros ridiculus vel egestas convallis ipsum, commodo ut venenatis nullam porta iaculis, suspendisse ante proin leo, felis risus etiam.",
},
{
rank: 9,
title: "to witness",
desc:
"Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst.",
},
{
rank: 10,
title: "the effect",
desc:
"Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris",
},
],
};
function select (m) {
data.selected = m
render()
function select(m) {
data.selected = m;
render();
}
function render () {
vnode = patch(vnode, view(data))
function render() {
vnode = patch(vnode, view(data));
}
const fadeInOutStyle = {
opacity: '0', delayed: { opacity: '1' }, remove: { opacity: '0' }
}
opacity: "0",
delayed: { opacity: "1" },
remove: { opacity: "0" },
};
const detailView = (movie) =>
h('div.page', { style: fadeInOutStyle }, [
h('div.header', [
h('div.header-content.detail', {
style: { opacity: '1', remove: { opacity: '0' } },
}, [
h('div.rank', [
h('span.header-rank.hero', { hero: { id: 'rank' + movie.rank } }, movie.rank),
h('div.rank-circle', {
style: {
transform: 'scale(0)',
delayed: { transform: 'scale(1)' },
destroy: { transform: 'scale(0)' }
h("div.page", { style: fadeInOutStyle }, [
h("div.header", [
h(
"div.header-content.detail",
{
style: { opacity: "1", remove: { opacity: "0" } },
},
[
h("div.rank", [
h(
"span.header-rank.hero",
{ hero: { id: "rank" + movie.rank } },
movie.rank
),
h("div.rank-circle", {
style: {
transform: "scale(0)",
delayed: { transform: "scale(1)" },
destroy: { transform: "scale(0)" },
},
}),
]),
h(
"div.hero.header-title",
{ hero: { id: movie.title } },
movie.title
),
h("div.spacer"),
h(
"div.close",
{
on: {
click: () => {
select(undefined);
},
},
style: {
transform: "scale(0)",
delayed: { transform: "scale(1)" },
destroy: { transform: "scale(0)" },
},
},
}),
]),
h('div.hero.header-title', { hero: { id: movie.title } }, movie.title),
h('div.spacer'),
h('div.close', {
on: { click: () => { select(undefined) } },
"x"
),
]
),
]),
h("div.page-content", [
h(
"div.desc",
{
style: {
transform: 'scale(0)',
delayed: { transform: 'scale(1)' },
destroy: { transform: 'scale(0)' }
opacity: "0",
transform: "translateX(3em)",
delayed: { opacity: "1", transform: "translate(0)" },
remove: {
opacity: "0",
position: "absolute",
top: "0",
left: "0",
transform: "translateX(3em)",
},
},
}, 'x'),
]),
]),
h('div.page-content', [
h('div.desc', {
style: {
opacity: '0',
transform: 'translateX(3em)',
delayed: { opacity: '1', transform: 'translate(0)' },
remove: {
opacity: '0',
position: 'absolute',
top: '0',
left: '0',
transform: 'translateX(3em)'
}
}
}, [
h('h2', 'Description:'),
h('span', movie.desc),
]),
},
[h("h2", "Description:"), h("span", movie.desc)]
),
]),
])
]);
const overviewView = (movies) =>
h('div.page', { style: fadeInOutStyle }, [
h('div.header', [
h('div.header-content.overview', {
style: fadeInOutStyle,
}, [
h('div.header-title', {
style: {
transform: 'translateY(-2em)',
delayed: { transform: 'translate(0)' },
destroy: { transform: 'translateY(-2em)' }
}
}, 'Top 10 movies'),
h('div.spacer'),
]),
h("div.page", { style: fadeInOutStyle }, [
h("div.header", [
h(
"div.header-content.overview",
{
style: fadeInOutStyle,
},
[
h(
"div.header-title",
{
style: {
transform: "translateY(-2em)",
delayed: { transform: "translate(0)" },
destroy: { transform: "translateY(-2em)" },
},
},
"Top 10 movies"
),
h("div.spacer"),
]
),
]),
h('div.page-content', [
h('div.list', {
style: {
opacity: '0',
delayed: { opacity: '1' },
remove: { opacity: '0', position: 'absolute', top: '0', left: '0' }
}
}, movies.map((movie) =>
h('div.row', {
on: { click: () => { select(movie) } },
}, [
h('div.hero.rank', [
h('span.hero', { hero: { id: 'rank' + movie.rank } }, movie.rank)
]),
h('div.hero', { hero: { id: movie.title } }, movie.title)
])
)),
h("div.page-content", [
h(
"div.list",
{
style: {
opacity: "0",
delayed: { opacity: "1" },
remove: { opacity: "0", position: "absolute", top: "0", left: "0" },
},
},
movies.map((movie) =>
h(
"div.row",
{
on: {
click: () => {
select(movie);
},
},
},
[
h("div.hero.rank", [
h(
"span.hero",
{ hero: { id: "rank" + movie.rank } },
movie.rank
),
]),
h("div.hero", { hero: { id: movie.title } }, movie.title),
]
)
)
),
]),
])
]);
const view = (data) =>
h('div.page-container', [
h("div.page-container", [
data.selected ? detailView(data.selected) : overviewView(data.movies),
])
]);
window.addEventListener('DOMContentLoaded', () => {
var container = document.getElementById('container')
vnode = patch(container, view(data))
render()
})
window.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("container");
vnode = patch(container, view(data));
render();
});

@ -16,17 +16,18 @@
display: inline-block;
cursor: pointer;
background: #fff;
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
padding: .5em .8em;
transition: box-shadow .05s ease-in-out;
-webkit-transition: box-shadow .05s ease-in-out;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
padding: 0.5em 0.8em;
transition: box-shadow 0.05s ease-in-out;
-webkit-transition: box-shadow 0.05s ease-in-out;
}
.btn:hover {
box-shadow: 0 0 2px rgba(0, 0, 0, .2);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
}
.btn:active, .active, .active:hover {
box-shadow: 0 0 1px rgba(0, 0, 0, .2),
inset 0 0 4px rgba(0, 0, 0, .1);
.btn:active,
.active,
.active:hover {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2), inset 0 0 4px rgba(0, 0, 0, 0.1);
}
.add {
float: right;
@ -44,12 +45,14 @@
box-sizing: border-box;
width: 100%;
left: 0px;
margin: .5em 0;
margin: 0.5em 0;
padding: 1em;
background: #fff;
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
-webkit-transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
transition: transform 0.5s ease-in-out, opacity 0.5s ease-out,
left 0.5s ease-in-out;
-webkit-transition: transform 0.5s ease-in-out, opacity 0.5s ease-out,
left 0.5s ease-in-out;
}
.row div {
display: inline-block;
@ -69,7 +72,7 @@
position: absolute;
top: 0;
right: 0;
color: #C25151;
color: #c25151;
width: 1.4em;
height: 1.4em;
text-align: center;

@ -1,31 +1,98 @@
import { init } from '../../build/package/init.js'
import { classModule } from '../../build/package/modules/class.js'
import { propsModule } from '../../build/package/modules/props.js'
import { styleModule } from '../../build/package/modules/style.js'
import { eventListenersModule } from '../../build/package/modules/eventlisteners.js'
import { h } from '../../build/package/h.js'
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "../../build/index.js";
var patch = init([classModule, propsModule, styleModule, eventListenersModule])
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
var vnode
let vnode;
var nextKey = 11
var margin = 8
var sortBy = 'rank'
var totalHeight = 0
var originalData = [
{ rank: 1, title: 'The Shawshank Redemption', desc: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', elmHeight: 0 },
{ rank: 2, title: 'The Godfather', desc: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', elmHeight: 0 },
{ rank: 3, title: 'The Godfather: Part II', desc: 'The early life and career of Vito Corleone in 1920s New York is portrayed while his son, Michael, expands and tightens his grip on his crime syndicate stretching from Lake Tahoe, Nevada to pre-revolution 1958 Cuba.', elmHeight: 0 },
{ rank: 4, title: 'The Dark Knight', desc: 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, the caped crusader must come to terms with one of the greatest psychological tests of his ability to fight injustice.', elmHeight: 0 },
{ rank: 5, title: 'Pulp Fiction', desc: 'The lives of two mob hit men, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', elmHeight: 0 },
{ rank: 6, title: 'Schindler\'s List', desc: 'In Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.', elmHeight: 0 },
{ rank: 7, title: '12 Angry Men', desc: 'A dissenting juror in a murder trial slowly manages to convince the others that the case is not as obviously clear as it seemed in court.', elmHeight: 0 },
{ rank: 8, title: 'The Good, the Bad and the Ugly', desc: 'A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried in a remote cemetery.', elmHeight: 0 },
{ rank: 9, title: 'The Lord of the Rings: The Return of the King', desc: 'Gandalf and Aragorn lead the World of Men against Sauron\'s army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.', elmHeight: 0 },
{ rank: 10, title: 'Fight Club', desc: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker and they form an underground fight club that evolves into something much, much more...', elmHeight: 0 },
]
var data = [
let nextKey = 11;
const margin = 8;
let sortBy = "rank";
let totalHeight = 0;
const originalData = [
{
rank: 1,
title: "The Shawshank Redemption",
desc:
"Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.",
elmHeight: 0,
},
{
rank: 2,
title: "The Godfather",
desc:
"The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
elmHeight: 0,
},
{
rank: 3,
title: "The Godfather: Part II",
desc:
"The early life and career of Vito Corleone in 1920s New York is portrayed while his son, Michael, expands and tightens his grip on his crime syndicate stretching from Lake Tahoe, Nevada to pre-revolution 1958 Cuba.",
elmHeight: 0,
},
{
rank: 4,
title: "The Dark Knight",
desc:
"When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, the caped crusader must come to terms with one of the greatest psychological tests of his ability to fight injustice.",
elmHeight: 0,
},
{
rank: 5,
title: "Pulp Fiction",
desc:
"The lives of two mob hit men, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.",
elmHeight: 0,
},
{
rank: 6,
title: "Schindler's List",
desc:
"In Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.",
elmHeight: 0,
},
{
rank: 7,
title: "12 Angry Men",
desc:
"A dissenting juror in a murder trial slowly manages to convince the others that the case is not as obviously clear as it seemed in court.",
elmHeight: 0,
},
{
rank: 8,
title: "The Good, the Bad and the Ugly",
desc:
"A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried in a remote cemetery.",
elmHeight: 0,
},
{
rank: 9,
title: "The Lord of the Rings: The Return of the King",
desc:
"Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.",
elmHeight: 0,
},
{
rank: 10,
title: "Fight Club",
desc:
"An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker and they form an underground fight club that evolves into something much, much more...",
elmHeight: 0,
},
];
let data = [
originalData[0],
originalData[1],
originalData[2],
@ -36,84 +103,145 @@ var data = [
originalData[7],
originalData[8],
originalData[9],
]
];
function changeSort (prop) {
sortBy = prop
function changeSort(prop) {
sortBy = prop;
data.sort((a, b) => {
if (a[prop] > b[prop]) {
return 1
return 1;
}
if (a[prop] < b[prop]) {
return -1
return -1;
}
return 0
})
render()
return 0;
});
render();
}
function add () {
var n = originalData[Math.floor(Math.random() * 10)]
data = [{ rank: nextKey++, title: n.title, desc: n.desc, elmHeight: 0 }].concat(data)
render()
render()
function add() {
const n = originalData[Math.floor(Math.random() * 10)];
data = [
{ rank: nextKey++, title: n.title, desc: n.desc, elmHeight: 0 },
].concat(data);
render();
render();
}
function remove (movie) {
function remove(movie) {
data = data.filter((m) => {
return m !== movie
})
render()
return m !== movie;
});
render();
}
function movieView (movie) {
return h('div.row', {
key: movie.rank,
style: {
opacity: '0',
transform: 'translate(-200px)',
delayed: { transform: `translateY(${movie.offset}px)`, opacity: '1' },
remove: { opacity: '0', transform: `translateY(${movie.offset}px) translateX(200px)` }
function movieView(movie) {
return h(
"div.row",
{
key: movie.rank,
style: {
opacity: "0",
transform: "translate(-200px)",
delayed: { transform: `translateY(${movie.offset}px)`, opacity: "1" },
remove: {
opacity: "0",
transform: `translateY(${movie.offset}px) translateX(200px)`,
},
},
hook: {
insert: (vnode) => {
movie.elmHeight = vnode.elm.offsetHeight;
},
},
},
hook: { insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight } },
}, [
h('div', { style: { fontWeight: 'bold' } }, movie.rank),
h('div', movie.title),
h('div', movie.desc),
h('div.btn.rm-btn', { on: { click: () => { remove(movie) } } }, 'x'),
])
[
h("div", { style: { fontWeight: "bold" } }, movie.rank),
h("div", movie.title),
h("div", movie.desc),
h(
"div.btn.rm-btn",
{
on: {
click: () => {
remove(movie);
},
},
},
"x"
),
]
);
}
function render () {
function render() {
data = data.reduce((acc, m) => {
var last = acc[acc.length - 1]
m.offset = last ? last.offset + last.elmHeight + margin : margin
return acc.concat(m)
}, [])
totalHeight = data.length === 0
? 0
: data[data.length - 1].offset + data[data.length - 1].elmHeight
vnode = patch(vnode, view(data))
const last = acc[acc.length - 1];
m.offset = last ? last.offset + last.elmHeight + margin : margin;
return acc.concat(m);
}, []);
totalHeight =
data.length === 0
? 0
: data[data.length - 1].offset + data[data.length - 1].elmHeight;
vnode = patch(vnode, view(data));
}
function view (data) {
return h('div', [
h('h1', 'Top 10 movies'),
h('div', [
h('a.btn.add', { on: { click: add } }, 'Add'),
'Sort by: ',
h('span.btn-group', [
h('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: () => { changeSort('rank') } } }, 'Rank'),
h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: () => { changeSort('title') } } }, 'Title'),
h('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: () => { changeSort('desc') } } }, 'Description'),
function view(data) {
return h("div", [
h("h1", "Top 10 movies"),
h("div", [
h("a.btn.add", { on: { click: add } }, "Add"),
"Sort by: ",
h("span.btn-group", [
h(
"a.btn.rank",
{
class: { active: sortBy === "rank" },
on: {
click: () => {
changeSort("rank");
},
},
},
"Rank"
),
h(
"a.btn.title",
{
class: { active: sortBy === "title" },
on: {
click: () => {
changeSort("title");
},
},
},
"Title"
),
h(
"a.btn.desc",
{
class: { active: sortBy === "desc" },
on: {
click: () => {
changeSort("desc");
},
},
},
"Description"
),
]),
]),
h('div.list', { style: { height: totalHeight + 'px' } }, data.map(movieView)),
])
h(
"div.list",
{ style: { height: totalHeight + "px" } },
data.map(movieView)
),
]);
}
window.addEventListener('DOMContentLoaded', () => {
var container = document.getElementById('container')
vnode = patch(container, view(data))
render()
})
window.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("container");
vnode = patch(container, view(data));
render();
});

@ -16,17 +16,18 @@
display: inline-block;
cursor: pointer;
background: #fff;
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
padding: .5em .8em;
transition: box-shadow .05s ease-in-out;
-webkit-transition: box-shadow .05s ease-in-out;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
padding: 0.5em 0.8em;
transition: box-shadow 0.05s ease-in-out;
-webkit-transition: box-shadow 0.05s ease-in-out;
}
.btn:hover {
box-shadow: 0 0 2px rgba(0, 0, 0, .2);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
}
.btn:active, .active, .active:hover {
box-shadow: 0 0 1px rgba(0, 0, 0, .2),
inset 0 0 4px rgba(0, 0, 0, .1);
.btn:active,
.active,
.active:hover {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2), inset 0 0 4px rgba(0, 0, 0, 0.1);
}
.add {
float: right;
@ -44,12 +45,14 @@
box-sizing: border-box;
width: 100%;
left: 0px;
margin: .5em 0;
margin: 0.5em 0;
padding: 1em;
background: #fff;
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
-webkit-transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
transition: transform 0.5s ease-in-out, opacity 0.5s ease-out,
left 0.5s ease-in-out;
-webkit-transition: transform 0.5s ease-in-out, opacity 0.5s ease-out,
left 0.5s ease-in-out;
}
.row div {
display: inline-block;
@ -69,7 +72,7 @@
position: absolute;
top: 0;
right: 0;
color: #C25151;
color: #c25151;
width: 1.4em;
height: 1.4em;
text-align: center;

@ -1,15 +1,22 @@
import { init } from '../../build/package/init.js'
import { attributesModule } from '../../build/package/modules/attributes.js'
import { h } from '../../build/package/h.js'
import { init, attributesModule, h } from "../../build/index.js";
var patch = init([attributesModule])
const patch = init([attributesModule]);
window.addEventListener('DOMContentLoaded', () => {
var container = document.getElementById('container')
var vnode = h('div', [
h('svg', { attrs: { width: 100, height: 100 } }, [
h('circle', { attrs: { cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow' } })
])
])
patch(container, vnode)
})
window.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("container");
const vnode = h("div", [
h("svg", { attrs: { width: 100, height: 100 } }, [
h("circle", {
attrs: {
cx: 50,
cy: 50,
r: 40,
stroke: "green",
"stroke-width": 4,
fill: "yellow",
},
}),
]),
]);
patch(container, vnode);
});

@ -1,48 +1,50 @@
const chalk = require('chalk')
const Table = require('tty-table')
const chalk = require("chalk");
const Table = require("tty-table");
exports['reporter:benchmark'] = ['type', BenchmarkReporter]
exports["reporter:benchmark"] = ["type", BenchmarkReporter];
function BenchmarkReporter (baseReporterDecorator) {
baseReporterDecorator(this)
const resultsPerBrowser = new Map()
function BenchmarkReporter(baseReporterDecorator) {
baseReporterDecorator(this);
const resultsPerBrowser = new Map();
this.onBrowserInfo = function (browser, info) {
if (!info.benchmark) return
if (!info.benchmark) return;
if (!resultsPerBrowser.has(browser.name)) {
resultsPerBrowser.set(browser.name, info.benchmark)
resultsPerBrowser.set(browser.name, info.benchmark);
}
}
};
this.onRunComplete = function () {
if (resultsPerBrowser.size === 0) return
this.writeCommonMsg(chalk.underline.bold('\nBENCHMARK (times in seconds):\n'))
if (resultsPerBrowser.size === 0) return;
this.writeCommonMsg(
chalk.underline.bold("\nBENCHMARK (times in seconds):\n")
);
resultsPerBrowser.forEach((results, browserName) => {
this.writeCommonMsg(` ${chalk.bold(browserName)}:\n`)
this.writeCommonMsg(` ${chalk.bold(browserName)}:\n`);
const rows = results.map(({ cur, ref }, i) => ({
i: String(i),
cur: cur.toFixed(0),
ref: ref.toFixed(0),
diff: `${(cur / ref * 100).toFixed(2)}%`,
}))
diff: `${((cur / ref) * 100).toFixed(2)}%`,
}));
const header = [
{
value: 'i',
align: 'right'
value: "i",
align: "right",
},
{
value: 'ref',
align: 'right'
value: "ref",
align: "right",
},
{
value: 'cur',
align: 'right'
value: "cur",
align: "right",
},
{
value: 'diff',
align: 'right'
value: "diff",
align: "right",
},
]
console.log(Table(header, rows).render())
})
resultsPerBrowser.clear()
}
];
console.log(Table(header, rows).render());
});
resultsPerBrowser.clear();
};
}

@ -1,56 +0,0 @@
const ci = !!process.env.CI
const watch = !!process.env.WATCH
const live = !!process.env.LIVE
const ip = 'bs-local.com'
const browserstack = require('./browserstack-karma.cjs')
// https://www.browserstack.com/open-source (text search "parallels")
const BROWSERSTACK_OPEN_SOURCE_CONCURRENCY = 5
const browsers = ci
? Object.keys(browserstack)
: live
? undefined
: watch
? ['Chrome']
: ['Chrome', 'Firefox']
module.exports = function (config) {
config.set({
basePath: '.',
frameworks: ['mocha'],
// list of files / patterns to load in the browser
files: [
{ pattern: process.env.FILES_PATTERN },
],
plugins: [
'karma-mocha',
require('karma-mocha-reporter'),
require('./karma-benchmark-reporter.cjs'),
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-browserstack-launcher',
],
hostname: ci ? ip : 'localhost',
browserStack: {
name: 'Snabbdom',
retryLimit: 1,
},
client: {
captureConsole: true,
},
customLaunchers: browserstack,
reporters: ['mocha', 'benchmark', 'BrowserStack'],
mochaReporter: {
showDiff: true
},
port: 9876,
colors: true,
autoWatch: true,
browsers: browsers,
singleRun: !watch && !live,
concurrency: ci ? BROWSERSTACK_OPEN_SOURCE_CONCURRENCY : Infinity,
})
}

@ -0,0 +1,78 @@
const ci = !!process.env.CI;
const watch = !!process.env.WATCH;
const live = !!process.env.LIVE;
const es5 = !!process.env.ES5;
const ip = "bs-local.com";
const browserstack = require("./browserstack-karma.js");
// https://www.browserstack.com/open-source (text search "parallels")
const BROWSERSTACK_OPEN_SOURCE_CONCURRENCY = 5;
const getBrowserstackBrowsers = () =>
Object.keys(browserstack).filter((k) => !!browserstack[k].es5 === es5);
const browsers = ci
? getBrowserstackBrowsers()
: live
? undefined
: watch
? ["Chrome"]
: ["ChromeHeadless", "FirefoxHeadless"];
module.exports = function (config) {
config.set({
basePath: ".",
frameworks: ["mocha", "karma-typescript"],
// list of files / patterns to load in the browser
files: process.env.FILES_PATTERN.split(",")
.map((p) => ({ pattern: p }))
.concat({ pattern: "src/**/*.ts" }),
preprocessors: {
"**/*.ts": "karma-typescript",
"**/*.tsx": "karma-typescript",
},
plugins: [
"karma-mocha",
"karma-typescript",
"karma-mocha-reporter",
require("./karma-benchmark-reporter.cjs"),
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-browserstack-launcher",
],
hostname: ci ? ip : "localhost",
karmaTypescriptConfig: {
compilerOptions: {
...require("./tsconfig.json").compilerOptions,
...require("./test/tsconfig.json").compilerOptions,
sourceMap: false,
inlineSourceMap: true,
target: es5 ? "es5" : "es6",
},
bundlerOptions: {
sourceMap: true,
},
include: process.env.FILES_PATTERN.split(",").concat("src/**/*.ts"),
},
browserStack: {
name: "Snabbdom",
retryLimit: 1,
},
client: {
captureConsole: true,
},
customLaunchers: browserstack,
reporters: ["karma-typescript", "mocha", "benchmark", "BrowserStack"],
mochaReporter: {
showDiff: true,
},
port: 9876,
colors: true,
autoWatch: true,
browsers: browsers,
singleRun: !watch && !live,
concurrency: ci ? BROWSERSTACK_OPEN_SOURCE_CONCURRENCY : Infinity,
});
};

34737
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -2,203 +2,112 @@
"name": "snabbdom",
"version": "2.1.0",
"description": "A virtual DOM library with focus on simplicity, modularity, powerful features and performance.",
"type": "module",
"exports": {
"./init": "./build/package/init.js",
"./h": "./build/package/h.js",
"./helpers/attachto": "./build/package/helpers/attachto.js",
"./hooks": "./build/package/hooks.js",
"./htmldomapi": "./build/package/htmldomapi.js",
"./is": "./build/package/is.js",
"./jsx": "./build/package/jsx.js",
"./modules/attributes": "./build/package/modules/attributes.js",
"./modules/class": "./build/package/modules/class.js",
"./modules/dataset": "./build/package/modules/dataset.js",
"./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
"./modules/hero": "./build/package/modules/hero.js",
"./modules/module": "./build/package/modules/module.js",
"./modules/props": "./build/package/modules/props.js",
"./modules/style": "./build/package/modules/style.js",
"./thunk": "./build/package/thunk.js",
"./tovnode": "./build/package/tovnode.js",
"./vnode": "./build/package/vnode.js"
"homepage": "https://github.com/snabbdom/snabbdom#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/snabbdom/snabbdom.git"
},
"keywords": [
"virtual",
"dom",
"light",
"kiss",
"performance"
],
"author": "Simon Friis Vindum",
"license": "MIT",
"bugs": {
"url": "https://github.com/snabbdom/snabbdom/issues"
},
"engines": {
"node": ">=8.3.0"
},
"main": "build/snabbdom.cjs.js",
"module": "build/index.js",
"scripts": {
"build": "tsc && npm run bundle:cjs",
"bundle:cjs": "rollup build/index.js --format cjs --file build/snabbdom.cjs.js",
"format": "prettier --write .",
"prepare": "husky install",
"lint": "eslint --ext .ts,.tsx,.js --ignore-path .gitignore .",
"unit": "cross-env FILES_PATTERN=\"test/unit/*.ts,test/unit/*.tsx\" karma start karma.conf.js",
"benchmark": "cross-env FILES_PATTERN=\"test-bundles/benchmark/**/*.js\" karma start karma.conf.cjs --concurrency=1",
"release": "release-it",
"test:ci": "npm test && cross-env ES5=true npm run unit",
"test": "npm run build && npm run lint && npm run unit"
},
"devDependencies": {
"@babel/core": "7.12.16",
"@babel/preset-env": "7.12.16",
"@commitlint/cli": "11.0.0",
"@commitlint/config-conventional": "11.0.0",
"@commitlint/travis-cli": "11.0.0",
"@types/chai": "4.2.12",
"@types/faker": "5.1.6",
"@release-it/conventional-changelog": "^2.0.1",
"@types/chai": "4.2.15",
"@types/faker": "5.1.7",
"@types/lodash.shuffle": "4.2.6",
"@types/mathjs": "6.0.5",
"@types/mocha": "8.0.3",
"@typescript-eslint/eslint-plugin": "4.15.1",
"babel-loader": "8.2.2",
"@types/mathjs": "6.0.11",
"@types/mocha": "8.2.1",
"@typescript-eslint/eslint-plugin": "4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"benchmark": "2.1.4",
"chai": "4.2.0",
"chai": "4.3.4",
"chalk": "4.1.0",
"core-js": "3.8.3",
"commithelper": "^1.1.1",
"conventional-changelog-angular": "^5.0.12",
"core-js": "3.9.1",
"cross-env": "7.0.3",
"editorconfig-checker": "3.2.0",
"eslint": "7.20.0",
"eslint-config-standard-with-typescript": "20.0.0",
"eslint": "7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-markdown": "2.0.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "4.3.1",
"faker": "5.4.0",
"globby": "11.0.2",
"husky": "5.0.9",
"is-path-inside": "3.0.2",
"karma": "6.1.1",
"husky": "5.1.3",
"karma": "6.2.0",
"karma-browserstack-launcher": "1.6.0",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "2.1.0",
"karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-typescript": "^5.5.0",
"latest-snabbdom-release": "npm:snabbdom@2.1.0",
"lint-staged": "^10.5.4",
"lodash.shuffle": "4.2.0",
"mathjs": "9.2.0",
"mocha": "8.1.3",
"npm-run-all": "4.1.5",
"mathjs": "9.3.0",
"mocha": "8.3.2",
"p-map-series": "2.1.0",
"p-reduce": "2.1.0",
"pinst": "2.1.4",
"regenerator-runtime": "0.13.7",
"remark-cli": "8.0.1",
"remark-toc": "7.1.0",
"standard-version": "9.1.0",
"tsconfigs": "5.0.0",
"prettier": "^2.2.1",
"release-it": "^14.4.1",
"rollup": "^2.41.2",
"tty-table": "4.1.3",
"ttypescript": "1.5.12",
"typescript": "4.0.3",
"webpack": "5.22.0",
"webpack-cli": "4.5.0"
"typescript": "4.2.3"
},
"scripts": {
"postinstall": "husky install",
"prepublishOnly": "pinst --disable",
"postpublish": "pinst --enable",
"docs": "remark . --output",
"check-clean": "git diff --exit-code",
"lint:js": "eslint --ext .ts,.tsx,.cjs,.md,.mjs --ignore-path .gitignore .",
"lint:editorconfig": "editorconfig-checker",
"lint": "run-s lint:editorconfig lint:js",
"unit": "cross-env FILES_PATTERN=\"test-bundles/unit/**/*.js\" karma start karma.conf.cjs",
"benchmark": "cross-env FILES_PATTERN=\"test-bundles/benchmark/**/*.js\" karma start karma.conf.cjs --concurrency=1",
"make-release-commit": "standard-version",
"test": "run-s lint compile bundle-tests unit",
"compile": "ttsc --build src/test/tsconfig.json",
"bundle-tests": "webpack --config tests.webpack.config.cjs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/snabbdom/snabbdom.git"
"prettier": {},
"lint-staged": {
"*.(ts|tsx|js|md)": "prettier --write"
},
"keywords": [
"virtual",
"dom",
"light",
"kiss",
"performance"
],
"author": "Simon Friis Vindum",
"license": "MIT",
"bugs": {
"url": "https://github.com/snabbdom/snabbdom/issues"
"release-it": {
"git": {
"commitMessage": "chore(release): v${version}"
},
"github": {
"release": true
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": "angular",
"infile": "CHANGELOG.md"
}
}
},
"remarkConfig": {
"plugins": [
[
"toc",
{
"tight": true
}
"commithelper": {
"scopeOverrides": {
"chore": [
"tools",
"refactor",
"release",
"test",
"deps",
"docs",
"examples"
]
],
"settings": {
"listItemIndent": "1",
"bullet": "*",
"tablePipeAlign": false
}
},
"homepage": "https://github.com/snabbdom/snabbdom#readme",
"files": [
"/build/package/h.d.ts",
"/build/package/h.js",
"/build/package/h.js.map",
"/build/package/helpers/attachto.d.ts",
"/build/package/helpers/attachto.js",
"/build/package/helpers/attachto.js.map",
"/build/package/hooks.d.ts",
"/build/package/hooks.js",
"/build/package/hooks.js.map",
"/build/package/htmldomapi.d.ts",
"/build/package/htmldomapi.js",
"/build/package/htmldomapi.js.map",
"/build/package/init.d.ts",
"/build/package/init.js",
"/build/package/init.js.map",
"/build/package/is.d.ts",
"/build/package/is.js",
"/build/package/is.js.map",
"/build/package/jsx-global.d.ts",
"/build/package/jsx.d.ts",
"/build/package/jsx.js",
"/build/package/jsx.js.map",
"/build/package/modules/attributes.d.ts",
"/build/package/modules/attributes.js",
"/build/package/modules/attributes.js.map",
"/build/package/modules/class.d.ts",
"/build/package/modules/class.js",
"/build/package/modules/class.js.map",
"/build/package/modules/dataset.d.ts",
"/build/package/modules/dataset.js",
"/build/package/modules/dataset.js.map",
"/build/package/modules/eventlisteners.d.ts",
"/build/package/modules/eventlisteners.js",
"/build/package/modules/eventlisteners.js.map",
"/build/package/modules/hero.d.ts",
"/build/package/modules/hero.js",
"/build/package/modules/hero.js.map",
"/build/package/modules/module.d.ts",
"/build/package/modules/module.js",
"/build/package/modules/module.js.map",
"/build/package/modules/props.d.ts",
"/build/package/modules/props.js",
"/build/package/modules/props.js.map",
"/build/package/modules/style.d.ts",
"/build/package/modules/style.js",
"/build/package/modules/style.js.map",
"/build/package/thunk.d.ts",
"/build/package/thunk.js",
"/build/package/thunk.js.map",
"/build/package/tovnode.d.ts",
"/build/package/tovnode.js",
"/build/package/tovnode.js.map",
"/build/package/vnode.d.ts",
"/build/package/vnode.js",
"/build/package/vnode.js.map",
"/src/package/h.ts",
"/src/package/helpers/attachto.ts",
"/src/package/hooks.ts",
"/src/package/htmldomapi.ts",
"/src/package/init.ts",
"/src/package/is.ts",
"/src/package/jsx-global.ts",
"/src/package/jsx.ts",
"/src/package/modules/attributes.ts",
"/src/package/modules/class.ts",
"/src/package/modules/dataset.ts",
"/src/package/modules/eventlisteners.ts",
"/src/package/modules/hero.ts",
"/src/package/modules/module.ts",
"/src/package/modules/props.ts",
"/src/package/modules/style.ts",
"/src/package/thunk.ts",
"/src/package/tovnode.ts",
"/src/package/vnode.ts"
]
}
}

@ -1,45 +1,48 @@
// Has not been maintained for a while.
/* eslint-disable */
var Benchmark = require('benchmark');
var a = require('../snabbdom.js');
var b = require('../oldsnabbdom.js');
var Benchmark = require("benchmark");
var a = require("../snabbdom.js");
var b = require("../oldsnabbdom.js");
global.a = a;
global.b = b;
var suite = new Benchmark.Suite();
a.spanNum = function spanNum (n) {
return a.h('span', { key: n }, n.toString());
a.spanNum = function spanNum(n) {
return a.h("span", { key: n }, n.toString());
};
b.spanNum = function spanNum (n) {
return b.h('span', { key: n }, n.toString());
b.spanNum = function spanNum(n) {
return b.h("span", { key: n }, n.toString());
};
var elms = global.elms = 10;
var arr = global.arr = [];
for (var n = 0; n < elms; ++n) { arr[n] = n; }
var elms = (global.elms = 10);
var arr = (global.arr = []);
for (var n = 0; n < elms; ++n) {
arr[n] = n;
}
document.addEventListener('DOMContentLoaded', function () {
var elm = global.elm = document.getElementById('container');
document.addEventListener("DOMContentLoaded", function () {
var elm = (global.elm = document.getElementById("container"));
// add tests
suite.add('a/ insert first', {
setup: function () {
var vnode1 = a.h('div', arr.map(a.spanNum));
var vnode2 = a.h('div', ['new'].concat(arr).map(a.spanNum));
},
fn: function () {
var emptyNode = a.emptyNodeAt(elm);
a.patch(emptyNode, vnode1);
a.patch(vnode1, vnode2);
a.patch(vnode2, a.emptyNode);
},
})
.add('b/ insert first', {
suite
.add("a/ insert first", {
setup: function () {
var vnode1 = b.h('div', arr.map(b.spanNum));
var vnode2 = b.h('div', ['new'].concat(arr).map(b.spanNum));
var vnode1 = a.h("div", arr.map(a.spanNum));
var vnode2 = a.h("div", ["new"].concat(arr).map(a.spanNum));
},
fn: function () {
var emptyNode = a.emptyNodeAt(elm);
a.patch(emptyNode, vnode1);
a.patch(vnode1, vnode2);
a.patch(vnode2, a.emptyNode);
},
})
.add("b/ insert first", {
setup: function () {
var vnode1 = b.h("div", arr.map(b.spanNum));
var vnode2 = b.h("div", ["new"].concat(arr).map(b.spanNum));
},
fn: function () {
var emptyNode = b.emptyNodeAt(elm);
@ -48,13 +51,13 @@ document.addEventListener('DOMContentLoaded', function () {
b.patch(vnode2, b.emptyNode);
},
})
// add listeners
.on('cycle', function (event) {
// add listeners
.on("cycle", function (event) {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').pluck('name'));
.on("complete", function () {
console.log("Fastest is " + this.filter("fastest").pluck("name"));
})
// run async
// run async
.run({ async: true });
});

@ -0,0 +1,81 @@
import { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";
export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(
data: any,
children: VNodes | undefined,
sel: string | undefined
): void {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
const childData = children[i].data;
if (childData !== undefined) {
addNS(childData, children[i].children as VNodes, children[i].sel);
}
}
}
}
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
sel: string,
data: VNodeData | null,
children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
let data: VNodeData = {};
let children: any;
let text: any;
let i: number;
if (c !== undefined) {
if (b !== null) {
data = b;
}
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) {
text = c;
} else if (c && c.sel) {
children = [c];
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b;
} else if (is.primitive(b)) {
text = b;
} else if (b && b.sel) {
children = [b];
} else {
data = b;
}
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i]))
children[i] = vnode(
undefined,
undefined,
undefined,
children[i],
undefined
);
}
}
if (
sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
}

@ -0,0 +1,64 @@
import { VNode, VNodeData } from "../vnode";
export interface AttachData {
[key: string]: any;
[i: number]: any;
placeholder?: any;
real?: Node;
}
interface VNodeDataWithAttach extends VNodeData {
attachData: AttachData;
}
interface VNodeWithAttachData extends VNode {
data: VNodeDataWithAttach;
}
function pre(vnode: VNodeWithAttachData, newVnode: VNodeWithAttachData): void {
const attachData = vnode.data.attachData;
// Copy created placeholder and real element from old vnode
newVnode.data.attachData.placeholder = attachData.placeholder;
newVnode.data.attachData.real = attachData.real;
// Mount real element in vnode so the patch process operates on it
vnode.elm = vnode.data.attachData.real;
}
function post(_: any, vnode: VNodeWithAttachData): void {
// Mount dummy placeholder in vnode so potential reorders use it
vnode.elm = vnode.data.attachData.placeholder;
}
function destroy(vnode: VNodeWithAttachData): void {
// Remove placeholder
if (vnode.elm !== undefined) {
(vnode.elm.parentNode as HTMLElement).removeChild(vnode.elm);
}
// Remove real element from where it was inserted
vnode.elm = vnode.data.attachData.real;
}
function create(_: any, vnode: VNodeWithAttachData): void {
const real = vnode.elm;
const attachData = vnode.data.attachData;
const placeholder = document.createElement("span");
// Replace actual element with dummy placeholder
// Snabbdom will then insert placeholder instead
vnode.elm = placeholder;
attachData.target.appendChild(real);
attachData.real = real;
attachData.placeholder = placeholder;
}
export function attachTo(target: Element, vnode: VNode): VNode {
if (vnode.data === undefined) vnode.data = {};
if (vnode.data.hook === undefined) vnode.data.hook = {};
const data = vnode.data;
const hook = vnode.data.hook;
data.attachData = { target: target, placeholder: undefined, real: undefined };
hook.create = create;
hook.prepatch = pre;
hook.postpatch = post;
hook.destroy = destroy;
return vnode;
}

@ -0,0 +1,25 @@
import { VNode } from "./vnode";
export type PreHook = () => any;
export type InitHook = (vNode: VNode) => any;
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any;
export type InsertHook = (vNode: VNode) => any;
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any;
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any;
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any;
export type DestroyHook = (vNode: VNode) => any;
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any;
export type PostHook = () => any;
export interface Hooks {
pre?: PreHook;
init?: InitHook;
create?: CreateHook;
insert?: InsertHook;
prepatch?: PrePatchHook;
update?: UpdateHook;
postpatch?: PostPatchHook;
destroy?: DestroyHook;
remove?: RemoveHook;
post?: PostHook;
}

@ -0,0 +1,117 @@
export interface DOMAPI {
createElement: (
tagName: any,
options?: ElementCreationOptions
) => HTMLElement;
createElementNS: (
namespaceURI: string,
qualifiedName: string,
options?: ElementCreationOptions
) => Element;
createTextNode: (text: string) => Text;
createComment: (text: string) => Comment;
insertBefore: (
parentNode: Node,
newNode: Node,
referenceNode: Node | null
) => void;
removeChild: (node: Node, child: Node) => void;
appendChild: (node: Node, child: Node) => void;
parentNode: (node: Node) => Node | null;
nextSibling: (node: Node) => Node | null;
tagName: (elm: Element) => string;
setTextContent: (node: Node, text: string | null) => void;
getTextContent: (node: Node) => string | null;
isElement: (node: Node) => node is Element;
isText: (node: Node) => node is Text;
isComment: (node: Node) => node is Comment;
}
function createElement(
tagName: any,
options?: ElementCreationOptions
): HTMLElement {
return document.createElement(tagName, options);
}
function createElementNS(
namespaceURI: string,
qualifiedName: string,
options?: ElementCreationOptions
): Element {
return document.createElementNS(namespaceURI, qualifiedName, options);
}
function createTextNode(text: string): Text {
return document.createTextNode(text);
}
function createComment(text: string): Comment {
return document.createComment(text);
}
function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node | null
): void {
parentNode.insertBefore(newNode, referenceNode);
}
function removeChild(node: Node, child: Node): void {
node.removeChild(child);
}
function appendChild(node: Node, child: Node): void {
node.appendChild(child);
}
function parentNode(node: Node): Node | null {
return node.parentNode;
}
function nextSibling(node: Node): Node | null {
return node.nextSibling;
}
function tagName(elm: Element): string {
return elm.tagName;
}
function setTextContent(node: Node, text: string | null): void {
node.textContent = text;
}
function getTextContent(node: Node): string | null {
return node.textContent;
}
function isElement(node: Node): node is Element {
return node.nodeType === 1;
}
function isText(node: Node): node is Text {
return node.nodeType === 3;
}
function isComment(node: Node): node is Comment {
return node.nodeType === 8;
}
export const htmlDomApi: DOMAPI = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
};

@ -0,0 +1,33 @@
// core
export { DOMAPI, htmlDomApi } from "./htmldomapi";
export { init } from "./init";
export { ThunkData, Thunk, ThunkFn, thunk } from "./thunk";
export { Key, VNode, VNodeData, vnode } from "./vnode";
// helpers
export { AttachData, attachTo } from "./helpers/attachto";
export { array, primitive } from "./is";
export { toVNode } from "./tovnode";
export {
VNodes,
VNodeChildElement,
ArrayOrElement,
VNodeChildren,
h,
} from "./h";
// types
export * from "./hooks";
export { Module } from "./modules/module";
// modules
export { Attrs, attributesModule } from "./modules/attributes";
export { Classes, classModule } from "./modules/class";
export { Dataset, datasetModule } from "./modules/dataset";
export { On, eventListenersModule } from "./modules/eventlisteners";
export { Hero, heroModule } from "./modules/hero";
export { Props, propsModule } from "./modules/props";
export { VNodeStyle, styleModule } from "./modules/style";
// JSX
export { JsxVNodeChild, JsxVNodeChildren, FunctionComponent, jsx } from "./jsx";

@ -0,0 +1,391 @@
import { Module } from "./modules/module";
import { vnode, VNode } from "./vnode";
import * as is from "./is";
import { htmlDomApi, DOMAPI } from "./htmldomapi";
type NonUndefined<T> = T extends undefined ? never : T;
function isUndef(s: any): boolean {
return s === undefined;
}
function isDef<A>(s: A): s is NonUndefined<A> {
return s !== undefined;
}
type VNodeQueue = VNode[];
const emptyNode = vnode("", {}, [], undefined, undefined);
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
const isSameKey = vnode1.key === vnode2.key;
const isSameIs = vnode1.data?.is === vnode2.data?.is;
const isSameSel = vnode1.sel === vnode2.sel;
return isSameSel && isSameKey && isSameIs;
}
function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}
type KeyToIndexMap = { [key: string]: number };
type ArraysOf<T> = {
[K in keyof T]: Array<T[K]>;
};
type ModuleHooks = ArraysOf<Required<Module>>;
function createKeyToOldIdx(
children: VNode[],
beginIdx: number,
endIdx: number
): KeyToIndexMap {
const map: KeyToIndexMap = {};
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key;
if (key !== undefined) {
map[key] = i;
}
}
return map;
}
const hooks: Array<keyof Module> = [
"create",
"update",
"remove",
"destroy",
"pre",
"post",
];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number;
let j: number;
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook);
}
}
}
function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : "";
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return vnode(
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
}
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm) as Node;
api.removeChild(parent, childElm);
}
};
}
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any;
let data = vnode.data;
if (data !== undefined) {
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
const children = vnode.children;
const sel = vnode.sel;
if (sel === "!") {
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf("#");
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag, data)
: api.createElement(tag, data));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
hook.create?.(emptyNode, vnode);
if (hook.insert) {
insertedVnodeQueue.push(vnode);
}
}
} else {
vnode.elm = api.createTextNode(vnode.text!);
}
return vnode.elm;
}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
function invokeDestroyHook(vnode: VNode) {
const data = vnode.data;
if (data !== undefined) {
data?.hook?.destroy?.(vnode);
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j];
if (child != null && typeof child !== "string") {
invokeDestroyHook(child);
}
}
}
}
}
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number
): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number;
let rm: () => void;
const ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm!, listeners);
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
const removeHook = ch?.data?.hook?.remove;
if (isDef(removeHook)) {
removeHook(ch, rm);
} else {
rm();
}
} else {
// Text node
api.removeChild(parentElm, ch.elm!);
}
}
}
}
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm)!;
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm) as Node;
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}

@ -0,0 +1,4 @@
export const array = Array.isArray;
export function primitive(s: any): s is string | number {
return typeof s === "string" || typeof s === "number";
}

@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-namespace, import/export */
import { vnode, VNode, VNodeData } from "./vnode";
import { h, ArrayOrElement } from "./h";
// See https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
namespace JSXInternal {
export type Element = VNode;
export interface IntrinsicElements {
[elemName: string]: VNodeData;
}
}
// for conditional rendering we support boolean child element e.g cond && <tag />
export type JsxVNodeChild =
| VNode
| string
| number
| boolean
| undefined
| null;
export type JsxVNodeChildren = ArrayOrElement<JsxVNodeChild>;
export type FunctionComponent = (
props: { [prop: string]: any } | null,
children?: VNode[]
) => VNode;
function flattenAndFilter(
children: JsxVNodeChildren[],
flattened: VNode[]
): VNode[] {
for (const child of children) {
// filter out falsey children, except 0 since zero can be a valid value e.g inside a chart
if (
child !== undefined &&
child !== null &&
child !== false &&
child !== ""
) {
if (Array.isArray(child)) {
flattenAndFilter(child, flattened);
} else if (
typeof child === "string" ||
typeof child === "number" ||
typeof child === "boolean"
) {
flattened.push(
vnode(undefined, undefined, undefined, String(child), undefined)
);
} else {
flattened.push(child);
}
}
}
return flattened;
}
/**
* jsx/tsx compatible factory function
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions
*/
export function jsx(
tag: string | FunctionComponent,
data: VNodeData | null,
...children: JsxVNodeChildren[]
): VNode {
const flatChildren = flattenAndFilter(children, []);
if (typeof tag === "function") {
// tag is a function component
return tag(data, flatChildren);
} else {
if (
flatChildren.length === 1 &&
!flatChildren[0].sel &&
flatChildren[0].text
) {
// only child is a simple text node, pass as text for a simpler vtree
return h(tag, data, flatChildren[0].text);
} else {
return h(tag, data, flatChildren);
}
}
}
export namespace jsx {
export import JSX = JSXInternal; // eslint-disable-line @typescript-eslint/no-unused-vars
}

@ -0,0 +1,59 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
export type Attrs = Record<string, string | number | boolean>;
const xlinkNS = "http://www.w3.org/1999/xlink";
const xmlNS = "http://www.w3.org/XML/1998/namespace";
const colonChar = 58;
const xChar = 120;
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
let key: string;
const elm: Element = vnode.elm as Element;
let oldAttrs = (oldVnode.data as VNodeData).attrs;
let attrs = (vnode.data as VNodeData).attrs;
if (!oldAttrs && !attrs) return;
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
for (key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
if (old !== cur) {
if (cur === true) {
elm.setAttribute(key, "");
} else if (cur === false) {
elm.removeAttribute(key);
} else {
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur as any);
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur as any);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur as any);
} else {
elm.setAttribute(key, cur as any);
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key);
}
}
}
export const attributesModule: Module = {
create: updateAttrs,
update: updateAttrs,
};

@ -0,0 +1,32 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
export type Classes = Record<string, boolean>;
function updateClass(oldVnode: VNode, vnode: VNode): void {
let cur: any;
let name: string;
const elm: Element = vnode.elm as Element;
let oldClass = (oldVnode.data as VNodeData).class;
let klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
for (name in oldClass) {
if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
// was `true` and now not provided
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? "add" : "remove"](name);
}
}
}
export const classModule: Module = { create: updateClass, update: updateClass };

@ -0,0 +1,50 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
export type Dataset = Record<string, string>;
const CAPS_REGEX = /[A-Z]/g;
function updateDataset(oldVnode: VNode, vnode: VNode): void {
const elm: HTMLElement = vnode.elm as HTMLElement;
let oldDataset = (oldVnode.data as VNodeData).dataset;
let dataset = (vnode.data as VNodeData).dataset;
let key: string;
if (!oldDataset && !dataset) return;
if (oldDataset === dataset) return;
oldDataset = oldDataset || {};
dataset = dataset || {};
const d = elm.dataset;
for (key in oldDataset) {
if (!dataset[key]) {
if (d) {
if (key in d) {
delete d[key];
}
} else {
elm.removeAttribute(
"data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase()
);
}
}
}
for (key in dataset) {
if (oldDataset[key] !== dataset[key]) {
if (d) {
d[key] = dataset[key];
} else {
elm.setAttribute(
"data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase(),
dataset[key]
);
}
}
}
}
export const datasetModule: Module = {
create: updateDataset,
update: updateDataset,
};

@ -0,0 +1,110 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
type Listener<T> = (this: VNode, ev: T, vnode: VNode) => void;
export type On = {
[N in keyof HTMLElementEventMap]?:
| Listener<HTMLElementEventMap[N]>
| Array<Listener<HTMLElementEventMap[N]>>;
} & {
[event: string]: Listener<any> | Array<Listener<any>>;
};
type SomeListener<N extends keyof HTMLElementEventMap> =
| Listener<HTMLElementEventMap[N]>
| Listener<any>;
function invokeHandler<N extends keyof HTMLElementEventMap>(
handler: SomeListener<N> | Array<SomeListener<N>>,
vnode: VNode,
event?: Event
): void {
if (typeof handler === "function") {
// call function handler
handler.call(vnode, event, vnode);
} else if (typeof handler === "object") {
// call multiple handlers
for (let i = 0; i < handler.length; i++) {
invokeHandler(handler[i], vnode, event);
}
}
}
function handleEvent(event: Event, vnode: VNode) {
const name = event.type;
const on = (vnode.data as VNodeData).on;
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event);
}
}
function createListener() {
return function handler(event: Event) {
handleEvent(event, (handler as any).vnode);
};
}
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
const oldOn = (oldVnode.data as VNodeData).on;
const oldListener = (oldVnode as any).listener;
const oldElm: Element = oldVnode.elm as Element;
const on = vnode && (vnode.data as VNodeData).on;
const elm: Element = (vnode && vnode.elm) as Element;
let name: string;
// optimization for reused immutable handlers
if (oldOn === on) {
return;
}
// remove existing listeners which no longer used
if (oldOn && oldListener) {
// if element changed or deleted we remove all existing listeners unconditionally
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false);
}
}
}
}
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
const listener = ((vnode as any).listener =
(oldVnode as any).listener || createListener());
// update vnode for listener
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false);
}
}
}
}
}
export const eventListenersModule: Module = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners,
};

@ -0,0 +1,220 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
export type Hero = { id: string };
const raf =
(typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout;
const nextFrame = function (fn: any) {
raf(function () {
raf(fn);
});
};
function setNextFrame(obj: any, prop: string, val: any): void {
nextFrame(function () {
obj[prop] = val;
});
}
function getTextNodeRect(textNode: Text): ClientRect | undefined {
let rect: ClientRect | undefined;
if (document.createRange) {
const range = document.createRange();
range.selectNodeContents(textNode);
if (range.getBoundingClientRect) {
rect = range.getBoundingClientRect();
}
}
return rect;
}
function calcTransformOrigin(
isTextNode: boolean,
textRect: ClientRect | undefined,
boundingRect: ClientRect
): string {
if (isTextNode) {
if (textRect) {
// calculate pixels to center of text from left edge of bounding box
const relativeCenterX =
textRect.left + textRect.width / 2 - boundingRect.left;
const relativeCenterY =
textRect.top + textRect.height / 2 - boundingRect.top;
return `${relativeCenterX}px ${relativeCenterY}px`;
}
}
return "0 0"; // top left
}
function getTextDx(
oldTextRect: ClientRect | undefined,
newTextRect: ClientRect | undefined
): number {
if (oldTextRect && newTextRect) {
return (
oldTextRect.left +
oldTextRect.width / 2 -
(newTextRect.left + newTextRect.width / 2)
);
}
return 0;
}
function getTextDy(
oldTextRect: ClientRect | undefined,
newTextRect: ClientRect | undefined
): number {
if (oldTextRect && newTextRect) {
return (
oldTextRect.top +
oldTextRect.height / 2 -
(newTextRect.top + newTextRect.height / 2)
);
}
return 0;
}
function isTextElement(elm: Element | Text): elm is Text {
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3;
}
let removed: any, created: any;
function pre() {
removed = {};
created = [];
}
function create(oldVnode: VNode, vnode: VNode): void {
const hero = (vnode.data as VNodeData).hero;
if (hero && hero.id) {
created.push(hero.id);
created.push(vnode);
}
}
function destroy(vnode: VNode): void {
const hero = (vnode.data as VNodeData).hero;
if (hero && hero.id) {
const elm = vnode.elm;
(vnode as any).isTextNode = isTextElement(elm as Element | Text); // is this a text node?
(vnode as any).boundingRect = (elm as Element).getBoundingClientRect(); // save the bounding rectangle to a new property on the vnode
(vnode as any).textRect = (vnode as any).isTextNode
? getTextNodeRect((elm as Element).childNodes[0] as Text)
: null; // save bounding rect of inner text node
const computedStyle = window.getComputedStyle(elm as Element, undefined); // get current styles (includes inherited properties)
(vnode as any).savedStyle = JSON.parse(JSON.stringify(computedStyle)); // save a copy of computed style values
removed[hero.id] = vnode;
}
}
function post() {
let i: number,
id: any,
newElm: Element,
oldVnode: VNode,
oldElm: Element,
hRatio: number,
wRatio: number,
oldRect: ClientRect,
newRect: ClientRect,
dx: number,
dy: number,
origTransform: string | null,
origTransition: string | null,
newStyle: CSSStyleDeclaration,
oldStyle: CSSStyleDeclaration,
newComputedStyle: CSSStyleDeclaration,
isTextNode: boolean,
newTextRect: ClientRect | undefined,
oldTextRect: ClientRect | undefined;
for (i = 0; i < created.length; i += 2) {
id = created[i];
newElm = created[i + 1].elm;
oldVnode = removed[id];
if (oldVnode) {
isTextNode = (oldVnode as any).isTextNode && isTextElement(newElm); // Are old & new both text?
newStyle = (newElm as HTMLElement).style;
newComputedStyle = window.getComputedStyle(newElm, undefined); // get full computed style for new element
oldElm = oldVnode.elm as Element;
oldStyle = (oldElm as HTMLElement).style;
// Overall element bounding boxes
newRect = newElm.getBoundingClientRect();
oldRect = (oldVnode as any).boundingRect; // previously saved bounding rect
// Text node bounding boxes & distances
if (isTextNode) {
newTextRect = getTextNodeRect(newElm.childNodes[0] as Text);
oldTextRect = (oldVnode as any).textRect;
dx = getTextDx(oldTextRect, newTextRect);
dy = getTextDy(oldTextRect, newTextRect);
} else {
// Calculate distances between old & new positions
dx = oldRect.left - newRect.left;
dy = oldRect.top - newRect.top;
}
hRatio = newRect.height / Math.max(oldRect.height, 1);
wRatio = isTextNode ? hRatio : newRect.width / Math.max(oldRect.width, 1); // text scales based on hRatio
// Animate new element
origTransform = newStyle.transform;
origTransition = newStyle.transition;
if (newComputedStyle.display === "inline") {
// inline elements cannot be transformed
newStyle.display = "inline-block"; // this does not appear to have any negative side effects
}
newStyle.transition = origTransition + "transform 0s";
newStyle.transformOrigin = calcTransformOrigin(
isTextNode,
newTextRect,
newRect
);
newStyle.opacity = "0";
newStyle.transform = `${origTransform}translate(${dx}px, ${dy}px) scale(${
1 / wRatio
}, ${1 / hRatio})`;
setNextFrame(newStyle, "transition", origTransition);
setNextFrame(newStyle, "transform", origTransform);
setNextFrame(newStyle, "opacity", "1");
// Animate old element
for (const key in (oldVnode as any).savedStyle) {
// re-apply saved inherited properties
if (String(parseInt(key)) !== key) {
const ms = key.substring(0, 2) === "ms";
const moz = key.substring(0, 3) === "moz";
const webkit = key.substring(0, 6) === "webkit";
if (!ms && !moz && !webkit) {
// ignore prefixed style properties
(oldStyle as any)[key] = (oldVnode as any).savedStyle[key];
}
}
}
oldStyle.position = "absolute";
oldStyle.top = `${oldRect.top}px`; // start at existing position
oldStyle.left = `${oldRect.left}px`;
oldStyle.width = `${oldRect.width}px`; // Needed for elements who were sized relative to their parents
oldStyle.height = `${oldRect.height}px`; // Needed for elements who were sized relative to their parents
oldStyle.margin = "0"; // Margin on hero element leads to incorrect positioning
oldStyle.transformOrigin = calcTransformOrigin(
isTextNode,
oldTextRect,
oldRect
);
oldStyle.transform = "";
oldStyle.opacity = "1";
document.body.appendChild(oldElm);
setNextFrame(
oldStyle,
"transform",
`translate(${-dx}px, ${-dy}px) scale(${wRatio}, ${hRatio})`
); // scale must be on far right for translate to be correct
setNextFrame(oldStyle, "opacity", "0");
oldElm.addEventListener("transitionend", function (ev: TransitionEvent) {
if (ev.propertyName === "transform") {
document.body.removeChild(ev.target as Node);
}
});
}
}
removed = created = undefined;
}
export const heroModule: Module = { pre, create, destroy, post };

@ -0,0 +1,17 @@
import {
PreHook,
CreateHook,
UpdateHook,
DestroyHook,
RemoveHook,
PostHook,
} from "../hooks";
export type Module = Partial<{
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}>;

@ -0,0 +1,28 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
export type Props = Record<string, any>;
function updateProps(oldVnode: VNode, vnode: VNode): void {
let key: string;
let cur: any;
let old: any;
const elm = vnode.elm;
let oldProps = (oldVnode.data as VNodeData).props;
let props = (vnode.data as VNodeData).props;
if (!oldProps && !props) return;
if (oldProps === props) return;
oldProps = oldProps || {};
props = props || {};
for (key in props) {
cur = props[key];
old = oldProps[key];
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur;
}
}
}
export const propsModule: Module = { create: updateProps, update: updateProps };

@ -0,0 +1,124 @@
import { VNode, VNodeData } from "../vnode";
import { Module } from "./module";
export type VNodeStyle = Record<string, string> & {
delayed?: Record<string, string>;
remove?: Record<string, string>;
};
// Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
const raf =
(typeof window !== "undefined" &&
window.requestAnimationFrame.bind(window)) ||
setTimeout;
const nextFrame = function (fn: any) {
raf(function () {
raf(fn);
});
};
let reflowForced = false;
function setNextFrame(obj: any, prop: string, val: any): void {
nextFrame(function () {
obj[prop] = val;
});
}
function updateStyle(oldVnode: VNode, vnode: VNode): void {
let cur: any;
let name: string;
const elm = vnode.elm;
let oldStyle = (oldVnode.data as VNodeData).style;
let style = (vnode.data as VNodeData).style;
if (!oldStyle && !style) return;
if (oldStyle === style) return;
oldStyle = oldStyle || {};
style = style || {};
const oldHasDel = "delayed" in oldStyle;
for (name in oldStyle) {
if (!style[name]) {
if (name[0] === "-" && name[1] === "-") {
(elm as any).style.removeProperty(name);
} else {
(elm as any).style[name] = "";
}
}
}
for (name in style) {
cur = style[name];
if (name === "delayed" && style.delayed) {
for (const name2 in style.delayed) {
cur = style.delayed[name2];
if (!oldHasDel || cur !== (oldStyle.delayed as any)[name2]) {
setNextFrame((elm as any).style, name2, cur);
}
}
} else if (name !== "remove" && cur !== oldStyle[name]) {
if (name[0] === "-" && name[1] === "-") {
(elm as any).style.setProperty(name, cur);
} else {
(elm as any).style[name] = cur;
}
}
}
}
function applyDestroyStyle(vnode: VNode): void {
let style: any;
let name: string;
const elm = vnode.elm;
const s = (vnode.data as VNodeData).style;
if (!s || !(style = s.destroy)) return;
for (name in style) {
(elm as any).style[name] = style[name];
}
}
function applyRemoveStyle(vnode: VNode, rm: () => void): void {
const s = (vnode.data as VNodeData).style;
if (!s || !s.remove) {
rm();
return;
}
if (!reflowForced) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(vnode.elm as any).offsetLeft;
reflowForced = true;
}
let name: string;
const elm = vnode.elm;
let i = 0;
const style = s.remove;
let amount = 0;
const applied: string[] = [];
for (name in style) {
applied.push(name);
(elm as any).style[name] = style[name];
}
const compStyle = getComputedStyle(elm as Element);
const props = (compStyle as any)["transition-property"].split(", ");
for (; i < props.length; ++i) {
if (applied.indexOf(props[i]) !== -1) amount++;
}
(elm as Element).addEventListener(
"transitionend",
function (ev: TransitionEvent) {
if (ev.target === elm) --amount;
if (amount === 0) rm();
}
);
}
function forceReflow() {
reflowForced = false;
}
export const styleModule: Module = {
pre: forceReflow,
create: updateStyle,
update: updateStyle,
destroy: applyDestroyStyle,
remove: applyRemoveStyle,
};

@ -1,62 +0,0 @@
import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'
export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement<T> = T | T[]
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg'
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
const childData = children[i].data
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
}
}
}
}
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
let data: VNodeData = {}
let children: any
let text: any
let i: number
if (c !== undefined) {
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
text = b
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel)
}
return vnode(sel, data, children, text, undefined)
};

@ -1,64 +0,0 @@
import { VNode, VNodeData } from '../vnode'
export interface AttachData {
[key: string]: any
[i: number]: any
placeholder?: any
real?: Node
}
interface VNodeDataWithAttach extends VNodeData {
attachData: AttachData
}
interface VNodeWithAttachData extends VNode {
data: VNodeDataWithAttach
}
function pre (vnode: VNodeWithAttachData, newVnode: VNodeWithAttachData): void {
const attachData = vnode.data.attachData
// Copy created placeholder and real element from old vnode
newVnode.data.attachData.placeholder = attachData.placeholder
newVnode.data.attachData.real = attachData.real
// Mount real element in vnode so the patch process operates on it
vnode.elm = vnode.data.attachData.real
}
function post (_: any, vnode: VNodeWithAttachData): void {
// Mount dummy placeholder in vnode so potential reorders use it
vnode.elm = vnode.data.attachData.placeholder
}
function destroy (vnode: VNodeWithAttachData): void {
// Remove placeholder
if (vnode.elm !== undefined) {
(vnode.elm.parentNode as HTMLElement).removeChild(vnode.elm)
}
// Remove real element from where it was inserted
vnode.elm = vnode.data.attachData.real
}
function create (_: any, vnode: VNodeWithAttachData): void {
const real = vnode.elm
const attachData = vnode.data.attachData
const placeholder = document.createElement('span')
// Replace actual element with dummy placeholder
// Snabbdom will then insert placeholder instead
vnode.elm = placeholder
attachData.target.appendChild(real)
attachData.real = real
attachData.placeholder = placeholder
}
export function attachTo (target: Element, vnode: VNode): VNode {
if (vnode.data === undefined) vnode.data = {}
if (vnode.data.hook === undefined) vnode.data.hook = {}
const data = vnode.data
const hook = vnode.data.hook
data.attachData = { target: target, placeholder: undefined, real: undefined }
hook.create = create
hook.prepatch = pre
hook.postpatch = post
hook.destroy = destroy
return vnode
};

@ -1,25 +0,0 @@
import { VNode } from './vnode'
export type PreHook = () => any
export type InitHook = (vNode: VNode) => any
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
export type InsertHook = (vNode: VNode) => any
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any
export type DestroyHook = (vNode: VNode) => any
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
export type PostHook = () => any
export interface Hooks {
pre?: PreHook
init?: InitHook
create?: CreateHook
insert?: InsertHook
prepatch?: PrePatchHook
update?: UpdateHook
postpatch?: PostPatchHook
destroy?: DestroyHook
remove?: RemoveHook
post?: PostHook
}

@ -1,95 +0,0 @@
export interface DOMAPI {
createElement: (tagName: any, options?: ElementCreationOptions) => HTMLElement
createElementNS: (namespaceURI: string, qualifiedName: string, options?: ElementCreationOptions) => Element
createTextNode: (text: string) => Text
createComment: (text: string) => Comment
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void
removeChild: (node: Node, child: Node) => void
appendChild: (node: Node, child: Node) => void
parentNode: (node: Node) => Node | null
nextSibling: (node: Node) => Node | null
tagName: (elm: Element) => string
setTextContent: (node: Node, text: string | null) => void
getTextContent: (node: Node) => string | null
isElement: (node: Node) => node is Element
isText: (node: Node) => node is Text
isComment: (node: Node) => node is Comment
}
function createElement (tagName: any, options?: ElementCreationOptions): HTMLElement {
return document.createElement(tagName, options)
}
function createElementNS (namespaceURI: string, qualifiedName: string, options?: ElementCreationOptions): Element {
return document.createElementNS(namespaceURI, qualifiedName, options)
}
function createTextNode (text: string): Text {
return document.createTextNode(text)
}
function createComment (text: string): Comment {
return document.createComment(text)
}
function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode.insertBefore(newNode, referenceNode)
}
function removeChild (node: Node, child: Node): void {
node.removeChild(child)
}
function appendChild (node: Node, child: Node): void {
node.appendChild(child)
}
function parentNode (node: Node): Node | null {
return node.parentNode
}
function nextSibling (node: Node): Node | null {
return node.nextSibling
}
function tagName (elm: Element): string {
return elm.tagName
}
function setTextContent (node: Node, text: string | null): void {
node.textContent = text
}
function getTextContent (node: Node): string | null {
return node.textContent
}
function isElement (node: Node): node is Element {
return node.nodeType === 1
}
function isText (node: Node): node is Text {
return node.nodeType === 3
}
function isComment (node: Node): node is Comment {
return node.nodeType === 8
}
export const htmlDomApi: DOMAPI = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
}

@ -1,337 +0,0 @@
import { Module } from './modules/module'
import { vnode, VNode } from './vnode'
import * as is from './is'
import { htmlDomApi, DOMAPI } from './htmldomapi'
type NonUndefined<T> = T extends undefined ? never : T
function isUndef (s: any): boolean {
return s === undefined
}
function isDef<A> (s: A): s is NonUndefined<A> {
return s !== undefined
}
type VNodeQueue = VNode[]
const emptyNode = vnode('', {}, [], undefined, undefined)
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
const isSameKey = vnode1.key === vnode2.key
const isSameIs = vnode1.data?.is === vnode2.data?.is
const isSameSel = vnode1.sel === vnode2.sel
return isSameSel && isSameKey && isSameIs
}
function isVnode (vnode: any): vnode is VNode {
return vnode.sel !== undefined
}
type KeyToIndexMap = {[key: string]: number}
type ArraysOf<T> = {
[K in keyof T]: Array<T[K]>;
}
type ModuleHooks = ArraysOf<Required<Module>>
function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
const map: KeyToIndexMap = {}
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key
if (key !== undefined) {
map[key] = i
}
}
return map
}
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number
let j: number
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]]
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook)
}
}
}
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : ''
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}
function createRmCb (childElm: Node, listeners: number) {
return function rmCb () {
if (--listeners === 0) {
const parent = api.parentNode(childElm) as Node
api.removeChild(parent, childElm)
}
}
}
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
let data = vnode.data
if (data !== undefined) {
const init = data.hook?.init
if (isDef(init)) {
init(vnode)
data = vnode.data
}
}
const children = vnode.children
const sel = vnode.sel
if (sel === '!') {
if (isUndef(vnode.text)) {
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text!)
} else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf('#')
const dotIdx = sel.indexOf('.', hashIdx)
const hash = hashIdx > 0 ? hashIdx : sel.length
const dot = dotIdx > 0 ? dotIdx : sel.length
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag, data)
: api.createElement(tag, data)
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i]
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
}
}
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text))
}
const hook = vnode.data!.hook
if (isDef(hook)) {
hook.create?.(emptyNode, vnode)
if (hook.insert) {
insertedVnodeQueue.push(vnode)
}
}
} else {
vnode.elm = api.createTextNode(vnode.text!)
}
return vnode.elm
}
function addVnodes (
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
}
}
}
function invokeDestroyHook (vnode: VNode) {
const data = vnode.data
if (data !== undefined) {
data?.hook?.destroy?.(vnode)
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j]
if (child != null && typeof child !== 'string') {
invokeDestroyHook(child)
}
}
}
}
}
function removeVnodes (parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number
let rm: () => void
const ch = vnodes[startIdx]
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch)
listeners = cbs.remove.length + 1
rm = createRmCb(ch.elm!, listeners)
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
const removeHook = ch?.data?.hook?.remove
if (isDef(removeHook)) {
removeHook(ch, rm)
} else {
rm()
}
} else { // Text node
api.removeChild(parentElm, ch.elm!)
}
}
}
}
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
idxInOld = oldKeyToIdx[newStartVnode.key as string]
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
if (oldVnode === vnode) return
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
vnode.data.hook?.update?.(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
removeVnodes(parent, [oldVnode], 0, 0)
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}
}

@ -1,4 +0,0 @@
export const array = Array.isArray
export function primitive (s: any): s is (string | number) {
return typeof s === 'string' || typeof s === 'number'
}

@ -1,22 +0,0 @@
import { VNode as _VNode, VNodeData as _VNodeData } from './vnode'
// workaround
// https://github.com/typescript-eslint/typescript-eslint/issues/1596
type VNode = _VNode
type VNodeData = _VNodeData
/* eslint-disable @typescript-eslint/no-unused-vars */
declare global {
/**
* opt-in jsx intrinsic global interfaces
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
type Element = VNode
interface IntrinsicElements {
[elemName: string]: VNodeData
}
}
}
/* eslint-enable @typescript-eslint/no-unused-vars */

@ -1,43 +0,0 @@
import { vnode, VNode, VNodeData } from './vnode'
import { h, ArrayOrElement } from './h'
// for conditional rendering we support boolean child element e.g cond && <tag />
export type JsxVNodeChild = VNode | string | number | boolean | undefined | null
export type JsxVNodeChildren = ArrayOrElement<JsxVNodeChild>
export type FunctionComponent = (props: {[prop: string]: any} | null, children?: VNode[]) => VNode
function flattenAndFilter (children: JsxVNodeChildren[], flattened: VNode[]): VNode[] {
for (const child of children) {
// filter out falsey children, except 0 since zero can be a valid value e.g inside a chart
if (child !== undefined && child !== null && child !== false && child !== '') {
if (Array.isArray(child)) {
flattenAndFilter(child, flattened)
} else if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') {
flattened.push(vnode(undefined, undefined, undefined, String(child), undefined))
} else {
flattened.push(child)
}
}
}
return flattened
}
/**
* jsx/tsx compatible factory function
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions
*/
export function jsx (tag: string | FunctionComponent, data: VNodeData | null, ...children: JsxVNodeChildren[]): VNode {
const flatChildren = flattenAndFilter(children, [])
if (typeof tag === 'function') {
// tag is a function component
return tag(data, flatChildren)
} else {
if (flatChildren.length === 1 && !flatChildren[0].sel && flatChildren[0].text) {
// only child is a simple text node, pass as text for a simpler vtree
return h(tag, data, flatChildren[0].text)
} else {
return h(tag, data, flatChildren)
}
}
}

@ -1,56 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type Attrs = Record<string, string | number | boolean>
const xlinkNS = 'http://www.w3.org/1999/xlink'
const xmlNS = 'http://www.w3.org/XML/1998/namespace'
const colonChar = 58
const xChar = 120
function updateAttrs (oldVnode: VNode, vnode: VNode): void {
let key: string
const elm: Element = vnode.elm as Element
let oldAttrs = (oldVnode.data as VNodeData).attrs
let attrs = (vnode.data as VNodeData).attrs
if (!oldAttrs && !attrs) return
if (oldAttrs === attrs) return
oldAttrs = oldAttrs || {}
attrs = attrs || {}
// update modified attributes, add new attributes
for (key in attrs) {
const cur = attrs[key]
const old = oldAttrs[key]
if (old !== cur) {
if (cur === true) {
elm.setAttribute(key, '')
} else if (cur === false) {
elm.removeAttribute(key)
} else {
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur as any)
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur as any)
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur as any)
} else {
elm.setAttribute(key, cur as any)
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key)
}
}
}
export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }

@ -1,35 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type Classes = Record<string, boolean>
function updateClass (oldVnode: VNode, vnode: VNode): void {
let cur: any
let name: string
const elm: Element = vnode.elm as Element
let oldClass = (oldVnode.data as VNodeData).class
let klass = (vnode.data as VNodeData).class
if (!oldClass && !klass) return
if (oldClass === klass) return
oldClass = oldClass || {}
klass = klass || {}
for (name in oldClass) {
if (
oldClass[name] &&
!Object.prototype.hasOwnProperty.call(klass, name)
) {
// was `true` and now not provided
elm.classList.remove(name)
}
}
for (name in klass) {
cur = klass[name]
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? 'add' : 'remove'](name)
}
}
}
export const classModule: Module = { create: updateClass, update: updateClass }

@ -1,42 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type Dataset = Record<string, string>
const CAPS_REGEX = /[A-Z]/g
function updateDataset (oldVnode: VNode, vnode: VNode): void {
const elm: HTMLElement = vnode.elm as HTMLElement
let oldDataset = (oldVnode.data as VNodeData).dataset
let dataset = (vnode.data as VNodeData).dataset
let key: string
if (!oldDataset && !dataset) return
if (oldDataset === dataset) return
oldDataset = oldDataset || {}
dataset = dataset || {}
const d = elm.dataset
for (key in oldDataset) {
if (!dataset[key]) {
if (d) {
if (key in d) {
delete d[key]
}
} else {
elm.removeAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase())
}
}
}
for (key in dataset) {
if (oldDataset[key] !== dataset[key]) {
if (d) {
d[key] = dataset[key]
} else {
elm.setAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase(), dataset[key])
}
}
}
}
export const datasetModule: Module = { create: updateDataset, update: updateDataset }

@ -1,101 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
type Listener<T> = (this: VNode, ev: T, vnode: VNode) => void
export type On = {
[N in keyof HTMLElementEventMap]?: Listener<HTMLElementEventMap[N]> | Array<Listener<HTMLElementEventMap[N]>>
} & {
[event: string]: Listener<any> | Array<Listener<any>>
}
type SomeListener<N extends keyof HTMLElementEventMap> = Listener<HTMLElementEventMap[N]> | Listener<any>
function invokeHandler<N extends keyof HTMLElementEventMap> (handler: SomeListener<N> | Array<SomeListener<N>>, vnode: VNode, event?: Event): void {
if (typeof handler === 'function') {
// call function handler
handler.call(vnode, event, vnode)
} else if (typeof handler === 'object') {
// call multiple handlers
for (let i = 0; i < handler.length; i++) {
invokeHandler(handler[i], vnode, event)
}
}
}
function handleEvent (event: Event, vnode: VNode) {
const name = event.type
const on = (vnode.data as VNodeData).on
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event)
}
}
function createListener () {
return function handler (event: Event) {
handleEvent(event, (handler as any).vnode)
}
}
function updateEventListeners (oldVnode: VNode, vnode?: VNode): void {
const oldOn = (oldVnode.data as VNodeData).on
const oldListener = (oldVnode as any).listener
const oldElm: Element = oldVnode.elm as Element
const on = vnode && (vnode.data as VNodeData).on
const elm: Element = (vnode && vnode.elm) as Element
let name: string
// optimization for reused immutable handlers
if (oldOn === on) {
return
}
// remove existing listeners which no longer used
if (oldOn && oldListener) {
// if element changed or deleted we remove all existing listeners unconditionally
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false)
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false)
}
}
}
}
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
const listener = (vnode as any).listener = (oldVnode as any).listener || createListener()
// update vnode for listener
listener.vnode = vnode
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false)
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false)
}
}
}
}
}
export const eventListenersModule: Module = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
}

@ -1,182 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type Hero = { id: string }
const raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout
const nextFrame = function (fn: any) {
raf(function () {
raf(fn)
})
}
function setNextFrame (obj: any, prop: string, val: any): void {
nextFrame(function () {
obj[prop] = val
})
}
function getTextNodeRect (textNode: Text): ClientRect | undefined {
let rect: ClientRect | undefined
if (document.createRange) {
const range = document.createRange()
range.selectNodeContents(textNode)
if (range.getBoundingClientRect) {
rect = range.getBoundingClientRect()
}
}
return rect
}
function calcTransformOrigin (
isTextNode: boolean,
textRect: ClientRect | undefined,
boundingRect: ClientRect
): string {
if (isTextNode) {
if (textRect) {
// calculate pixels to center of text from left edge of bounding box
const relativeCenterX = textRect.left + textRect.width / 2 - boundingRect.left
const relativeCenterY = textRect.top + textRect.height / 2 - boundingRect.top
return relativeCenterX + 'px ' + relativeCenterY + 'px'
}
}
return '0 0' // top left
}
function getTextDx (
oldTextRect: ClientRect | undefined,
newTextRect: ClientRect | undefined
): number {
if (oldTextRect && newTextRect) {
return ((oldTextRect.left + oldTextRect.width / 2) - (newTextRect.left + newTextRect.width / 2))
}
return 0
}
function getTextDy (
oldTextRect: ClientRect | undefined,
newTextRect: ClientRect | undefined
): number {
if (oldTextRect && newTextRect) {
return ((oldTextRect.top + oldTextRect.height / 2) - (newTextRect.top + newTextRect.height / 2))
}
return 0
}
function isTextElement (elm: Element | Text): elm is Text {
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3
}
let removed: any, created: any
function pre () {
removed = {}
created = []
}
function create (oldVnode: VNode, vnode: VNode): void {
const hero = (vnode.data as VNodeData).hero
if (hero && hero.id) {
created.push(hero.id)
created.push(vnode)
}
}
function destroy (vnode: VNode): void {
const hero = (vnode.data as VNodeData).hero
if (hero && hero.id) {
const elm = vnode.elm;
(vnode as any).isTextNode = isTextElement(elm as Element | Text); // is this a text node?
(vnode as any).boundingRect = (elm as Element).getBoundingClientRect(); // save the bounding rectangle to a new property on the vnode
(vnode as any).textRect = (vnode as any).isTextNode ? getTextNodeRect((elm as Element).childNodes[0] as Text) : null // save bounding rect of inner text node
const computedStyle = window.getComputedStyle(elm as Element, undefined); // get current styles (includes inherited properties)
(vnode as any).savedStyle = JSON.parse(JSON.stringify(computedStyle)) // save a copy of computed style values
removed[hero.id] = vnode
}
}
function post () {
let i: number, id: any, newElm: Element, oldVnode: VNode, oldElm: Element,
hRatio: number, wRatio: number,
oldRect: ClientRect, newRect: ClientRect, dx: number, dy: number,
origTransform: string | null, origTransition: string | null,
newStyle: CSSStyleDeclaration, oldStyle: CSSStyleDeclaration,
newComputedStyle: CSSStyleDeclaration, isTextNode: boolean,
newTextRect: ClientRect | undefined, oldTextRect: ClientRect | undefined
for (i = 0; i < created.length; i += 2) {
id = created[i]
newElm = created[i + 1].elm
oldVnode = removed[id]
if (oldVnode) {
isTextNode = (oldVnode as any).isTextNode && isTextElement(newElm) // Are old & new both text?
newStyle = (newElm as HTMLElement).style
newComputedStyle = window.getComputedStyle(newElm, undefined) // get full computed style for new element
oldElm = oldVnode.elm as Element
oldStyle = (oldElm as HTMLElement).style
// Overall element bounding boxes
newRect = newElm.getBoundingClientRect()
oldRect = (oldVnode as any).boundingRect // previously saved bounding rect
// Text node bounding boxes & distances
if (isTextNode) {
newTextRect = getTextNodeRect(newElm.childNodes[0] as Text)
oldTextRect = (oldVnode as any).textRect
dx = getTextDx(oldTextRect, newTextRect)
dy = getTextDy(oldTextRect, newTextRect)
} else {
// Calculate distances between old & new positions
dx = oldRect.left - newRect.left
dy = oldRect.top - newRect.top
}
hRatio = newRect.height / (Math.max(oldRect.height, 1))
wRatio = isTextNode ? hRatio : newRect.width / (Math.max(oldRect.width, 1)) // text scales based on hRatio
// Animate new element
origTransform = newStyle.transform
origTransition = newStyle.transition
if (newComputedStyle.display === 'inline') {
// inline elements cannot be transformed
newStyle.display = 'inline-block' // this does not appear to have any negative side effects
}
newStyle.transition = origTransition + 'transform 0s'
newStyle.transformOrigin = calcTransformOrigin(isTextNode, newTextRect, newRect)
newStyle.opacity = '0'
newStyle.transform = origTransform +
'translate(' + dx + 'px, ' + dy + 'px) ' +
'scale(' + 1 / wRatio + ', ' + 1 / hRatio + ')'
setNextFrame(newStyle, 'transition', origTransition)
setNextFrame(newStyle, 'transform', origTransform)
setNextFrame(newStyle, 'opacity', '1')
// Animate old element
for (const key in (oldVnode as any).savedStyle) { // re-apply saved inherited properties
if (String(parseInt(key)) !== key) {
const ms = key.substring(0, 2) === 'ms'
const moz = key.substring(0, 3) === 'moz'
const webkit = key.substring(0, 6) === 'webkit'
if (!ms && !moz && !webkit) {
// ignore prefixed style properties
(oldStyle as any)[key] = (oldVnode as any).savedStyle[key]
}
}
}
oldStyle.position = 'absolute'
oldStyle.top = oldRect.top + 'px' // start at existing position
oldStyle.left = oldRect.left + 'px'
oldStyle.width = oldRect.width + 'px' // Needed for elements who were sized relative to their parents
oldStyle.height = oldRect.height + 'px' // Needed for elements who were sized relative to their parents
oldStyle.margin = '0' // Margin on hero element leads to incorrect positioning
oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect)
oldStyle.transform = ''
oldStyle.opacity = '1'
document.body.appendChild(oldElm)
setNextFrame(oldStyle, 'transform', 'translate(' + -dx + 'px, ' + -dy + 'px) scale(' + wRatio + ', ' + hRatio + ')') // scale must be on far right for translate to be correct
setNextFrame(oldStyle, 'opacity', '0')
oldElm.addEventListener('transitionend', function (ev: TransitionEvent) {
if (ev.propertyName === 'transform') {
document.body.removeChild(ev.target as Node)
}
})
}
}
removed = created = undefined
}
export const heroModule: Module = { pre, create, destroy, post }

@ -1,10 +0,0 @@
import { PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook } from '../hooks'
export type Module = Partial<{
pre: PreHook
create: CreateHook
update: UpdateHook
destroy: DestroyHook
remove: RemoveHook
post: PostHook
}>

@ -1,28 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type Props = Record<string, any>
function updateProps (oldVnode: VNode, vnode: VNode): void {
let key: string
let cur: any
let old: any
const elm = vnode.elm
let oldProps = (oldVnode.data as VNodeData).props
let props = (vnode.data as VNodeData).props
if (!oldProps && !props) return
if (oldProps === props) return
oldProps = oldProps || {}
props = props || {}
for (key in props) {
cur = props[key]
old = oldProps[key]
if (old !== cur && (key !== 'value' || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur
}
}
}
export const propsModule: Module = { create: updateProps, update: updateProps }

@ -1,118 +0,0 @@
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type VNodeStyle = Record<string, string> & {
delayed?: Record<string, string>
remove?: Record<string, string>
}
// Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
const raf = (typeof window !== 'undefined' && (window.requestAnimationFrame).bind(window)) || setTimeout
const nextFrame = function (fn: any) {
raf(function () {
raf(fn)
})
}
let reflowForced = false
function setNextFrame (obj: any, prop: string, val: any): void {
nextFrame(function () {
obj[prop] = val
})
}
function updateStyle (oldVnode: VNode, vnode: VNode): void {
let cur: any
let name: string
const elm = vnode.elm
let oldStyle = (oldVnode.data as VNodeData).style
let style = (vnode.data as VNodeData).style
if (!oldStyle && !style) return
if (oldStyle === style) return
oldStyle = oldStyle || {}
style = style || {}
const oldHasDel = 'delayed' in oldStyle
for (name in oldStyle) {
if (!style[name]) {
if (name[0] === '-' && name[1] === '-') {
(elm as any).style.removeProperty(name)
} else {
(elm as any).style[name] = ''
}
}
}
for (name in style) {
cur = style[name]
if (name === 'delayed' && style.delayed) {
for (const name2 in style.delayed) {
cur = style.delayed[name2]
if (!oldHasDel || cur !== (oldStyle.delayed as any)[name2]) {
setNextFrame((elm as any).style, name2, cur)
}
}
} else if (name !== 'remove' && cur !== oldStyle[name]) {
if (name[0] === '-' && name[1] === '-') {
(elm as any).style.setProperty(name, cur)
} else {
(elm as any).style[name] = cur
}
}
}
}
function applyDestroyStyle (vnode: VNode): void {
let style: any
let name: string
const elm = vnode.elm
const s = (vnode.data as VNodeData).style
if (!s || !(style = s.destroy)) return
for (name in style) {
(elm as any).style[name] = style[name]
}
}
function applyRemoveStyle (vnode: VNode, rm: () => void): void {
const s = (vnode.data as VNodeData).style
if (!s || !s.remove) {
rm()
return
}
if (!reflowForced) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(vnode.elm as any).offsetLeft
reflowForced = true
}
let name: string
const elm = vnode.elm
let i = 0
const style = s.remove
let amount = 0
const applied: string[] = []
for (name in style) {
applied.push(name);
(elm as any).style[name] = style[name]
}
const compStyle = getComputedStyle(elm as Element)
const props = (compStyle as any)['transition-property'].split(', ')
for (; i < props.length; ++i) {
if (applied.indexOf(props[i]) !== -1) amount++
}
(elm as Element).addEventListener('transitionend', function (ev: TransitionEvent) {
if (ev.target === elm) --amount
if (amount === 0) rm()
})
}
function forceReflow () {
reflowForced = false
}
export const styleModule: Module = {
pre: forceReflow,
create: updateStyle,
update: updateStyle,
destroy: applyDestroyStyle,
remove: applyRemoveStyle
}

@ -1,64 +0,0 @@
import { VNode, VNodeData } from './vnode'
import { h } from './h'
export interface ThunkData extends VNodeData {
fn: () => VNode
args: any[]
}
export interface Thunk extends VNode {
data: ThunkData
}
export interface ThunkFn {
(sel: string, fn: Function, args: any[]): Thunk
(sel: string, key: any, fn: Function, args: any[]): Thunk
}
function copyToThunk (vnode: VNode, thunk: VNode): void {
(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args
thunk.data = vnode.data
thunk.children = vnode.children
thunk.text = vnode.text
thunk.elm = vnode.elm
}
function init (thunk: VNode): void {
const cur = thunk.data as VNodeData
const vnode = (cur.fn as any).apply(undefined, cur.args)
copyToThunk(vnode, thunk)
}
function prepatch (oldVnode: VNode, thunk: VNode): void {
let i: number
const old = oldVnode.data as VNodeData
const cur = thunk.data as VNodeData
const oldArgs = old.args
const args = cur.args
if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) {
copyToThunk((cur.fn as any).apply(undefined, args), thunk)
return
}
for (i = 0; i < (args as any).length; ++i) {
if ((oldArgs as any)[i] !== (args as any)[i]) {
copyToThunk((cur.fn as any).apply(undefined, args), thunk)
return
}
}
copyToThunk(oldVnode, thunk)
}
export const thunk = function thunk (sel: string, key?: any, fn?: any, args?: any): VNode {
if (args === undefined) {
args = fn
fn = key
key = undefined
}
return h(sel, {
key: key,
hook: { init, prepatch },
fn: fn,
args: args
})
} as ThunkFn

@ -1,37 +0,0 @@
import { vnode, VNode } from './vnode'
import { htmlDomApi, DOMAPI } from './htmldomapi'
export function toVNode (node: Node, domApi?: DOMAPI): VNode {
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
let text: string
if (api.isElement(node)) {
const id = node.id ? '#' + node.id : ''
const cn = node.getAttribute('class')
const c = cn ? '.' + cn.split(' ').join('.') : ''
const sel = api.tagName(node).toLowerCase() + id + c
const attrs: any = {}
const children: VNode[] = []
let name: string
let i: number, n: number
const elmAttrs = node.attributes
const elmChildren = node.childNodes
for (i = 0, n = elmAttrs.length; i < n; i++) {
name = elmAttrs[i].nodeName
if (name !== 'id' && name !== 'class') {
attrs[name] = elmAttrs[i].nodeValue
}
}
for (i = 0, n = elmChildren.length; i < n; i++) {
children.push(toVNode(elmChildren[i], domApi))
}
return vnode(sel, { attrs }, children, undefined, node)
} else if (api.isText(node)) {
text = api.getTextContent(node) as string
return vnode(undefined, undefined, undefined, text, node)
} else if (api.isComment(node)) {
text = api.getTextContent(node) as string
return vnode('!', {}, [], text, node as any)
} else {
return vnode('', {}, [], undefined, node as any)
}
}

@ -1,47 +0,0 @@
import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'
export type Key = string | number
export interface VNode {
sel: string | undefined
data: VNodeData | undefined
children: Array<VNode | string> | undefined
elm: Node | undefined
text: string | undefined
key: Key | undefined
}
export interface VNodeData {
props?: Props
attrs?: Attrs
class?: Classes
style?: VNodeStyle
dataset?: Dataset
on?: On
hero?: Hero
attachData?: AttachData
hook?: Hooks
key?: Key
ns?: string // for SVGs
fn?: () => VNode // for thunks
args?: any[] // for thunks
is?: string // for custom elements v1
[key: string]: any // for any other 3rd party module
}
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}

@ -1,160 +0,0 @@
import 'core-js/stable/array/fill.js'
import faker from 'faker'
import { VNode } from '../../package/vnode'
import { h } from '../../package/h'
import { init as curInit } from '../../package/init'
import { init as refInit } from 'latest-snabbdom-release/init'
import { assert } from 'chai'
import pReduce from 'p-reduce'
import pMapSeries from 'p-map-series'
import { std, mean } from 'mathjs'
const RUNS = 5
const PATCHES_PER_RUN = 100
const WARM_UP_RUNS = 1
const REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES = 1
const BENCHMARK_TIMEOUT_MINUTES = 10
const REQUIRED_PRECISION = 0.02
/* eslint-disable @typescript-eslint/no-unused-vars */
declare global {
// eslint-disable-next-line @typescript-eslint/naming-convention
const __karma__: {
info(info: unknown): void
}
}
/* eslint-enable @typescript-eslint/no-unused-vars */
const ALLOWED_REGRESSION = 0.03
describe('core benchmark', () => {
it('does not regress', async function Benchmark () {
this.timeout(BENCHMARK_TIMEOUT_MINUTES * 1000 * 60)
faker.seed(0)
const inputs = Array(PATCHES_PER_RUN).fill(null).map(() => {
return new Array(faker.random.number(20))
.fill(null)
.map(() => ({
name: faker.company.companyName(),
catchPhrase: faker.company.catchPhrase(),
suffix: faker.company.companySuffix(),
products: Array(faker.random.number(3))
.fill(null)
.map(() => ({
name: faker.commerce.productName(),
color: faker.commerce.color(),
price: faker.commerce.price() + faker.finance.currencySymbol(),
})),
founded: faker.date.past()
}))
})
type Input = (typeof inputs)[0]
const view = (companies: Input): VNode => h('table', [
h('caption', ['Companies']),
h('thead', [
h('tr', [
'Details',
'Products',
].map((th) => h('th', [th])))
]),
h('tbody', companies.map(function companyView (company) {
return h('tr', [
h('td', [
h('div', [
h('b', [company.name]),
company.suffix && `\xa0${company.suffix}`
]),
h('div', h('i', [company.catchPhrase])),
h('td', [
h('dt', ['Founded']),
h('dd', [company.founded.toLocaleDateString()])
])
]),
h('td', [h('ul', company.products.map(function productView (product) {
return h('li', [h('dl', [
h('dt', ['Name']),
h('dd', [product.name]),
h('dt', ['Color']),
h('dd', [product.color]),
h('dt', ['Price']),
h('dd', [product.price]),
])])
}))])
])
}))
])
type Patcher = ReturnType<typeof refInit | typeof curInit>
interface SingleRunResult {
i: number
cur: number
ref: number
}
const subjectToResult = async (subject: Patcher, subjectId: string): Promise<number> => {
await new Promise((resolve) => {
requestAnimationFrame(resolve)
})
const markName = `mark:${subjectId}`
const measureName = `measure:${subjectId}`
performance.mark(markName)
const lastVnode = await pReduce(
inputs,
async function subjectToResultReducer (acc: HTMLElement | VNode, input, i) {
const vnode = view(input)
subject(acc, vnode)
if (i % REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES === 0) {
await new Promise((resolve) => {
requestAnimationFrame(resolve)
})
}
return vnode
},
document.body.appendChild(document.createElement('section')),
)
performance.measure(measureName, markName)
if (!('elm' in lastVnode)) throw new Error()
if (!lastVnode.elm) throw new Error()
document.body.removeChild(lastVnode.elm)
const measure = performance.getEntriesByName(measureName)[0]
performance.clearMarks(markName)
performance.clearMeasures(measureName)
return measure.duration
}
const singleRun = async (_: null, runI: number): Promise<SingleRunResult> => {
const cur = await subjectToResult(curInit([]), `cur:${runI}`)
const ref = await subjectToResult(refInit([]), `ref:${runI}`)
return { i: runI, cur, ref }
}
const runResults = (await pMapSeries(Array(RUNS + WARM_UP_RUNS).fill(null), singleRun))
.slice(WARM_UP_RUNS)
__karma__.info({ benchmark: runResults })
const results = {
ref: runResults.map((result) => result.ref),
cur: runResults.map((result) => result.cur),
}
const means = {
ref: mean(results.ref),
cur: mean(results.cur),
}
const stds = {
ref: std(results.ref, 'uncorrected'),
cur: std(results.cur, 'uncorrected'),
}
;(['ref', 'cur'] as const).forEach((subject) => {
const stdRatio = stds[subject] / means[subject]
assert.isAtMost(stdRatio, REQUIRED_PRECISION, `${subject} not precise enough`)
})
assert.isAtMost(means.cur, means.ref * (1 + ALLOWED_REGRESSION))
})
})

@ -1,40 +0,0 @@
{
"extends": "tsconfigs/browser-executable",
"references": [
{ "path": "../package/tsconfig.json" }
],
"compilerOptions": {
"types": [
"mocha"
],
"strictFunctionTypes": true,
"target": "ES2015",
"outDir": "../../build/test",
"baseUrl": "../..",
"paths": {
"latest-snabbdom-release/*": ["node_modules/latest-snabbdom-release/build/package/*"]
},
"plugins": [
{
"transform": "../ts-transform-js-extension.cjs",
"import": "transform",
"after": true,
"type": "raw"
}
],
"jsx": "react",
"jsxFactory": "jsx"
},
"files": [
"benchmark/core.ts",
"unit/attachto.ts",
"unit/attributes.ts",
"unit/core.ts",
"unit/dataset.ts",
"unit/eventlisteners.ts",
"unit/htmldomapi.ts",
"unit/jsx.tsx",
"unit/style.ts",
"unit/thunk.ts"
]
}

@ -1,100 +0,0 @@
import { assert } from 'chai'
import { init } from '../../package/init'
import { RemoveHook } from '../../package/hooks'
import { attachTo } from '../../package/helpers/attachto'
import { h } from '../../package/h'
const patch = init([])
describe('attachTo', function () {
let elm: any, vnode0: any
beforeEach(function () {
elm = document.createElement('div')
vnode0 = elm
})
it('adds element to target', function () {
const vnode1 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
attachTo(elm, h('div#attached', 'Test')),
]),
])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.children.length, 2)
})
it('updates element at target', function () {
const vnode1 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
attachTo(elm, h('div#attached', 'First text')),
]),
])
const vnode2 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
attachTo(elm, h('div#attached', 'New text')),
]),
])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.children[0].innerHTML, 'First text')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.children[0].innerHTML, 'New text')
})
it('element can be inserted before modal', function () {
const vnode1 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
attachTo(elm, h('div#attached', 'Text')),
]),
])
const vnode2 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
h('div', 'A new element'),
attachTo(elm, h('div#attached', 'Text')),
]),
])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.children[0].innerHTML, 'Text')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.children[0].innerHTML, 'Text')
})
it('removes element at target', function () {
const vnode1 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
attachTo(elm, h('div#attached', 'First text')),
]),
])
const vnode2 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
]),
])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.children[0].innerHTML, 'First text')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.children.length, 1)
})
it('remove hook receives real element', function () {
const rm: RemoveHook = (vnode, cb) => {
const elm = vnode.elm as HTMLDivElement
assert.strictEqual(elm.tagName, 'DIV')
assert.strictEqual(elm.innerHTML, 'First text')
cb()
}
const vnode1 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
attachTo(elm, h('div#attached', { hook: { remove: rm } }, 'First text')),
]),
])
const vnode2 = h('div', [
h('div#wrapper', [
h('div', 'Some element'),
]),
])
elm = patch(vnode0, vnode1).elm
elm = patch(vnode1, vnode2).elm
})
})

@ -1,98 +0,0 @@
import { assert } from 'chai'
import { init } from '../../package/init'
import { attributesModule } from '../../package/modules/attributes'
import { h } from '../../package/h'
const patch = init([
attributesModule
])
describe('attributes', function () {
let elm: any, vnode0: any
beforeEach(function () {
elm = document.createElement('div')
vnode0 = elm
})
it('have their provided values', function () {
const vnode1 = h('div', { attrs: { href: '/foo', minlength: 1, selected: true, disabled: false } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.getAttribute('href'), '/foo')
assert.strictEqual(elm.getAttribute('minlength'), '1')
assert.strictEqual(elm.hasAttribute('selected'), true)
assert.strictEqual(elm.getAttribute('selected'), '')
assert.strictEqual(elm.hasAttribute('disabled'), false)
})
it('can be memoized', function () {
const cachedAttrs = { href: '/foo', minlength: 1, selected: true }
const vnode1 = h('div', { attrs: cachedAttrs })
const vnode2 = h('div', { attrs: cachedAttrs })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.getAttribute('href'), '/foo')
assert.strictEqual(elm.getAttribute('minlength'), '1')
assert.strictEqual(elm.getAttribute('selected'), '')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.getAttribute('href'), '/foo')
assert.strictEqual(elm.getAttribute('minlength'), '1')
assert.strictEqual(elm.getAttribute('selected'), '')
})
it('are not omitted when falsy values are provided', function () {
const vnode1 = h('div', { attrs: { href: null as any, minlength: 0, value: '', title: 'undefined' } })
elm = patch(vnode0, vnode1).elm
assert.ok(elm.hasAttribute('href'))
assert.ok(elm.hasAttribute('minlength'))
assert.ok(elm.hasAttribute('value'))
assert.ok(elm.hasAttribute('title'))
})
it('are set correctly when namespaced', function () {
const vnode1 = h('div', { attrs: { 'xlink:href': '#foo' } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#foo')
})
it('should not touch class nor id fields', function () {
elm = document.createElement('div')
elm.id = 'myId'
elm.className = 'myClass'
vnode0 = elm
const vnode1 = h('div#myId.myClass', { attrs: {} }, ['Hello'])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.tagName, 'DIV')
assert.strictEqual(elm.id, 'myId')
assert.strictEqual(elm.className, 'myClass')
assert.strictEqual(elm.textContent, 'Hello')
})
describe('boolean attribute', function () {
it('is present and empty string if the value is truthy', function () {
const vnode1 = h('div', { attrs: { required: true, readonly: 1, noresize: 'truthy' } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.hasAttribute('required'), true)
assert.strictEqual(elm.getAttribute('required'), '')
assert.strictEqual(elm.hasAttribute('readonly'), true)
assert.strictEqual(elm.getAttribute('readonly'), '1')
assert.strictEqual(elm.hasAttribute('noresize'), true)
assert.strictEqual(elm.getAttribute('noresize'), 'truthy')
})
it('is omitted if the value is false', function () {
const vnode1 = h('div', { attrs: { required: false } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.hasAttribute('required'), false)
assert.strictEqual(elm.getAttribute('required'), null)
})
it('is not omitted if the value is falsy', function () {
const vnode1 = h('div', { attrs: { readonly: 0, noresize: null as any } })
elm = patch(vnode0, vnode1).elm
assert.ok(elm.hasAttribute('readonly'))
assert.ok(elm.hasAttribute('noresize'))
})
})
describe('Object.prototype property', function () {
it('is not considered as a boolean attribute and shouldn\'t be omitted', function () {
const vnode1 = h('div', { attrs: { constructor: true } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.hasAttribute('constructor'), true)
assert.strictEqual(elm.getAttribute('constructor'), '')
const vnode2 = h('div', { attrs: { constructor: false } })
elm = patch(vnode0, vnode2).elm
assert.strictEqual(elm.hasAttribute('constructor'), false)
})
})
})

File diff suppressed because it is too large Load Diff

@ -1,59 +0,0 @@
import { assert } from 'chai'
import { datasetModule } from '../../package/modules/dataset'
import { init } from '../../package/init'
import { h } from '../../package/h'
const patch = init([
datasetModule
])
describe('dataset', function () {
before(function () {
if (!Object.hasOwnProperty.call(HTMLElement.prototype, 'dataset')) {
this.skip()
}
})
let elm: any, vnode0: any
beforeEach(function () {
elm = document.createElement('div')
vnode0 = elm
})
it('is set on initial element creation', function () {
elm = patch(vnode0, h('div', { dataset: { foo: 'foo' } })).elm
assert.strictEqual(elm.dataset.foo, 'foo')
})
it('updates dataset', function () {
const vnode1 = h('i', { dataset: { foo: 'foo', bar: 'bar' } })
const vnode2 = h('i', { dataset: { baz: 'baz' } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.dataset.foo, 'foo')
assert.strictEqual(elm.dataset.bar, 'bar')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.dataset.baz, 'baz')
assert.strictEqual(elm.dataset.foo, undefined)
})
it('can be memoized', function () {
const cachedDataset = { foo: 'foo', bar: 'bar' }
const vnode1 = h('i', { dataset: cachedDataset })
const vnode2 = h('i', { dataset: cachedDataset })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.dataset.foo, 'foo')
assert.strictEqual(elm.dataset.bar, 'bar')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.dataset.foo, 'foo')
assert.strictEqual(elm.dataset.bar, 'bar')
})
it('handles string conversions', function () {
const vnode1 = h('i', { dataset: { empty: '', dash: '-', dashed: 'foo-bar', camel: 'fooBar', integer: 0 as any, float: 0.1 as any } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.dataset.empty, '')
assert.strictEqual(elm.dataset.dash, '-')
assert.strictEqual(elm.dataset.dashed, 'foo-bar')
assert.strictEqual(elm.dataset.camel, 'fooBar')
assert.strictEqual(elm.dataset.integer, '0')
assert.strictEqual(elm.dataset.float, '0.1')
})
})

@ -1,126 +0,0 @@
import { assert } from 'chai'
import { VNode } from '../../package/vnode'
import { init } from '../../package/init'
import { eventListenersModule } from '../../package/modules/eventlisteners'
import { h } from '../../package/h'
const patch = init([
eventListenersModule
])
describe('event listeners', function () {
let elm: any, vnode0: any
beforeEach(function () {
elm = document.createElement('div')
vnode0 = elm
})
it('attaches click event handler to element', function () {
const result = []
function clicked (ev: Event) {
result.push(ev)
}
const vnode = h('div', { on: { click: clicked } }, [
h('a', 'Click my parent'),
])
elm = patch(vnode0, vnode).elm
elm.click()
assert.strictEqual(1, result.length)
})
it('does not attach new listener', function () {
const result: number[] = []
// function clicked(ev) { result.push(ev); }
const vnode1 = h('div', {
on: {
click: function (ev) {
result.push(1)
}
}
}, [
h('a', 'Click my parent'),
])
const vnode2 = h('div', {
on: {
click: function (ev) {
result.push(2)
}
}
}, [
h('a', 'Click my parent'),
])
elm = patch(vnode0, vnode1).elm
elm.click()
elm = patch(vnode1, vnode2).elm
elm.click()
assert.deepEqual(result, [1, 2])
})
it('detach attached click event handler to element', function () {
const result: Event[] = []
function clicked (ev: Event) {
result.push(ev)
}
const vnode1 = h('div', { on: { click: clicked } }, [
h('a', 'Click my parent'),
])
elm = patch(vnode0, vnode1).elm
elm.click()
assert.strictEqual(1, result.length)
const vnode2 = h('div', { on: {} }, [
h('a', 'Click my parent'),
])
elm = patch(vnode1, vnode2).elm
elm.click()
assert.strictEqual(1, result.length)
})
it('multiple event handlers for same event on same element', function () {
let called = 0
function clicked (ev: Event, vnode: VNode) {
++called
// Check that the first argument is an event
assert.strictEqual(true, 'target' in ev)
// Check that the second argument was a vnode
assert.strictEqual(vnode.sel, 'div')
}
const vnode1 = h('div', { on: { click: [clicked, clicked, clicked] } }, [
h('a', 'Click my parent'),
])
elm = patch(vnode0, vnode1).elm
elm.click()
assert.strictEqual(3, called)
const vnode2 = h('div', { on: { click: [clicked, clicked] } }, [
h('a', 'Click my parent'),
])
elm = patch(vnode1, vnode2).elm
elm.click()
assert.strictEqual(5, called)
})
it('access to virtual node in event handler', function () {
const result: VNode[] = []
function clicked (this: VNode, ev: Event, vnode: VNode) {
result.push(this)
result.push(vnode)
}
const vnode1 = h('div', { on: { click: clicked } }, [
h('a', 'Click my parent'),
])
elm = patch(vnode0, vnode1).elm
elm.click()
assert.strictEqual(2, result.length)
assert.strictEqual(vnode1, result[0])
assert.strictEqual(vnode1, result[1])
})
it('shared handlers in parent and child nodes', function () {
const result = []
const sharedHandlers = {
click: function (ev: Event) { result.push(ev) }
}
const vnode1 = h('div', { on: sharedHandlers }, [
h('a', { on: sharedHandlers }, 'Click my parent'),
])
elm = patch(vnode0, vnode1).elm
elm.click()
assert.strictEqual(1, result.length)
elm.firstChild.click()
assert.strictEqual(3, result.length)
})
})

@ -1,55 +0,0 @@
import { assert } from 'chai'
import { init } from '../../package/init'
import { h } from '../../package/h'
import { attributesModule } from '../../package/modules/attributes'
const patch = init([
attributesModule
])
describe('svg', function () {
let elm: any, vnode0: any
beforeEach(function () {
elm = document.createElement('svg')
vnode0 = elm
})
it('removes child svg elements', function () {
const a = h('svg', {}, [
h('g'),
h('g')
])
const b = h('svg', {}, [
h('g')
])
const result = patch(patch(vnode0, a), b).elm as SVGElement
assert.strictEqual(result.childNodes.length, 1)
})
it('adds correctly xlink namespaced attribute', function () {
const xlinkNS = 'http://www.w3.org/1999/xlink'
const testUrl = '/test'
const a = h('svg', {}, [
h('use', {
attrs: { 'xlink:href': testUrl }
}, [])
])
const result = patch(vnode0, a).elm as SVGElement
assert.strictEqual(result.childNodes.length, 1)
const child = result.childNodes[0] as SVGUseElement
assert.strictEqual(child.getAttribute('xlink:href'), testUrl)
assert.strictEqual(child.getAttributeNS(xlinkNS, 'href'), testUrl)
})
it('adds correctly xml namespaced attribute', function () {
const xmlNS = 'http://www.w3.org/XML/1998/namespace'
const testAttrValue = 'und'
const a = h('svg', { attrs: { 'xml:lang': testAttrValue } }, [])
const result = patch(vnode0, a).elm as SVGElement
assert.strictEqual(result.getAttributeNS(xmlNS, 'lang'), testAttrValue)
assert.strictEqual(result.getAttribute('xml:lang'), testAttrValue)
})
})

@ -1,170 +0,0 @@
import { assert } from 'chai'
import { init } from '../../package/init'
import { styleModule } from '../../package/modules/style'
import { h } from '../../package/h'
import { toVNode } from '../../package/tovnode'
const patch = init([
styleModule
])
const featureDiscoveryElm = document.createElement('div')
featureDiscoveryElm.style.setProperty('--foo', 'foo')
const hasCssVariables = featureDiscoveryElm.style.getPropertyValue('--foo') === 'foo'
describe('style', function () {
let elm: any, vnode0: any
beforeEach(function () {
elm = document.createElement('div')
vnode0 = elm
})
it('is being styled', function () {
elm = patch(vnode0, h('div', { style: { fontSize: '12px' } })).elm
assert.strictEqual(elm.style.fontSize, '12px')
})
it('can be memoized', function () {
const cachedStyles = { fontSize: '14px', display: 'inline' }
const vnode1 = h('i', { style: cachedStyles })
const vnode2 = h('i', { style: cachedStyles })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.style.fontSize, '14px')
assert.strictEqual(elm.style.display, 'inline')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.style.fontSize, '14px')
assert.strictEqual(elm.style.display, 'inline')
})
it('updates styles', function () {
const vnode1 = h('i', { style: { fontSize: '14px', display: 'inline' } })
const vnode2 = h('i', { style: { fontSize: '12px', display: 'block' } })
const vnode3 = h('i', { style: { fontSize: '10px', display: 'block' } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.style.fontSize, '14px')
assert.strictEqual(elm.style.display, 'inline')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.style.fontSize, '12px')
assert.strictEqual(elm.style.display, 'block')
elm = patch(vnode2, vnode3).elm
assert.strictEqual(elm.style.fontSize, '10px')
assert.strictEqual(elm.style.display, 'block')
})
it('explicialy removes styles', function () {
const vnode1 = h('i', { style: { fontSize: '14px' } })
const vnode2 = h('i', { style: { fontSize: '' } })
const vnode3 = h('i', { style: { fontSize: '10px' } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.style.fontSize, '14px')
patch(vnode1, vnode2)
assert.strictEqual(elm.style.fontSize, '')
patch(vnode2, vnode3)
assert.strictEqual(elm.style.fontSize, '10px')
})
it('implicially removes styles from element', function () {
const vnode1 = h('div', [h('i', { style: { fontSize: '14px' } })])
const vnode2 = h('div', [h('i')])
const vnode3 = h('div', [h('i', { style: { fontSize: '10px' } })])
patch(vnode0, vnode1)
assert.strictEqual(elm.firstChild.style.fontSize, '14px')
patch(vnode1, vnode2)
assert.strictEqual(elm.firstChild.style.fontSize, '')
patch(vnode2, vnode3)
assert.strictEqual(elm.firstChild.style.fontSize, '10px')
})
it('updates css variables', function () {
if (!hasCssVariables) {
this.skip()
} else {
const vnode1 = h('div', { style: { '--myVar': 1 as any } })
const vnode2 = h('div', { style: { '--myVar': 2 as any } })
const vnode3 = h('div', { style: { '--myVar': 3 as any } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '1')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '2')
elm = patch(vnode2, vnode3).elm
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '3')
}
})
it('explicialy removes css variables', function () {
if (!hasCssVariables) {
this.skip()
} else {
const vnode1 = h('i', { style: { '--myVar': 1 as any } })
const vnode2 = h('i', { style: { '--myVar': '' } })
const vnode3 = h('i', { style: { '--myVar': 2 as any } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '1')
patch(vnode1, vnode2)
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '')
patch(vnode2, vnode3)
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '2')
}
})
it('implicially removes css variables from element', function () {
if (!hasCssVariables) {
this.skip()
} else {
const vnode1 = h('div', [h('i', { style: { '--myVar': 1 as any } })])
const vnode2 = h('div', [h('i')])
const vnode3 = h('div', [h('i', { style: { '--myVar': 2 as any } })])
patch(vnode0, vnode1)
assert.strictEqual(elm.firstChild.style.getPropertyValue('--myVar'), '1')
patch(vnode1, vnode2)
assert.strictEqual(elm.firstChild.style.getPropertyValue('--myVar'), '')
patch(vnode2, vnode3)
assert.strictEqual(elm.firstChild.style.getPropertyValue('--myVar'), '2')
}
})
it('updates delayed styles in next frame', function (done) {
const vnode1 = h('i', { style: { fontSize: '14px', delayed: { fontSize: '16px' } as any } })
const vnode2 = h('i', { style: { fontSize: '18px', delayed: { fontSize: '20px' } as any } })
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.style.fontSize, '14px')
requestAnimationFrame(() => {
requestAnimationFrame(() => {
assert.strictEqual(elm.style.fontSize, '16px')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.style.fontSize, '18px')
requestAnimationFrame(() => {
requestAnimationFrame(() => {
assert.strictEqual(elm.style.fontSize, '20px')
done()
})
})
})
})
})
it('applies tranform as transition on remove', function (done) {
const btn = h('button', {
style: {
transition: 'transform 0.1s',
remove: { transform: 'translateY(100%)' } as any
}
}, ['A button'])
const vnode1 = h('div.parent', {}, [btn])
const vnode2 = h('div.parent', {}, [null])
document.body.appendChild(vnode0)
patch(vnode0, vnode1)
patch(vnode1, vnode2)
const button = document.querySelector('button') as HTMLButtonElement
assert.notStrictEqual(button, null)
button.addEventListener('transitionend', function () {
assert.strictEqual(document.querySelector('button'), null)
done()
})
})
describe('using toVNode()', function () {
it('handles (ignoring) comment nodes', function () {
const comment = document.createComment('yolo')
const prevElm = document.createElement('div')
prevElm.appendChild(comment)
const nextVNode = h('div', [h('span', 'Hi')])
elm = patch(toVNode(prevElm), nextVNode).elm
assert.strictEqual(elm, prevElm)
assert.strictEqual(elm.tagName, 'DIV')
assert.strictEqual(elm.childNodes.length, 1)
assert.strictEqual(elm.childNodes[0].tagName, 'SPAN')
assert.strictEqual(elm.childNodes[0].textContent, 'Hi')
})
})
})

@ -1,234 +0,0 @@
import { assert } from 'chai'
import { init } from '../../package/init'
import { h } from '../../package/h'
import { thunk } from '../../package/thunk'
import { VNode } from '../../package/vnode'
const patch = init([
])
describe('thunk', function () {
let elm: any, vnode0: any
beforeEach(function () {
elm = vnode0 = document.createElement('div')
})
it('returns vnode with data and render function', function () {
function numberInSpan (n: number) {
return h('span', 'Number is ' + n)
}
const vnode = thunk('span', 'num', numberInSpan, [22])
assert.deepEqual(vnode.sel, 'span')
assert.deepEqual(vnode.data.key, 'num')
assert.deepEqual(vnode.data.args, [22])
})
it('calls render function once on data change', function () {
let called = 0
function numberInSpan (n: number) {
called++
return h('span', { key: 'num' }, 'Number is ' + n)
}
const vnode1 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
const vnode2 = h('div', [
thunk('span', 'num', numberInSpan, [2])
])
patch(vnode0, vnode1)
assert.strictEqual(called, 1)
patch(vnode1, vnode2)
assert.strictEqual(called, 2)
})
it('does not call render function on data unchanged', function () {
let called = 0
function numberInSpan (n: number) {
called++
return h('span', { key: 'num' }, 'Number is ' + n)
}
const vnode1 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
const vnode2 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
patch(vnode0, vnode1)
assert.strictEqual(called, 1)
patch(vnode1, vnode2)
assert.strictEqual(called, 1)
})
it('calls render function once on data-length change', function () {
let called = 0
function numberInSpan (n: number) {
called++
return h('span', { key: 'num' }, 'Number is ' + n)
}
const vnode1 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
const vnode2 = h('div', [
thunk('span', 'num', numberInSpan, [1, 2])
])
patch(vnode0, vnode1)
assert.strictEqual(called, 1)
patch(vnode1, vnode2)
assert.strictEqual(called, 2)
})
it('calls render function once on function change', function () {
let called = 0
function numberInSpan (n: number) {
called++
return h('span', { key: 'num' }, 'Number is ' + n)
}
function numberInSpan2 (n: number) {
called++
return h('span', { key: 'num' }, 'Number really is ' + n)
}
const vnode1 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
const vnode2 = h('div', [
thunk('span', 'num', numberInSpan2, [1])
])
patch(vnode0, vnode1)
assert.strictEqual(called, 1)
patch(vnode1, vnode2)
assert.strictEqual(called, 2)
})
it('renders correctly', function () {
let called = 0
function numberInSpan (n: number) {
called++
return h('span', { key: 'num' }, 'Number is ' + n)
}
const vnode1 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
const vnode2 = h('div', [
thunk('span', 'num', numberInSpan, [1])
])
const vnode3 = h('div', [
thunk('span', 'num', numberInSpan, [2])
])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 1')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 1')
elm = patch(vnode2, vnode3).elm
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 2')
assert.strictEqual(called, 2)
})
it('supports leaving out the `key` argument', function () {
function vnodeFn (s: string) {
return h('span.number', 'Hello ' + s)
}
const vnode1 = thunk('span.number', vnodeFn, ['World!'])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.innerText, 'Hello World!')
})
it('renders correctly when root', function () {
let called = 0
function numberInSpan (n: number) {
called++
return h('span', { key: 'num' }, 'Number is ' + n)
}
const vnode1 = thunk('span', 'num', numberInSpan, [1])
const vnode2 = thunk('span', 'num', numberInSpan, [1])
const vnode3 = thunk('span', 'num', numberInSpan, [2])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.innerHTML, 'Number is 1')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.innerHTML, 'Number is 1')
elm = patch(vnode2, vnode3).elm
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.innerHTML, 'Number is 2')
assert.strictEqual(called, 2)
})
it('can be replaced and removed', function () {
function numberInSpan (n: number) {
return h('span', { key: 'num' }, 'Number is ' + n)
}
function oddEven (n: number): VNode {
const prefix = (n % 2) === 0 ? 'Even' : 'Odd'
return h('div', { key: oddEven as any }, prefix + ': ' + n)
}
const vnode1 = h('div', [thunk('span', 'num', numberInSpan, [1])])
const vnode2 = h('div', [thunk('div', 'oddEven', oddEven, [4])])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 1')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'div')
assert.strictEqual(elm.firstChild.innerHTML, 'Even: 4')
})
it('can be replaced and removed when root', function () {
function numberInSpan (n: number) {
return h('span', { key: 'num' }, 'Number is ' + n)
}
function oddEven (n: number): VNode {
const prefix = (n % 2) === 0 ? 'Even' : 'Odd'
return h('div', { key: oddEven as any }, prefix + ': ' + n)
}
const vnode1 = thunk('span', 'num', numberInSpan, [1])
const vnode2 = thunk('div', 'oddEven', oddEven, [4])
elm = patch(vnode0, vnode1).elm
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
assert.strictEqual(elm.innerHTML, 'Number is 1')
elm = patch(vnode1, vnode2).elm
assert.strictEqual(elm.tagName.toLowerCase(), 'div')
assert.strictEqual(elm.innerHTML, 'Even: 4')
})
it('invokes destroy hook on thunks', function () {
let called = 0
function destroyHook () {
called++
}
function numberInSpan (n: number) {
return h('span', { key: 'num', hook: { destroy: destroyHook } }, 'Number is ' + n)
}
const vnode1 = h('div', [
h('div', 'Foo'),
thunk('span', 'num', numberInSpan, [1]),
h('div', 'Foo')
])
const vnode2 = h('div', [
h('div', 'Foo'),
h('div', 'Foo')
])
patch(vnode0, vnode1)
patch(vnode1, vnode2)
assert.strictEqual(called, 1)
})
it('invokes remove hook on thunks', function () {
let called = 0
function hook () {
called++
}
function numberInSpan (n: number) {
return h('span', { key: 'num', hook: { remove: hook } }, 'Number is ' + n)
}
const vnode1 = h('div', [
h('div', 'Foo'),
thunk('span', 'num', numberInSpan, [1]),
h('div', 'Foo')
])
const vnode2 = h('div', [
h('div', 'Foo'),
h('div', 'Foo')
])
patch(vnode0, vnode1)
patch(vnode1, vnode2)
assert.strictEqual(called, 1)
})
})

@ -0,0 +1,69 @@
import { VNode, VNodeData } from "./vnode";
import { h } from "./h";
export interface ThunkData extends VNodeData {
fn: () => VNode;
args: any[];
}
export interface Thunk extends VNode {
data: ThunkData;
}
export interface ThunkFn {
(sel: string, fn: (...args: any[]) => any, args: any[]): Thunk;
(sel: string, key: any, fn: (...args: any[]) => any, args: any[]): Thunk;
}
function copyToThunk(vnode: VNode, thunk: VNode): void {
(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args;
thunk.data = vnode.data;
thunk.children = vnode.children;
thunk.text = vnode.text;
thunk.elm = vnode.elm;
}
function init(thunk: VNode): void {
const cur = thunk.data as VNodeData;
const vnode = (cur.fn as any)(...cur.args!);
copyToThunk(vnode, thunk);
}
function prepatch(oldVnode: VNode, thunk: VNode): void {
let i: number;
const old = oldVnode.data as VNodeData;
const cur = thunk.data as VNodeData;
const oldArgs = old.args;
const args = cur.args;
if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) {
copyToThunk((cur.fn as any)(...args!), thunk);
return;
}
for (i = 0; i < (args as any).length; ++i) {
if ((oldArgs as any)[i] !== (args as any)[i]) {
copyToThunk((cur.fn as any)(...args!), thunk);
return;
}
}
copyToThunk(oldVnode, thunk);
}
export const thunk = function thunk(
sel: string,
key?: any,
fn?: any,
args?: any
): VNode {
if (args === undefined) {
args = fn;
fn = key;
key = undefined;
}
return h(sel, {
key: key,
hook: { init, prepatch },
fn: fn,
args: args,
});
} as ThunkFn;

@ -0,0 +1,37 @@
import { vnode, VNode } from "./vnode";
import { htmlDomApi, DOMAPI } from "./htmldomapi";
export function toVNode(node: Node, domApi?: DOMAPI): VNode {
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
let text: string;
if (api.isElement(node)) {
const id = node.id ? "#" + node.id : "";
const cn = node.getAttribute("class");
const c = cn ? "." + cn.split(" ").join(".") : "";
const sel = api.tagName(node).toLowerCase() + id + c;
const attrs: any = {};
const children: VNode[] = [];
let name: string;
let i: number, n: number;
const elmAttrs = node.attributes;
const elmChildren = node.childNodes;
for (i = 0, n = elmAttrs.length; i < n; i++) {
name = elmAttrs[i].nodeName;
if (name !== "id" && name !== "class") {
attrs[name] = elmAttrs[i].nodeValue;
}
}
for (i = 0, n = elmChildren.length; i < n; i++) {
children.push(toVNode(elmChildren[i], domApi));
}
return vnode(sel, { attrs }, children, undefined, node);
} else if (api.isText(node)) {
text = api.getTextContent(node) as string;
return vnode(undefined, undefined, undefined, text, node);
} else if (api.isComment(node)) {
text = api.getTextContent(node) as string;
return vnode("!", {}, [], text, node as any);
} else {
return vnode("", {}, [], undefined, node as any);
}
}

@ -1,32 +0,0 @@
const ts = require('typescript')
module.exports.transform = (ctx) => (sf) => ts.visitNode(sf, (node) => {
const visitor = (node) => {
const originalPath = (
ts.isImportDeclaration(node) ||
ts.isExportDeclaration(node)) &&
node.moduleSpecifier
? node.moduleSpecifier.getText(sf).slice(1, -1)
: (
ts.isImportTypeNode(node) &&
ts.isLiteralTypeNode(node.argument) &&
ts.isStringLiteral(node.argument.literal)
)
? node.argument.literal.text
: null
if (originalPath === null) return node
if (!originalPath.startsWith('.')) return node
const pathWithExtension = originalPath.endsWith('.js')
? originalPath
: originalPath + '.js'
const newNode = ts.getMutableClone(node)
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
newNode.moduleSpecifier = ts.createLiteral(pathWithExtension)
} else if (ts.isImportTypeNode(node)) {
newNode.argument = ts.createLiteralTypeNode(pathWithExtension)
}
return newNode
}
return ts.visitEachChild(node, visitor, ctx)
})

@ -0,0 +1,49 @@
import { Hooks } from "./hooks";
import { AttachData } from "./helpers/attachto";
import { VNodeStyle } from "./modules/style";
import { On } from "./modules/eventlisteners";
import { Attrs } from "./modules/attributes";
import { Classes } from "./modules/class";
import { Props } from "./modules/props";
import { Dataset } from "./modules/dataset";
import { Hero } from "./modules/hero";
export type Key = string | number;
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
hero?: Hero;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: any[]; // for thunks
is?: string; // for custom elements v1
[key: string]: any; // for any other 3rd party module
}
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}

@ -0,0 +1,184 @@
import "core-js/stable/array/fill.js";
import faker from "faker";
import { VNode, h, init as curInit } from "../../src/index";
import { init as refInit } from "latest-snabbdom-release/build/package/init";
import { assert } from "chai";
import pReduce from "p-reduce";
import pMapSeries from "p-map-series";
import { std, mean } from "mathjs";
const RUNS = 5;
const PATCHES_PER_RUN = 100;
const WARM_UP_RUNS = 1;
const REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES = 1;
const BENCHMARK_TIMEOUT_MINUTES = 10;
const REQUIRED_PRECISION = 0.02;
/* eslint-disable @typescript-eslint/no-unused-vars */
declare global {
// eslint-disable-next-line @typescript-eslint/naming-convention
const __karma__: {
info(info: unknown): void;
};
}
/* eslint-enable @typescript-eslint/no-unused-vars */
const ALLOWED_REGRESSION = 0.03;
describe("core benchmark", () => {
it("does not regress", async function Benchmark() {
this.timeout(BENCHMARK_TIMEOUT_MINUTES * 1000 * 60);
faker.seed(0);
const inputs = Array(PATCHES_PER_RUN)
.fill(null)
.map(() => {
return new Array(faker.random.number(20)).fill(null).map(() => ({
name: faker.company.companyName(),
catchPhrase: faker.company.catchPhrase(),
suffix: faker.company.companySuffix(),
products: Array(faker.random.number(3))
.fill(null)
.map(() => ({
name: faker.commerce.productName(),
color: faker.commerce.color(),
price: faker.commerce.price() + faker.finance.currencySymbol(),
})),
founded: faker.date.past(),
}));
});
type Input = typeof inputs[0];
const view = (companies: Input): VNode =>
h("table", [
h("caption", ["Companies"]),
h("thead", [
h(
"tr",
["Details", "Products"].map((th) => h("th", [th]))
),
]),
h(
"tbody",
companies.map(function companyView(company) {
return h("tr", [
h("td", [
h("div", [
h("b", [company.name]),
company.suffix && `\xa0${company.suffix}`,
]),
h("div", h("i", [company.catchPhrase])),
h("td", [
h("dt", ["Founded"]),
h("dd", [company.founded.toLocaleDateString()]),
]),
]),
h("td", [
h(
"ul",
company.products.map(function productView(product) {
return h("li", [
h("dl", [
h("dt", ["Name"]),
h("dd", [product.name]),
h("dt", ["Color"]),
h("dd", [product.color]),
h("dt", ["Price"]),
h("dd", [product.price]),
]),
]);
})
),
]),
]);
})
),
]);
type Patcher = ReturnType<typeof refInit | typeof curInit>;
interface SingleRunResult {
i: number;
cur: number;
ref: number;
}
const subjectToResult = async (
subject: Patcher,
subjectId: string
): Promise<number> => {
await new Promise((resolve) => {
requestAnimationFrame(resolve);
});
const markName = `mark:${subjectId}`;
const measureName = `measure:${subjectId}`;
performance.mark(markName);
const lastVnode = await pReduce(
inputs,
async function subjectToResultReducer(
acc: HTMLElement | VNode,
input,
i
) {
const vnode = view(input);
subject(acc, vnode);
if (i % REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES === 0) {
await new Promise((resolve) => {
requestAnimationFrame(resolve);
});
}
return vnode;
},
document.body.appendChild(document.createElement("section"))
);
performance.measure(measureName, markName);
if (!("elm" in lastVnode)) throw new Error();
if (!lastVnode.elm) throw new Error();
document.body.removeChild(lastVnode.elm);
const measure = performance.getEntriesByName(measureName)[0];
performance.clearMarks(markName);
performance.clearMeasures(measureName);
return measure.duration;
};
const singleRun = async (
_: null,
runI: number
): Promise<SingleRunResult> => {
const cur = await subjectToResult(curInit([]), `cur:${runI}`);
const ref = await subjectToResult(refInit([]), `ref:${runI}`);
return { i: runI, cur, ref };
};
const runResults = (
await pMapSeries(Array(RUNS + WARM_UP_RUNS).fill(null), singleRun)
).slice(WARM_UP_RUNS);
__karma__.info({ benchmark: runResults });
const results = {
ref: runResults.map((result) => result.ref),
cur: runResults.map((result) => result.cur),
};
const means = {
ref: mean(results.ref),
cur: mean(results.cur),
};
const stds = {
ref: std(results.ref, "uncorrected"),
cur: std(results.cur, "uncorrected"),
};
(["ref", "cur"] as const).forEach((subject) => {
const stdRatio = stds[subject] / means[subject];
assert.isAtMost(
stdRatio,
REQUIRED_PRECISION,
`${subject} not precise enough`
);
});
assert.isAtMost(means.cur, means.ref * (1 + ALLOWED_REGRESSION));
});
});

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"jsxFactory": "jsx"
},
"include": ["./**/*.ts", "./unit/*.tsx", "../src/index.ts"]
}

@ -0,0 +1,93 @@
import { assert } from "chai";
import { init, RemoveHook, attachTo, h } from "../../src/index";
const patch = init([]);
describe("attachTo", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("div");
vnode0 = elm;
});
it("adds element to target", function () {
const vnode1 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
attachTo(elm, h("div#attached", "Test")),
]),
]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.children.length, 2);
});
it("updates element at target", function () {
const vnode1 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
attachTo(elm, h("div#attached", "First text")),
]),
]);
const vnode2 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
attachTo(elm, h("div#attached", "New text")),
]),
]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.children[0].innerHTML, "First text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children[0].innerHTML, "New text");
});
it("element can be inserted before modal", function () {
const vnode1 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
attachTo(elm, h("div#attached", "Text")),
]),
]);
const vnode2 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
h("div", "A new element"),
attachTo(elm, h("div#attached", "Text")),
]),
]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.children[0].innerHTML, "Text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children[0].innerHTML, "Text");
});
it("removes element at target", function () {
const vnode1 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
attachTo(elm, h("div#attached", "First text")),
]),
]);
const vnode2 = h("div", [h("div#wrapper", [h("div", "Some element")])]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.children[0].innerHTML, "First text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 1);
});
it("remove hook receives real element", function () {
const rm: RemoveHook = (vnode, cb) => {
const elm = vnode.elm as HTMLDivElement;
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.innerHTML, "First text");
cb();
};
const vnode1 = h("div", [
h("div#wrapper", [
h("div", "Some element"),
attachTo(
elm,
h("div#attached", { hook: { remove: rm } }, "First text")
),
]),
]);
const vnode2 = h("div", [h("div#wrapper", [h("div", "Some element")])]);
elm = patch(vnode0, vnode1).elm;
elm = patch(vnode1, vnode2).elm;
});
});

@ -0,0 +1,106 @@
import { assert } from "chai";
import { init, attributesModule, h } from "../../src/index";
const patch = init([attributesModule]);
describe("attributes", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("div");
vnode0 = elm;
});
it("have their provided values", function () {
const vnode1 = h("div", {
attrs: { href: "/foo", minlength: 1, selected: true, disabled: false },
});
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.getAttribute("href"), "/foo");
assert.strictEqual(elm.getAttribute("minlength"), "1");
assert.strictEqual(elm.hasAttribute("selected"), true);
assert.strictEqual(elm.getAttribute("selected"), "");
assert.strictEqual(elm.hasAttribute("disabled"), false);
});
it("can be memoized", function () {
const cachedAttrs = { href: "/foo", minlength: 1, selected: true };
const vnode1 = h("div", { attrs: cachedAttrs });
const vnode2 = h("div", { attrs: cachedAttrs });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.getAttribute("href"), "/foo");
assert.strictEqual(elm.getAttribute("minlength"), "1");
assert.strictEqual(elm.getAttribute("selected"), "");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.getAttribute("href"), "/foo");
assert.strictEqual(elm.getAttribute("minlength"), "1");
assert.strictEqual(elm.getAttribute("selected"), "");
});
it("are not omitted when falsy values are provided", function () {
const vnode1 = h("div", {
attrs: { href: null as any, minlength: 0, value: "", title: "undefined" },
});
elm = patch(vnode0, vnode1).elm;
assert.ok(elm.hasAttribute("href"));
assert.ok(elm.hasAttribute("minlength"));
assert.ok(elm.hasAttribute("value"));
assert.ok(elm.hasAttribute("title"));
});
it("are set correctly when namespaced", function () {
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(
elm.getAttributeNS("http://www.w3.org/1999/xlink", "href"),
"#foo"
);
});
it("should not touch class nor id fields", function () {
elm = document.createElement("div");
elm.id = "myId";
elm.className = "myClass";
vnode0 = elm;
const vnode1 = h("div#myId.myClass", { attrs: {} }, ["Hello"]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.id, "myId");
assert.strictEqual(elm.className, "myClass");
assert.strictEqual(elm.textContent, "Hello");
});
describe("boolean attribute", function () {
it("is present and empty string if the value is truthy", function () {
const vnode1 = h("div", {
attrs: { required: true, readonly: 1, noresize: "truthy" },
});
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.hasAttribute("required"), true);
assert.strictEqual(elm.getAttribute("required"), "");
assert.strictEqual(elm.hasAttribute("readonly"), true);
assert.strictEqual(elm.getAttribute("readonly"), "1");
assert.strictEqual(elm.hasAttribute("noresize"), true);
assert.strictEqual(elm.getAttribute("noresize"), "truthy");
});
it("is omitted if the value is false", function () {
const vnode1 = h("div", { attrs: { required: false } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.hasAttribute("required"), false);
assert.strictEqual(elm.getAttribute("required"), null);
});
it("is not omitted if the value is falsy", function () {
const vnode1 = h("div", {
attrs: { readonly: 0, noresize: null as any },
});
elm = patch(vnode0, vnode1).elm;
assert.ok(elm.hasAttribute("readonly"));
assert.ok(elm.hasAttribute("noresize"));
});
});
describe("Object.prototype property", function () {
it("is not considered as a boolean attribute and shouldn't be omitted", function () {
const vnode1 = h("div", { attrs: { constructor: true } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.hasAttribute("constructor"), true);
assert.strictEqual(elm.getAttribute("constructor"), "");
const vnode2 = h("div", { attrs: { constructor: false } });
elm = patch(vnode0, vnode2).elm;
assert.strictEqual(elm.hasAttribute("constructor"), false);
});
});
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,64 @@
import { assert } from "chai";
import { datasetModule, init, h } from "../../src/index";
const patch = init([datasetModule]);
describe("dataset", function () {
before(function () {
if (!Object.hasOwnProperty.call(HTMLElement.prototype, "dataset")) {
this.skip();
}
});
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("div");
vnode0 = elm;
});
it("is set on initial element creation", function () {
elm = patch(vnode0, h("div", { dataset: { foo: "foo" } })).elm;
assert.strictEqual(elm.dataset.foo, "foo");
});
it("updates dataset", function () {
const vnode1 = h("i", { dataset: { foo: "foo", bar: "bar" } });
const vnode2 = h("i", { dataset: { baz: "baz" } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.dataset.foo, "foo");
assert.strictEqual(elm.dataset.bar, "bar");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.dataset.baz, "baz");
assert.strictEqual(elm.dataset.foo, undefined);
});
it("can be memoized", function () {
const cachedDataset = { foo: "foo", bar: "bar" };
const vnode1 = h("i", { dataset: cachedDataset });
const vnode2 = h("i", { dataset: cachedDataset });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.dataset.foo, "foo");
assert.strictEqual(elm.dataset.bar, "bar");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.dataset.foo, "foo");
assert.strictEqual(elm.dataset.bar, "bar");
});
it("handles string conversions", function () {
const vnode1 = h("i", {
dataset: {
empty: "",
dash: "-",
dashed: "foo-bar",
camel: "fooBar",
integer: 0 as any,
float: 0.1 as any,
},
});
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.dataset.empty, "");
assert.strictEqual(elm.dataset.dash, "-");
assert.strictEqual(elm.dataset.dashed, "foo-bar");
assert.strictEqual(elm.dataset.camel, "fooBar");
assert.strictEqual(elm.dataset.integer, "0");
assert.strictEqual(elm.dataset.float, "0.1");
});
});

@ -0,0 +1,125 @@
import { assert } from "chai";
import { VNode, init, eventListenersModule, h } from "../../src/index";
const patch = init([eventListenersModule]);
describe("event listeners", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("div");
vnode0 = elm;
});
it("attaches click event handler to element", function () {
const result = [];
function clicked(ev: Event) {
result.push(ev);
}
const vnode = h("div", { on: { click: clicked } }, [
h("a", "Click my parent"),
]);
elm = patch(vnode0, vnode).elm;
elm.click();
assert.strictEqual(1, result.length);
});
it("does not attach new listener", function () {
const result: number[] = [];
// function clicked(ev) { result.push(ev); }
const vnode1 = h(
"div",
{
on: {
click: function () {
result.push(1);
},
},
},
[h("a", "Click my parent")]
);
const vnode2 = h(
"div",
{
on: {
click: function () {
result.push(2);
},
},
},
[h("a", "Click my parent")]
);
elm = patch(vnode0, vnode1).elm;
elm.click();
elm = patch(vnode1, vnode2).elm;
elm.click();
assert.deepEqual(result, [1, 2]);
});
it("detach attached click event handler to element", function () {
const result: Event[] = [];
function clicked(ev: Event) {
result.push(ev);
}
const vnode1 = h("div", { on: { click: clicked } }, [
h("a", "Click my parent"),
]);
elm = patch(vnode0, vnode1).elm;
elm.click();
assert.strictEqual(1, result.length);
const vnode2 = h("div", { on: {} }, [h("a", "Click my parent")]);
elm = patch(vnode1, vnode2).elm;
elm.click();
assert.strictEqual(1, result.length);
});
it("multiple event handlers for same event on same element", function () {
let called = 0;
function clicked(ev: Event, vnode: VNode) {
++called;
// Check that the first argument is an event
assert.strictEqual(true, "target" in ev);
// Check that the second argument was a vnode
assert.strictEqual(vnode.sel, "div");
}
const vnode1 = h("div", { on: { click: [clicked, clicked, clicked] } }, [
h("a", "Click my parent"),
]);
elm = patch(vnode0, vnode1).elm;
elm.click();
assert.strictEqual(3, called);
const vnode2 = h("div", { on: { click: [clicked, clicked] } }, [
h("a", "Click my parent"),
]);
elm = patch(vnode1, vnode2).elm;
elm.click();
assert.strictEqual(5, called);
});
it("access to virtual node in event handler", function () {
const result: VNode[] = [];
function clicked(this: VNode, ev: Event, vnode: VNode) {
result.push(this);
result.push(vnode);
}
const vnode1 = h("div", { on: { click: clicked } }, [
h("a", "Click my parent"),
]);
elm = patch(vnode0, vnode1).elm;
elm.click();
assert.strictEqual(2, result.length);
assert.strictEqual(vnode1, result[0]);
assert.strictEqual(vnode1, result[1]);
});
it("shared handlers in parent and child nodes", function () {
const result = [];
const sharedHandlers = {
click: function (ev: Event) {
result.push(ev);
},
};
const vnode1 = h("div", { on: sharedHandlers }, [
h("a", { on: sharedHandlers }, "Click my parent"),
]);
elm = patch(vnode0, vnode1).elm;
elm.click();
assert.strictEqual(1, result.length);
elm.firstChild.click();
assert.strictEqual(3, result.length);
});
});

@ -0,0 +1,50 @@
import { assert } from "chai";
import { init, h, attributesModule } from "../../src/index";
const patch = init([attributesModule]);
describe("svg", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("svg");
vnode0 = elm;
});
it("removes child svg elements", function () {
const a = h("svg", {}, [h("g"), h("g")]);
const b = h("svg", {}, [h("g")]);
const result = patch(patch(vnode0, a), b).elm as SVGElement;
assert.strictEqual(result.childNodes.length, 1);
});
it("adds correctly xlink namespaced attribute", function () {
const xlinkNS = "http://www.w3.org/1999/xlink";
const testUrl = "/test";
const a = h("svg", {}, [
h(
"use",
{
attrs: { "xlink:href": testUrl },
},
[]
),
]);
const result = patch(vnode0, a).elm as SVGElement;
assert.strictEqual(result.childNodes.length, 1);
const child = result.childNodes[0] as SVGUseElement;
assert.strictEqual(child.getAttribute("xlink:href"), testUrl);
assert.strictEqual(child.getAttributeNS(xlinkNS, "href"), testUrl);
});
it("adds correctly xml namespaced attribute", function () {
const xmlNS = "http://www.w3.org/XML/1998/namespace";
const testAttrValue = "und";
const a = h("svg", { attrs: { "xml:lang": testAttrValue } }, []);
const result = patch(vnode0, a).elm as SVGElement;
assert.strictEqual(result.getAttributeNS(xmlNS, "lang"), testAttrValue);
assert.strictEqual(result.getAttribute("xml:lang"), testAttrValue);
});
});

@ -1,42 +1,44 @@
import { assert } from 'chai'
// workaround linter issue
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { jsx } from '../../package/jsx'
import '../../package/jsx-global'
import { assert } from "chai";
import { jsx } from "../../src/index";
describe('snabbdom', function () {
describe('jsx', function () {
it('can be used as a jsxFactory method', function () {
const vnode = <div title="Hello World">Hello World</div>
describe("snabbdom", function () {
describe("jsx", function () {
it("can be used as a jsxFactory method", function () {
const vnode = <div title="Hello World">Hello World</div>;
assert.deepStrictEqual(vnode, {
sel: 'div',
data: { title: 'Hello World' },
sel: "div",
data: { title: "Hello World" },
children: undefined,
elm: undefined,
text: 'Hello World',
key: undefined
})
})
text: "Hello World",
key: undefined,
});
});
it('creates text property for text only child', function () {
const vnode = <div>foo bar</div>
it("creates text property for text only child", function () {
const vnode = <div>foo bar</div>;
assert.deepStrictEqual(vnode, {
sel: 'div',
sel: "div",
data: {},
children: undefined,
elm: undefined,
text: 'foo bar',
key: undefined
})
})
text: "foo bar",
key: undefined,
});
});
it('creates an array of children for multiple children', function () {
const vnode = <div>{'foo'}{'bar'}</div>
it("creates an array of children for multiple children", function () {
const vnode = (
<div>
{"foo"}
{"bar"}
</div>
);
assert.deepStrictEqual(vnode, {
sel: 'div',
sel: "div",
data: {},
children: [
{
@ -44,83 +46,85 @@ describe('snabbdom', function () {
data: undefined,
children: undefined,
elm: undefined,
text: 'foo',
key: undefined
text: "foo",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: 'bar',
key: undefined
text: "bar",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined
})
})
key: undefined,
});
});
it('flattens children', function () {
it("flattens children", function () {
const vnode = (
<section>
<h1>A Heading</h1>
some description
{['part1', 'part2'].map(part => <span>{part}</span>)}
{["part1", "part2"].map((part) => (
<span>{part}</span>
))}
</section>
)
);
assert.deepStrictEqual(vnode, {
sel: 'section',
sel: "section",
data: {},
children: [
{
sel: 'h1',
sel: "h1",
data: {},
children: undefined,
elm: undefined,
text: 'A Heading',
key: undefined
text: "A Heading",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: 'some description',
key: undefined
text: "some description",
key: undefined,
},
{
sel: 'span',
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: 'part1',
key: undefined
text: "part1",
key: undefined,
},
{
sel: 'span',
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: 'part2',
key: undefined
text: "part2",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined
})
})
key: undefined,
});
});
it('removes falsey children', function () {
const showLogin = false
const showCaptcha = false
const loginAttempts = 0
const userName = ''
const profilePic = undefined
const isLoggedIn = true
it("removes falsey children", function () {
const showLogin = false;
const showCaptcha = false;
const loginAttempts = 0;
const userName = "";
const profilePic = undefined;
const isLoggedIn = true;
const vnode = (
<div>
Login Form
@ -131,10 +135,10 @@ describe('snabbdom', function () {
Login Attempts: {loginAttempts}
Logged In: {isLoggedIn}
</div>
)
);
assert.deepStrictEqual(vnode, {
sel: 'div',
sel: "div",
data: {},
children: [
{
@ -142,126 +146,130 @@ describe('snabbdom', function () {
data: undefined,
children: undefined,
elm: undefined,
text: 'Login Form',
key: undefined
text: "Login Form",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: 'Login Attempts: ',
key: undefined
text: "Login Attempts: ",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: '0',
key: undefined
text: "0",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: 'Logged In: ',
key: undefined
text: "Logged In: ",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: 'true',
key: undefined
text: "true",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined
})
})
key: undefined,
});
});
it('works with a function component', function () {
it("works with a function component", function () {
// workaround linter issue
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Part = ({ part }: {part: string}) => <span>{part}</span>
const Part = ({ part }: { part: string }) => <span>{part}</span>;
const vnode = (
<div>
<a attrs={{ href: 'https://github.com/snabbdom/snabbdom' }}>Snabbdom</a>
<a attrs={{ href: "https://github.com/snabbdom/snabbdom" }}>
Snabbdom
</a>
and tsx
{['work', 'like', 'a', 'charm!'].map(part => <Part part={part}></Part>)}
{'💃🕺🎉'}
{["work", "like", "a", "charm!"].map((part) => (
<Part part={part}></Part>
))}
{"💃🕺🎉"}
</div>
)
);
assert.deepStrictEqual(vnode, {
sel: 'div',
sel: "div",
data: {},
children: [
{
sel: 'a',
data: { attrs: { href: 'https://github.com/snabbdom/snabbdom' } },
sel: "a",
data: { attrs: { href: "https://github.com/snabbdom/snabbdom" } },
children: undefined,
elm: undefined,
text: 'Snabbdom',
key: undefined
text: "Snabbdom",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: 'and tsx',
key: undefined
text: "and tsx",
key: undefined,
},
{
sel: 'span',
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: 'work',
key: undefined
text: "work",
key: undefined,
},
{
sel: 'span',
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: 'like',
key: undefined
text: "like",
key: undefined,
},
{
sel: 'span',
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: 'a',
key: undefined
text: "a",
key: undefined,
},
{
sel: 'span',
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: 'charm!',
key: undefined
text: "charm!",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: '💃🕺🎉',
key: undefined
text: "💃🕺🎉",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined
})
})
})
})
key: undefined,
});
});
});
});

@ -0,0 +1,174 @@
import { assert } from "chai";
import { init, styleModule, h, toVNode } from "../../src/index";
const patch = init([styleModule]);
const featureDiscoveryElm = document.createElement("div");
featureDiscoveryElm.style.setProperty("--foo", "foo");
const hasCssVariables =
featureDiscoveryElm.style.getPropertyValue("--foo") === "foo";
describe("style", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("div");
vnode0 = elm;
});
it("is being styled", function () {
elm = patch(vnode0, h("div", { style: { fontSize: "12px" } })).elm;
assert.strictEqual(elm.style.fontSize, "12px");
});
it("can be memoized", function () {
const cachedStyles = { fontSize: "14px", display: "inline" };
const vnode1 = h("i", { style: cachedStyles });
const vnode2 = h("i", { style: cachedStyles });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.style.fontSize, "14px");
assert.strictEqual(elm.style.display, "inline");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.style.fontSize, "14px");
assert.strictEqual(elm.style.display, "inline");
});
it("updates styles", function () {
const vnode1 = h("i", { style: { fontSize: "14px", display: "inline" } });
const vnode2 = h("i", { style: { fontSize: "12px", display: "block" } });
const vnode3 = h("i", { style: { fontSize: "10px", display: "block" } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.style.fontSize, "14px");
assert.strictEqual(elm.style.display, "inline");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.style.fontSize, "12px");
assert.strictEqual(elm.style.display, "block");
elm = patch(vnode2, vnode3).elm;
assert.strictEqual(elm.style.fontSize, "10px");
assert.strictEqual(elm.style.display, "block");
});
it("explicialy removes styles", function () {
const vnode1 = h("i", { style: { fontSize: "14px" } });
const vnode2 = h("i", { style: { fontSize: "" } });
const vnode3 = h("i", { style: { fontSize: "10px" } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.style.fontSize, "14px");
patch(vnode1, vnode2);
assert.strictEqual(elm.style.fontSize, "");
patch(vnode2, vnode3);
assert.strictEqual(elm.style.fontSize, "10px");
});
it("implicially removes styles from element", function () {
const vnode1 = h("div", [h("i", { style: { fontSize: "14px" } })]);
const vnode2 = h("div", [h("i")]);
const vnode3 = h("div", [h("i", { style: { fontSize: "10px" } })]);
patch(vnode0, vnode1);
assert.strictEqual(elm.firstChild.style.fontSize, "14px");
patch(vnode1, vnode2);
assert.strictEqual(elm.firstChild.style.fontSize, "");
patch(vnode2, vnode3);
assert.strictEqual(elm.firstChild.style.fontSize, "10px");
});
it("updates css variables", function () {
if (!hasCssVariables) {
this.skip();
} else {
const vnode1 = h("div", { style: { "--myVar": 1 as any } });
const vnode2 = h("div", { style: { "--myVar": 2 as any } });
const vnode3 = h("div", { style: { "--myVar": 3 as any } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "1");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "2");
elm = patch(vnode2, vnode3).elm;
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "3");
}
});
it("explicialy removes css variables", function () {
if (!hasCssVariables) {
this.skip();
} else {
const vnode1 = h("i", { style: { "--myVar": 1 as any } });
const vnode2 = h("i", { style: { "--myVar": "" } });
const vnode3 = h("i", { style: { "--myVar": 2 as any } });
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "1");
patch(vnode1, vnode2);
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "");
patch(vnode2, vnode3);
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "2");
}
});
it("implicially removes css variables from element", function () {
if (!hasCssVariables) {
this.skip();
} else {
const vnode1 = h("div", [h("i", { style: { "--myVar": 1 as any } })]);
const vnode2 = h("div", [h("i")]);
const vnode3 = h("div", [h("i", { style: { "--myVar": 2 as any } })]);
patch(vnode0, vnode1);
assert.strictEqual(elm.firstChild.style.getPropertyValue("--myVar"), "1");
patch(vnode1, vnode2);
assert.strictEqual(elm.firstChild.style.getPropertyValue("--myVar"), "");
patch(vnode2, vnode3);
assert.strictEqual(elm.firstChild.style.getPropertyValue("--myVar"), "2");
}
});
it("updates delayed styles in next frame", function (done) {
const vnode1 = h("i", {
style: { fontSize: "14px", delayed: { fontSize: "16px" } as any },
});
const vnode2 = h("i", {
style: { fontSize: "18px", delayed: { fontSize: "20px" } as any },
});
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.style.fontSize, "14px");
requestAnimationFrame(() => {
requestAnimationFrame(() => {
assert.strictEqual(elm.style.fontSize, "16px");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.style.fontSize, "18px");
requestAnimationFrame(() => {
requestAnimationFrame(() => {
assert.strictEqual(elm.style.fontSize, "20px");
done();
});
});
});
});
});
it("applies tranform as transition on remove", function (done) {
const btn = h(
"button",
{
style: {
transition: "transform 0.1s",
remove: { transform: "translateY(100%)" } as any,
},
},
["A button"]
);
const vnode1 = h("div.parent", {}, [btn]);
const vnode2 = h("div.parent", {}, [null]);
document.body.appendChild(vnode0);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
const button = document.querySelector("button") as HTMLButtonElement;
assert.notStrictEqual(button, null);
button.addEventListener("transitionend", function () {
assert.strictEqual(document.querySelector("button"), null);
done();
});
});
describe("using toVNode()", function () {
it("handles (ignoring) comment nodes", function () {
const comment = document.createComment("yolo");
const prevElm = document.createElement("div");
prevElm.appendChild(comment);
const nextVNode = h("div", [h("span", "Hi")]);
elm = patch(toVNode(prevElm), nextVNode).elm;
assert.strictEqual(elm, prevElm);
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].tagName, "SPAN");
assert.strictEqual(elm.childNodes[0].textContent, "Hi");
});
});
});

@ -0,0 +1,210 @@
import { assert } from "chai";
import { init, h, thunk, VNode } from "../../src/index";
const patch = init([]);
describe("thunk", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = vnode0 = document.createElement("div");
});
it("returns vnode with data and render function", function () {
function numberInSpan(n: number) {
return h("span", `Number is ${n}`);
}
const vnode = thunk("span", "num", numberInSpan, [22]);
assert.deepEqual(vnode.sel, "span");
assert.deepEqual(vnode.data.key, "num");
assert.deepEqual(vnode.data.args, [22]);
});
it("calls render function once on data change", function () {
let called = 0;
function numberInSpan(n: number) {
called++;
return h("span", { key: "num" }, `Number is ${n}`);
}
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [2])]);
patch(vnode0, vnode1);
assert.strictEqual(called, 1);
patch(vnode1, vnode2);
assert.strictEqual(called, 2);
});
it("does not call render function on data unchanged", function () {
let called = 0;
function numberInSpan(n: number) {
called++;
return h("span", { key: "num" }, `Number is ${n}`);
}
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [1])]);
patch(vnode0, vnode1);
assert.strictEqual(called, 1);
patch(vnode1, vnode2);
assert.strictEqual(called, 1);
});
it("calls render function once on data-length change", function () {
let called = 0;
function numberInSpan(n: number) {
called++;
return h("span", { key: "num" }, `Number is ${n}`);
}
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [1, 2])]);
patch(vnode0, vnode1);
assert.strictEqual(called, 1);
patch(vnode1, vnode2);
assert.strictEqual(called, 2);
});
it("calls render function once on function change", function () {
let called = 0;
function numberInSpan(n: number) {
called++;
return h("span", { key: "num" }, `Number is ${n}`);
}
function numberInSpan2(n: number) {
called++;
return h("span", { key: "num" }, `Number really is ${n}`);
}
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode2 = h("div", [thunk("span", "num", numberInSpan2, [1])]);
patch(vnode0, vnode1);
assert.strictEqual(called, 1);
patch(vnode1, vnode2);
assert.strictEqual(called, 2);
});
it("renders correctly", function () {
let called = 0;
function numberInSpan(n: number) {
called++;
return h("span", { key: "num" }, `Number is ${n}`);
}
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode3 = h("div", [thunk("span", "num", numberInSpan, [2])]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
assert.strictEqual(elm.firstChild.innerHTML, "Number is 1");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
assert.strictEqual(elm.firstChild.innerHTML, "Number is 1");
elm = patch(vnode2, vnode3).elm;
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
assert.strictEqual(elm.firstChild.innerHTML, "Number is 2");
assert.strictEqual(called, 2);
});
it("supports leaving out the `key` argument", function () {
function vnodeFn(s: string) {
return h("span.number", "Hello " + s);
}
const vnode1 = thunk("span.number", vnodeFn, ["World!"]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.innerText, "Hello World!");
});
it("renders correctly when root", function () {
let called = 0;
function numberInSpan(n: number) {
called++;
return h("span", { key: "num" }, `Number is ${n}`);
}
const vnode1 = thunk("span", "num", numberInSpan, [1]);
const vnode2 = thunk("span", "num", numberInSpan, [1]);
const vnode3 = thunk("span", "num", numberInSpan, [2]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.tagName.toLowerCase(), "span");
assert.strictEqual(elm.innerHTML, "Number is 1");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.tagName.toLowerCase(), "span");
assert.strictEqual(elm.innerHTML, "Number is 1");
elm = patch(vnode2, vnode3).elm;
assert.strictEqual(elm.tagName.toLowerCase(), "span");
assert.strictEqual(elm.innerHTML, "Number is 2");
assert.strictEqual(called, 2);
});
it("can be replaced and removed", function () {
function numberInSpan(n: number) {
return h("span", { key: "num" }, `Number is ${n}`);
}
function oddEven(n: number): VNode {
const prefix = n % 2 === 0 ? "Even" : "Odd";
return h("div", { key: oddEven as any }, `${prefix}: ${n}`);
}
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
const vnode2 = h("div", [thunk("div", "oddEven", oddEven, [4])]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
assert.strictEqual(elm.firstChild.innerHTML, "Number is 1");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "div");
assert.strictEqual(elm.firstChild.innerHTML, "Even: 4");
});
it("can be replaced and removed when root", function () {
function numberInSpan(n: number) {
return h("span", { key: "num" }, `Number is ${n}`);
}
function oddEven(n: number): VNode {
const prefix = n % 2 === 0 ? "Even" : "Odd";
return h("div", { key: oddEven as any }, `${prefix}: ${n}`);
}
const vnode1 = thunk("span", "num", numberInSpan, [1]);
const vnode2 = thunk("div", "oddEven", oddEven, [4]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.tagName.toLowerCase(), "span");
assert.strictEqual(elm.innerHTML, "Number is 1");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.tagName.toLowerCase(), "div");
assert.strictEqual(elm.innerHTML, "Even: 4");
});
it("invokes destroy hook on thunks", function () {
let called = 0;
function destroyHook() {
called++;
}
function numberInSpan(n: number) {
return h(
"span",
{ key: "num", hook: { destroy: destroyHook } },
`Number is ${n}`
);
}
const vnode1 = h("div", [
h("div", "Foo"),
thunk("span", "num", numberInSpan, [1]),
h("div", "Foo"),
]);
const vnode2 = h("div", [h("div", "Foo"), h("div", "Foo")]);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
assert.strictEqual(called, 1);
});
it("invokes remove hook on thunks", function () {
let called = 0;
function hook() {
called++;
}
function numberInSpan(n: number) {
return h(
"span",
{ key: "num", hook: { remove: hook } },
`Number is ${n}`
);
}
const vnode1 = h("div", [
h("div", "Foo"),
thunk("span", "num", numberInSpan, [1]),
h("div", "Foo"),
]);
const vnode2 = h("div", [h("div", "Foo"), h("div", "Foo")]);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
assert.strictEqual(called, 1);
});
});

@ -1,40 +0,0 @@
const path = require('path')
const isPathInside = require('is-path-inside')
const globby = require('globby')
const outputPath = path.resolve(__dirname, 'build/test')
const makeTestsWebpackConfig = async () => ({
mode: 'development',
target: 'browserslist:defaults',
entry: Object.fromEntries(
(await globby(path.resolve(outputPath, '**/*.js')))
.map((item) => [path.relative(outputPath, item), item])
),
output: {
path: path.resolve(__dirname, 'test-bundles'),
filename: ({ chunk }) => chunk.name
},
module: {
rules: [
{
exclude: (input) => isPathInside(input, path.resolve(__dirname, 'node_modules')),
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
}
]
]
}
}
}
]
}
})
module.exports = makeTestsWebpackConfig

@ -0,0 +1,13 @@
{
"compilerOptions": {
"removeComments": false,
"sourceMap": true,
"declaration": true,
"strict": true,
"strictFunctionTypes": false,
"moduleResolution": "node",
"target": "es6",
"outDir": "build"
},
"include": ["./src/index.ts"]
}
Loading…
Cancel
Save