${state.title}
- List of ${state.paragraphs.length} paragraphs: -- ${
- state.paragraphs
- .map(p => `
- ${p.title} `) - }
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..943f5e9f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # not working due missing www. +open_collective: hyperHTML +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: https://www.patreon.com/webreflection diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..58f187be --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 00000000..73cf8d65 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,31 @@ +# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: build + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - run: npm test + - run: npm run coverage --if-present + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 58b805fe..3cb01d4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store -node_modules/ \ No newline at end of file +node_modules/ +coverage/ + diff --git a/.npmignore b/.npmignore index f4794d23..2b02e166 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,14 @@ +coverage/* +esm/.eslintrc +logo/* node_modules/* test/* +_config.yml .DS_Store .gitignore -.travis.yml \ No newline at end of file +.travis.yml +.github/ISSUE_TEMPLATE.md +babel-plugins.json +CHANGELOG.md +package-lock.json +rollup.config.js diff --git a/.travis.yml b/.travis.yml index 85e9c12e..cc127136 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ language: node_js node_js: - - 7 + - stable git: depth: 1 branches: only: - master + - /^greenkeeper/.*$/ +after_success: + - "npm run coveralls" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..60ac2164 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,147 @@ +# hyper(html) Changelog + +### v.2.23 + * monkey patched rollup generated code to export once the same module shared within sub-modules + +### v2.22 + * using latest domtagger + +### v2.21 + * refactored out all dependencies + +### v2.20 + * re-tested every single supported browser nd fixed few outstanding issues with the 2.19 release + +### v2.19 + * refactored out most of the code + * finally managed to have coveralls show coverage stats + * attributes can have spaces around as per DOM standard - [#244](https://github.com/WebReflection/hyperHTML/issues/224) + * fixed SVG (non-critical) errors when interpolations are used for numerically expected values + * fixed minor issues with Edge attributes + * changed the unique id so if any of your logic was trusting `_hyper: ....;` comments you need to update your logic - [#300](https://github.com/WebReflection/hyperHTML/issues/300) + +### v2.16.8 + * improved MutationObserver and fallback so that double `dis/connected` events won't happen again + * exposed `observe` utility for 3rd parts so that it is possible to observe any node, not only those defined via template literals. Once observed, a node can have `connected` and `disconnected` listeners that will be triggered automatically. + +### v2.16 + * modified `Wire` class to better handle "_same target_" case, making the `haunted.html` demo work same way as if it was bound to the node, through `valueOf()` invoke which would result in just exactly the same node if the wired content produced a node instead of a fragment. While regular users won't be affected, this is an implementation detail that changes a lot for libraries integrating `hyperHTML.wire` in their logic, making wires as fast as `bind` in most component related use cases. + +### v2.15 + * added [invokable slots](https://github.com/WebReflection/hyperHTML/pull/282#issuecomment-433614081) to let developers explore patterns through callbacks that will receive a unique live node for weak references while rendered. + +### v2.14 + * updated [domdiff](https://github.com/WebReflection/domdiff#domdiff) to match [petit-dom](https://github.com/yelouafi/petit-dom) performance + * up to 3X performance on huge lists + * improved reliability over random changes + * unfortunately there's a +0.6K overall size increase due amount of extra logic involved + +### v2.13.2 + * added support for custom CSS properties as object keys. + +### v2.13.1 + * worked around [TypeScript transpilation bug with Template Literals](https://twitter.com/WebReflection/status/1038115439539363840). + +### v2.13 + * added the ability to define custom attributes via `hyperHTML.define("hyper-attribute", callback)`, so that `
` would invoke `callback(target, anyValue)` where `p` would be the target.
+
+### v2.12
+ * added `hyper.Component#dispatch(type, detail)` method to simplify events dispatching between lightweight components, bubbling a cancelable Custom Event with a `.component` property that points at the dispatcher, while the `event.currentTarget` will be the first node found within the component render.
+
+### v2.11
+ * updated [domdiff](https://github.com/WebReflection/domdiff#domdiff) to v1.0
+
+### v2.10.12
+ * patched missing `.children` in SVG node in IE / Edge https://github.com/WebReflection/hyperHTML/issues/244
+
+### v2.10.10
+ * updated [domdiff](https://github.com/WebReflection/domdiff#domdiff) to solve issue #243 (breaking with some sorted list)
+
+### v2.10.5
+ * various fixes and changes after [changes applied to ECMAScript 2015](https://github.com/tc39/ecma262/pull/890)
+
+### v2.8.0
+ * updated [domdiff](https://github.com/WebReflection/domdiff#domdiff) engine to boost performance with segments and lists
+
+### v2.7.2
+ * fixed #218 which was a variant of #200
+
+### v2.7.0
+ * the `Component.for(obj)` is now created first time via `new Component(obj)` - #216
+
+### v2.6.0
+ * declarative hyper.Component via `Component.for(context, uid?)` - #202
+ * hyperHTML TypeScript information - #201
+
+### v2.5.12
+ * fixed #200: textarea/style with initial undefined value
+
+### v2.5.11
+ * fixed #198: connected/disconnected events for nested components
+
+### v2.5.10
+ * more rigid / explicit RegExp to avoid glitches with self-closing tags
+
+### v2.5.8
+ * improved `VOID_ELEMENTS` regular expression (aligned with the _viperHTML_ one)
+
+### v2.5.7
+ * fixed `no.js` patch when wrong count of args is passed
+
+### v2.5.6
+ * added `no.js` file for environments without the ability to use modern JS or based on other languages such Dart.
+
+### v2.5.5
+ * build runs on macOS too
+ * added umd.js file
+
+### v2.5.2
+ * fixed weird SVG case (see #172)
+
+### v2.5.1
+ * improved self-closing reliability recycling and sharing attributes RegExp
+
+### v2.5.0
+ * updated `domdiff` library to the latest version
+ * implemented self-closing tags (and after various tests)
+
+### v2.4.3
+ * ensure attributes values are updated when different from previous one
+ * avoid the usage of the word `global` in the whole code
+
+### v2.4.2
+ * fix scripts with actual content too.
+
+### v2.4.1
+ * fix a bug with scripts that don't trigger network requests in both Firefox and Safari (see bug #152)
+
+### v2.4.0
+ * created a `Wire` class to handle via `domdiff` multiple wired nodes.
+ * brought back multi nodes per wire, a feature lost since **v2.0**
+ * simplified `Component` handling too, making it compatible again with multi wired content.
+ * fixed some check to make IE9+ tests green again
+
+### v2.3.0
+ * dropped the `engine` already. Too complex, no real benefits, refactored the whole internal logic to use [domdiff](https://github.com/WebReflection/domdiff) instead. Deprecated [hyperhtml-majinbuu](https://github.com/WebReflection/hyperhtml-majinbuu) and solved diffing "_forever_".
+
+### v2.2.0
+ * the whole `hyperHTML.engine` has been refactored to use [dom-splicer](https://github.com/WebReflection/dom-splicer) as an effort to make engine development easier
+
+### v2.1.3
+ * the MutationObserver is installed only once and only if there are components that have _on(dis)?connect_ handlers.
+
+### v2.1.2
+ * using a new folders convention with `esm/index.js` as main module and `cjs/index.js` as transformed artifact. This plays very well with bundlers when you `import {hyper} from 'hyperhtml/esm'` or `const {hyper} = require('hyperhtml/cjs');`
+
+### v2.1.1
+ * fast changes where prepending or appending same lists; now dropping upfront or removing at the end are part of the fast path too.
+
+### v2.1.0
+
+ * created a simple default merge engine focused on performance
+ * remove majinbuu as core dependency, created [hyperhtml-majinbuu](https://github.com/WebReflection/hyperhtml-majinbuu) project to swap it back via `hyperHTML.engine = require('hyperhtml-majinbuu')` or as ESM
+ * reduced final bundle size down to 4.1K via brotli
+
+## v2.0.0
+
+Refactoring following ticket #140
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..fb3f291f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,67 @@
+# Contribute
+
+## Introduction
+
+First, thank you for considering contributing to hyperhtml! It's people like you that make the open source community such a great community! ๐
+
+We welcome any type of contribution, not only code. You can help with
+- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)
+- **Marketing**: writing blog posts, howto's, printing stickers, ...
+- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...
+- **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them.
+- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/hyperhtml).
+
+## Your First Contribution
+
+Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
+
+## Submitting code
+
+Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests.
+
+## Code review process
+
+The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.
+It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?
+
+## Financial contributions
+
+We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/hyperhtml).
+Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
+
+## Questions
+
+If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!).
+You can also reach us at hello@hyperhtml.opencollective.com.
+
+## Credits
+
+### Contributors
+
+Thank you to all the people who have already contributed to hyperhtml!
+
+
+
+### Backers
+
+Thank you to all our backers! [[Become a backer](https://opencollective.com/hyperhtml#backer)]
+
+
+
+
+### Sponsors
+
+Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/hyperhtml#sponsor))
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DEEPDIVE.md b/DEEPDIVE.md
deleted file mode 100644
index 214523e5..00000000
--- a/DEEPDIVE.md
+++ /dev/null
@@ -1,231 +0,0 @@
-# hyperHTML Developer DeepDive
-
-Being extremely lightweight and simple, there's not much to learn about `hyperHTML`, but there's surely more to discuss about patterns floating around it.
-
-This document aim is to provide as many details as possible per each operation that's possible via `hyperHTML`.
-
-
-## How can the template be unique
-
-One thing I have recently discovered about template literals, and I wish I knew it before, is that these are as static as any other string.
-
-This means that not only ``(`a` === `a`)`` is true, but also ``((a=>a)`a${1}b` === (a=>a)`a${2}b`)``.
-
-This is the first bit to understand: each template string is unique and if the target node has been using this template before, it won't actually parse it again at all and it will simply use the rest of the arguments passed along to update nested bits already known.
-
-
-#### How are known nodes discovered
-
-The very first time a new template is used against a node, its full content is injected into such node using a special `` random comment instead of real values.
-
-Such comment is used to discover both attributes, text nodes, and whole fragments or HTML content, the very first time only.
-
-```js
-// simulation of initial fake injection
-function fakeContent(statics, ...values) {
- const content = statics.join('');
- this.innerHTML = content;
-}
-
-const render = fakeContent.bind(document.body);
-
-// inject fakeContent to the target
-render`a ${'b'} c`;
-
-// check its content
-console.log(
- document.body.childNodes
-);
-// [text, comment, text]
-```
-
-As basic example, it would be now possible to drop that `comment` with a text node that will update its content when `values[0]` is passed on, and once all nodes have been mapped 1:1 to the amount of extra arguments passed along to update each sub-value, real data is used and real data will be used from that very moment on to populate nodes and attributes.
-
-At this point `hyperHTML` uses an [expando property](https://developer.mozilla.org/en-US/docs/Glossary/Expando), for the sake of wider compatibility and to avoid too much [GC pressure](https://mail.mozilla.org/pipermail/es-discuss/2014-December/040565.html) due a potentially heavily populated WeakMap, to save the list of callbacks used to update only what's needed to be updated, being specific text nodes, attributes, or fragments, and together with the static template reference.
-
-From now on, every time the same template is used, all `hyperHTML` has to do is the following:
-
-```js
-// used when the template is known
-function update(statics, ...values) {
- // list of callbacks that target directly the node
- const updates = this[EXPANDO].updates;
- // per each value passed along
- values.forEach((value, i) =>
- // update the related content
- updates[i](value));
- // it's a 1:1 DOM relationships
-}
-```
-
-
-## How are attributes updated
-
-Quite often developers thinks about DOM attributes as something you can get or set via `node.setAttribute(name, value)`.
-Well, that's half of the story, 'cause [attributes are just nodes](https://dom.spec.whatwg.org/#interface-attr) like anything else in the DOM.
-
-This means that a generic attribute can be updated simply setting its `.value` property, like an `input` element would update its view when we set its value. It'd be silly to `input.setAttribute('value', content)` when we can just `input.value = content`, right?
-
-And that's how attributes are updated here. Trapped in a closure, a new template render will simply target the specific attribute and change its value, with the only exception of those attributes prefixed with `on`.
-
-These are meant to be event handlers. And since nobody likes to deal with handlers as string content, `hyperHTML` recognizes these attributes and it actually remove them from the node, but it will assign to the attribute owner, the node, the DOM Level 0 event.
-
-```js
-// simulation of the attribute update mechanism
-if (attribute.name.indexOf('on') === 0) {
- node.removeAttribute(attribute.name);
- updateAttributeUsing(callback => {
- node[attribute.name] = callback;
- });
-} else {
- updateAttributeUsing(value => {
- attribute.value = value;
- });
-}
-```
-
-#### Attributes, the right way
-
-I'm pretty sure at some point someone will file a bug about having the following situation not working:
-```js
-function update(render, state) {
- // WRONG WAY TO SET ATTRIBUTES
- render`
-
${'this will accept HTML'}
- -
- ${'this will update a raw
Hello and Goodbye
` -); - -// but second time it will be empty -render`Hello and Goodbye
` // empty -``` - -To simplify the boilerplate needed to make fragments work as expected, `hyperHTML` offers a method called **wire**. - -Instead of returning the fragment directly, a `.wire()` connects directly the rendered node to the template. - -```js -var render = hyperHTML.wire(); -render`Hello Again!
` === -render`Hello Again!
`; // true - -render`Hello Again!
`; -//Hello Again!
-``` diff --git a/HELPERS.md b/HELPERS.md new file mode 100644 index 00000000..d0f8aefb --- /dev/null +++ b/HELPERS.md @@ -0,0 +1,57 @@ +## [babel-plugin-remove-ungap] + +Remove the [@ungap ponyfill modules] from your bundle. This will decrease the size of +your bundle if you are targeting modern browsers only or if your build already includes +other polyfills. This has been tested with [hyperHTML] and [lighterhtml] bundles. + + +## [babel-plugin-template-html-minifier] + +Run [html-minifier] on hyperHTML templates. + + +## [babel-plugin-bare-import-rewrite] + +This can be used as an alternative to [rollup-plugin-node-resolve], or can be used with certain node.js +web servers to allow browsing live from source. + +Known web server integrations: +* [fastify-babel] plugin for [fastify] enables running any babel plugins, generally expects `payload.filename` as set by [fastify-static] +* [express-transform-bare-module-specifiers] for [express] servers + + +## [vinyl-rollup] + +This module copies the output of rollup builds to a stream of vinyl-fs objects for [gulp]. +In addition it optionally adds files from modules that were bundled into the stream. This +makes it easy to ensure that LICENSE and package.json files associated with bundled modules +are published on the web server without publishing node.js server-side dependencies to the web. +This can also be used to copy complete modules if required for licensing or if bundled code +requires additional assets that are not part of the bundled JS (images for example). + + +## [babel-plugin-bundled-import-meta] + +If `node_modules/some-web-component/index.js` uses `import.meta.url` to calculate the actual +path to `node_modules/some-web-components/image.png`, rollup does not compensate. This babel +plugin rewrites references to `import.meta.url` so it points to the original location where +it is expected that the additional assets (images and such) can be found. This plugin works +well with `vinyl-rollup` with `copyModules: true`. + + +[babel-plugin-remove-ungap]: https://github.com/cfware/babel-plugin-remove-ungap#readme +[@ungap ponyfill modules]: https://github.com/ungap/ungap.github.io#readme +[hyperHTML]: https://github.com/WebReflection/hyperHTML#readme +[lighterhtml]: https://github.com/WebReflection/lighterhtml#readme +[babel-plugin-template-html-minifier]: https://github.com/cfware/babel-plugin-template-html-minifier#readme +[html-minifier]: https://github.com/kangax/html-minifier#readme +[babel-plugin-bare-import-rewrite]: https://github.com/cfware/babel-plugin-bare-import-rewrite#readme +[rollup-plugin-node-resolve]: https://github.com/rollup/rollup-plugin-node-resolve#readme +[fastify]: https://github.com/fastify/fastify#readme +[fastify-babel]: https://github.com/cfware/fastify-babel#readme +[fastify-static]: https://github.com/fastify/fastify-static#readme +[express-transform-bare-module-specifiers]: https://github.com/nodecg/express-transform-bare-module-specifiers#readme +[express]: https://github.com/expressjs/express#readme +[vinyl-rollup]: https://github.com/cfware/vinyl-rollup#readme +[gulp]: https://github.com/gulpjs/gulp#readme +[babel-plugin-bundled-import-meta]: https://github.com/cfware/babel-plugin-bundled-import-meta#readme diff --git a/LICENSE.txt b/LICENSE.txt index deddd54d..9b3119f5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,19 +1,15 @@ -Copyright (C) 2017 by Andrea Giammarchi - @WebReflection +ISC License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Copyright (c) 2017, Andrea Giammarchi, @WebReflection -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index bc33aa8f..72f7e51a 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,204 @@ -# hyperHTML [](https://travis-ci.org/WebReflection/hyperHTML) +# hyper(HTML) + +### Maintainance Only + +This module is great, works great, and served me greatly, but there's a pletora of modern, faster, more capable alternatives me, among many other OSS developers, offer so that if obvious bugs are proven to exist, these will be fixed, but there won't be a major release and I won't remove legacy support for stuff that, as previously mentioned, works just fine and it's battle-tested from IE to the latest Chrome. + +Removing that legacy support brings pretty much nothing in terms of size too: this module is already smaller than 90% of alternatives out there, dropping 0.xK so that there's less code that, behind feature detection, is not even used in modern browsers, won't benefit anyone. + +Thansk for your understanding and for not opening PRs which goal is to drop a check for legacy browsers ... these won't likely be merged ever as that'd be a major release update and I don't think anyone is interested in that. + +### ๐ฃ Community Announcement + +Please ask questions in the [dedicated discussions repository](https://github.com/WebReflection/discussions), to help the community around this project grow โฅ -A Fast & Light Virtual DOM Alternative - [release post](https://medium.com/@WebReflection/hyperhtml-a-virtual-dom-alternative-279db455ee0e#.lc65pz9vd), -now [available for both client and server](https://github.com/WebReflection/viperHTML). - - - -The easiest way to describe `hyperHTML` is through [an example](https://webreflection.github.io/hyperHTML/test/tick.html). -```js -// this is React's first tick example -// https://facebook.github.io/react/docs/state-and-lifecycle.html -function tick() { - const element = ( -This is: ${'text'}
`;``` for text, and ```render`${'html' || node || array}
`;``` for other cases. - An array will result into html, if its content has strings, or a document fragment, if it contains nodes. - I've thought a pinch of extra handy magic would've been nice there ๐. +### 2.34 Highlights - * _can I use different renders for a single node?_ - Sure thing. However, the best performance gain is reached with nodes that always use the same template string. - If you have a very unpredictable conditional template, you might want to create two different nodes and apply `hyperHTML` with the same template for both of them, swapping them when necessary. - In every other case, the new template will create new content and map it once per change. + * the new `?boolean=${value}` syntax from [ยตhtml](https://github.com/WebReflection/uhtml#readme) has landed in *hyperHTML* too. Feel free to [read this long discussion](https://github.com/WebReflection/discussions/discussions/13) to better understand *why* this syntax is necessary. - * _is this project just the same as [yo-yo](https://github.com/maxogden/yo-yo) or [bel](https://github.com/shama/bel) ?_ - First of all, I didn't even know those projects were existing when I've written `hyperHTML`, and while the goal is quite similar, the implementation is very different. - For instance, `hyperHTML` performance seems to be superior than [yo-yo-perf](https://github.com/shama/yo-yo-perf). - You can directly test [hyperHTML DBMonster](https://webreflection.github.io/hyperHTML/test/dbmonster.html) benchmark and see it goes _N_ times faster than `yo-yo` version on both Desktop and Mobile browsers ๐. +### V2.5 Highlights + * `${{user}}
`; + define: (intent, callback) => { + if (intent.indexOf('-') < 0) { + if (!(intent in intents)) { + length = keys.push(intent); + } + intents[intent] = callback; + } else { + attributes[intent] = callback; + } + }, + + // this method is used internally as last resort + // to retrieve a value out of an object + invoke: (object, callback) => { + for (let i = 0; i < length; i++) { + let key = keys[i]; + if (hasOwnProperty.call(object, key)) { + return intents[key](object[key], callback); + } + } + } +}; diff --git a/cjs/objects/Updates.js b/cjs/objects/Updates.js new file mode 100644 index 00000000..0df2e324 --- /dev/null +++ b/cjs/objects/Updates.js @@ -0,0 +1,377 @@ +'use strict'; +const CustomEvent = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/custom-event')); +const WeakSet = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/essential-weakset')); +const isArray = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/is-array')); +const createContent = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/create-content')); + +const disconnected = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('disconnected')); +const domdiff = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('domdiff')); +const domtagger = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('domtagger')); +const hyperStyle = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('hyperhtml-style')); +const Wire = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('hyperhtml-wire')); + +const { + CONNECTED, DISCONNECTED, DOCUMENT_FRAGMENT_NODE, OWNER_SVG_ELEMENT +} = require('../shared/constants.js'); + +const Component = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('../classes/Component.js')); +const Intent = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('./Intent.js')); + +const componentType = Component.prototype.nodeType; +const wireType = Wire.prototype.nodeType; + +const observe = disconnected({Event: CustomEvent, WeakSet}); + +exports.Tagger = Tagger; +exports.observe = observe; + +// returns an intent to explicitly inject content as html +const asHTML = html => ({html}); + +// returns nodes from wires and components +const asNode = (item, i) => { + switch (item.nodeType) { + case wireType: + // in the Wire case, the content can be + // removed, post-pended, inserted, or pre-pended and + // all these cases are handled by domdiff already + /* istanbul ignore next */ + return (1 / i) < 0 ? + (i ? item.remove(true) : item.lastChild) : + (i ? item.valueOf(true) : item.firstChild); + case componentType: + return asNode(item.render(), i); + default: + return item; + } +} + +// returns true if domdiff can handle the value +const canDiff = value => 'ELEMENT_NODE' in value; + +// borrowed from uhandlers +// https://github.com/WebReflection/uhandlers +const booleanSetter = (node, key, oldValue) => newValue => { + if (oldValue !== !!newValue) { + if ((oldValue = !!newValue)) + node.setAttribute(key, ''); + else + node.removeAttribute(key); + } +}; + +const hyperSetter = (node, name, svg) => svg ? + value => { + try { + node[name] = value; + } + catch (nope) { + node.setAttribute(name, value); + } + } : + value => { + node[name] = value; + }; + +// when a Promise is used as interpolation value +// its result must be parsed once resolved. +// This callback is in charge of understanding what to do +// with a returned value once the promise is resolved. +const invokeAtDistance = (value, callback) => { + callback(value.placeholder); + if ('text' in value) { + Promise.resolve(value.text).then(String).then(callback); + } else if ('any' in value) { + Promise.resolve(value.any).then(callback); + } else if ('html' in value) { + Promise.resolve(value.html).then(asHTML).then(callback); + } else { + Promise.resolve(Intent.invoke(value, callback)).then(callback); + } +}; + +// quick and dirty way to check for Promise/ish values +const isPromise_ish = value => value != null && 'then' in value; + +// list of attributes that should not be directly assigned +const readOnly = /^(?:form|list)$/i; + +// reused every slice time +const slice = [].slice; + +// simplifies text node creation +const text = (node, text) => node.ownerDocument.createTextNode(text); + +function Tagger(type) { + this.type = type; + return domtagger(this); +} + +Tagger.prototype = { + + // there are four kind of attributes, and related behavior: + // * events, with a name starting with `on`, to add/remove event listeners + // * special, with a name present in their inherited prototype, accessed directly + // * regular, accessed through get/setAttribute standard DOM methods + // * style, the only regular attribute that also accepts an object as value + // so that you can style=${{width: 120}}. In this case, the behavior has been + // fully inspired by Preact library and its simplicity. + attribute(node, name, original) { + const isSVG = OWNER_SVG_ELEMENT in node; + let oldValue; + // if the attribute is the style one + // handle it differently from others + if (name === 'style') + return hyperStyle(node, original, isSVG); + // direct accessors for and friends + else if (name.slice(0, 1) === '.') + return hyperSetter(node, name.slice(1), isSVG); + // boolean accessors for and friends + else if (name.slice(0, 1) === '?') + return booleanSetter(node, name.slice(1)); + // the name is an event one, + // add/remove event listeners accordingly + else if (/^on/.test(name)) { + let type = name.slice(2); + if (type === CONNECTED || type === DISCONNECTED) { + observe(node); + } + else if (name.toLowerCase() + in node) { + type = type.toLowerCase(); + } + return newValue => { + if (oldValue !== newValue) { + if (oldValue) + node.removeEventListener(type, oldValue, false); + oldValue = newValue; + if (newValue) + node.addEventListener(type, newValue, false); + } + }; + } + // the attribute is special ('value' in input) + // and it's not SVG *or* the name is exactly data, + // in this case assign the value directly + else if ( + name === 'data' || + (!isSVG && name in node && !readOnly.test(name)) + ) { + return newValue => { + if (oldValue !== newValue) { + oldValue = newValue; + if (node[name] !== newValue && newValue == null) { + // cleanup on null to avoid silly IE/Edge bug + node[name] = ''; + node.removeAttribute(name); + } + else + node[name] = newValue; + } + }; + } + else if (name in Intent.attributes) { + oldValue; + return any => { + const newValue = Intent.attributes[name](node, any); + if (oldValue !== newValue) { + oldValue = newValue; + if (newValue == null) + node.removeAttribute(name); + else + node.setAttribute(name, newValue); + } + }; + } + // in every other case, use the attribute node as it is + // update only the value, set it as node only when/if needed + else { + let owner = false; + const attribute = original.cloneNode(true); + return newValue => { + if (oldValue !== newValue) { + oldValue = newValue; + if (attribute.value !== newValue) { + if (newValue == null) { + if (owner) { + owner = false; + node.removeAttributeNode(attribute); + } + attribute.value = newValue; + } else { + attribute.value = newValue; + if (!owner) { + owner = true; + node.setAttributeNode(attribute); + } + } + } + } + }; + } + }, + + // in a hyper(node)`${{user}}
`; + define: (intent, callback) => { + if (intent.indexOf('-') < 0) { + if (!(intent in intents)) { + length = keys.push(intent); + } + intents[intent] = callback; + } else { + attributes[intent] = callback; + } + }, + + // this method is used internally as last resort + // to retrieve a value out of an object + invoke: (object, callback) => { + for (let i = 0; i < length; i++) { + let key = keys[i]; + if (hasOwnProperty.call(object, key)) { + return intents[key](object[key], callback); + } + } + } +}; diff --git a/esm/objects/Updates.js b/esm/objects/Updates.js new file mode 100644 index 00000000..103b45ff --- /dev/null +++ b/esm/objects/Updates.js @@ -0,0 +1,377 @@ +import CustomEvent from '@ungap/custom-event'; +import WeakSet from '@ungap/essential-weakset'; +import isArray from '@ungap/is-array'; +import createContent from '@ungap/create-content'; + +import disconnected from 'disconnected'; +import domdiff from 'domdiff'; +import domtagger from 'domtagger'; +import hyperStyle from 'hyperhtml-style'; +import Wire from 'hyperhtml-wire'; + +import { + CONNECTED, DISCONNECTED, + DOCUMENT_FRAGMENT_NODE, + OWNER_SVG_ELEMENT +} from '../shared/constants.js'; + +import Component from '../classes/Component.js'; +import Intent from './Intent.js'; + +const componentType = Component.prototype.nodeType; +const wireType = Wire.prototype.nodeType; + +const observe = disconnected({Event: CustomEvent, WeakSet}); + +export {Tagger, observe}; + +// returns an intent to explicitly inject content as html +const asHTML = html => ({html}); + +// returns nodes from wires and components +const asNode = (item, i) => { + switch (item.nodeType) { + case wireType: + // in the Wire case, the content can be + // removed, post-pended, inserted, or pre-pended and + // all these cases are handled by domdiff already + /* istanbul ignore next */ + return (1 / i) < 0 ? + (i ? item.remove(true) : item.lastChild) : + (i ? item.valueOf(true) : item.firstChild); + case componentType: + return asNode(item.render(), i); + default: + return item; + } +} + +// returns true if domdiff can handle the value +const canDiff = value => 'ELEMENT_NODE' in value; + +// borrowed from uhandlers +// https://github.com/WebReflection/uhandlers +const booleanSetter = (node, key, oldValue) => newValue => { + if (oldValue !== !!newValue) { + if ((oldValue = !!newValue)) + node.setAttribute(key, ''); + else + node.removeAttribute(key); + } +}; + +const hyperSetter = (node, name, svg) => svg ? + value => { + try { + node[name] = value; + } + catch (nope) { + node.setAttribute(name, value); + } + } : + value => { + node[name] = value; + }; + +// when a Promise is used as interpolation value +// its result must be parsed once resolved. +// This callback is in charge of understanding what to do +// with a returned value once the promise is resolved. +const invokeAtDistance = (value, callback) => { + callback(value.placeholder); + if ('text' in value) { + Promise.resolve(value.text).then(String).then(callback); + } else if ('any' in value) { + Promise.resolve(value.any).then(callback); + } else if ('html' in value) { + Promise.resolve(value.html).then(asHTML).then(callback); + } else { + Promise.resolve(Intent.invoke(value, callback)).then(callback); + } +}; + +// quick and dirty way to check for Promise/ish values +const isPromise_ish = value => value != null && 'then' in value; + +// list of attributes that should not be directly assigned +const readOnly = /^(?:form|list)$/i; + +// reused every slice time +const slice = [].slice; + +// simplifies text node creation +const text = (node, text) => node.ownerDocument.createTextNode(text); + +function Tagger(type) { + this.type = type; + return domtagger(this); +} + +Tagger.prototype = { + + // there are four kind of attributes, and related behavior: + // * events, with a name starting with `on`, to add/remove event listeners + // * special, with a name present in their inherited prototype, accessed directly + // * regular, accessed through get/setAttribute standard DOM methods + // * style, the only regular attribute that also accepts an object as value + // so that you can style=${{width: 120}}. In this case, the behavior has been + // fully inspired by Preact library and its simplicity. + attribute(node, name, original) { + const isSVG = OWNER_SVG_ELEMENT in node; + let oldValue; + // if the attribute is the style one + // handle it differently from others + if (name === 'style') + return hyperStyle(node, original, isSVG); + // direct accessors for and friends + else if (name.slice(0, 1) === '.') + return hyperSetter(node, name.slice(1), isSVG); + // boolean accessors for and friends + else if (name.slice(0, 1) === '?') + return booleanSetter(node, name.slice(1)); + // the name is an event one, + // add/remove event listeners accordingly + else if (/^on/.test(name)) { + let type = name.slice(2); + if (type === CONNECTED || type === DISCONNECTED) { + observe(node); + } + else if (name.toLowerCase() + in node) { + type = type.toLowerCase(); + } + return newValue => { + if (oldValue !== newValue) { + if (oldValue) + node.removeEventListener(type, oldValue, false); + oldValue = newValue; + if (newValue) + node.addEventListener(type, newValue, false); + } + }; + } + // the attribute is special ('value' in input) + // and it's not SVG *or* the name is exactly data, + // in this case assign the value directly + else if ( + name === 'data' || + (!isSVG && name in node && !readOnly.test(name)) + ) { + return newValue => { + if (oldValue !== newValue) { + oldValue = newValue; + if (node[name] !== newValue && newValue == null) { + // cleanup on null to avoid silly IE/Edge bug + node[name] = ''; + node.removeAttribute(name); + } + else + node[name] = newValue; + } + }; + } + else if (name in Intent.attributes) { + oldValue; + return any => { + const newValue = Intent.attributes[name](node, any); + if (oldValue !== newValue) { + oldValue = newValue; + if (newValue == null) + node.removeAttribute(name); + else + node.setAttribute(name, newValue); + } + }; + } + // in every other case, use the attribute node as it is + // update only the value, set it as node only when/if needed + else { + let owner = false; + const attribute = original.cloneNode(true); + return newValue => { + if (oldValue !== newValue) { + oldValue = newValue; + if (attribute.value !== newValue) { + if (newValue == null) { + if (owner) { + owner = false; + node.removeAttributeNode(attribute); + } + attribute.value = newValue; + } else { + attribute.value = newValue; + if (!owner) { + owner = true; + node.setAttributeNode(attribute); + } + } + } + } + }; + } + }, + + // in a hyper(node)`- // ${(new Date).toLocaleString()} - //
- // `, 1000); - function hyperHTML(statics) { - return EXPANDO in this && - this[EXPANDO].s === statics ? - update.apply(this, arguments) : - upgrade.apply(this, arguments); - } - - // A wire โฐ is a bridge between a document fragment - // and its inevitably lost list of rendered nodes - // - // var render = hyperHTML.wire(); - // render` - //- // ${some.text} - //
- //Content before
${ - // 'any content in between' - // }Content after
- // `; - // - // Note: this is the most expensive - // update of them all. - function setVirtualContent(node) { - var - fragment = document.createDocumentFragment(), - childNodes = [] - ; - return function any(value) { - var i, parentNode = node.parentNode; - switch (typeof value) { - case 'string': - case 'number': - case 'boolean': - removeNodeList(childNodes, 0); - injectHTML(fragment, value); - childNodes = slice.call(fragment.childNodes); - parentNode.insertBefore(fragment, node); - break; - default: - if (Array.isArray(value)) { - if (value.length === 0) { - any(value[0]); - } else if(typeof value[0] === 'string') { - any(value.join('')); - } else { - i = indexOfDiffereces(childNodes, value); - if (-1 < i) { - removeNodeList(childNodes, i); - value = value.slice(i); - appendNodes(fragment, value); - parentNode.insertBefore(fragment, node); - childNodes.push.apply(childNodes, value); - } - } - } else { - removeNodeList(childNodes, 0); - childNodes = value.nodeType === 11 ? - slice.call(value.childNodes) : - [value]; - parentNode.insertBefore(value, node); - } - break; - } - }; - } - - // basic closure to update nodes textContent - // - // render` - //- // ${'spaces around means textContent'} - //
`; - function setTextContent(node) { - return function text(value) { - node.textContent = value; - }; - } - - - // ------------------------- - // Helpers - // ------------------------- - - // it does exactly what it says - function appendNodes(node, childNodes) { - for (var - i = 0, - length = childNodes.length; - i < length; i++ - ) { - node.appendChild(childNodes[i]); - } - } - - // given two collections, find - // the first index that has different content. - // If the two lists are the same, return -1 - // to indicate no differences were found. - function indexOfDiffereces(a, b) { - if (a === b) return -1; - var - i = 0, - aLength = a.length, - bLength = b.length - ; - while (i < aLength) { - if (i < bLength && a[i] === b[i]) i++; - else return i; - } - return i === bLength ? -1 : i; - } - - // inject HTML into a template node - // and populate a fragment with resulting nodes - // - // Note: for partial layout such `${{user}}
`; + define: function define(intent, callback) { + if (intent.indexOf('-') < 0) { + if (!(intent in intents)) { + length = keys.push(intent); + } + + intents[intent] = callback; + } else { + attributes[intent] = callback; + } + }, + // this method is used internally as last resort + // to retrieve a value out of an object + invoke: function invoke(object, callback) { + for (var i = 0; i < length; i++) { + var key = keys[i]; + + if (hasOwnProperty.call(object, key)) { + return intents[key](object[key], callback); + } + } + } + }; + + var isArray = Array.isArray || + /* istanbul ignore next */ + function (toString) { + /* istanbul ignore next */ + var $ = toString.call([]); + /* istanbul ignore next */ + + return function isArray(object) { + return toString.call(object) === $; + }; + }({}.toString); + + /*! (c) Andrea Giammarchi - ISC */ + var createContent = function (document) { + + var FRAGMENT = 'fragment'; + var TEMPLATE = 'template'; + var HAS_CONTENT = ('content' in create(TEMPLATE)); + var createHTML = HAS_CONTENT ? function (html) { + var template = create(TEMPLATE); + template.innerHTML = html; + return template.content; + } : function (html) { + var content = create(FRAGMENT); + var template = create(TEMPLATE); + var childNodes = null; + + if (/^[^\S]*?<(col(?:group)?|t(?:head|body|foot|r|d|h))/i.test(html)) { + var selector = RegExp.$1; + template.innerHTML = '