-
Notifications
You must be signed in to change notification settings - Fork 780
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
Introduce plugin architecture. #124
Changes from 1 commit
ccf3cbd
04ceecb
6404b60
84977a8
3da2f33
5b7600c
1b765da
6dee98c
f0503df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,39 @@ | ||
{ | ||
"name": "hyperapp", | ||
"description": "1kb JavaScript library for building frontend applications.", | ||
"version": "0.6.0", | ||
"main": "dist/hyperapp.js", | ||
"jsnext:main": "src/index.js", | ||
"module": "src/index.js", | ||
"license": "MIT", | ||
"repository": "hyperapp/hyperapp", | ||
"files": [ | ||
"src", | ||
"dist" | ||
], | ||
"author": "Jorge Bucaran", | ||
"keywords": [ | ||
"hyperx", | ||
"hyperscript", | ||
"jsx", | ||
"react", | ||
"elm", | ||
"virtual-dom" | ||
], | ||
"scripts": { | ||
"test": "jest --coverage --no-cache", | ||
"build": "rollup -cm -n hyperapp -f umd -i src/index.js -o dist/hyperapp.js", | ||
"prepublish": "npm run build", | ||
"release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" | ||
}, | ||
"babel": { | ||
"presets": "es2015" | ||
}, | ||
"devDependencies": { | ||
"babel-preset-es2015": "^6.22.0", | ||
"hyperx": "^2.0.5", | ||
"jest": "^18.1.0", | ||
"rollup": "^0.41.4", | ||
"rollup-plugin-uglify": "^1.0.1" | ||
} | ||
"name": "hyperapp", | ||
"description": "1kb JavaScript library for building frontend applications.", | ||
"version": "0.6.0", | ||
"main": "dist/hyperapp.js", | ||
"jsnext:main": "src/index.js", | ||
"module": "src/index.js", | ||
"license": "MIT", | ||
"repository": "hyperapp/hyperapp", | ||
"files": [ | ||
"src", | ||
"dist" | ||
], | ||
"author": "Jorge Bucaran", | ||
"keywords": [ | ||
"hyperx", | ||
"hyperscript", | ||
"jsx", | ||
"react", | ||
"elm", | ||
"virtual-dom" | ||
], | ||
"scripts": { | ||
"test": "jest --coverage --no-cache --verbose", | ||
"build": "rollup -cm -n hyperapp -f umd -i src/index.js -o dist/hyperapp.js", | ||
"prepublish": "npm run build", | ||
"release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" | ||
}, | ||
"babel": { | ||
"presets": "es2015" | ||
}, | ||
"devDependencies": { | ||
"babel-preset-es2015": "^6.22.0", | ||
"hyperx": "^2.0.5", | ||
"jest": "^18.1.0", | ||
"rollup": "^0.41.4", | ||
"rollup-plugin-uglify": "^1.0.1" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,47 @@ | ||
export default function (options) { | ||
var model = options.model | ||
var view = options.view | ||
export default function (app) { | ||
var view = app.view || function () { | ||
return "" | ||
} | ||
|
||
var model | ||
var actions = {} | ||
var effects = options.effects || {} | ||
var reducers = options.reducers || {} | ||
var subs = options.subscriptions | ||
|
||
var router = options.router | ||
|
||
var reducers | ||
var effects | ||
var subscriptions = [] | ||
var hooks = [] | ||
var plugins = app.plugins | ||
var node | ||
var root | ||
var batch = [] | ||
|
||
var hooks = merge({ | ||
onAction: Function.prototype, | ||
onUpdate: Function.prototype, | ||
onError: function (err) { | ||
var onError = function (err) { | ||
if (!hook("onError", function (fn) { | ||
fn(err) | ||
})) { | ||
throw err | ||
} | ||
}, options.hooks) | ||
} | ||
|
||
initializeActions(actions, effects, false) | ||
initializeActions(actions, reducers, true) | ||
use(app) | ||
|
||
domContentLoaded(function () { | ||
root = options.root || document.body.appendChild(document.createElement("div")) | ||
for (var key in plugins) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the other hand for loop is more verbose and we'd need to check if it's empty or not with .length. I'll experiment with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Size might be more, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just out of curiosity how come There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lukejacksonn for in is used to loop through props of an object. This can be used to traverse an array too, but it's a "hack". The best practice to traverse arrays is using a regular for loop or forEach. I've changed them all to use a regular for loop, so say bye to for in! :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎉 |
||
use(plugins[key](app)) | ||
} | ||
|
||
if (typeof view === "function") { | ||
render(model, view) | ||
} | ||
init(actions, effects, false) | ||
init(actions, reducers, true) | ||
|
||
if (router) { | ||
router(function (newView) { | ||
render(model, view = newView ? newView : view, node) | ||
}, options) | ||
} | ||
load(function () { | ||
root = app.root || document.body.appendChild(document.createElement("div")) | ||
|
||
for (var key in subs) { | ||
subs[key](model, actions, hooks.onError) | ||
render(model, view) | ||
|
||
for (var key in subscriptions) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
subscriptions[key](model, actions, onError) | ||
} | ||
}) | ||
|
||
function initializeActions(container, group, shouldUpdate, lastName) { | ||
function init(container, group, shouldRender, lastName) { | ||
Object.keys(group).forEach(function (key) { | ||
if (!container[key]) { | ||
container[key] = {} | ||
|
@@ -53,32 +52,72 @@ export default function (options) { | |
|
||
if (typeof action === "function") { | ||
container[key] = function (data) { | ||
hooks.onAction(name, data) | ||
hook("onAction", function (fn) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you implement the data structure I suggested this would be hooks.onAction.forEach(hook => hook(name, data)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, this sounds good. Let me see how I can do that. |
||
fn(name, data) | ||
}) | ||
|
||
if (shouldRender) { | ||
var oldModel = model | ||
model = merge(model, action(model, data)) | ||
|
||
hook("onUpdate", function (fn) { | ||
fn(oldModel, model, data) | ||
}) | ||
|
||
render(model, view) | ||
|
||
if (shouldUpdate) { | ||
hooks.onUpdate(model, model = merge(model, action(model, data)), data) | ||
render(model, view, node) | ||
return actions | ||
} else { | ||
return action(model, actions, data, hooks.onError) | ||
return action(model, actions, data, onError) | ||
} | ||
} | ||
} else { | ||
initializeActions(container[key], action, shouldUpdate, name) | ||
init(container[key], action, shouldRender, name) | ||
} | ||
}) | ||
} | ||
|
||
function domContentLoaded(initApp) { | ||
function load(fn) { | ||
if (document.readyState[0] !== "l") { | ||
initApp() | ||
fn() | ||
} else { | ||
document.addEventListener("DOMContentLoaded", initApp) | ||
document.addEventListener("DOMContentLoaded", fn) | ||
} | ||
} | ||
|
||
function render(model, view, lastNode) { | ||
patch(root, node = view(model, actions), lastNode, 0) | ||
function use(app) { | ||
if (app.model) { | ||
model = merge(model, app.model) | ||
} | ||
|
||
reducers = merge(reducers, app.reducers) | ||
effects = merge(effects, app.effects) | ||
|
||
if (app.subscriptions) { | ||
subscriptions = subscriptions.concat(app.subscriptions) | ||
} | ||
|
||
if (app.hooks) { | ||
hooks.push(app.hooks) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of pushing objects into an array I would iterate through the objects and construct this data-structure: var hooks = {
onRender: [f1, f2, f3],
onError: [f4]
} This has the advantage that access is fast and easy. Basically it should be handled the same as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dodekeract Can you provide some pseudocode? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. var hooks = {
onRender: []
}
function addToHooks(hook, name) {
hooks[name].push(hook)
}
function handlePluginHooks (hooks) {
for (var name in hooks) {
addToHooks(hooks[name], name)
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems more verbose. Specially if more hooks are added in the future. I'll see if there are any performance gains! 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There will be performance gains. https://jsperf.com/for-in-or-for
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you talking about for-in used with arrays? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, yes. Hard to keep track of 10 conversations at the same time. |
||
} | ||
} | ||
|
||
function hook(name, fn) { | ||
for (var key in hooks) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
var hookfn = hooks[key][name] | ||
if (hookfn) { | ||
fn(hookfn) | ||
} | ||
} | ||
return hookfn !== undefined | ||
} | ||
|
||
function render(model, view) { | ||
hook("onRender", function (fn) { | ||
view = fn(model, view) | ||
}) | ||
|
||
patch(root, node, node = view(model, actions), 0) | ||
|
||
for (var i = 0; i < batch.length; i++) { | ||
batch[i]() | ||
|
@@ -141,15 +180,7 @@ export default function (options) { | |
} | ||
|
||
for (var i = 0; i < node.children.length; i++) { | ||
var childNode = node.children[i] | ||
|
||
if ( | ||
childNode !== undefined && | ||
typeof childNode !== "boolean" && | ||
childNode !== null | ||
) { | ||
element.appendChild(createElementFrom(childNode)) | ||
} | ||
element.appendChild(createElementFrom(node.children[i])) | ||
} | ||
} | ||
|
||
|
@@ -213,7 +244,7 @@ export default function (options) { | |
} | ||
} | ||
|
||
function patch(parent, node, oldNode, index) { | ||
function patch(parent, oldNode, node, index) { | ||
if (oldNode === undefined) { | ||
parent.appendChild(createElementFrom(node)) | ||
|
||
|
@@ -227,20 +258,7 @@ export default function (options) { | |
} | ||
|
||
} else if (shouldUpdate(node, oldNode)) { | ||
var element = parent.childNodes[index] | ||
|
||
if (typeof node === "boolean") { | ||
parent.removeChild(element) | ||
|
||
} else { | ||
var newElement = createElementFrom(node) | ||
|
||
if (element === undefined) { | ||
parent.appendChild(newElement) | ||
} else { | ||
parent.replaceChild(newElement, element) | ||
} | ||
} | ||
parent.replaceChild(createElementFrom(node), parent.childNodes[index]) | ||
|
||
} else if (node.tag) { | ||
var element = parent.childNodes[index] | ||
|
@@ -252,9 +270,7 @@ export default function (options) { | |
for (var i = 0; i < len || i < oldLen; i++) { | ||
var child = node.children[i] | ||
|
||
if (child !== null) { | ||
patch(element, child, oldNode.children[i], i) | ||
} | ||
patch(element, oldNode.children[i], child, i) | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,60 @@ | ||
export default function(tag, data) { | ||
data = data || {} | ||
|
||
var children = [] | ||
children.push.apply(children, arguments) | ||
children.shift() | ||
children.shift() | ||
// var children = children.push.apply(children, arguments).splice(2) | ||
var head = children[0] | ||
|
||
children = Array.isArray(head) || head === undefined ? head : children | ||
|
||
if (typeof tag === "function") { | ||
return tag(data, children || []) | ||
} | ||
|
||
if (tag === "svg") { | ||
svg(tag, data, children) | ||
} | ||
|
||
return { | ||
tag: tag, | ||
data: data, | ||
children: [].concat.apply([], children) | ||
} | ||
var i, node, children, stack = [] | ||
|
||
export default function (tag, data) { | ||
var canConcat, oldCanConcat | ||
|
||
children = [] | ||
i = arguments.length | ||
|
||
while (i-- > 2) { | ||
stack.push(arguments[i]) | ||
} | ||
|
||
while (stack.length) { | ||
if (Array.isArray(node = stack.pop())) { | ||
i = node.length | ||
|
||
while (i--) { | ||
stack.push(node[i]) | ||
} | ||
} else if (node != null && node !== true && node !== false) { | ||
if (typeof node === "number") { | ||
node = node + "" | ||
} | ||
|
||
canConcat = typeof node === "string" | ||
|
||
if (canConcat && oldCanConcat) { | ||
children[children.length - 1] += node | ||
} else { | ||
children.push(node) | ||
oldCanConcat = canConcat | ||
} | ||
} | ||
} | ||
|
||
if (typeof tag === "function") { | ||
return tag(data, children) | ||
} | ||
|
||
if (tag === "svg") { | ||
svg(tag, data, children) | ||
} | ||
|
||
return { | ||
tag: tag, | ||
data: data || {}, | ||
children: children | ||
} | ||
} | ||
|
||
function svg(tag, data, children) { | ||
data.ns = "http://www.w3.org/2000/svg" | ||
|
||
for (var i = 0; i < children.length; i++) { | ||
var node = children[i] | ||
|
||
if (node.data) { | ||
svg(node.tag, node.data, node.children) | ||
} | ||
} | ||
data.ns = "http://www.w3.org/2000/svg" | ||
|
||
for (var i = 0; i < children.length; i++) { | ||
var node = children[i] | ||
if (node.data) { | ||
svg(node.tag, node.data, node.children) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import h from "./h" | ||
import app from "./app" | ||
import router from "./router" | ||
import Router from "./router" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this TitleCase? It's not a class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't care about that convention. I think it sucks to limit myself to a convention for a construct I rarely use in JavaScript anyway. I don't use Classes/Prototypes. Router is more distinctive. It's not a component, like in React, but they also use Router, which is familiar. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jbucaran the thing is that it implies that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't use new or this; as an extension, neither does HyperApp. So, why should Router imply that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it's TitleCase. For someone familiar with javascript (and not that much with hyperapp's source-code) that usually means it has to be instantiated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plugins written in title case look better, just like Components. app({
plugins: [
Router,
Logger,
TimeDebugger,
FusionGenerator,
FluxCapacitor
]
}) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Router could be a plugin but that doesn't mean it has to be. If it is then it negates the
with an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @lukejacksonn. Notice the correct name is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For what it's worth, the convention in Hapi is to capitalize anything you 'require'. It makes it more clear which variables are from an external file, and I agree with @jbucaran that it looks nicer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to know! Components are also capitalized in HyperApp, but they are not classes. |
||
|
||
export { h, app, router } | ||
export { h, app, Router } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very strange to me.. Consider moving the callback function before the if?
and in anyway it looks strange, but that's better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not going to work, because we pass onError to effects and subscriptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wat? no, no. I not meant that. It's no matter how the var is named. The point is to not be inside the if condition
and that (as currently) are the same thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something, but I still think this is not going to work when sharing onError with multiple plugins. This is a minor thing, so please send me a PR after we land this one 👍.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the new hook system this problem has gone away!