nanocomponent
Advanced tools
Comparing version 1.0.0 to 1.1.0
153
index.js
@@ -1,10 +0,9 @@ | ||
var widgetEl = require('cache-element/widget') | ||
var observeResize = require('observe-resize') | ||
var onIntersect = require('on-intersect') | ||
var politeEl = require('polite-element') | ||
var cachedEl = require('cache-element') | ||
var onload = require('on-load') | ||
var assert = require('assert') | ||
var isDom = require('is-dom') | ||
var html = require('bel') | ||
var elType = 'div' | ||
module.exports = nanocomponent | ||
@@ -17,13 +16,88 @@ | ||
if (isDom(val)) return staticEl(val) | ||
if (typeof val === 'function') return cachedEl(val) | ||
if (isDom(val)) return createStaticElement(val) | ||
if (typeof val === 'function') return createDynamicElement(val) | ||
if (val.placeholder) { | ||
assert.equal(typeof val.render, 'function', 'nanocomponent: .placeholder cannot exist without a .render method') | ||
val.render = politeEl(val.placeholder, val.render) | ||
assert.equal(typeof val.render, 'function', 'nanocomponent: needs a .render function') | ||
var placeholderHandler = val.placeholder | ||
var onunloadHandler = val.onunload | ||
var onresizeHandler = val.onresize | ||
var onenterHandler = val.onenter | ||
var onexitHandler = val.onexit | ||
var onloadHandler = val.onload | ||
var renderHandler = val.render | ||
var stopPlaceholderResize = null | ||
var stopRenderResize = null | ||
var enableIntersect = null | ||
var enableResize = null | ||
if (isDom(val)) return createStaticElement(val) | ||
else if (typeof val === 'function') return createDynamicElement(val) | ||
else { | ||
if (onresizeHandler) applyResize() | ||
if (onenterHandler || onexitHandler) applyOnintersect() | ||
applyOnloadhandler() | ||
if (placeholderHandler) applyPlaceholder() | ||
return renderHandler | ||
} | ||
return widgetEl(val) | ||
function applyOnloadhandler () { | ||
var _render = renderHandler | ||
createOnunload() | ||
createOnload() | ||
renderHandler = createDynamicElement(_render, onloadHandler, onunloadHandler) | ||
} | ||
function applyOnintersect () { | ||
enableIntersect = function (el) { | ||
onIntersect(el, onenterHandler, onexitHandler) | ||
} | ||
} | ||
function applyPlaceholder () { | ||
var _render = renderHandler | ||
renderHandler = function () { | ||
var args = new Array(arguments.length) | ||
for (var i = 0; i < args.length; i++) { | ||
args[i] = arguments[i] | ||
} | ||
var el = politeEl(placeholderHandler, _render) | ||
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 staticEl (element) { | ||
function createStaticElement (element) { | ||
var isRendered = false | ||
@@ -39,3 +113,3 @@ var isProxied = false | ||
} else if (!isProxied) { | ||
proxy = document.createElement(elType) | ||
proxy = html`<div></div>` | ||
proxy.isSameNode = function (el) { | ||
@@ -57,1 +131,56 @@ return (el === element) | ||
} | ||
function createDynamicElement (render, _onload, _onunload) { | ||
var isRendered = 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) { | ||
args = _args | ||
element = render.apply(render, args) | ||
onload(element, handleLoad, handleUnload) | ||
return element | ||
} else { | ||
if (!compare(_args, args)) { | ||
element = render.apply(render, args) | ||
onload(element, handleLoad, handleUnload) | ||
return element | ||
} else if (!isProxied) { | ||
proxy = html`<div></div>` | ||
proxy.isSameNode = function (el) { | ||
return (el === element) | ||
} | ||
} else { | ||
return proxy | ||
} | ||
} | ||
} | ||
function handleLoad (el) { | ||
isRendered = true | ||
if (_onload) _onload(el) | ||
} | ||
function handleUnload (el) { | ||
isProxied = false | ||
proxy = null | ||
if (_onunload) _onunload(el) | ||
} | ||
} | ||
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 | ||
} | ||
return true | ||
} |
{ | ||
"name": "nanocomponent", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "Create performant HTML elements", | ||
"main": "index.js", | ||
"scripts": { | ||
"deps": "dependency-check . && dependency-check . --extra --no-dev", | ||
"test": "standard && npm run deps && NODE_ENV=test node test", | ||
"test:cov": "standard && npm run deps && NODE_ENV=test istanbul cover test.js" | ||
"deps": "dependency-check . && dependency-check . --extra --no-dev -i yo-yoify", | ||
"start": "bankai example --open", | ||
"test": "standard && npm run deps", | ||
"test:cov": "standard && npm run deps" | ||
}, | ||
@@ -19,9 +20,17 @@ "repository": "yoshuawuyts/nanocomponent", | ||
"license": "MIT", | ||
"browserify": { | ||
"transform": [ | ||
"yo-yoify" | ||
] | ||
}, | ||
"dependencies": { | ||
"cache-element": "^2.0.0", | ||
"is-dom": "^1.0.6", | ||
"observe-resize": "^1.1.0", | ||
"on-intersect": "^1.1.0", | ||
"on-load": "^3.2.0", | ||
"polite-element": "^1.0.2" | ||
"polite-element": "^1.0.2", | ||
"yo-yoify": "^3.5.0" | ||
}, | ||
"devDependencies": { | ||
"bankai": "^5.2.1", | ||
"bel": "^4.5.1", | ||
@@ -31,2 +40,3 @@ "dependency-check": "^2.7.0", | ||
"standard": "^8.6.0", | ||
"tachyons": "^4.6.1", | ||
"tape": "^4.6.3", | ||
@@ -33,0 +43,0 @@ "uglifyify": "^3.0.4", |
211
README.md
# nanocomponent [![stability][0]][1] | ||
[![npm version][2]][3] [![build status][4]][5] [![test coverage][6]][7] | ||
[![npm version][2]][3] [![build status][4]][5] | ||
[![downloads][8]][9] [![js-standard-style][10]][11] | ||
@@ -8,7 +8,7 @@ | ||
## Features | ||
- works with every framework | ||
- significantly speeds up perceived performance | ||
- works with virtually every framework | ||
- speeds up perceived performance | ||
- improves rendering performance | ||
- graciously falls back if APIs are not available | ||
- weighs `~2kb` | ||
- weighs `~4kb` | ||
@@ -24,3 +24,4 @@ ## Usage | ||
`) | ||
console.log(staticElement()) | ||
var el = staticElement() | ||
console.log(el) | ||
``` | ||
@@ -35,3 +36,5 @@ | ||
}) | ||
console.log(cachedElement()) | ||
console.log(cachedElement('hey folks')) // render | ||
console.log(cachedElement('hey folks')) // return cached element | ||
console.log(cachedElement('hey humans')) // render again | ||
``` | ||
@@ -53,3 +56,7 @@ | ||
}) | ||
console.log(politeElement()) | ||
setTimeout(function () { | ||
console.log(politeElement()) | ||
}, 1000) | ||
``` | ||
@@ -60,18 +67,58 @@ | ||
var widgetElement = component({ | ||
onload: function () { | ||
onload: function (el) { | ||
el.textContent = 'totally loaded now' | ||
}, | ||
onunload: function (el) { | ||
el.textContent = 'no more free lunch' | ||
}, | ||
onupdate: function (foo, bar) { | ||
// do stuff with new arguments | ||
onupdate: function (el, verb) { | ||
el.textContent = `totally ${verb}ing now` | ||
}, | ||
render: function (foo, bar) { | ||
render: function (verb) { | ||
return html` | ||
<p>lol not loaded yet</p> | ||
<p>lol not ${verb}ed yet</p> | ||
` | ||
} | ||
}) | ||
console.log(widgetElement()) | ||
console.log(widgetElement('load')) | ||
var el = widgetElement('blep') | ||
document.body.appendChild(el) | ||
document.body.removeChild(el) | ||
``` | ||
### 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()) | ||
``` | ||
### 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()) | ||
``` | ||
## API | ||
@@ -81,8 +128,128 @@ ### render = nanocomponent(HtmlOrFunctionOrObject) | ||
are passed in: | ||
- __HTMLElement:__ when a valid HTML node is passed | ||
- __HTMLElement:__ cache the result of the function until it's removed from the | ||
DOM | ||
- __function:__ cache the result of the function until new arguments are passed | ||
- __object:__ create an object with different methods attached | ||
or it's removed from the DOM | ||
- __object:__ create an object with different methods attached. Cached until | ||
new arguments are passed in or when it's removed from the DOM | ||
When passing an object, the 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. | ||
### 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. | ||
## 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. | ||
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). | ||
### 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. | ||
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. | ||
### 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 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 | ||
@@ -93,2 +260,18 @@ ```sh | ||
## See Also | ||
- [bendrucker/document-ready](https://github.com/bendrucker/document-ready) | ||
- [shama/bel](https://github.com/shama/bel) | ||
- [shama/on-load](https://github.com/shama/on-load) | ||
- [yoshuawuyts/choo](https://github.com/yoshuawuyts/choo) | ||
- [yoshuawuyts/nanomorph](https://github.com/yoshuawuyts/nanomorph) | ||
- [yoshuawuyts/nanoraf](https://github.com/yoshuawuyts/nanoraf) | ||
- [yoshuawuyts/nanotick](https://github.com/yoshuawuyts/nanotick) | ||
- [yoshuawuyts/observe-resize](https://github.com/yoshuawuyts/observe-resize) | ||
- [yoshuawuyts/on-intersect](https://github.com/yoshuawuyts/on-intersect) | ||
- [yoshuawuyts/polite-element](https://github.com/yoshuawuyts/polite-element) | ||
## Similar Packages | ||
- [shama/base-element](https://github.com/shama/base-element) | ||
- [yoshuawuyts/cache-element](https://github.com/yoshuawuyts/cache-element) | ||
## License | ||
@@ -95,0 +278,0 @@ [MIT](https://tldrlegal.com/license/mit-license) |
Sorry, the diff of this file is not supported yet
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
20113
240
1
285
1
6
9
8
1
+ Addedobserve-resize@^1.1.0
+ Addedon-intersect@^1.1.0
+ Addedyo-yoify@^3.5.0
+ Addedacorn@5.7.47.4.1(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addedfalafel@2.2.5(transitive)
+ Addedhyperscript-attribute-to-property@1.0.2(transitive)
+ Addedhyperx@2.5.4(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedinsert-styles@1.2.1(transitive)
+ Addedisarray@1.0.02.0.5(transitive)
+ Addedobserve-resize@1.1.3(transitive)
+ Addedon-intersect@1.1.0(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedreadable-stream@2.3.8(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedthrough2@2.0.5(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedxtend@4.0.2(transitive)
+ Addedyo-yoify@3.7.3(transitive)
- Removedcache-element@^2.0.0
- Removedcache-element@2.0.1(transitive)