nanocomponent
Advanced tools
Comparing version 2.0.3 to 3.0.0
196
index.js
@@ -1,164 +0,60 @@ | ||
var observeResize = require('observe-resize') | ||
var onIntersect = require('on-intersect') | ||
var politeEl = require('polite-element') | ||
'use strict' | ||
var onload = require('on-load') | ||
var assert = require('assert') | ||
var html = require('bel') | ||
module.exports = nanocomponent | ||
module.exports = Nanocomponent | ||
// Create performant HTML elements | ||
// (DOMElement|fn|obj) -> fn | ||
function nanocomponent (val) { | ||
assert.equal(typeof val, 'object', 'nanocomponent: val should type function') | ||
assert.equal(typeof val.render, 'function', 'nanocomponent: needs a .render function') | ||
function Nanocomponent (val) { | ||
this._hasWindow = typeof window !== undefined | ||
this._placeholder = null | ||
this._onload = onload | ||
this._element = null | ||
this._loaded = false | ||
} | ||
var placeholderHandler = val.placeholder | ||
var onunloadHandler = val.onunload | ||
var onresizeHandler = val.onresize | ||
var onenterHandler = val.onenter | ||
var updateHandler = val.onupdate | ||
var onexitHandler = val.onexit | ||
var onloadHandler = val.onload | ||
var renderHandler = val.render | ||
Nanocomponent.prototype.render = function () { | ||
assert.equal(typeof this._render, 'function', 'nanocomponent: this._render should be implemented') | ||
var stopPlaceholderResize = null | ||
var stopRenderResize = null | ||
var enableIntersect = null | ||
var enableResize = null | ||
var self = this | ||
var len = arguments.length | ||
var args = new Array(len) | ||
for (var i = 0; i < len; i++) args[i] = arguments[i] | ||
if (onresizeHandler) applyResize() | ||
if (onenterHandler || onexitHandler) applyOnintersect() | ||
applyOnloadhandler() | ||
if (placeholderHandler) applyPlaceholder() | ||
return renderHandler | ||
function applyOnloadhandler () { | ||
var _render = renderHandler | ||
createOnunload() | ||
createOnload() | ||
renderHandler = createElement(_render, updateHandler, onloadHandler, onunloadHandler) | ||
if (!this._hasWindow) { | ||
this._element = this._render.apply(this, args) | ||
return this._element | ||
} else if (!this._element) { | ||
this._element = this._render.apply(this, args) | ||
this._onload(this._element, function () { | ||
self._loaded = true | ||
if (self._load) self._load() | ||
}, function () { | ||
self._placeholder = null | ||
self._element = null | ||
self._loaded = false | ||
if (self._unload) self._unload() | ||
}) | ||
return this._element | ||
} else { | ||
var shouldUpdate = this._update.apply(this, args) | ||
if (shouldUpdate) this._render.apply(this, args) | ||
if (!this._placeholder) this._placeholder = this._createPlaceholder() | ||
return this._placeholder | ||
} | ||
function applyOnintersect () { | ||
enableIntersect = function (el) { | ||
onIntersect(el, onenterHandler, onexitHandler) | ||
} | ||
} | ||
function applyPlaceholder () { | ||
var _render = renderHandler | ||
var placeholderRendered = null | ||
renderHandler = function () { | ||
var args = new Array(arguments.length) | ||
for (var i = 0; i < args.length; i++) { | ||
args[i] = arguments[i] | ||
} | ||
if (placeholderRendered) return _render.apply(_render, args) | ||
var el = politeEl(function applyPlaceholderHandler () { | ||
placeholderRendered = true | ||
return placeholderHandler.apply(placeholderHandler, args) | ||
}, function applyRenderHandler () { | ||
return _render.apply(_render, args) | ||
}) | ||
var ret = el.apply(el, args) | ||
return ret | ||
} | ||
} | ||
function applyResize () { | ||
enableResize = function (el) { | ||
stopRenderResize = observeResize(el, onresizeHandler) | ||
} | ||
} | ||
function createOnunload () { | ||
var _onunload = onunloadHandler | ||
onunloadHandler = function (el) { | ||
if (stopPlaceholderResize) { | ||
stopPlaceholderResize() | ||
stopPlaceholderResize = null | ||
} | ||
if (stopRenderResize) { | ||
stopRenderResize() | ||
stopRenderResize = null | ||
} | ||
if (_onunload) _onunload(el) | ||
} | ||
} | ||
function createOnload () { | ||
var _onload = onloadHandler | ||
onloadHandler = function (el) { | ||
if (_onload) _onload(el) | ||
if (enableResize) enableResize(el) | ||
if (enableIntersect) enableIntersect(el) | ||
} | ||
} | ||
} | ||
function createElement (render, update, _onload, _onunload) { | ||
var isRendered = false | ||
var isMounted = false | ||
var isProxied = false | ||
var element = null | ||
var proxy = null | ||
var args = null | ||
return function () { | ||
var _args = new Array(arguments.length) | ||
for (var i = 0; i < _args.length; i++) { | ||
_args[i] = arguments[i] | ||
} | ||
if (!isRendered) { | ||
isRendered = true | ||
args = _args | ||
element = render.apply(render, args) | ||
onload(element, handleLoad, handleUnload) | ||
} | ||
if (!isMounted) return element | ||
if (!compare(_args, args)) { | ||
args = _args | ||
if (update) { | ||
// call update with element as the first arg | ||
args.unshift(element) | ||
update.apply(render, args.slice(0)) | ||
} | ||
} | ||
if (!isProxied) { | ||
proxy = html`<div></div>` | ||
proxy.isSameNode = function (el) { | ||
return (el === element) | ||
} | ||
} | ||
return proxy | ||
} | ||
function handleLoad (el) { | ||
isMounted = true | ||
if (_onload) _onload(el) | ||
} | ||
function handleUnload (el) { | ||
isMounted = false | ||
isProxied = false | ||
proxy = null | ||
if (_onunload) _onunload(el) | ||
} | ||
// default ._update method - should be replaced with custom logic | ||
Nanocomponent.prototype._update = function () { | ||
return true | ||
} | ||
function compare (args1, args2) { | ||
var length = args1.length | ||
if (length !== args2.length) return false | ||
for (var i = 0; i < length; i++) { | ||
if (args1[i] !== args2[i]) return false | ||
Nanocomponent.prototype._createPlaceholder = function () { | ||
var el = document.createElement('div') | ||
el.setAttribute('data-nanocomponent', '') | ||
var self = this | ||
el.isSameNode = function (el) { | ||
return el === self._element | ||
} | ||
return true | ||
return el | ||
} |
{ | ||
"name": "nanocomponent", | ||
"version": "2.0.3", | ||
"version": "3.0.0", | ||
"description": "Create performant HTML elements", | ||
@@ -16,23 +16,18 @@ "main": "index.js", | ||
"node", | ||
"perf", | ||
"universal" | ||
], | ||
"license": "MIT", | ||
"browserify": { | ||
"transform": [ | ||
"yo-yoify" | ||
] | ||
}, | ||
"dependencies": { | ||
"observe-resize": "^1.1.0", | ||
"on-intersect": "^1.1.0", | ||
"on-load": "^3.2.0", | ||
"polite-element": "^1.0.2", | ||
"yo-yoify": "^3.5.0" | ||
"on-load": "^3.2.0" | ||
}, | ||
"devDependencies": { | ||
"bankai": "^5.2.1", | ||
"bel": "^4.5.1", | ||
"bankai": "^5.5.2", | ||
"bel": "^4.6.0", | ||
"choo": "^5.1.4", | ||
"choo-log": "^6.0.0", | ||
"dependency-check": "^2.7.0", | ||
"inherits": "^2.0.3", | ||
"istanbul": "^0.4.5", | ||
"leaflet": "^1.0.3", | ||
"nanologger": "^1.0.2", | ||
"standard": "^8.6.0", | ||
@@ -42,4 +37,5 @@ "tachyons": "^4.6.1", | ||
"uglifyify": "^3.0.4", | ||
"unassertify": "^2.0.4" | ||
"unassertify": "^2.0.4", | ||
"yo-yoify": "^3.5.0" | ||
} | ||
} |
255
README.md
@@ -5,218 +5,77 @@ # nanocomponent [![stability][0]][1] | ||
Create performant HTML elements. | ||
`1kb` library to wrap native DOM libraries to work with DOM diffing algorithms. | ||
## Features | ||
- works with [virtually every framework][adapt] | ||
- speeds up perceived performance | ||
- improves rendering performance | ||
- graciously falls back if APIs are not available | ||
- weighs `~4kb` | ||
## Usage | ||
### Defer rendering until the browser has spare time available | ||
```js | ||
var politeElement = component({ | ||
placeholder: function () { | ||
return html` | ||
<p>lol not loaded yet</p> | ||
` | ||
}, | ||
render: function () { | ||
return html` | ||
<p>HOW'S IT GOING CHAP</p> | ||
` | ||
} | ||
}) | ||
// Implementer API | ||
var Nanocomponent = require('nanocomponent') | ||
var html = require('bel') | ||
console.log(politeElement()) | ||
setTimeout(function () { | ||
console.log(politeElement()) | ||
}, 1000) | ||
``` | ||
function MyButton () { | ||
if (!(this instanceof MyButton)) return new MyButton() | ||
this._color = null | ||
Nanocomponent.call(this) | ||
} | ||
MyButton.prototype = Object.create(Nanocomponent) | ||
### Trigger lifecycle events when loading and unloading elements to the DOM | ||
```js | ||
var widgetElement = component({ | ||
onload: function (el) { | ||
el.textContent = 'totally loaded now' | ||
}, | ||
onunload: function (el) { | ||
el.textContent = 'no more free lunch' | ||
}, | ||
onupdate: function (el, verb) { | ||
el.textContent = `totally ${verb}ing now` | ||
}, | ||
render: function (verb) { | ||
return html` | ||
<p>lol not ${verb}ed yet</p> | ||
` | ||
} | ||
}) | ||
console.log(widgetElement('load')) | ||
var el = widgetElement('blep') | ||
document.body.appendChild(el) | ||
document.body.removeChild(el) | ||
``` | ||
MyButton.prototype._render = function (color) { | ||
this._color = color | ||
return html` | ||
<button style="background-color: ${color}"> | ||
Click Me | ||
</button> | ||
` | ||
} | ||
### Trigger lifecycle events when coming in and out of view | ||
```js | ||
var viewportElement = component({ | ||
onenter: function (el) { | ||
el.textContent = 'BEHOLD THE GOBLIN' | ||
}, | ||
onexit: function (el) { | ||
el.textContent = 'THE PONIES HAVE COME' | ||
}, | ||
render: function () { | ||
return html` | ||
<h1>WHO COULD IT BE</h1> | ||
` | ||
} | ||
}) | ||
console.log(viewportElement()) | ||
MyButton.prototype._update = function (newColor) { | ||
return newColor !== this._color | ||
} | ||
``` | ||
### Trigger lifecycle events when resizing | ||
```js | ||
var resizeElement = component({ | ||
onresize: function (el) { | ||
var parent = el.parentNode | ||
console.log('element dimensions', el.getBoundingRectangle()) | ||
console.log('parent dimensions', parent.getBoundingRectangle()) | ||
}, | ||
render: function () { | ||
return html` | ||
<div>hello planet</div> | ||
` | ||
} | ||
}) | ||
console.log(resizeElement()) | ||
// Consumer API | ||
var MyButton = require('./my-button.js') | ||
var myButton = MyButton() | ||
document.body.appendChild(myButton.render()) | ||
``` | ||
### Rendering inside frameworks | ||
Check out [nanocomponent-adapters][adapt] on how to use `nanocomponent` in | ||
your fav framework. | ||
## API | ||
### render = nanocomponent(HtmlOrFunctionOrObject) | ||
Create an object with different methods attached. Cached until new arguments | ||
are passed in or when it's removed from the DOM. Availble methods are: | ||
- __render(...args):__ (required) Render DOM elements. | ||
- __placeholder(..args)__ Render DOM elements and delegate the `render` call to | ||
the next `requestIdleCallback` tick. This is useful to spread CPU intensive | ||
work over more time and not block the render loop. When executed on the | ||
server `placeholder` will always be rendered in favor of `render`. This makes | ||
it easier for client-side JS to pick up where the server left off | ||
(rehydration). | ||
- __onupdate(el, ...args):__ Allows you to change the internal DOM state when | ||
new arguments are passed in. It's called when the returned `render()` call is | ||
called after an initial render with different arguments. Argument equality is | ||
shallowly checked using a `===` check on each argument. The first argument is | ||
the currently rendered argument. | ||
- __onenter:__ called when the element comes into view, relies on | ||
`window.IntersectionObserver` | ||
- __onexit:__ Called when the element goes out of view, relies on | ||
`window.IntersectionObserver` | ||
- __onload(el):__ Called when the element is appended onto the DOM | ||
- __onunload(el):__ Called when the element is removed from the DOM | ||
- __onresize:__ Called when the element changes size. :warning: This method can | ||
be called in high frequency and can cause strain on your CPU. Caution and/or | ||
debounce methods are advised. | ||
### `Nanocomponent.prototype()` | ||
Inheritable Nanocomponent prototype. Should be inherited from using | ||
`Nanococomponent.call(this)` and `prototype = Object.create(Nanocomponent)`. | ||
### el = render(...args) | ||
Call the corresponding `render` function an receive DOM elements. As long as an | ||
element exists on the DOM, subsequent calls to `render` will return an empty | ||
element with a `.isSameNode()` method on it which can be used as a caching hint | ||
for HTML diffing trees. | ||
Internal properties are: | ||
- __this._placeholder:__ placeholder element that's returned on subsequent | ||
`render()` calls that don't pass the `._update()` check. | ||
- __this._element:__ rendered element that should be returned from the first | ||
`._render()` call. Used to apply `._load()` and `._unload()` listeners on. | ||
- __this._hasWindow:__ boolean if `window` exists. Can be used to create | ||
elements that render both in the browser and in Node. | ||
- __this._loaded:__ boolean if the element is currently loaded on the DOM. | ||
- __this._onload:__ reference to the [on-load][on-load] library. | ||
## FAQ | ||
### Why'd you build this package? | ||
I've been building web stuff for a while now, and have seen a fair share of | ||
frameworks become popular, take over developer mindshare and then disappear | ||
again a few years later. With each framework iteration the basic libraries tend | ||
to be rewritten from scratch: form validation, modals, infinite scolls, charts. | ||
The list is long. | ||
### `DOMNode|placeholder = Nanocomponent.prototype.render()` | ||
Create an instance of the component. Calls `prototype._render()` if | ||
`prototype._update()` returns `true`. As long as the element is mounted on the | ||
DOM, subsequent calls to `.render()` will return a placeholder element with a | ||
`.isSameNode()` method that compares arguments with the previously rendered | ||
node. This is useful for diffing algorithms like | ||
[nanomorph](https://github.com/yoshuawuyts/nanomorph) which use this method to | ||
determine if a portion of the tree should be walked. | ||
I think it'd be cool if we could create generic JS components that work | ||
natively with any JS framework through slim bindings. This would encourage | ||
reusability between frameworks, which means it becomes easier to pick up | ||
different frameworks (no need to relearn the ecosystem) and bring new | ||
frameworks to maturity (less new code to implement). | ||
### `Nanocomponent.prototype._render([arguments])` | ||
Render an HTML node with arguments. For `prototype._load()` and | ||
`prototype._unload()` to work, make sure you return the same node on subsequent | ||
renders. The Node that's initially returned is saved as `this._element`. | ||
### This sounds a lot like WebComponents, how is this different? | ||
WebComponents are a specification by the W3C that's been in the works for a | ||
while now. Certain parts have been put on hold by browser vendors until kinks | ||
are ironed out, and the some of the available parts are not widely adopted - or | ||
at least not the way they were meant to be used. | ||
### `Nanocomponent.prototype._update([arguments])` | ||
Return a boolean to determine if `prototype._render()` should be called. | ||
Evaluates to `true` if not implemented. Not called on the first render. | ||
When people talk about WebComponents they usually refer to the Custom Elements | ||
specification. This spec allows you to create new HTML tags and provides you | ||
with a set of lifecycle events. The `onload` / `onunload` / `render` / | ||
`onupdate` events are indeed quite similar to Nanocomponent. The biggest | ||
difference however, is the way in which elements are registered. Custom | ||
Elements are globally scoped in the browser and must have unique names. | ||
Nanocomponent is a plain JS function and will not run into namespacing issues. | ||
It's quite feasible to wrap a Nanocomponent instance to create a Webcomponent. | ||
The other way around is harder. Nanocomponent also exposes more events. | ||
### `Nanocomponent.prototype._load()` | ||
Called when the component is mounted on the DOM. | ||
### I read somewhere that Nanocomponent uses some of the same techniques as React Fiber / React Stack. Could you talk some more about this? | ||
Sure! React Fiber (or React Stack, I'm not sure what name they're ending up | ||
with) is using the same APIs under the hood as Nanocomponent, but approaches it | ||
from the other side of the spectrum. | ||
### `Nanocomponent.prototype._unload()` | ||
Called when the component is removed from the DOM. | ||
Nanocomponent is intended to create individual components that can run in any | ||
framework and have tight control over their performance. React Fiber is | ||
a framework where the whole render tree is optimized on each loop. | ||
Nanocomponent operates on the component layer, React Fiber at the framework | ||
layer. | ||
This doesn't mean that either one is "better" - every abstraction carries | ||
overhead, and different situations require different solutions. I think it's | ||
great performance is being tackled from multiple sides of the spectrum. | ||
### What do you mean by "Nanocomponent works with any framework"? | ||
Nanocomponent returns DOM elements that work in any framework that knows how to | ||
render raw DOM nodes. This includes pretty much every popular framework and | ||
compile-to-js language. Because the lifecycle events are self-encapsulated and | ||
we don't expose globals this means Nanocomponent doesn't have any problems | ||
running inside any other framework. I think Nanocomponent is quite similar in | ||
browser framework land to how C / CPP packages operate | ||
### I don't believe in silver bullets, tell me about the tradeoffs | ||
All abstractions in JS come with a cost. Luckily the cost of Nanomorph is | ||
fairly low (4kb, CPU and memory seem to be cool - haven't noticed any | ||
significant repaint costs or anything), but it should not be neglected. Measure | ||
and inform yourself. | ||
Nanomorph relies on fairly new DOM APIs to do what it does. If you're running | ||
v. old browsers this package won't help you - we're sorry. As long as | ||
`window.MutationObserver` is available we should be good; the others are | ||
optimizations on top. These are the fancy APIs we're using: | ||
- `window.MutationObserver` | ||
- `window.IntersectionObserver` | ||
- `window.requestAnimationFrame` | ||
- `window.requestIdleCallback` | ||
Third of all this package is optimized for an environment that supports | ||
`require()`. I believe in iterating on proven ideas, and given the stability of | ||
`require()` and the tooling & community around it I'm betting that it'll stick | ||
around for a while. | ||
Some frameworks (like React Fiber / React Stack) might have some cool | ||
optimizations that allow super fine grained control over each and every element | ||
in the tree - nanocomponent is similar but has its own rules to wait for | ||
resources, so it might turn into an interesting situation where everyone is | ||
super politely waiting for resources and like is nice to each other. I don't | ||
know, but I'm guessing it'll be fun hah. | ||
Also these components won't work in environments that don't have a DOM, but I | ||
bet it'd be cool to look at those environments and figure out which primitives | ||
they have and create equivalent functionality through generic components. | ||
### Uahahghhhhblll | ||
Yes! | ||
### Does this render on the server? | ||
Yup, it does. If it doesn't for your particular setup we'd like to hear. | ||
## Installation | ||
@@ -259,1 +118,3 @@ ```sh | ||
[adapt]: https://github.com/yoshuawuyts/nanocomponent-adapters/ | ||
[on-load]: https://github.com/shama/on-load |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25079
1
9
734
15
119
1
- Removedobserve-resize@^1.1.0
- Removedon-intersect@^1.1.0
- Removedpolite-element@^1.0.2
- Removedyo-yoify@^3.5.0
- Removedacorn@5.7.47.4.1(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedfalafel@2.2.5(transitive)
- Removedhyperscript-attribute-to-property@1.0.2(transitive)
- Removedhyperx@2.5.4(transitive)
- Removedinherits@2.0.4(transitive)
- Removedinsert-styles@1.2.1(transitive)
- Removedis-dom@1.1.0(transitive)
- Removedis-object@1.0.2(transitive)
- Removedis-window@1.0.2(transitive)
- Removedisarray@1.0.02.0.5(transitive)
- Removednanomorph@4.0.5(transitive)
- Removedobserve-resize@1.1.3(transitive)
- Removedon-intersect@1.1.0(transitive)
- Removedpolite-element@1.0.5(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedthrough2@2.0.5(transitive)
- Removedutil-deprecate@1.0.2(transitive)
- Removedxtend@4.0.2(transitive)
- Removedyo-yoify@3.7.3(transitive)