components, angular dbmon

This commit is contained in:
Leo Horie 2016-05-03 23:39:01 -04:00
parent ba378d3652
commit 3282ef3f77
30 changed files with 1270 additions and 248 deletions

View file

@ -1,3 +1,5 @@
# Contributing
# FAQ # FAQ
## How do I go about contributing ideas or new features? ## How do I go about contributing ideas or new features?
@ -16,11 +18,15 @@ Ideally, provide code to reproduce the issue (via jsfiddle, a gist, etc). Even b
Assuming you have forked this repo, you can open the `index.html` file in a module's `tests` folder and look at console output to see only tests for that module, or you can run `ospec/bin/ospec` from the command line to run all tests under a Node.js environment. Additionally, you can modify a test to use `o.only(description, test)` instead of `o(description, test)` if you wish to run only a specific test. Assuming you have forked this repo, you can open the `index.html` file in a module's `tests` folder and look at console output to see only tests for that module, or you can run `ospec/bin/ospec` from the command line to run all tests under a Node.js environment. Additionally, you can modify a test to use `o.only(description, test)` instead of `o(description, test)` if you wish to run only a specific test.
There is no need to `npm install` anything in order to run the test suite, however NodeJS is required to run the test suite from the command line.
## How do I build Mithril? ## How do I build Mithril?
Run `node bundler/bundler.js` from the command line to generate the bundled file. If all you're trying to do is run examples in the codebase, you don't need to build Mithril, you can just open the various html files and things should just work.
To generate the bundled file, run `node bundler/bundler.js` from the command line. There is no need to `npm install` anything, but NodeJS is required to run the build script.
@ -56,7 +62,7 @@ In addition, ES6 features are usually less performant than equivalent ES5 code,
## Why doesn't the Mithril codebase use trailing semi-colons? Would a PR to add them be welcome? ## Why doesn't the Mithril codebase use trailing semi-colons? Would a PR to add them be welcome?
I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent. If you're not comfortable with ASI rules, bear in mind that this codebase is heavily optimized and large modifications will require an advanced level of javascript mastery. I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent.

View file

@ -22,7 +22,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="../../module/module.js"></script> <script src="../../module/module.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>
<script> <script>

55
examples/dbmonster/angular/app.js vendored Normal file
View file

@ -0,0 +1,55 @@
var renderStage = 0
perfMonitor.startFPSMonitor()
perfMonitor.startMemMonitor()
perfMonitor.initProfiler("render")
var AppComponent = ng.core.Component({selector: "my-app"})
.View({
directives: [ng.common.CORE_DIRECTIVES],
template: "<div>" +
"<table class='table table-striped latest-data'>" +
"<tbody>" +
"<tr *ngFor='let db of databases'>" +
"<td class='dbname'>{{db.dbname}}</td>" +
"<td class='query-count'>" +
"<span [class]='db.lastSample.countClassName'>{{db.lastSample.nbQueries}}</span>" +
"</td>" +
"<td *ngFor='let q of db.lastSample.topFiveQueries' [class]='q.elapsedClassName'>" +
"{{q.formatElapsed}}" +
"<div class='popover left'>" +
"<div class='popover-content'>{{q.query}}</div>" +
"<div class='arrow'></div>" +
"</div>" +
"</td>" +
"</tr>" +
"</tbody>" +
"</table>" +
"</div>"
})
.Class({
constructor: function() {
this.databases = []
this.update()
},
update: function() {
var self = this
self.databases = ENV.generateData(true).toArray()
setTimeout(function() {self.update()}, ENV.timeout)
if (renderStage === 0) {
renderStage = 1
perfMonitor.startProfile("render")
}
},
ngAfterViewChecked: function() {
if (renderStage === 1) {
perfMonitor.endProfile("render")
renderStage = 0
}
},
})
document.addEventListener("DOMContentLoaded", function() {
ng.core.enableProdMode()
ng.platform.browser.bootstrap(AppComponent)
})

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<link href="../lib/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link href="../styles.css" rel="stylesheet" type="text/css" />
<meta charset="utf-8">
<title>dbmon (Angular2)</title>
</head>
<body>
<my-app></my-app>
<script src="https://code.angularjs.org/2.0.0-beta.17/angular2-polyfills.js"></script>
<script src="https://code.angularjs.org/2.0.0-beta.17/Rx.umd.js"></script>
<script src="https://code.angularjs.org/2.0.0-beta.17/angular2-all.umd.js"></script>
<script src="../ENV.js"></script>
<script src="http://localvoid.github.io/perf-monitor/0.1/perf-monitor.js"></script>
<script src="app.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,10 @@
var m = require("../../../render/hyperscript") var m = require("../../../render/hyperscript")
var render = require("../../../render/render")(window).render var render = require("../../../render/render")(window).render
perfMonitor.startFPSMonitor()
perfMonitor.startMemMonitor()
perfMonitor.initProfiler("render")
var data = [] var data = []
var root = document.getElementById("app") var root = document.getElementById("app")
@ -11,29 +15,29 @@ update()
function update() { function update() {
data = ENV.generateData().toArray() data = ENV.generateData().toArray()
Monitoring.renderRate.ping() perfMonitor.startProfile("render")
render(root, view())
render(root, [view()]) perfMonitor.endProfile("render")
setTimeout(update, ENV.timeout) setTimeout(update, ENV.timeout)
} }
function view() { function view() {
return m("div", [ return m("div", [
m("table", { className: "table table-striped latest-data" }, [ m("table", {className: "table table-striped latest-data"}, [
m("tbody", m("tbody",
data.map(function(db) { data.map(function(db) {
return m("tr", {key: db.dbname}, [ return m("tr", {key: db.dbname}, [
m("td", { className: "dbname" }, db.dbname), m("td", {className: "dbname"}, db.dbname),
m("td", { className: "query-count" }, [ m("td", {className: "query-count"}, [
m("span", { className: db.lastSample.countClassName }, db.lastSample.nbQueries) m("span", {className: db.lastSample.countClassName}, db.lastSample.nbQueries)
]), ]),
db.lastSample.topFiveQueries.map(function(query) { db.lastSample.topFiveQueries.map(function(query) {
return m("td", { className: query.elapsedClassName }, [ return m("td", {className: query.elapsedClassName}, [
m("span", query.formatElapsed), query.formatElapsed,
m("div", { className: "popover left" }, [ m("div", {className: "popover left"}, [
m("div", { className: "popover-content" }, query.query), m("div", {className: "popover-content"}, query.query),
m("div", { className: "arrow" }) m("div", {className: "arrow"})
]) ])
]) ])
}) })

View file

@ -9,12 +9,11 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="../../../module/module.js"></script> <script src="../../../module/module.js"></script>
<script src="../../../render/normalizeChildren.js"></script> <script src="../../../render/node.js"></script>
<script src="../../../render/hyperscript.js"></script> <script src="../../../render/hyperscript.js"></script>
<script src="../../../render/render.js"></script> <script src="../../../render/render.js"></script>
<script src="../ENV.js"></script> <script src="../ENV.js"></script>
<script src="../memory-stats.js"></script> <script src="http://localvoid.github.io/perf-monitor/0.1/perf-monitor.js"></script>
<script src="../monitor.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>

View file

@ -1,36 +1,40 @@
"use strict"; "use strict"
var h = React.createElement var h = React.createElement
perfMonitor.startFPSMonitor()
perfMonitor.startMemMonitor()
perfMonitor.initProfiler("render")
var Query = React.createClass({ var Query = React.createClass({
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
if (nextProps.elapsedClassName !== this.props.elapsedClassName) return true; if (nextProps.elapsedClassName !== this.props.elapsedClassName) return true
if (nextProps.formatElapsed !== this.props.formatElapsed) return true; if (nextProps.formatElapsed !== this.props.formatElapsed) return true
if (nextProps.query !== this.props.query) return true; if (nextProps.query !== this.props.query) return true
return false; return false
}, },
render: function render() { render: function render() {
return h("td", { className: "Query " + this.props.elapsedClassName }, return h("td", {className: "Query " + this.props.elapsedClassName},
this.props.formatElapsed, this.props.formatElapsed,
h("div", { className: "popover left" }, h("div", {className: "popover left"},
h("div", { className: "popover-content" }, this.props.query), h("div", {className: "popover-content"}, this.props.query),
h("div", { className: "arrow" }) h("div", {className: "arrow"})
) )
); )
} }
}); })
var Database = React.createClass({ var Database = React.createClass({
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
if (nextProps.lastMutationId === this.props.lastMutationId) return false; if (nextProps.lastMutationId === this.props.lastMutationId) return false
return true; return true
}, },
render: function render() { render: function render() {
var lastSample = this.props.lastSample; var lastSample = this.props.lastSample
return h("tr", { key: this.props.dbname }, return h("tr", {key: this.props.dbname},
h("td", { className: "dbname" }, this.props.dbname), h("td", {className: "dbname"}, this.props.dbname),
h("td", { className: "query-count" }, h("td", {className: "query-count"},
h("span", { className: this.props.lastSample.countClassName }, this.props.lastSample.nbQueries) h("span", {className: this.props.lastSample.countClassName}, this.props.lastSample.nbQueries)
), ),
this.props.lastSample.topFiveQueries.map(function (query, index) { this.props.lastSample.topFiveQueries.map(function (query, index) {
return h(Query, { return h(Query, {
@ -39,34 +43,40 @@ var Database = React.createClass({
elapsed: query.elapsed, elapsed: query.elapsed,
formatElapsed: query.formatElapsed, formatElapsed: query.formatElapsed,
elapsedClassName: query.elapsedClassName elapsedClassName: query.elapsedClassName
}); })
}) })
); )
} }
}); })
var DBMon = React.createClass({ var DBMon = React.createClass({
getInitialState: function getInitialState() { getInitialState: function getInitialState() {
return { return {
databases: [] databases: []
}; }
}, },
loadSamples: function loadSamples() { loadSamples: function loadSamples() {
var data = ENV.generateData(true).toArray()
perfMonitor.startProfile("render")
this.setState({ this.setState({
databases: ENV.generateData(true).toArray() databases: data
}); })
Monitoring.renderRate.ping();
setTimeout(this.loadSamples, ENV.timeout); perfMonitor.endProfile("render")
setTimeout(this.loadSamples, ENV.timeout)
}, },
componentDidMount: function componentDidMount() { componentDidMount: function componentDidMount() {
this.loadSamples(); this.loadSamples()
}, },
render: function render() { render: function render() {
return h("div", null, return h("div", null,
h("table", { className: "table table-striped latest-data" }, h("table", {className: "table table-striped latest-data"},
h("tbody", null, this.state.databases.map(function (database) { h("tbody", null, this.state.databases.map(function (database) {
return h(Database, { return h(Database, {
key: database.dbname, key: database.dbname,
@ -74,11 +84,11 @@ var DBMon = React.createClass({
dbname: database.dbname, dbname: database.dbname,
samples: database.samples, samples: database.samples,
lastSample: database.lastSample lastSample: database.lastSample
}); })
})) }))
) )
); )
} }
}); })
ReactDOM.render(h(DBMon, null), document.getElementById('app')); ReactDOM.render(h(DBMon, null), document.getElementById('app'))

View file

@ -8,11 +8,10 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="https://fb.me/react-15.0.1.js"></script> <script src="https://fb.me/react-15.0.2.js"></script>
<script src="https://fb.me/react-dom-15.0.1.js"></script> <script src="https://fb.me/react-dom-15.0.2.js"></script>
<script src="../ENV.js"></script> <script src="../ENV.js"></script>
<script src="../memory-stats.js"></script> <script src="http://localvoid.github.io/perf-monitor/0.1/perf-monitor.js"></script>
<script src="../monitor.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>

View file

@ -8,7 +8,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="../../module/module.js"></script> <script src="../../module/module.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>
<script> <script>

View file

@ -8,7 +8,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="../../module/module.js"></script> <script src="../../module/module.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>
<script> <script>

View file

@ -8,7 +8,7 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="../../module/module.js"></script> <script src="../../module/module.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>
<script> <script>

View file

@ -10,34 +10,17 @@ var router = require("../../router/router")(window, "#")
var api = { var api = {
home : function() { home : function() {
T.timeEnd("Setup") T.timeEnd("Setup")
return request({ return request({method: "GET", url: T.apiUrl + "/threads/"})
method: "GET",
url: T.apiUrl + "/threads/",
})
}, },
thread : function(id) { thread : function(id) {
T.timeEnd("Setup") T.timeEnd("Setup")
return request({ return request({method: "GET", url: T.apiUrl + "/comments/" + id}).then(T.transformResponse)
method: "GET",
url: T.apiUrl + "/comments/" + id,
}).then(T.transformResponse)
}, },
newThread : function(text) { newThread : function(text) {
return request({ return request({method: "POST", url: T.apiUrl + "/threads/create",data: {text: text}})
method: "POST",
url: T.apiUrl + "/threads/create",
data: {text: text},
});
}, },
newComment : function(text, id) { newComment : function(text, id) {
return request({ return request({method: "POST", url: T.apiUrl + "/comments/create", data: {text: text, parent: id}});
url: T.apiUrl + "/comments/create",
method: "POST",
data: {
text: text,
parent: id,
}
});
} }
}; };
@ -82,9 +65,9 @@ function createThread() {
return false return false
} }
function showReplying(node) { function showReplying(vnode) {
node.replying = true vnode.state.replying = true
node.newComment = "" vnode.state.newComment = ""
return false return false
} }
@ -99,39 +82,53 @@ function submitComment(node) {
} }
//shared //shared
function header() { var Header = {
return [ view: function() {
m("p.head_links", [ return [
m("a[href='https://github.com/koglerjs/threaditjs/tree/master/examples/mithril']", "Source"), m("p.head_links", [
" | ", m("a[href='https://github.com/koglerjs/threaditjs/tree/master/examples/mithril']", "Source"),
m("a[href='http://threaditjs.com']", "ThreaditJS Home"), " | ",
]), m("a[href='http://threaditjs.com']", "ThreaditJS Home"),
m("h2", [ ]),
m("a[href='#/']", "ThreaditJS: Mithril"), m("h2", [
]), m("a[href='#/']", "ThreaditJS: Mithril"),
] ]),
]
}
} }
//home //home
function home() { var Home = {
return {tag: "[", key: "home", attrs: {oncreate: loadThreads}, children: [ oninit: loadThreads,
header(), view: function() {
m(".main", [ return [
loaded === false ? m("h2", "Loading") : m(Header),
error ? m("h2", "Error! Try refreshing.") : m(".main", [
notFound ? m("h2", "Not found! Don't try refreshing!") : loaded === false ? m("h2", "Loading") :
[ error ? m("h2", "Error! Try refreshing.") :
threads.map(threadListItem), notFound ? m("h2", "Not found! Don't try refreshing!") : [
newThread(), threads.map(function(thread) {
] return [
]) m("p", [
]} m("a", {href: "#/thread/" + thread.id}, trust(T.trimTitle(thread.text))),
]),
m("p.comment_count", thread.comment_count + " comment(s)"),
m("hr"),
]
}),
m(NewThread),
]
])
]
}
} }
function newThread() { var NewThread = {
return m("form", {onsubmit: createThread}, [ view: function() {
m("textarea#threadText"), return m("form", {onsubmit: createThread}, [
m("input", {type:"submit", value: "Post!"}), m("textarea#threadText"),
]) m("input", {type:"submit", value: "Post!"}),
])
}
} }
function threadListItem(thread) { function threadListItem(thread) {
@ -145,39 +142,49 @@ function threadListItem(thread) {
} }
//thread //thread
function thread(args) { var Thread = {
if (current) T.time("Thread render") oninit: function(vnode) {
return {tag: "[", key: args.id, attrs: {oncreate: function() {loadThread(args.id)}, onremove: unloadThread}, children: [ loadThread(vnode.attrs.id)
header(), },
current ? m(".main", {oncreate: function() {T.timeEnd("Thread render")}}, [ onremove: unloadThread,
threadNode({node: current.root}) view: function() {
]) : null if (current) T.time("Thread render")
]} return [
m(Header),
current ? m(".main", {oncreate: function() {T.timeEnd("Thread render")}}, [
m(ThreadNode, {node: current.root})
]) : null
]
}
} }
function threadNode(args) { var ThreadNode = {
return m(".comment", [ view: function(vnode) {
m("p", trust(args.node.text)), return m(".comment", [
m(".reply", reply(args)), m("p", trust(vnode.attrs.node.text)),
m(".children", [ m(".reply", m(Reply, vnode.attrs)),
args.node.children.map(function(child) { m(".children", [
return threadNode({node: child}) vnode.attrs.node.children.map(function(child) {
}) return m(ThreadNode, {node: child})
})
])
]) ])
]) }
} }
function reply(args) { var Reply = {
return args.node.replying view: function(vnode) {
? m("form", {onsubmit: function() {return submitComment(args.node)}}, [ return vnode.state.replying
m("textarea", { ? m("form", {onsubmit: function() {return submitComment(vnode.attrs.node)}}, [
value: args.node.newComment, //FIXME decouple UI state from data m("textarea", {
oninput: function(e) { value: vnode.state.newComment,
args.node.newComment = e.target.value oninput: function(e) {
}, vnode.state.newComment = e.target.value
}), },
m("input", {type:"submit", value: "Reply!"}), }),
m(".preview", trust(T.previewComment(args.node.newComment))), m("input", {type:"submit", value: "Reply!"}),
]) m(".preview", trust(T.previewComment(vnode.state.newComment))),
: m("a", {onclick: function() {return showReplying(args.node)}}, "Reply!") ])
: m("a", {onclick: function() {return showReplying(vnode)}}, "Reply!")
}
} }
//router //router
@ -186,10 +193,10 @@ function run() {
} }
var replayRoute = router.defineRoutes({ var replayRoute = router.defineRoutes({
"/thread/:id" : thread, "/thread/:id" : Thread,
"/" : home "/" : Home
}, function(view, args) { }, function(view, args) {
render(document.body, [view(args)]) render(document.body, [m(view, args)])
}, function() { }, function() {
router.setPath("/") router.setPath("/")
}) })

View file

@ -10,7 +10,7 @@
<body> <body>
<script src="http://threaditjs.com/shared.js"></script> <script src="http://threaditjs.com/shared.js"></script>
<script src="../../module/module.js"></script> <script src="../../module/module.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/trust.js"></script> <script src="../../render/trust.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>

View file

@ -13,7 +13,7 @@
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
<script src="../../module/module.js"></script> <script src="../../module/module.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>
<script src="../../querystring/build.js"></script> <script src="../../querystring/build.js"></script>

72
index.html Normal file
View file

@ -0,0 +1,72 @@
<!doctype html>
<html>
<head>
<style>* {font-size:40px;}</style>
</head>
<body>
<script src="./module/module.js"></script>
<script src="./render/node.js"></script>
<script src="./render/hyperscript.js"></script>
<script src="./render/render.js"></script>
<script>
var m = require("./render/hyperscript")
var render = require("./render/render")(window, run).render
var count = 10
function inc() {count++}
var Counter1 = {
oninit: function(vnode) {console.log("init", vnode)},
oncreate: function(vnode) {console.log("create", vnode)},
onupdate: function(vnode) {console.log("update", vnode)},
onremove: function(vnode) {console.log("remove", vnode)},
onbeforeremove: function(vnode, done) {console.log("before remove", vnode);done()},
view: function({attrs, state}) {
return m("a", {
onclick: () => {
count++
state.visible = !state.visible
}
}, attrs.count + (state.visible ? "visible" : ""))
}
}
/*
class Counter2 {
oninit(vnode) {
request().then(function(foo) {
vnode.state.foo = foo
})
}
onupdate(vnode) {
vnode.dom.style.borderRight = vnode.attrs.count + "px solid red"
}
view(vnode) {
return m("a", {onclick: inc}, vnode.attrs.count)
}
}
function Counter3() {
return m("a", {
onupdate: function(vnode) {
vnode.dom.style.borderRight = vnode.attrs.count + "px solid red"
},
onclick: inc
}, vnode.attrs.count)
}
*/
function view() {
return m("div", [
m("a", {onclick: inc}, count),
m("br"),
count % 7 !== 0 ? {tag: Counter1, attrs: {count: count}, text: "test", state: {}} : null,
])
}
function run() {
render(document.body, view())
}
run()
</script>
</body>
</html>

View file

@ -70,16 +70,22 @@ module.exports = new function init() {
var body = fn.toString() var body = fn.toString()
var arg = (body.match(/\(([\w_$]+)/) || body.match(/([\w_$]+)\s*=>/) || []).pop() var arg = (body.match(/\(([\w_$]+)/) || body.match(/([\w_$]+)\s*=>/) || []).pop()
if (body.indexOf(arg) === body.lastIndexOf(arg)) throw new Error("`" + arg + "()` should be called at least once") if (body.indexOf(arg) === body.lastIndexOf(arg)) throw new Error("`" + arg + "()` should be called at least once")
fn(function done() { try {
if (timeout !== undefined) { fn(function done() {
timeout = clearTimeout(timeout) if (timeout !== undefined) {
if (delay !== Infinity) record(null) timeout = clearTimeout(timeout)
if (!isDone) next() if (delay !== Infinity) record(null)
else throw new Error("`" + arg + "()` should only be called once") if (!isDone) next()
isDone = true else throw new Error("`" + arg + "()` should only be called once")
} isDone = true
else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms") }
}, function(t) {delay = t}) else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms")
}, function(t) {delay = t})
}
catch (e) {
record(e.message, e)
next()
}
if (timeout === 0) { if (timeout === 0) {
timeout = setTimeout(function() { timeout = setTimeout(function() {
timeout = undefined timeout = undefined
@ -157,11 +163,13 @@ module.exports = new function init() {
} }
} }
} }
function record(message) { function record(message, error) {
var result = {pass: message === null} var result = {pass: message === null}
if (result.pass === false) { if (result.pass === false) {
var error = new Error if (error == null) {
if (error.stack === undefined) new function() {try {throw error} catch (e) {error = e}} error = new Error
if (error.stack === undefined) new function() {try {throw error} catch (e) {error = e}}
}
result.context = subjects.join(" > ") result.context = subjects.join(" > ")
result.message = message result.message = message
result.error = error.stack result.error = error.stack
@ -170,6 +178,7 @@ module.exports = new function init() {
} }
function serialize(value) { function serialize(value) {
if (value === null || typeof value === "object") return String(value) if (value === null || typeof value === "object") return String(value)
else if (typeof value === "function") return value.name || "<anonymous function>"
try {return JSON.stringify(value)} catch (e) {return String(value)} try {return JSON.stringify(value)} catch (e) {return String(value)}
} }
function highlight(message) { function highlight(message) {
@ -178,7 +187,7 @@ module.exports = new function init() {
function report() { function report() {
for (var i = 0, r; r = results[i]; i++) { for (var i = 0, r; r = results[i]; i++) {
if (!r.pass) console.info(r.context + ": " + highlight(r.message) + "\n\n" + r.error.match(/^(?:(?!^Error|[\/\\]ospec[\/\\]ospec\.js).)*$/m) + "\n\n", hasProcess ? "" : "color:red", hasProcess ? "" : "color:black") if (!r.pass) console.error(r.context + ": " + highlight(r.message) + "\n\n" + r.error.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/m) + "\n\n", hasProcess ? "" : "color:red", hasProcess ? "" : "color:black")
} }
console.log(results.length + " tests completed in " + Math.round(new Date - start) + "ms") console.log(results.length + " tests completed in " + Math.round(new Date - start) + "ms")
} }

View file

@ -4,6 +4,7 @@
"description": "A framework for building brilliant applications", "description": "A framework for building brilliant applications",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "node bundler/bundler",
"test": "node ospec/bin/ospec" "test": "node ospec/bin/ospec"
}, },
"author": "Leo Horie", "author": "Leo Horie",

View file

@ -1,43 +1,51 @@
"use strict" "use strict"
var normalizeChildren = require("../render/normalizeChildren") var Node = require("../render/node")
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:\s*=\s*("|'|)(.*?)\2)?\]/ var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:\s*=\s*("|'|)(.*?)\2)?\]/
var selectorCache = {} var selectorCache = {}
function hyperscript(selector) { function hyperscript(selector) {
if (selectorCache[selector] === undefined) { if (typeof selector === "string") {
var match, tag, id, classes = [], attributes = {} if (selectorCache[selector] === undefined) {
while (match = selectorParser.exec(selector)) { var match, tag, id, classes = [], attributes = {}
var type = match[1], value = match[2] while (match = selectorParser.exec(selector)) {
if (type === "" && value !== "") tag = value var type = match[1], value = match[2]
else if (type === "#") attributes.id = value if (type === "" && value !== "") tag = value
else if (type === ".") classes.push(value) else if (type === "#") attributes.id = value
else if (match[3][0] === "[") { else if (type === ".") classes.push(value)
var pair = attrParser.exec(match[3]) else if (match[3][0] === "[") {
attributes[pair[1]] = pair[3] || true var pair = attrParser.exec(match[3])
} attributes[pair[1]] = pair[3] || true
}
if (classes.length > 0) attributes.className = classes.join(" ")
selectorCache[selector] = function(attrs, children) {
var hasAttrs = false, childList, text
var className = attrs.className || attrs.class
for (var key in attributes) attrs[key] = attributes[key]
if (className !== undefined) {
if (attrs.class !== undefined) {
attrs.class = undefined
attrs.className = className
}
if (attributes.className !== undefined) attrs.className = attributes.className + " " + className
}
for (var key in attrs) {
if (key !== "key") {
hasAttrs = true
break
} }
} }
if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children if (classes.length > 0) attributes.className = classes.join(" ")
else childList = children selectorCache[selector] = function(attrs, children) {
return namespace({tag: tag || "div", key: attrs.key, attrs: hasAttrs ? attrs : undefined, children: childList, text: text}) var hasAttrs = false, childList, text
var className = attrs.className || attrs.class
for (var key in attributes) attrs[key] = attributes[key]
if (className !== undefined) {
if (attrs.class !== undefined) {
attrs.class = undefined
attrs.className = className
}
if (attributes.className !== undefined) attrs.className = attributes.className + " " + className
}
for (var key in attrs) {
if (key !== "key") {
hasAttrs = true
break
}
}
if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children
else childList = children
var vnode = Node(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined)
switch (vnode.tag) {
case "svg": changeNS("http://www.w3.org/2000/svg", vnode); break
case "math": changeNS("http://www.w3.org/1998/Math/MathML", vnode); break
}
return vnode
}
} }
} }
var attrs, children, childrenIndex var attrs, children, childrenIndex
@ -54,15 +62,8 @@ function hyperscript(selector) {
for (var i = childrenIndex; i < arguments.length; i++) children.push(arguments[i]) for (var i = childrenIndex; i < arguments.length; i++) children.push(arguments[i])
} }
return selectorCache[selector](attrs || {}, normalizeChildren(children)) if (typeof selector === "string") return selectorCache[selector](attrs || {}, Node.normalizeChildren(children))
} return Node(selector, attrs && attrs.key, attrs, Node.normalizeChildren(children), undefined, undefined)
function namespace(vnode) {
switch (vnode.tag) {
case "svg": changeNS("http://www.w3.org/2000/svg", vnode); break
case "math": changeNS("http://www.w3.org/1998/Math/MathML", vnode); break
}
return vnode
} }
function changeNS(ns, vnode) { function changeNS(ns, vnode) {

16
render/node.js Normal file
View file

@ -0,0 +1,16 @@
function Node(tag, key, attrs, children, text, dom) {
return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}}
}
Node.normalize = function(node) {
if (node instanceof Array) return Node("[", undefined, undefined, Node.normalizeChildren(node), undefined, undefined)
else if (node != null && typeof node !== "object") return Node("#", undefined, undefined, node, undefined, undefined)
return node
}
Node.normalizeChildren = function normalizeChildren(children) {
for (var i = 0; i < children.length; i++) {
children[i] = Node.normalize(children[i])
}
return children
}
module.exports = Node

View file

@ -1,7 +0,0 @@
module.exports = function normalizeChildren(children) {
for (var i = 0; i < children.length; i++) {
if (children[i] instanceof Array) children[i] = {tag: "[", key: undefined, attrs: undefined, children: normalizeChildren(children[i]), text: undefined}
else if (children[i] != null && typeof children[i] !== "object") children[i] = {tag: "#", key: undefined, attrs: undefined, children: children[i], text: undefined}
}
return children
}

View file

@ -1,6 +1,6 @@
"use strict" "use strict"
var normalizeChildren = require("../render/normalizeChildren") var Node = require("../render/node")
module.exports = function($window, onevent) { module.exports = function($window, onevent) {
var $doc = $window.document var $doc = $window.document
@ -16,15 +16,19 @@ module.exports = function($window, onevent) {
} }
function createNode(vnode, hooks) { function createNode(vnode, hooks) {
var tag = vnode.tag var tag = vnode.tag
if (vnode.attrs && vnode.attrs.oncreate) { if (vnode.attrs) {
hooks.push(vnode.attrs.oncreate.bind(vnode, vnode)) if (vnode.attrs.oninit) vnode.attrs.oninit.call(vnode, vnode)
if (vnode.attrs.oncreate) hooks.push(vnode.attrs.oncreate.bind(vnode, vnode))
} }
switch (tag) { if (typeof tag === "string") {
case "#": return createText(vnode) switch (tag) {
case "<": return createHTML(vnode) case "#": return createText(vnode)
case "[": return createFragment(vnode, hooks) case "<": return createHTML(vnode)
default: return createElement(vnode, hooks) case "[": return createFragment(vnode, hooks)
default: return createElement(vnode, hooks)
}
} }
else return createComponent(vnode, hooks)
} }
function createText(vnode) { function createText(vnode) {
return vnode.dom = $doc.createTextNode(vnode.children) return vnode.dom = $doc.createTextNode(vnode.children)
@ -72,7 +76,7 @@ module.exports = function($window, onevent) {
if (vnode.text != null) { if (vnode.text != null) {
if (vnode.text !== "") element.textContent = vnode.text if (vnode.text !== "") element.textContent = vnode.text
else vnode.children = [{tag: "#", children: vnode.text}] else vnode.children = [Node("#", undefined, undefined, vnode.text, undefined, undefined)]
} }
if (vnode.children != null) { if (vnode.children != null) {
@ -81,6 +85,14 @@ module.exports = function($window, onevent) {
} }
return element return element
} }
function createComponent(vnode, hooks) {
vnode.instance = Node.normalize(vnode.tag.view(vnode))
initLifecycle(vnode.tag, vnode, hooks)
var element = createNode(vnode.instance, hooks)
vnode.dom = vnode.instance.dom
vnode.domSize = vnode.instance.domSize
return element
}
//update //update
function updateNodes(parent, old, vnodes, hooks, nextSibling) { function updateNodes(parent, old, vnodes, hooks, nextSibling) {
@ -98,7 +110,7 @@ module.exports = function($window, onevent) {
else if (o != null && v != null && o.key === v.key) { else if (o != null && v != null && o.key === v.key) {
oldStart++, start++ oldStart++, start++
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling)
if (recycling) insertNode(parent, toFragment(v), nextSibling) if (recycling) insertNode(parent, toFragment(o), nextSibling)
} }
else { else {
var o = old[oldEnd] var o = old[oldEnd]
@ -116,7 +128,7 @@ module.exports = function($window, onevent) {
if (o === v) oldEnd--, end-- if (o === v) oldEnd--, end--
else if (o != null && v != null && o.key === v.key) { else if (o != null && v != null && o.key === v.key) {
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling) updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling)
if (recycling) insertNode(parent, toFragment(v), nextSibling) if (recycling) insertNode(parent, toFragment(o), nextSibling)
nextSibling = o.dom nextSibling = o.dom
oldEnd--, end-- oldEnd--, end--
} }
@ -148,16 +160,17 @@ module.exports = function($window, onevent) {
function updateNode(parent, old, vnode, hooks, nextSibling, recycling) { function updateNode(parent, old, vnode, hooks, nextSibling, recycling) {
var oldTag = old.tag, tag = vnode.tag var oldTag = old.tag, tag = vnode.tag
if (oldTag === tag) { if (oldTag === tag) {
if (recycling) { vnode.state = old.state
if (vnode.attrs && vnode.attrs.oncreate) hooks.push(vnode.attrs.oncreate.bind(vnode, vnode)) if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks, recycling)
} if (typeof oldTag === "string") {
else if (vnode.attrs && vnode.attrs.onupdate) hooks.push(vnode.attrs.onupdate.bind(vnode, vnode)) switch (oldTag) {
switch (oldTag) { case "#": updateText(old, vnode); break
case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break
case "<": updateHTML(parent, old, vnode, nextSibling); break case "[": updateFragment(parent, old, vnode, hooks, nextSibling); break
case "[": updateFragment(parent, old, vnode, hooks, nextSibling); break default: updateElement(old, vnode, hooks)
default: updateElement(old, vnode, hooks) }
} }
else updateComponent(parent, old, vnode, hooks, nextSibling, recycling)
} }
else { else {
removeNode(parent, old, null, false) removeNode(parent, old, null, false)
@ -189,7 +202,7 @@ module.exports = function($window, onevent) {
domSize += child.domSize || 1 domSize += child.domSize || 1
} }
} }
if (domSize != 1) vnode.domSize = domSize if (domSize !== 1) vnode.domSize = domSize
} }
} }
function updateElement(old, vnode, hooks) { function updateElement(old, vnode, hooks) {
@ -199,11 +212,18 @@ module.exports = function($window, onevent) {
if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text
} }
else { else {
if (old.text != null) old.children = [{tag: "#", children: old.text, dom: old.dom.firstChild}] if (old.text != null) old.children = [Node("#", undefined, undefined, old.text, undefined, old.dom.firstChild)]
if (vnode.text != null) vnode.children = [{tag: "#", children: vnode.text}] if (vnode.text != null) vnode.children = [Node("#", undefined, undefined, vnode.text, undefined, undefined)]
updateNodes(element, old.children, vnode.children, hooks, null) updateNodes(element, old.children, vnode.children, hooks, null)
} }
} }
function updateComponent(parent, old, vnode, hooks, nextSibling, recycling) {
vnode.instance = Node.normalize(vnode.tag.view(vnode))
updateLifecycle(vnode.tag, vnode, hooks, recycling)
updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling)
vnode.dom = vnode.instance.dom
vnode.domSize = vnode.instance.domSize
}
function isRecyclable(old, vnodes) { function isRecyclable(old, vnodes) {
if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) { if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) {
var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0 var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0
@ -262,9 +282,20 @@ module.exports = function($window, onevent) {
} }
} }
function removeNode(parent, vnode, context, deferred) { function removeNode(parent, vnode, context, deferred) {
if (vnode.attrs && vnode.attrs.onbeforeremove && deferred === false) { if (deferred === false) {
vnode.attrs.onbeforeremove.call(vnode, vnode, function() {removeNode(parent, vnode, context, true)}) var expected = 0, called = 0
return var callback = function() {
if (++called === expected) removeNode(parent, vnode, context, true)
}
if (vnode.attrs && vnode.attrs.onbeforeremove) {
expected++
vnode.attrs.onbeforeremove.call(vnode, vnode, callback)
}
if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) {
expected++
vnode.tag.onbeforeremove.call(vnode, vnode, callback)
}
if (expected > 0) return
} }
onremove(vnode) onremove(vnode)
@ -285,9 +316,10 @@ module.exports = function($window, onevent) {
} }
function onremove(vnode) { function onremove(vnode) {
if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode, vnode) if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode, vnode)
if (typeof vnode.tag !== "string" && vnode.tag.onremove) vnode.tag.onremove.call(vnode, vnode)
var children = vnode.children var children = vnode.children
if (children) { if (children instanceof Array) {
for (var i = 0; i < children.length; i++) { for (var i = 0; i < children.length; i++) {
var child = children[i] var child = children[i]
if (child != null) onremove(child) if (child != null) onremove(child)
@ -341,7 +373,7 @@ module.exports = function($window, onevent) {
} }
} }
function isLifecycleMethod(attr) { function isLifecycleMethod(attr) {
return attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove"
} }
function isAttribute(attr) { function isAttribute(attr) {
return attr === "href" || attr === "list" || attr === "form"// || attr === "type" || attr === "width" || attr === "height" return attr === "href" || attr === "list" || attr === "form"// || attr === "type" || attr === "width" || attr === "height"
@ -364,15 +396,23 @@ module.exports = function($window, onevent) {
} }
} }
//lifecycle
function initLifecycle(source, vnode, hooks) {
if (source.oninit != null) source.oninit.call(vnode, vnode)
if (source.oncreate != null) hooks.push(source.oncreate.bind(vnode, vnode))
}
function updateLifecycle(source, vnode, hooks, recycling) {
if (recycling) initLifecycle(source, vnode, hooks)
else if (source.onupdate != null) hooks.push(source.onupdate.bind(vnode, vnode))
}
function render(dom, vnodes) { function render(dom, vnodes) {
//if (dom.lastRedraw + 16 > performance.now() && vnodes.length > 0) return
//dom.lastRedraw = performance.now()
var hooks = [] var hooks = []
var active = $doc.activeElement var active = $doc.activeElement
if (!dom.vnodes) dom.vnodes = [] if (dom.vnodes == null) dom.vnodes = []
if (!(vnodes instanceof Array)) vnodes = [vnodes] if (!(vnodes instanceof Array)) vnodes = [vnodes]
updateNodes(dom, dom.vnodes, normalizeChildren(vnodes), hooks, null) updateNodes(dom, dom.vnodes, Node.normalizeChildren(vnodes), hooks, null)
for (var i = 0; i < hooks.length; i++) hooks[i]() for (var i = 0; i < hooks.length; i++) hooks[i]()
dom.vnodes = vnodes dom.vnodes = vnodes
if ($doc.activeElement !== active) active.focus() if ($doc.activeElement !== active) active.focus()

View file

@ -9,10 +9,14 @@
<script src="../../test-utils/callAsync.js"></script> <script src="../../test-utils/callAsync.js"></script>
<script src="../../test-utils/domMock.js"></script> <script src="../../test-utils/domMock.js"></script>
<script src="../../render/normalizeChildren.js"></script> <script src="../../render/node.js"></script>
<script src="../../render/trust.js"></script>
<script src="../../render/hyperscript.js"></script> <script src="../../render/hyperscript.js"></script>
<script src="../../render/render.js"></script> <script src="../../render/render.js"></script>
<script src="test-hyperscript.js"></script> <script src="test-hyperscript.js"></script>
<script src="test-trust.js"></script>
<script src="test-normalize.js"></script>
<script src="test-normalizeChildren.js"></script>
<script src="test-createText.js"></script> <script src="test-createText.js"></script>
<script src="test-createHTML.js"></script> <script src="test-createHTML.js"></script>
<script src="test-createFragment.js"></script> <script src="test-createFragment.js"></script>
@ -23,6 +27,7 @@
<script src="test-updateFragment.js"></script> <script src="test-updateFragment.js"></script>
<script src="test-updateElement.js"></script> <script src="test-updateElement.js"></script>
<script src="test-updateNodes.js"></script> <script src="test-updateNodes.js"></script>
<script src="test-oninit.js"></script>
<script src="test-oncreate.js"></script> <script src="test-oncreate.js"></script>
<script src="test-onupdate.js"></script> <script src="test-onupdate.js"></script>
<script src="test-onremove.js"></script> <script src="test-onremove.js"></script>
@ -31,6 +36,7 @@
<script src="test-event.js"></script> <script src="test-event.js"></script>
<script src="test-input.js"></script> <script src="test-input.js"></script>
<script src="test-textContent.js"></script> <script src="test-textContent.js"></script>
<script src="test-component.js"></script>
<script>require("../../ospec/ospec").run()</script> <script>require("../../ospec/ospec").run()</script>
</body> </body>

View file

@ -0,0 +1,459 @@
"use strict"
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
o.spec("component", function() {
var $window, root, render
o.beforeEach(function() {
$window = domMock()
root = $window.document.createElement("div")
render = vdom($window).render
})
o.spec("basics", function() {
o("works", function() {
var component = {
view: function() {
return {tag: "div", attrs: {id: "a"}, text: "b"}
}
}
var node = {tag: component}
render(root, [node])
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("receives arguments", function() {
var component = {
view: function(vnode) {
return {tag: "div", attrs: vnode.attrs, text: vnode.text}
}
}
var node = {tag: component, attrs: {id: "a"}, text: "b"}
render(root, [node])
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("updates", function() {
var component = {
view: function(vnode) {
return {tag: "div", attrs: vnode.attrs, text: vnode.text}
}
}
render(root, [{tag: component, attrs: {id: "a"}, text: "b"}])
render(root, [{tag: component, attrs: {id: "c"}, text: "d"}])
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("c")
o(root.firstChild.firstChild.nodeValue).equals("d")
})
o("removes", function() {
var component = {
view: function(vnode) {
return {tag: "div"}
}
}
var div = {tag: "div", key: 2}
render(root, [{tag: component, key: 1}, div])
render(root, [{tag: "div", key: 2}])
o(root.childNodes.length).equals(1)
o(root.firstChild).equals(div.dom)
})
})
o.spec("return value", function() {
o("can return fragments", function() {
var component = {
view: function(vnode) {
return [
{tag: "label"},
{tag: "input"},
]
}
}
render(root, [{tag: component}])
o(root.childNodes.length).equals(2)
o(root.childNodes[0].nodeName).equals("LABEL")
o(root.childNodes[1].nodeName).equals("INPUT")
})
o("can return string", function() {
var component = {
view: function(vnode) {
return "a"
}
}
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("a")
})
o("can return falsy string", function() {
var component = {
view: function(vnode) {
return ""
}
}
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("")
})
o("can return number", function() {
var component = {
view: function(vnode) {
return 1
}
}
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("1")
})
o("can return falsy number", function() {
var component = {
view: function(vnode) {
return 0
}
}
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("0")
})
o("can return boolean", function() {
var component = {
view: function(vnode) {
return true
}
}
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("true")
})
o("can return falsy boolean", function() {
var component = {
view: function(vnode) {
return false
}
}
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("false")
})
o("can update when returning fragments", function() {
var component = {
view: function(vnode) {
return [
{tag: "label"},
{tag: "input"},
]
}
}
render(root, [{tag: component}])
render(root, [{tag: component}])
o(root.childNodes.length).equals(2)
o(root.childNodes[0].nodeName).equals("LABEL")
o(root.childNodes[1].nodeName).equals("INPUT")
})
o("can update when returning primitive", function() {
var component = {
view: function(vnode) {
return "a"
}
}
render(root, [{tag: component}])
render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("a")
})
o("can remove when returning fragments", function() {
var component = {
view: function(vnode) {
return [
{tag: "label"},
{tag: "input"},
]
}
}
var div = {tag: "div", key: 2}
render(root, [{tag: component, key: 1}, div])
render(root, [{tag: "div", key: 2}])
o(root.childNodes.length).equals(1)
o(root.firstChild).equals(div.dom)
})
o("can remove when returning primitive", function() {
var component = {
view: function(vnode) {
return "a"
}
}
var div = {tag: "div", key: 2}
render(root, [{tag: component, key: 1}, div])
render(root, [{tag: "div", key: 2}])
o(root.childNodes.length).equals(1)
o(root.firstChild).equals(div.dom)
})
})
o.spec("lifecycle", function() {
o("calls oninit", function() {
var called = 0
var component = {
oninit: function(vnode) {
called++
o(vnode.tag).equals(component)
o(vnode.dom).equals(undefined)
o(root.childNodes.length).equals(0)
},
view: function() {
return {tag: "div", attrs: {id: "a"}, text: "b"}
}
}
var node = {tag: component}
render(root, [node])
o(called).equals(1)
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("calls oninit when returning fragment", function() {
var called = 0
var component = {
oninit: function(vnode) {
called++
o(vnode.tag).equals(component)
o(vnode.dom).equals(undefined)
o(root.childNodes.length).equals(0)
},
view: function() {
return [{tag: "div", attrs: {id: "a"}, text: "b"}]
}
}
var node = {tag: component}
render(root, [node])
o(called).equals(1)
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("calls oncreate", function() {
var called = 0
var component = {
oncreate: function(vnode) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
},
view: function() {
return {tag: "div", attrs: {id: "a"}, text: "b"}
}
}
var node = {tag: component}
render(root, [node])
o(called).equals(1)
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("calls oncreate when returning fragment", function() {
var called = 0
var component = {
oncreate: function(vnode) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
},
view: function() {
return [{tag: "div", attrs: {id: "a"}, text: "b"}]
}
}
var node = {tag: component}
render(root, [node])
o(called).equals(1)
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("calls onupdate", function() {
var called = 0
var component = {
onupdate: function(vnode) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
},
view: function() {
return {tag: "div", attrs: {id: "a"}, text: "b"}
}
}
render(root, [{tag: component}])
o(called).equals(0)
render(root, [{tag: component}])
o(called).equals(1)
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("calls onupdate when returning fragment", function() {
var called = 0
var component = {
onupdate: function(vnode) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
},
view: function() {
return [{tag: "div", attrs: {id: "a"}, text: "b"}]
}
}
render(root, [{tag: component}])
o(called).equals(0)
render(root, [{tag: component}])
o(called).equals(1)
o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.attributes["id"].nodeValue).equals("a")
o(root.firstChild.firstChild.nodeValue).equals("b")
})
o("calls onremove", function() {
var called = 0
var component = {
onremove: function(vnode) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
},
view: function() {
return {tag: "div", attrs: {id: "a"}, text: "b"}
}
}
render(root, [{tag: component}])
o(called).equals(0)
render(root, [])
o(called).equals(1)
o(root.childNodes.length).equals(0)
})
o("calls onremove when returning fragment", function() {
var called = 0
var component = {
onremove: function(vnode) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
},
view: function() {
return [{tag: "div", attrs: {id: "a"}, text: "b"}]
}
}
render(root, [{tag: component}])
o(called).equals(0)
render(root, [])
o(called).equals(1)
o(root.childNodes.length).equals(0)
})
o("calls onbeforeremove", function() {
var called = 0
var component = {
onbeforeremove: function(vnode, done) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
done()
},
view: function() {
return {tag: "div", attrs: {id: "a"}, text: "b"}
}
}
render(root, [{tag: component}])
o(called).equals(0)
render(root, [])
o(called).equals(1)
o(root.childNodes.length).equals(0)
})
o("calls onbeforeremove when returning fragment", function() {
var called = 0
var component = {
onbeforeremove: function(vnode, done) {
called++
o(vnode.dom).notEquals(undefined)
o(vnode.dom).equals(root.firstChild)
o(root.childNodes.length).equals(1)
done()
},
view: function() {
return [{tag: "div", attrs: {id: "a"}, text: "b"}]
}
}
render(root, [{tag: component}])
o(called).equals(0)
render(root, [])
o(called).equals(1)
o(root.childNodes.length).equals(0)
})
})
})

View file

@ -376,4 +376,20 @@ o.spec("hyperscript", function() {
o(vnode.children[0].ns).equals("http://www.w3.org/1998/Math/MathML") o(vnode.children[0].ns).equals("http://www.w3.org/1998/Math/MathML")
}) })
}) })
o.spec("components", function() {
o("works", function() {
var component = {
view: function() {
return m("div")
}
}
var vnode = m(component, {id: "a"}, "b")
o(vnode.tag).equals(component)
o(vnode.attrs.id).equals("a")
o(vnode.children.length).equals(1)
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("b")
})
})
}) })

View file

@ -0,0 +1,58 @@
"use strict"
var o = require("../../ospec/ospec")
var Node = require("../../render/node")
o.spec("normalize", function() {
o("normalizes array into fragment", function() {
var node = Node.normalize([])
o(node.tag).equals("[")
o(node.children.length).equals(0)
})
o("normalizes nested array into fragment", function() {
var node = Node.normalize([[]])
o(node.tag).equals("[")
o(node.children.length).equals(1)
o(node.children[0].tag).equals("[")
o(node.children[0].children.length).equals(0)
})
o("normalizes string into text node", function() {
var node = Node.normalize("a")
o(node.tag).equals("#")
o(node.children).equals("a")
})
o("normalizes falsy string into text node", function() {
var node = Node.normalize("")
o(node.tag).equals("#")
o(node.children).equals("")
})
o("normalizes number into text node", function() {
var node = Node.normalize(1)
o(node.tag).equals("#")
o(node.children).equals(1)
})
o("normalizes falsy number into text node", function() {
var node = Node.normalize(0)
o(node.tag).equals("#")
o(node.children).equals(0)
})
o("normalizes boolean into text node", function() {
var node = Node.normalize(true)
o(node.tag).equals("#")
o(node.children).equals(true)
})
o("normalizes falsy boolean into text node", function() {
var node = Node.normalize(false)
o(node.tag).equals("#")
o(node.children).equals(false)
})
})

View file

@ -0,0 +1,20 @@
"use strict"
var o = require("../../ospec/ospec")
var Node = require("../../render/node")
o.spec("normalizeChildren", function() {
o("normalizes arrays into fragments", function() {
var children = Node.normalizeChildren([[]])
o(children[0].tag).equals("[")
o(children[0].children.length).equals(0)
})
o("normalizes strings into text nodes", function() {
var children = Node.normalizeChildren(["a"])
o(children[0].tag).equals("#")
o(children[0].children).equals("a")
})
})

216
render/tests/test-oninit.js Normal file
View file

@ -0,0 +1,216 @@
"use strict"
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
o.spec("oninit", function() {
var $window, root, render
o.beforeEach(function() {
$window = domMock()
root = $window.document.createElement("div")
render = vdom($window).render
})
o("calls oninit when creating element", function() {
var callback = o.spy()
var vnode = {tag: "div", attrs: {oninit: callback}}
render(root, [vnode])
o(callback.callCount).equals(1)
o(callback.this).equals(vnode)
o(callback.args[0]).equals(vnode)
})
o("calls oninit when creating text", function() {
var callback = o.spy()
var vnode = {tag: "#", attrs: {oninit: callback}, children: "a"}
render(root, [vnode])
o(callback.callCount).equals(1)
o(callback.this).equals(vnode)
o(callback.args[0]).equals(vnode)
})
o("calls oninit when creating fragment", function() {
var callback = o.spy()
var vnode = {tag: "[", attrs: {oninit: callback}, children: []}
render(root, [vnode])
o(callback.callCount).equals(1)
o(callback.this).equals(vnode)
o(callback.args[0]).equals(vnode)
})
o("calls oninit when creating html", function() {
var callback = o.spy()
var vnode = {tag: "<", attrs: {oninit: callback}, children: "a"}
render(root, [vnode])
o(callback.callCount).equals(1)
o(callback.this).equals(vnode)
o(callback.args[0]).equals(vnode)
})
o("calls oninit when replacing keyed", function() {
var createDiv = o.spy()
var createA = o.spy()
var vnode = {tag: "div", key: 1, attrs: {oninit: createDiv}}
var updated = {tag: "a", key: 1, attrs: {oninit: createA}}
render(root, [vnode])
render(root, [updated])
o(createDiv.callCount).equals(1)
o(createDiv.this).equals(vnode)
o(createDiv.args[0]).equals(vnode)
o(createA.callCount).equals(1)
o(createA.this).equals(updated)
o(createA.args[0]).equals(updated)
})
o("does not call oninit when noop", function() {
var create = o.spy()
var update = o.spy()
var vnode = {tag: "div", attrs: {oninit: create}}
var updated = {tag: "div", attrs: {oninit: update}}
render(root, [vnode])
render(root, [updated])
o(create.callCount).equals(1)
o(create.this).equals(vnode)
o(create.args[0]).equals(vnode)
o(update.callCount).equals(0)
})
o("does not call oninit when updating attr", function() {
var create = o.spy()
var update = o.spy()
var vnode = {tag: "div", attrs: {oninit: create}}
var updated = {tag: "div", attrs: {oninit: update, id: "a"}}
render(root, [vnode])
render(root, [updated])
o(create.callCount).equals(1)
o(create.this).equals(vnode)
o(create.args[0]).equals(vnode)
o(update.callCount).equals(0)
})
o("does not call oninit when updating children", function() {
var create = o.spy()
var update = o.spy()
var vnode = {tag: "div", attrs: {oninit: create}, children: [{tag: "a"}]}
var updated = {tag: "div", attrs: {oninit: update}, children: [{tag: "b"}]}
render(root, [vnode])
render(root, [updated])
o(create.callCount).equals(1)
o(create.this).equals(vnode)
o(create.args[0]).equals(vnode)
o(update.callCount).equals(0)
})
o("does not call oninit when updating keyed", function() {
var create = o.spy()
var update = o.spy()
var vnode = {tag: "div", key: 1, attrs: {oninit: create}}
var otherVnode = {tag: "a", key: 2}
var updated = {tag: "div", key: 1, attrs: {oninit: update}}
var otherUpdated = {tag: "a", key: 2}
render(root, [vnode, otherVnode])
render(root, [otherUpdated, updated])
o(create.callCount).equals(1)
o(create.this).equals(vnode)
o(create.args[0]).equals(vnode)
o(update.callCount).equals(0)
})
o("does not call oninit when removing", function() {
var create = o.spy()
var update = o.spy()
var vnode = {tag: "div", attrs: {oninit: create}}
render(root, [vnode])
render(root, [])
o(create.callCount).equals(1)
o(create.this).equals(vnode)
o(create.args[0]).equals(vnode)
})
o("calls oninit when recycling", function() {
var create = o.spy()
var update = o.spy()
var vnode = {tag: "div", key: 1, attrs: {oninit: create}}
var updated = {tag: "div", key: 1, attrs: {oninit: update}}
render(root, [vnode])
render(root, [])
render(root, [updated])
o(vnode.dom).equals(updated.dom)
o(create.callCount).equals(1)
o(create.this).equals(vnode)
o(create.args[0]).equals(vnode)
o(update.callCount).equals(1)
o(update.this).equals(updated)
o(update.args[0]).equals(updated)
})
o("calls oninit at the same step as onupdate", function() {
var create = o.spy()
var update = o.spy()
var callback = o.spy()
var vnode = {tag: "div", attrs: {onupdate: create}, children: []}
var updated = {tag: "div", attrs: {onupdate: update}, children: [{tag: "a", attrs: {oninit: callback}}]}
render(root, [vnode])
render(root, [updated])
o(create.callCount).equals(0)
o(update.callCount).equals(1)
o(update.this).equals(updated)
o(update.args[0]).equals(updated)
o(callback.callCount).equals(1)
o(callback.this).equals(updated.children[0])
o(callback.args[0]).equals(updated.children[0])
})
o("calls oninit before full DOM creation", function() {
var called = false
var vnode = {tag: "div", children: [
{tag: "a", attrs: {oninit: create}, children: [
{tag: "b"}
]}
]}
render(root, [vnode])
function create(vnode) {
called = true
o(vnode.dom).equals(undefined)
o(root.childNodes.length).equals(0)
}
o(called).equals(true)
})
o("does not set oninit as an event handler", function() {
var create = o.spy()
var vnode = {tag: "div", attrs: {oninit: create}, children: []}
render(root, [vnode])
o(vnode.dom.oninit).equals(undefined)
o(vnode.dom.attributes["oninit"]).equals(undefined)
})
o("calls oninit on recycle", function() {
var create = o.spy()
var vnodes = [{tag: "div", key: 1, attrs: {oninit: create}}]
var temp = []
var updated = [{tag: "div", key: 1, attrs: {oninit: create}}]
render(root, vnodes)
render(root, temp)
render(root, updated)
o(create.callCount).equals(2)
})
})

View file

@ -0,0 +1,20 @@
"use strict"
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var trust = require("../../render/trust")
o.spec("trust", function() {
o("works with html", function() {
var vnode = trust("<a></a>")
o(vnode.tag).equals("<")
o(vnode.children).equals("<a></a>")
})
o("works with text", function() {
var vnode = trust("abc")
o(vnode.tag).equals("<")
o(vnode.children).equals("abc")
})
})

View file

@ -1,3 +1,7 @@
"use strict"
var Node = require("../render/node")
module.exports = function(html) { module.exports = function(html) {
return {tag: "<", key: undefined, attrs: undefined, children: html, text: undefined} return Node("<", undefined, undefined, html, undefined, undefined)
} }