Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite implementation #170

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e818ce9
Mount working
tornqvist Dec 16, 2019
b710109
Persist in DOM between mounts
tornqvist Dec 29, 2019
8231cc4
Fix tests
tornqvist Dec 29, 2019
ebba149
Handle null values
tornqvist Dec 30, 2019
b825d54
Handle varying partials
tornqvist Dec 31, 2019
25488d3
Fix null recognized as partial
tornqvist Dec 31, 2019
a3ffc87
Add children reordering
tornqvist Jan 2, 2020
984da15
Fix bench
tornqvist Jan 2, 2020
904edc5
Rewrite w/ classes
tornqvist Jan 12, 2020
d676b7b
Adding components WIP
tornqvist Jan 19, 2020
9e1c974
Add nanomorph dep
tornqvist Feb 6, 2020
e8a93da
Add bench comparison
tornqvist Feb 6, 2020
b708fb2
Add component (WIP)
tornqvist Feb 6, 2020
ff5a991
Rewrite w/o so many objects, reiterated component
tornqvist Apr 28, 2020
3c73c9d
nanohtml/component working 🎉
tornqvist Apr 30, 2020
1ddaa7b
Fix onupdate call order
tornqvist May 19, 2020
35e0e65
Fragments WIP
tornqvist May 19, 2020
6a2974f
Fix fragments, text and any type of content
tornqvist May 19, 2020
0e4ae4e
Fix fragment within fragment
tornqvist May 20, 2020
41a10e1
Add support for async/generator partials
tornqvist May 22, 2020
397941e
Add support for top level async/generator partials
tornqvist May 23, 2020
ad2621c
Add support for gen/async attributes
tornqvist May 23, 2020
8de4413
Fix async partial race condition
tornqvist May 25, 2020
6f5224d
Fix child array ordering, perf sinkhole
tornqvist May 26, 2020
b980d23
Fix tinyfy choking on template literal
tornqvist May 26, 2020
84e3260
Add lazy wrapper
tornqvist May 26, 2020
6df29e8
Add tests for lazy wrapper
tornqvist May 27, 2020
20f408c
Fix child array append order
tornqvist May 29, 2020
322ccdd
Fix async partial child in array not rendering
tornqvist May 29, 2020
05b85b6
Add test for async child ordering
tornqvist May 29, 2020
864278f
Simplify lazy util
tornqvist Jun 8, 2020
e3d0ba5
Fix crash on updating modified DOM tree
tornqvist Jun 8, 2020
0dffa91
Migrate tests to new api
tornqvist Jun 9, 2020
a0f5725
Fix async server render
tornqvist Jun 9, 2020
976f9bf
Remove component ref util
tornqvist Jun 10, 2020
2d5bac8
Remove legacy files, move to lib
tornqvist Jun 10, 2020
4a64e65
Add comments
tornqvist Jun 10, 2020
e6e86de
Remove old example
tornqvist Jun 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions bench/client.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
var nanobench = require('nanobench')

var nanoHtml = require('../')
var { render, html } = require('../')
var createApp = require('./fixtures/app')

nanobench('nanohtml browser 10000 iterations', function (b) {
var app = createApp(nanoHtml)
for (var i = 0; i < 100; i++) app.render().toString()
var app = createApp(html)
var div = document.createElement('div')

document.body.appendChild(div)
for (var i = 0; i < 100; i++) render(app.render(), div)

b.start()
for (i = 0; i < 10000; i++) app.render().toString()
for (i = 0; i < 10000; i++) render(app.render(), div)
b.end()
})
225 changes: 225 additions & 0 deletions component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
var assert = require('nanoassert')
var { Partial } = require('./nanohtml')

var loadid = `__onload-${Math.random().toString(36).substr(-4)}`
var identifier = Symbol('nanohtml/component')
var tracking = new WeakMap()
var windows = new WeakSet()
var stack = []

exports.memo = memo
exports.onload = onload
exports.onupdate = onupdate
exports.Component = Component
exports.identifier = identifier

function Component (fn, key, args) {
if (this instanceof Component) {
this.beforeupdate = []
this.afterupdate = []
this.beforeload = []
this.args = args
this.key = key
this.fn = fn
return this
}

key = Symbol(fn.name || 'nanohtml/component')
return function (...args) {
return new Component(fn, key, args)
}
}

Component.prototype = Object.create(Partial.prototype)
Component.prototype.constructor = Component

Component.prototype.key = function key (key) {
this.key = key
return this
}

Component.prototype.resolve = function (ctx) {
var cached = ctx ? ctx.state.get(identifier) : null
this.index = this.args.length
if (cached) {
this.args = cached.args.map((arg, i) => {
return typeof this.args[i] === 'undefined' ? arg : this.args[i]
}).concat(this.args.slice(cached.args.length))
}
stack.unshift(this)
try {
const partial = this.fn(...this.args)
partial.key = this.key
return partial
} finally {
const component = stack.shift()
assert(component === this, 'nanohtml/component: stack out of sync')
}
}

Component.prototype.render = function (oldNode) {
var partial = this.resolve()
var ctx = Partial.prototype.render.call(partial, oldNode)
ctx.state.set(identifier, this)
this.rendered = partial
return ctx
}

Component.prototype.update = function (ctx) {
this.ctx = ctx // store context for async updates
var cached = ctx.state.get(identifier)
stack.unshift(this)
try {
const partial = this.rendered || this.resolve(ctx)
unwind(cached.beforeupdate, this.args)
Partial.prototype.update.call(partial, ctx)
unwind(this.afterupdate, this.args)
unwind(this.beforeload, [ctx.element])
ctx.state.set(identifier, this)
} finally {
const component = stack.shift()
assert(component === this, 'nanohtml/component: stack out of sync')
}
}

function unwind (arr, args) {
while (arr.length) {
const fn = arr.pop()
fn(...args)
}
}

function onupdate (fn) {
assert(stack.length, 'nanohtml/component: cannot call onupdate outside component render cycle')
var component = stack[0]
if (typeof fn === 'function') {
component.afterupdate.push(function (...args) {
var res = fn(...args)
if (typeof res === 'function') {
component.beforeupdate.push(res)
}
})
}

return function (...args) {
assert(component.ctx, 'nanohtml/component: cannot update while rendering')
var next = new Component(component.fn, component.key, args)
next.update(component.ctx)
}
}

function memo (initial) {
assert(stack.length, 'nanohtml/component: cannot call memo outside component render cycle')
var index = stack[0].index++
var { args } = stack[0]
var value = args[index]
if (typeof value === 'undefined') {
if (typeof initial === 'function') value = initial(...args)
else value = initial
args[index] = value
}
return value
}

/**
* An implementation of https://github.com/hyperdivision/fast-on-load
*
* Copyright 2020 Hyperdivision ApS (https://hyperdivision.dk)
* 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:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* 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.
*/
function onload (fn) {
assert(stack.length, 'nanohtml/component: cannot call onload outside component render cycle')
stack[0].beforeload.push(function (el) {
el.classList.add(loadid)

var entry
if (!tracking.has(el)) {
entry = {
on: [on],
off: [],
state: 2,
children: el.getElementsByClassName(loadid)
}
tracking.set(el, entry)
if (!windows.has(this)) createObserver(this)
} else {
// FIXME: this will reset the queue on every onload
entry = tracking.get(el)
entry.on = [on]
entry.off = []
}

function on (el) {
var res = fn(el)
if (typeof res === 'function') entry.off.push(res)
}
})
}

function createObserver (window) {
windows.add(window)

const document = window.document
const observer = new window.MutationObserver(onchange)

const isConnected = 'isConnected' in window.Node.prototype
? node => node.isConnected
: node => document.documentElement.contains(node)

observer.observe(document.documentElement, {
childList: true,
subtree: true
})

function callAll (nodes, idx) {
for (const node of nodes) {
if (!node.classList) continue
if (node.classList.contains(loadid)) call(node, idx)
const children = tracking.has(node)
? tracking.get(node).children
: node.getElementsByClassName(loadid)
for (const child of children) {
call(child, idx)
}
}
}

// State Enum
// 0: mounted
// 1: unmounted
// 2: undefined
function call (node, state) {
var entry = tracking.get(node)
if (!entry || entry.state === state) return
if (state === 0 && isConnected(node)) {
entry.state = 0
for (const fn of entry.on) fn(node)
} else if (state === 1 && !isConnected(node)) {
entry.state = 1
for (const fn of entry.off) fn(node)
}
}

function onchange (mutations) {
for (const { addedNodes, removedNodes } of mutations) {
callAll(removedNodes, 1)
callAll(addedNodes, 0)
}
}
}
1 change: 0 additions & 1 deletion dom.js

This file was deleted.

90 changes: 90 additions & 0 deletions lazy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const { Partial, Context } = require('./nanohtml')

module.exports = Lazy

function Lazy (primary, fallback) {
if (!(this instanceof Lazy)) {
primary = unwind(primary)
if (primary == null) return fallback()
if (!isPromise(primary)) return primary
return new Lazy(primary, fallback)
}

fallback = fallback()
if (fallback instanceof Partial) {
this.key = fallback.key
this.partial = fallback
} else {
this.key = Symbol('nanohtml/lazy')
}

this.primary = primary
this.fallback = fallback
}

Lazy.prototype = Object.create(Partial.prototype)
Lazy.prototype.constructor = Lazy

Lazy.prototype.render = function (oldNode) {
var { primary, fallback } = this

var ctx
if (fallback instanceof Partial) {
ctx = fallback.render(oldNode)
oldNode = ctx.element
} else {
oldNode = toNode(fallback)
ctx = new Context({
key: this.key,
element: oldNode,
editors: [],
bind (newNode) {
oldNode = newNode
}
})
}

ctx.queue(primary).then((res) => {
if (res instanceof Partial) {
var ctx = res.render(oldNode)
this.partial = res
this.key = res.key
res.update()
} else {

}
}).catch((err) => {
var newNode = fallback(err)
oldNode.parentNode.replaceChild()
})

return ctx
}

Lazy.prototype.update = function (ctx) {
if (this.partial) {
this.partial.update(ctx)
}
}

function unwind (obj, value) {
if (isGenerator(obj)) {
const res = obj.next(value)
if (res.done) return res.value
if (isPromise(res.value)) {
return res.value.then(unwind).then((val) => unwind(obj, val))
}
return unwind(obj, res.value)
} else if (isPromise(obj)) {
return obj.then(unwind)
}
return obj
}

function isPromise (obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
}

function isGenerator (obj) {
return obj && typeof obj.next === 'function' && typeof obj.throw === 'function'
}
2 changes: 0 additions & 2 deletions lib/bool-props.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use strict'

module.exports = [
'async', 'autofocus', 'autoplay', 'checked', 'controls', 'default',
'defaultchecked', 'defer', 'disabled', 'formnovalidate', 'hidden',
Expand Down
Loading