${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 98e0593f..2b02e166 100644 --- a/.npmignore +++ b/.npmignore @@ -1,8 +1,14 @@ -dist/* -docs/* +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/GETTING_STARTED.md b/GETTING_STARTED.md deleted file mode 100644 index c2bdec41..00000000 --- a/GETTING_STARTED.md +++ /dev/null @@ -1,243 +0,0 @@ -# Getting Started with _hyperHTML_ - -To describe it in a sentence, `hyperHTML` is a just-in-time ([JIT](https://en.wikipedia.org/wiki/Just-in-time_compilation)) template literals compiler that maps 1:1 interpolations to DOM nodes or attributes. - -Its strategy consists in creating a generic HTML content once, through an `innerHTML`_-like_ operation, -and map only parts of the resulting DOM tree to a collection of callbacks that will pass along interpolated values. - -Even if it all sounds complex, the algorithm behind `hyperHTML` fits into less than 2KB minified and gzipped, -it's absolutely cross browser, it delivers extreme performance even on mobile, and its usage is the opposite of complex. - -To complete this quick introduction, unless you are targeting also old browsers, -`hyperHTML` doesn't necessarily need transpilers, compilers, or even source maps; -it's entirely based on the [ECMAScript 2015](http://www.ecma-international.org/ecma-262/6.0/) Standard -and it has zero dependencies. - - -### About Template Literals - -[Template literals](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals) -are a modern and standard JavaScript way to create strings through back-ticks (`` ` ``) with the following peculiarities: - - * these can be *multi-line* without needing special escaping - * these can have interpolated `${placeholders}` containing *any* JavaScript content - * these can be tagged through a `callback` used as prefix - * these are always statically unique, even if defined in different places, and even if tagged - -```js -const tlBasic = `basic -template literal`; - -const tlInt = `Interpolating 1 + 2 = ${1 + 2}`; - -const tag = (statics, ...values) => console.log(statics, values); - -tag`look mom, ${Math.random() < .5 ? '()' : 'no'} brackets`; - -// ["look mom, ", " brackets"] | ["no"] -// list of statics parts | dynamic values -``` - -Being tags callbacks, it is also possible to bind them to a specific context, like a DOM node, -and this is the basic concept that gives `hyperHTML` super powers. - - -## How To Define _hyperHTML_ Templates - -There are two basic but fundamental rules to remember: - - 1. **attributes**, as well as eventual **callbacks**, must be defined inside single or double quoted attributes - 2. if there's any char different from `>` and `<` surrounding the interpolation, that content will be text, instead of HTML - -Both text content and attributes are automatically sanitized by native DOM functionalities, making templates automatically more secure. - -The following template summarizes above cases: -```js -const update = (render) => render` - ${'this is text'} -${'this is html'}
${ - 'this is also html- ${'but this is text'} -
- ${'this is text'} - do you get it? -`; -``` - -The resulting layout would look like the following one: -```html - this is text -this is html
this is also html- but this is text -
- this is text - do you get it? -``` - -If you would like to have an overview of real-world templates and their applications, -you can check the [header](https://github.com/WebReflection/hypermvc/blob/master/js/views/header.js), -[main](https://github.com/WebReflection/hypermvc/blob/master/js/views/main.js), -or [footer](https://github.com/WebReflection/hypermvc/blob/master/js/views/footer.js) parts -of the [app](https://github.com/WebReflection/hypermvc/blob/master/js/app.js) that renders -the [TodoMVC version of hyperHTML](https://webreflection.github.io/hypermvc/). - - -### Remember: you cannot do partial attributes - -In case you are wondering how to partially update a node attribute, remember the following: - -```js -// THIS IS BROKEN, IT WON'T WORK -const update = (render, extra) => render` - -`; - -// while THIS IS THE CORRECT WAY ๐ -const update = (render, extra) => render` - -`; -``` - -This simplifies both the discovery of an interpolated attribute at compilation time, -and it will also grant best performance changing complex attributes at once instead of _N_ partial times. - - -### Rendering Multiple Nodes -If the interpolation returns an array of fragments, or _wires_, -`hyperTHML` will figure out which one needs to be placed in the document, -replaced, removed, or be completely untouched, if it was already there. - -```js -const update = (render, listOfItems) => render` -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 ๐. +You can require or import _hyperHTML_ with any bundler and in different ways. - * _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. +If requiring or importing from `"hyperhtml"` doesn't work, try requiring from `"hyperhtml/cjs"` for CommonJS friendly bundlers (WebPack), or `"hyperhtml/esm"` for ESM compatible bundlers (Rollup). - * _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 ๐. +See [HELPERS.md](./HELPERS.md) for a list of additional tools which can be helpful for building hyperHTML based web applications. + +- - - +## Sponsors -For all other deeper dirty details, please check the [DeepDive](https://github.com/WebReflection/hyperHTML/blob/master/DEEPDIVE.md) page. +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/hyperhtml#sponsor)] +${{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 - // - // IE9~IE11 are not compatible with the template tag. - // If the content is a partial part of a table there is a fallback. - // Not the most elegant/robust way but good enough for common cases. - // (I don't want to include a whole DOM parser for IE only here). - function injectHTML(fragment, html) { - var - fallback = IE && /^[^\S]*?<(t(?:head|body|foot|r|d|h))/i.test(html), - template = fragment.ownerDocument.createElement('template') - ; - template.innerHTML = fallback ? ('${{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 = '