Merge branch 'rewrite' into array-isArray
This commit is contained in:
commit
fbb454c6fd
45 changed files with 1132 additions and 449 deletions
14
.npmignore
14
.npmignore
|
|
@ -1,20 +1,8 @@
|
|||
# Configuration files
|
||||
# Development-specific files
|
||||
.deploy.env
|
||||
.editorconfig
|
||||
.eslintrc.js
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.travis.yml
|
||||
|
||||
# Tests
|
||||
test-utils/
|
||||
tests/
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
examples/
|
||||
CONTRIBUTING.md
|
||||
|
||||
# Browser stub (use index.js w/ a bundler or mithril.js w/o one instead)
|
||||
module/
|
||||
browser.js
|
||||
|
|
|
|||
|
|
@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult
|
|||
|
||||
## Modularity
|
||||
|
||||
Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at <!-- size -->7.48 KB<!-- /size --> min+gzip
|
||||
Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at <!-- size -->7.59 KB<!-- /size --> min+gzip
|
||||
|
||||
In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering
|
||||
|
|
|
|||
|
|
@ -8,37 +8,37 @@ module.exports = function($window, redrawService) {
|
|||
var routeService = coreRouter($window)
|
||||
|
||||
var identity = function(v) {return v}
|
||||
var render, component, attrs, currentPath, updatePending = false
|
||||
var render, component, attrs, currentPath, lastUpdate
|
||||
var route = function(root, defaultRoute, routes) {
|
||||
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
|
||||
var update = function(routeResolver, comp, params, path) {
|
||||
component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, updatePending = false
|
||||
render = (routeResolver.render || identity).bind(routeResolver)
|
||||
run()
|
||||
}
|
||||
var run = function() {
|
||||
if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs)))
|
||||
}
|
||||
var bail = function() {
|
||||
routeService.setPath(defaultRoute)
|
||||
routeService.setPath(defaultRoute, null, {replace: true})
|
||||
}
|
||||
routeService.defineRoutes(routes, function(payload, params, path) {
|
||||
if (payload.view) update({}, payload, params, path)
|
||||
var update = lastUpdate = function(routeResolver, comp) {
|
||||
if (update !== lastUpdate) return
|
||||
component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, lastUpdate = null
|
||||
render = (routeResolver.render || identity).bind(routeResolver)
|
||||
run()
|
||||
}
|
||||
if (payload.view) update({}, payload)
|
||||
else {
|
||||
if (payload.onmatch) {
|
||||
updatePending = true
|
||||
Promise.resolve(payload.onmatch(params, path)).then(function(resolved) {
|
||||
if (updatePending) update(payload, resolved, params, path)
|
||||
update(payload, resolved)
|
||||
}, bail)
|
||||
}
|
||||
else update(payload, "div", params, path)
|
||||
else update(payload, "div")
|
||||
}
|
||||
}, bail)
|
||||
redrawService.subscribe(root, run)
|
||||
}
|
||||
route.set = function(path, data, options) {
|
||||
if (updatePending) options = {replace: true}
|
||||
updatePending = false
|
||||
if (lastUpdate != null) options = {replace: true}
|
||||
lastUpdate = null
|
||||
routeService.setPath(path, data, options)
|
||||
}
|
||||
route.get = function() {return currentPath}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ o.spec("route", function() {
|
|||
o(root.firstChild.nodeName).equals("DIV")
|
||||
})
|
||||
|
||||
o("routed mount points can redraw synchronoulsy (#1275)", function() {
|
||||
o("routed mount points can redraw synchronously (#1275)", function() {
|
||||
var view = o.spy()
|
||||
|
||||
$window.location.href = prefix + "/"
|
||||
|
|
@ -66,7 +66,9 @@ o.spec("route", function() {
|
|||
})
|
||||
|
||||
o("default route doesn't break back button", function(done) {
|
||||
$window.location.href = "http://google.com"
|
||||
$window.location.href = "http://old.com"
|
||||
$window.location.href = "http://new.com"
|
||||
|
||||
route(root, "/a", {
|
||||
"/a" : {
|
||||
view: function() {
|
||||
|
|
@ -78,9 +80,12 @@ o.spec("route", function() {
|
|||
callAsync(function() {
|
||||
o(root.firstChild.nodeName).equals("DIV")
|
||||
|
||||
o(route.get()).equals("/a")
|
||||
|
||||
$window.history.back()
|
||||
|
||||
o($window.location.pathname).equals("/")
|
||||
o($window.location.hostname).equals("old.com")
|
||||
|
||||
done()
|
||||
})
|
||||
|
|
@ -574,7 +579,7 @@ o.spec("route", function() {
|
|||
|
||||
o(matchCount).equals(1)
|
||||
o(renderCount).equals(2)
|
||||
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
|
@ -609,7 +614,7 @@ o.spec("route", function() {
|
|||
|
||||
o(matchCount).equals(1)
|
||||
o(renderCount).equals(2)
|
||||
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
|
@ -617,7 +622,7 @@ o.spec("route", function() {
|
|||
o("onmatch can redirect to another route", function(done) {
|
||||
var redirected = false
|
||||
var render = o.spy()
|
||||
|
||||
|
||||
$window.location.href = prefix + "/a"
|
||||
route(root, "/a", {
|
||||
"/a" : {
|
||||
|
|
@ -932,6 +937,63 @@ o.spec("route", function() {
|
|||
})
|
||||
})
|
||||
|
||||
o("when two async routes are racing, the last one set cancels the finalization of the first", function(done) {
|
||||
var renderA = o.spy()
|
||||
var renderB = o.spy()
|
||||
var onmatchA = o.spy(function(){
|
||||
return new Promise(function(fulfill) {
|
||||
setTimeout(function(){
|
||||
fulfill()
|
||||
}, 10)
|
||||
})
|
||||
})
|
||||
|
||||
$window.location.href = prefix + "/a"
|
||||
route(root, "/a", {
|
||||
"/a": {
|
||||
onmatch: onmatchA,
|
||||
render: renderA
|
||||
},
|
||||
"/b": {
|
||||
onmatch: function(){
|
||||
var p = new Promise(function(fulfill) {
|
||||
o(onmatchA.callCount).equals(1)
|
||||
o(renderA.callCount).equals(0)
|
||||
o(renderB.callCount).equals(0)
|
||||
|
||||
setTimeout(function(){
|
||||
o(onmatchA.callCount).equals(1)
|
||||
o(renderA.callCount).equals(0)
|
||||
o(renderB.callCount).equals(0)
|
||||
|
||||
fulfill()
|
||||
|
||||
p.then(function(){
|
||||
o(onmatchA.callCount).equals(1)
|
||||
o(renderA.callCount).equals(0)
|
||||
o(renderB.callCount).equals(1)
|
||||
|
||||
done()
|
||||
})
|
||||
}, 20)
|
||||
})
|
||||
return p
|
||||
},
|
||||
render: renderB
|
||||
}
|
||||
})
|
||||
|
||||
callAsync(function() {
|
||||
o(onmatchA.callCount).equals(1)
|
||||
o(renderA.callCount).equals(0)
|
||||
o(renderB.callCount).equals(0)
|
||||
route.set("/b")
|
||||
o(onmatchA.callCount).equals(1)
|
||||
o(renderA.callCount).equals(0)
|
||||
o(renderB.callCount).equals(0)
|
||||
})
|
||||
})
|
||||
|
||||
o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){
|
||||
var onmatch = o.spy()
|
||||
var render = o.spy(function() {return m("div")})
|
||||
|
|
@ -1133,19 +1195,19 @@ o.spec("route", function() {
|
|||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
callAsync(function() { // tick for popstate for /a
|
||||
callAsync(function() { // tick for promise in onmatch
|
||||
callAsync(function() { // tick for onpopstate for /b
|
||||
o(rendered).equals(false)
|
||||
o(resolved).equals("b")
|
||||
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
o("throttles", function(done, timeout) {
|
||||
timeout(200)
|
||||
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ m("div", {
|
|||
onbeforeupdate : function(vnode, old) { /*...*/ },
|
||||
// Called after the node is updated
|
||||
onupdate : function(vnode) { /*...*/ },
|
||||
// Called before the node is removed, call done() when ready for the node to be removed from the DOM
|
||||
onbeforeremove : function(vnode, done) { /*...*/ },
|
||||
// Called before the node is removed, return a Promise that resolves when ready for the node to be removed from the DOM
|
||||
onbeforeremove : function(vnode) { /*...*/ },
|
||||
// Called before the node is removed, but after onbeforeremove calls done()
|
||||
onremove : function(vnode) { /*...*/ }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,9 +63,12 @@ var ComponentWithHooks = {
|
|||
onupdate: function(vnode) {
|
||||
console.log("DOM updated")
|
||||
},
|
||||
onbeforeremove: function(vnode, done) {
|
||||
onbeforeremove: function(vnode) {
|
||||
console.log("exit animation can start")
|
||||
done()
|
||||
return new Promise(function(resolve) {
|
||||
// call after animation completes
|
||||
resolve()
|
||||
})
|
||||
},
|
||||
onremove: function(vnode) {
|
||||
console.log("removing DOM element")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## How do I go about contributing ideas or new features?
|
||||
|
||||
Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. And don't worry, we're nice :)
|
||||
Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it.
|
||||
|
||||
If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,23 @@
|
|||
|
||||
Allows attaching lifecycle methods to a fragment [vnode](vnodes.md)
|
||||
|
||||
```javascript
|
||||
var groupVisible = true
|
||||
var log = function() {
|
||||
console.log("group is now visible")
|
||||
}
|
||||
|
||||
m("ul", [
|
||||
m("li", "child 1"),
|
||||
m("li", "child 2"),
|
||||
groupVisible ? m.fragment({oninit: log}, [
|
||||
// a fragment containing two elements
|
||||
m("li", "child 3"),
|
||||
m("li", "child 4"),
|
||||
]) : null
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
|
|
|||
177
docs/framework-comparison.md
Normal file
177
docs/framework-comparison.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Framework comparison
|
||||
|
||||
- [React](#react)
|
||||
- [Angular](#angular)
|
||||
- [Vue](#vue)
|
||||
|
||||
If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril would help you solve your problems more effectively.
|
||||
|
||||
In this page, you will find common arguments about other frameworks and comments on where Mithril is similar or why it differs from them.
|
||||
|
||||
---
|
||||
|
||||
## React
|
||||
|
||||
React is a view library maintained by Facebook.
|
||||
|
||||
React and Mithril share a lot of similarities
|
||||
|
||||
- They both use virtual DOM, lifecycle methods and key-based reconciliation
|
||||
- They both organize views via components
|
||||
- They both use Javascript as a flow control mechanism within views
|
||||
|
||||
The most obvious difference between React and Mithril is in their scope. React is a view library, so a typical React-based application relies on third-party libraries for routing, XHR and state management. Using a library oriented approach allows developers to customize their stack to precisely match their needs. The not-so-nice way of saying that is that too much choice can lead to analysis paralysis, excessive configuration/boilerplate complexity, and [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding). At worst, it can lead to a hodge-podge of different dependencies and architectures, making it difficult for new team members to transfer knowledge from one project to the next.
|
||||
|
||||
Mithril has built-in modules for common necessities such as routing and XHR. This batteries-included approach is preferable for teams that value consistency and ease of onboarding.
|
||||
|
||||
### Performance
|
||||
|
||||
Both React and Mithril care strongly about rendering performance, but go about it in different ways. In the past React had two DOM rendering implementations (one using the DOM API, and one using `innerHTML`). Its upcoming fiber architecture introduces scheduling and prioritization of units of work. React also has a sophisticated build system that disables various checks and error messages for production deployments, and various browser-specific optimizations. In addition, there are also several performance-oriented libraries that leverage React's `shouldComponentUpdate` hook and immutable data structure libraries' fast object equality checking properties to reduce virtual DOM reconciliation times. Generally speaking, React's approach to performance is to engineer relatively complex solutions.
|
||||
|
||||
Mithril follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run.
|
||||
|
||||
Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop:
|
||||
|
||||
React v15.4.1 | Mithril 1.0
|
||||
------------- | -------
|
||||
55.8 ms | 4.5 ms
|
||||
|
||||
Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques.
|
||||
|
||||
Since this is a micro-benchmark, you are encourage to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate.
|
||||
|
||||
For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the [benchmark for React is here](https://jsfiddle.net/0ovkv64u/), and the [benchmark for Mithril is here](https://jsfiddle.net/o7hxooqL/). Note that we're benchmarking all of Mithril rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril's initialization speed is noticeably better than React.
|
||||
|
||||
Here's a slightly more meaningful benchmark: measuring the scripting time for creating 10,000 divs (and 10,000 text nodes). Again, here's the benchmark code for [React](https://jsfiddle.net/bfoeay4f/) and [Mithril](https://jsfiddle.net/fft0ht7n/). Their best results are shown below:
|
||||
|
||||
React v15.4.1 | Mithril 1.0
|
||||
------------- | -------
|
||||
99.7 ms | 42.8 ms
|
||||
|
||||
What these numbers show is that not only does Mithril initializes significantly faster, it can process upwards of 20,000 virtual DOM nodes before React is ready to use.
|
||||
|
||||
##### Update performance
|
||||
|
||||
Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running.
|
||||
|
||||
A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Sample results are shown below:
|
||||
|
||||
React v15.4.1 | Mithril 1.0
|
||||
------------- | -------
|
||||
12.1 ms | 6.4 ms
|
||||
|
||||
##### Development performance
|
||||
|
||||
Another thing to keep in mind is that because React adds extra checks and helpful error messages in development mode, it is slower in development than the production version used for the benchmarks above. To illustrate, [here's the 10,000 node benchmark from above using the development version of React](https://jsfiddle.net/r1jfckrd/).
|
||||
|
||||
### Complexity
|
||||
|
||||
Both React and Mithril have relatively small API surfaces compared to other frameworks, which help ease learning curve. However, whereas idiomatic Mithril can be written without loss of readability using plain ES5 and no other dependencies, idiomatic React relies heavily on complex tooling (e.g. Babel, JSX plugin, etc), and this level of complexity frequently extends to popular parts of its ecosystem, be it in the form of syntax extensions (e.g. non-standard object spread syntax in Redux), architectures (e.g. ones using immutable data libraries), or bells and whistles (e.g. hot module reloading).
|
||||
|
||||
### Learning curve
|
||||
|
||||
Both React and Mithril have relatively small learning curves. React's learning curve mostly involves understanding components and their lifecycle. The learning curve for Mithril components is nearly identical. There are obviously more APIs to learn in Mithril, since Mithril also includes routing and XHR, but the learning curve would be fairly similar to learning React, React Router and a XHR library like superagent or axios.
|
||||
|
||||
Idiomatic React requires working knowledge of JSX and its caveats, and therefore there's also a small learning curve related to Babel.
|
||||
|
||||
### Documentation
|
||||
|
||||
React documentation is clear and well written, and includes a good API reference, tutorials for getting started, as well as pages covering various advanced concepts.
|
||||
|
||||
Mithril documentation also includes [introductory](introduction.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference.
|
||||
|
||||
---
|
||||
|
||||
## Angular
|
||||
|
||||
Angular is a web application framework maintained by Google.
|
||||
|
||||
Angular and Mithril are fairly different, but they share a few similarities:
|
||||
|
||||
- Both support componentization
|
||||
- Both have an array of tools for various aspects of web applications (e.g. routing, XHR)
|
||||
|
||||
The most obvious difference between Angular and Mithril is in their complexity. This can be seen most easily in how views are implemented. Mithril views are plain Javascript, and flow control is done with Javascript built-in mechanisms such as ternary operators or `Array.prototype.map`. Angular, on the other hand, implements a directive system to extend HTML views so that it's possible to evaluate Javascript-like expressions within HTML attributes and interpolations. Angular actually ships with a parser and a compiler written in Javascript to achieve that. If that doesn't seem complex enough, there's actually two compilation modes (a default mode that generates Javascript functions dynamically for performance, and [a slower mode](https://docs.angularjs.org/api/ng/directive/ngCsp) for dealing with Content Security Policy restrictions).
|
||||
|
||||
### Performance
|
||||
|
||||
Angular has made a lot of progress in terms of performance over the years. Angular 1 used a mechanism known as dirty checking which tended to get slow due to the need to constantly diff large `$scope` structures. Angular 2 uses a template change detection mechanism that is much more performant. However, even despite Angular's improvements, Mithril is often faster than Angular, due to the ease of auditing that Mithril's small codebase size affords.
|
||||
|
||||
It's difficult to make a comparison of load times between Angular and Mithril for a couple of reasons. The first is that Angular 1 and 2 are in fact completely different codebases, and both versions are officially supported and maintained (and the vast majority of Angular codebases in the wild currently still use version 1). The second reason is that both Angular and Mithril are modular. In both cases, it's possible to remove a significant part of the framework that is not used in a given application.
|
||||
|
||||
With that being said, the smallest known Angular 2 bundle is a [29kb hello world](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/) compressed w/ the Brotli algorithm (it's 35kb using standard gzip), and with most of Angular's useful functionality removed. By comparison, a Mithril hello world - including the entire Mithril core - would not be over 8kb gzipped (a more optimized bundle could easily be half of that).
|
||||
|
||||
Also, remember that frameworks like Angular and Mithril are designed for non-trivial application, so an application that managed to use all of Angular's API surface would need to download several hundred kb of framework code, rather than merely 29kb.
|
||||
|
||||
##### Update performance
|
||||
|
||||
A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
|
||||
|
||||
Angular | Mithril
|
||||
------- | -------
|
||||
11.5 ms | 6.4 ms
|
||||
|
||||
### Complexity
|
||||
|
||||
Angular is superior to Mithril in the amount of tools it offers (in the form of various directives and services), but it is also far more complex. Compare [Angular's API surface](https://angular.io/docs/ts/latest/api/) with [Mithril's](api.md). You can make your own judgment on which API is more self-descriptive and more relevant to your needs.
|
||||
|
||||
Angular 2 has a lot more concepts to understand: on the language level, Typescript is the recommended language, and on top of that there's also Angular-specific template syntax such as bindings, pipes, "safe navigator operator". You also need to learn about architectural concepts such as modules, components, services, directives, etc, and where it's appropriate to use what.
|
||||
|
||||
### Learning curve
|
||||
|
||||
If we compare apples to apples, Angular 2 and Mithril have similar learning curves: in both, components are a central aspect of architecture, and both have reasonable routing and XHR tools.
|
||||
|
||||
With that being said, Angular has a lot more concepts to learn than Mithril. It offers Angular-specific APIs for many things that often can be trivially implemented (e.g. pluralization is essentially a switch statement, "required" validation is simply an equality check, etc). Angular templates also have several layers of abstractions to emulate what Javascript does natively in Mithril - Angular's `ng-if`/`ngIf` is a *directive*, which uses a custom *parser* and *compiler* to evaluate an expression string and emulate lexical scoping... and so on.
|
||||
|
||||
### Documentation
|
||||
|
||||
Angular 2 documentation provides an extensive introductory tutorial, and another tutorial that implements an application. It also has various guides for advanced concepts, a cheatsheet and a style guide. Unfortunately, at the moment, the API reference leaves much to be desired. Several APIs are either undocumented or provide no context for what the API might be used for.
|
||||
|
||||
Mithril documentation includes [introductory](introduction.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference.
|
||||
|
||||
---
|
||||
|
||||
## Vue
|
||||
|
||||
Vue is a view library similar to Angular.
|
||||
|
||||
Vue and Mithril have a lot of differences but they also share some similarities:
|
||||
|
||||
- They both use virtual DOM and lifecycle methods
|
||||
- Both organize views via components
|
||||
|
||||
Vue also provides tools for routing and state management as separate modules. Vue looks very similar to Angular and provides a similar directive system, HTML-based templates and logic flow directives. It differs from Angular in that it implements a monkeypatching reactive API that overwrites native methods in a component's data (whereas Angular 1 uses dirty checking and digest/apply cycles to achieve similar results). Similar to Angular 2, Vue compiles HTML templates into functions, but the compiled functions look more like Mithril or React views, rather than Angular's compiled rendering functions.
|
||||
|
||||
Vue is significantly smaller than Angular when comparing apples to apples, but not as small as Mithril (Vue core is around 23kb gzipped, whereas the equivalent rendering module in Mithril is around 4kb gzipped). Both have similar performance characteristics.
|
||||
|
||||
### Performance
|
||||
|
||||
Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop:
|
||||
|
||||
Vue | Mithril
|
||||
------- | -------
|
||||
21.8 ms | 4.5 ms
|
||||
|
||||
Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques.
|
||||
|
||||
##### Update performance
|
||||
|
||||
A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
|
||||
|
||||
Vue | Mithril
|
||||
------ | -------
|
||||
9.8 ms | 6.4 ms
|
||||
|
||||
### Complexity
|
||||
|
||||
One could argue that Vue templates are more complex than Mithril due to the fact that they use Vue-specific syntax for logic flow, whereas Mithril view language is always just Javascript.
|
||||
|
||||
As of Vue 2.0, it's also possible to write templates using hyperscript/JSX syntax (in addition to single-file components and the various webpack-based language transpilation plugins) so Vue codebases may be less consistent across projects and have higher onboarding costs in terms of technologies compared to idiomatic Mithril projects.
|
||||
|
||||
Vue provides both bi-directional data binding and an optional Redux-like state management library, and unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach has a risk of causing architectural fragmentation in long-lived projects.
|
||||
|
||||
The Mithril [tutorial](simple-application.md) implements an *idiomatic* application. This means that while it's *possible* to structure a Mithril codebase in different ways, there's a recommended way to architecture the application.
|
||||
|
||||
### Documentation
|
||||
|
||||
Both Vue and Mithril have thorough documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
- Tutorials
|
||||
- [Installation](installation.md)
|
||||
- [Introduction](introduction.md)
|
||||
- [Tutorial](tutorial.md)
|
||||
- [Tutorial](simple-application.md)
|
||||
- [Testing](testing.md)
|
||||
- [Examples](examples.md)
|
||||
- Key concepts
|
||||
|
|
@ -11,7 +11,9 @@
|
|||
- [Keys](keys.md)
|
||||
- Social
|
||||
- [Community chat](https://gitter.im/lhorie/mithril.js)
|
||||
- [Contributing](contributing.md)
|
||||
- [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS)
|
||||
- [How to contribute](contributing.md)
|
||||
- [Credits](credits.md)
|
||||
- Misc
|
||||
- [Framework comparison](framework-comparison.md)
|
||||
- [Change log/Migration](change-log.md)
|
||||
|
|
|
|||
|
|
@ -249,9 +249,9 @@ Hook | Description
|
|||
`oninit(vnode)` | Runs before a vnode is rendered into a real DOM element
|
||||
`oncreate(vnode)` | Runs after a vnode is appended to the DOM
|
||||
`onupdate(vnode)` | Runs every time a redraw occurs while the DOM element is attached to the document
|
||||
`onbeforeremove(vnode, done)` | Runs before a DOM element is removed from the document, and only triggers the actual removal of the DOM element when the `done` callback is called. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements.
|
||||
`onbeforeremove(vnode)` | Runs before a DOM element is removed from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements.
|
||||
`onremove(vnode)` | Runs before a DOM element is removed from the document. If a `onbeforeremove` hook is defined, `onremove` is called after `done` is called. This method is triggered on the element that is detached from its parent element, and all of its children
|
||||
`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children
|
||||
`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children
|
||||
|
||||
To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
# Installation
|
||||
|
||||
- [CDN](#cdn)
|
||||
- [NPM](#npm)
|
||||
|
||||
### CDN
|
||||
|
||||
If you're new to Javascript or just want a very simple setup to get your feet wet, you can get Mithril from a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network):
|
||||
|
|
@ -12,21 +15,27 @@ If you're new to Javascript or just want a very simple setup to get your feet we
|
|||
|
||||
### NPM
|
||||
|
||||
#### Quick start
|
||||
#### Quick start with Webpack
|
||||
|
||||
```bash
|
||||
# 1) install
|
||||
npm install mithril@rewrite --save
|
||||
npm install webpack --save
|
||||
|
||||
# 2) add this line into the scripts section in package.json
|
||||
# "scripts": {
|
||||
# "build": "bundle index.js --output app.js --watch"
|
||||
# "build": "webpack index.js app.js --watch"
|
||||
# }
|
||||
|
||||
# 3) create an `index.js` file
|
||||
|
||||
# 4) run bundler
|
||||
# 4) create an `index.html` file loading `app.js`
|
||||
|
||||
# 5) run bundler
|
||||
npm run build
|
||||
|
||||
# 6) open `index.html` in the (default) browser
|
||||
open index.html
|
||||
```
|
||||
|
||||
#### Step by step
|
||||
|
|
@ -42,12 +51,14 @@ npm init --yes
|
|||
# creates a file called package.json
|
||||
```
|
||||
|
||||
Then, run `npm install mithril@rewrite --save` to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file
|
||||
Then, run
|
||||
|
||||
```bash
|
||||
npm install mithril@rewrite --save
|
||||
```
|
||||
|
||||
to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file
|
||||
|
||||
You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules:
|
||||
|
||||
```javascript
|
||||
|
|
@ -59,33 +70,36 @@ m.render(document.body, "hello world")
|
|||
|
||||
Modularization is the practice of separating the code into files. Doing so makes it easier to find code, understand what code relies on what code, and test.
|
||||
|
||||
CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like Browserify and Webpack. It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](http://rollupjs.org/), [Babel](https://babeljs.io/) or [Traceur](https://github.com/google/traceur-compiler).
|
||||
CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like [Browserify](http://browserify.org/) and [Webpack](https://webpack.js.org/). It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](http://rollupjs.org/), [Babel](https://babeljs.io/) or [Traceur](https://github.com/google/traceur-compiler).
|
||||
|
||||
Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single Javascript file before running in a client-side application.
|
||||
|
||||
The easiest way to create a bundle is to setup an NPM script for Mithril's bundler. To do so, open the `package.json` that you created earlier, and add an entry to the `scripts` section:
|
||||
The easiest way to create a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line:
|
||||
|
||||
```bash
|
||||
npm install webpack --save
|
||||
```
|
||||
|
||||
Open the `package.json` that you created earlier, and add an entry to the `scripts` section:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"build": "bundle index.js --output app.js --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"mithril": "^1.0.0-rc.5"
|
||||
"build": "webpack index.js app.js --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember this is a JSON file, so object key names such as `"scripts"` and `"build"` must be inside of double quotes.
|
||||
|
||||
Now you can run the script via `npm run build` in your command line window. This looks up the `bundle` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above.
|
||||
Now you can run the script via `npm run build` in your command line window. This looks up the `webpack` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. If you want to run the `webpack` command directly from the command line, you need to either add `node_modules/.bin` to your PATH, or install webpack globally via `npm install webpack -g`. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers.
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
The `--watch` flag tells the `bundle` command to watch the file system and automatically recreate `app.js` if file changes are detected.
|
||||
The `--watch` flag tells webpack to watch the file system and automatically recreate `app.js` if file changes are detected.
|
||||
|
||||
Now that you have created a bundle, you can then reference the `app.js` file from an HTML file:
|
||||
|
||||
|
|
@ -124,28 +138,48 @@ m.mount(document.body, MyComponent)
|
|||
|
||||
Note that in this example, we're using `m.mount`, which wires up the component to Mithril's autoredraw system. In most applications, you will want to use `m.mount` (or `m.route` if your application has multiple screens) instead of `m.render` to take advantage of the autoredraw system, rather than re-rendering manually every time a change occurs.
|
||||
|
||||
#### Alternate ways to use Mithril
|
||||
---
|
||||
|
||||
##### Webpack
|
||||
### Alternate ways to use Mithril
|
||||
|
||||
Webpack is a popular tool for bundling modular code. The biggest advantage of Webpack is that it has a relatively large ecosystem of plugins. The downside of that is that it can be difficult to configure correctly.
|
||||
#### Live reload development environment
|
||||
|
||||
To use webpack, you must first install it by running `npm install webpack --save-dev`. Then you need to create a `webpack.config.js` file. Here's a basic configuration file that is equivalent to the Mithril bundler command in the first section of this page:
|
||||
Live reload is a feature where code changes automatically trigger the page to reload. [Budo](https://github.com/mattdesl/budo) is one tool that enables live reloading.
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
output: {filename: "app.js"},
|
||||
```bash
|
||||
# 1) install
|
||||
npm install mithril@rewrite --save
|
||||
npm install budo -g
|
||||
|
||||
# 2) add this line into the scripts section in package.json
|
||||
# "scripts": {
|
||||
# "start": "budo --live --open index.js"
|
||||
# }
|
||||
|
||||
# 3) create an `index.js` file
|
||||
|
||||
# 4) run budo
|
||||
npm start
|
||||
```
|
||||
|
||||
The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. Any changes in the source files will instantly get recompiled and the browser will refresh reflecting the changes.
|
||||
|
||||
#### Mithril bundler
|
||||
|
||||
Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet.
|
||||
|
||||
If you want to try it and give feedback, you can open `package.json` and change the npm script for webpack to this:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"build": "bundle index.js --output app.js --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To run webpack, use the command `webpack --watch`.
|
||||
|
||||
```
|
||||
webpack --watch
|
||||
```
|
||||
|
||||
##### Vanilla
|
||||
#### Vanilla
|
||||
|
||||
If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all:
|
||||
|
||||
|
|
@ -155,7 +189,7 @@ If you don't have the ability to run a bundler script due to company security po
|
|||
<title>Hello world</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="node_modules/mithril/mithril.js"></script>
|
||||
<script src="http://cdn.rawgit.com/lhorie/mithril.js/rewrite/mithril.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,15 @@
|
|||
|
||||
### What is Mithril?
|
||||
|
||||
Mithril is a framework for building Single Page Applications. It's small but batteries-included.
|
||||
Mithril is a client-side Javascript framework for building Single Page Applications.
|
||||
It's small (< 8kb gzip), fast and batteries-included.
|
||||
|
||||
If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page.
|
||||
|
||||
---
|
||||
|
||||
Note: This introduction assumes you have basic level of Javacript knowledge. If you don't, there are many great resources to learn. [Speaking Javascript](http://speakingjs.com/es5/index.html) is a good e-book for absolute beginners. If you're already familiar with other programming languages, the [Eloquent Javascript](http://eloquentjavascript.net/) e-book might be more suitable for you. [Codecademy](https://www.codecademy.com/learn/javascript) is another good resource that emphasizes learning via interactivity.
|
||||
|
||||
### Getting started
|
||||
|
||||
The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes.
|
||||
|
|
@ -86,7 +91,7 @@ m("main", [
|
|||
])
|
||||
```
|
||||
|
||||
Note: If you prefer `<html>` syntax, [it's possible via Babel](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m%20*%2F%0A%3Ch1%3EMy%20first%20app%3C%2Fh1%3E).
|
||||
Note: If you prefer `<html>` syntax, [it's possible to use it via a Babel plugin](https://babeljs.io/docs/plugins/transform-react-jsx/).
|
||||
|
||||
```markup
|
||||
// HTML syntax via Babel's JSX plugin
|
||||
|
|
|
|||
|
|
@ -132,9 +132,7 @@ users.map(function(u) {
|
|||
|
||||
// PREFER
|
||||
users.map(function(u) {
|
||||
return {tag: "[", key: u.id, children: [
|
||||
m("button", u.name)
|
||||
]}
|
||||
return m.fragment({key: u.id}, m("button", u.name))
|
||||
})
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ m(RedrawReporter, {data: "Hello"})
|
|||
|
||||
### onbeforeremove
|
||||
|
||||
The `onbeforeremove(vnode, done)` hook is called before a DOM element is detached from the document. Mithril only detaches the DOM element after the `done` callback is called. The `done` callback can be called asynchronously, making it possible to run exit animations before detaching the element.
|
||||
The `onbeforeremove(vnode)` hook is called before a DOM element is detached from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes.
|
||||
|
||||
This hook is only called on the DOM element that loses its `parentNode`, but it does not get called in its child elements.
|
||||
|
||||
|
|
@ -138,9 +138,11 @@ Like in other hooks, the `this` keyword in the `onbeforeremove` callback points
|
|||
|
||||
```javascript
|
||||
var Fader = {
|
||||
onbeforeremove: function(vnode, done) {
|
||||
onbeforeremove: function(vnode) {
|
||||
vnode.dom.classList.add("fade-out")
|
||||
setTimeout(done, 1000)
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "Bye")
|
||||
|
|
|
|||
|
|
@ -16,5 +16,4 @@
|
|||
- Optional
|
||||
- [Stream](stream.md)
|
||||
- Tooling
|
||||
- [Bundler](bundler.md)
|
||||
- [Ospec](ospec.md)
|
||||
- [Ospec](https://github.com/lhorie/mithril.js/blob/rewrite/ospec)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ Argument | Type | Required | Descr
|
|||
`options.password` | `String` | No | A password for HTTP authorization. Defaults to `undefined`. This option is provided for `XMLHttpRequest` compatibility, but you should avoid using it because it sends the password in plain text over the network.
|
||||
`options.withCredentials` | `Boolean` | No | Whether to send cookies to 3rd party domains. Defaults to `false`
|
||||
`options.config` | `xhr = Function(xhr)` | No | Exposes the underlying XMLHttpRequest object for low-level configuration. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function).
|
||||
`options.headers` | `Object` | No | Headers to append to the request before sending it (applied right before `options.config`).
|
||||
`options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function).
|
||||
`options.serialize` | `string = Function(any)` | No | A serialization method to be applied to `data`. Defaults to `JSON.stringify`, or if `options.data` is an instance of [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData), defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function) (i.e. `function(value) {return value}`).
|
||||
`options.deserialize` | `any = Function(string)` | No | A deserialization method to be applied to the response. Defaults to a small wrapper around `JSON.parse` that returns `null` for empty responses.
|
||||
|
|
@ -404,7 +405,21 @@ function parseCSV(data) {
|
|||
}
|
||||
```
|
||||
|
||||
Ignoring the fact that the parseCSV function above doesn't handle a lot of cases that a proper CSV parser would, the code above logs an array of arrays
|
||||
Ignoring the fact that the parseCSV function above doesn't handle a lot of cases that a proper CSV parser would, the code above logs an array of arrays.
|
||||
|
||||
Custom headers may also be helpful in this regard. For example, if you're requesting an SVG, you probably want to set the content type accordingly. To override the default JSON request type, set `options.headers` to an object of key-value pairs corresponding to request header names and values.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/files/image.svg",
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml; charset=utf-8",
|
||||
"Accept": "image/svg, text/*"
|
||||
},
|
||||
deserialize: function(value) {return value}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -485,4 +500,3 @@ m.request("/api/v1/users").then(function(users) {
|
|||
console.log("list of users:", users)
|
||||
})
|
||||
```
|
||||
|
||||
|
|
|
|||
118
docs/route.md
118
docs/route.md
|
|
@ -14,6 +14,9 @@
|
|||
- [Typical usage](#typical-usage)
|
||||
- [Navigating to different routes](#navigating-to-different-routes)
|
||||
- [Routing parameters](#routing-parameters)
|
||||
- [Key parameter](#key-parameter)
|
||||
- [Variadic routes](#variadic-routes)
|
||||
- [History state](#history-state)
|
||||
- [Changing router prefix](#changing-router-prefix)
|
||||
- [Advanced component resolution](#advanced-component-resolution)
|
||||
- [Wrapping a layout component](#wrapping-a-layout-component)
|
||||
|
|
@ -39,6 +42,8 @@ m.route(document.body, "/home", {
|
|||
})
|
||||
```
|
||||
|
||||
You can only have one `m.route` call per application.
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
|
@ -68,6 +73,8 @@ Argument | Type | Required | Description
|
|||
`path` | `String` | Yes | The path to route to, without a prefix. The path may include slots for routing parameters
|
||||
`data` | `Object` | No | Routing parameters. If `path` has routing parameter slots, the properties of this object are interpolated into the path string
|
||||
`options.replace` | `Boolean` | No | Whether to create a new history entry or to replace the current one. Defaults to false
|
||||
`options.state` | `Object` | No | The `state` object to pass to the underlying `history.pushState` / `history.replaceState` call. This state object becomes available in the `history.state` property, and is merged into the [routing parameters](#routing-parameters) object. Note that this option only works when using the pushState API, but is ignored if the router falls back to hashchange mode (i.e. if the pushState API is not available)
|
||||
`options.title` | `String` | No | The `title` string to pass to the underlying `history.pushState` / `history.replaceState` call.
|
||||
**returns** | | | Returns `undefined`
|
||||
|
||||
##### route.get
|
||||
|
|
@ -252,6 +259,28 @@ It's possible to have multiple arguments in a route, for example `/edit/:project
|
|||
|
||||
In addition to routing parameters, the `attrs` object also includes a `path` property that contains the current route path, and a `route` property that contains the matched routed.
|
||||
|
||||
#### Key parameter
|
||||
|
||||
When a user navigates from a parameterized route to the same route with a different parameter (e.g. going from `/page/1` to `/page/2` given a route `/page/:id`, the component would not be recreated from scratch since both routes resolve to the same component, and thus result in a virtual dom in-place diff. This has the side-effect of triggering the `onupdate` hook, rather than `oninit`/`oncreate`. However, it's relatively common for a developer to want to synchronize the recreation of the component to the route change event.
|
||||
|
||||
To achieve that, it's possible to combine route parameterization with the virtual dom [key reconciliation](keys.md) feature:
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/edit/1", {
|
||||
"/edit/:key": Edit,
|
||||
})
|
||||
```
|
||||
|
||||
This means that the [vnode](vnodes.md) that is created for the root component of the route has a route parameter object `key`. Route parameters become `attrs` in the vnode. Thus, when jumping from one page to another, the `key` changes and causes the component to be recreated from scratch (since the key tells the virtual dom engine that old and new components are different entities).
|
||||
|
||||
You can take that idea further to create components that recreate themselves when reloaded:
|
||||
|
||||
`m.route.set(m.route.get(), {key: Date.now()})`
|
||||
|
||||
Or even use the [`history state`](#history-state) feature to achieve reloadable components without polluting the URL:
|
||||
|
||||
`m.route.set(m.route.get(), null, {state: {key: Date.now()}})`
|
||||
|
||||
#### Variadic routes
|
||||
|
||||
It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes:
|
||||
|
|
@ -262,6 +291,44 @@ m.route(document.body, "/edit/pictures/image.jpg", {
|
|||
})
|
||||
```
|
||||
|
||||
#### History state
|
||||
|
||||
It's possible to take full advantage of the underlying `history.pushState` API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form.
|
||||
|
||||
For example, you could create a form like this:
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
term: "",
|
||||
search: function() {
|
||||
// save the state for this route
|
||||
// this is equivalent to `history.replaceState({term: state.term}, null, location.href)`
|
||||
m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}})
|
||||
|
||||
// navigate away
|
||||
location.href = "https://google.com/?q=" + state.term
|
||||
}
|
||||
}
|
||||
|
||||
var Form = {
|
||||
oninit: function(vnode) {
|
||||
state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button
|
||||
},
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}),
|
||||
m("button", {onclick: state.search}, "Search")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": Form,
|
||||
})
|
||||
```
|
||||
|
||||
This way, if the user searches and presses the back button to return to the application, the input will still be populated with the search term. This technique can improve the user experience of large forms and other apps where non-persisted state is laborious for a user to produce.
|
||||
|
||||
---
|
||||
|
||||
### Changing router prefix
|
||||
|
|
@ -323,31 +390,76 @@ In the example above, the layout merely consists of a `<div class="layout">` tha
|
|||
One way to wrap the layout is to define an anonymous component in the routes map:
|
||||
|
||||
```javascript
|
||||
// example 1
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
view: function() {
|
||||
return m(Layout, m(Home))
|
||||
},
|
||||
},
|
||||
"/form": {
|
||||
view: function() {
|
||||
return m(Layout, m(Form))
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
However, note that because the top level component is an anonymous component, jumping from route to route will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had [lifecycle methods](lifecycle-methods.md) defined, the `oninit` and `oncreate` hooks would fire on every route change. Depending on the application, this may or may not be desirable.
|
||||
However, note that because the top level component is an anonymous component, jumping from the `/` route to the `/form` route (or vice-versa) will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had [lifecycle methods](lifecycle-methods.md) defined, the `oninit` and `oncreate` hooks would fire on every route change. Depending on the application, this may or may not be desirable.
|
||||
|
||||
If you would prefer to have the Layout component be diffed and maintained intact rather than recreated from scratch, you should instead use a RouteResolver as the root object:
|
||||
|
||||
```javascript
|
||||
// example 2
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
render: function() {
|
||||
return m(Layout, m(Home))
|
||||
},
|
||||
},
|
||||
"/form": {
|
||||
render: function() {
|
||||
return m(Layout, m(Form))
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Note that in this case, if the Layout component the `oninit` and `oncreate` lifecycle methods would only fire on the Layout component on the first route change (assuming all routes use the same layout).
|
||||
|
||||
To clarify the difference between the two examples, example 1 is equivalent to this code:
|
||||
|
||||
```javascript
|
||||
// functionally equivalent to example 1
|
||||
var Anon1 = {
|
||||
view: function() {
|
||||
return m(Layout, m(Home))
|
||||
},
|
||||
}
|
||||
var Anon2 = {
|
||||
view: function() {
|
||||
return m(Layout, m(Form))
|
||||
},
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
render: function() {
|
||||
return m(Anon1)
|
||||
}
|
||||
},
|
||||
"/form": {
|
||||
render: function() {
|
||||
return m(Anon2)
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Since `Anon1` and `Anon2` are different components, their subtrees (including `Layout`) are recreated from scratch. This is also what happens when components are used directly without a RouteResolver.
|
||||
|
||||
In example 2, since `Layout` is the top-level component in both routes, the DOM for the `Layout` component is diffed (i.e. left intact if it has no changes), and only the change from `Home` to `Form` triggers a recreation of that subsection of the DOM.
|
||||
|
||||
---
|
||||
|
||||
#### Authentication
|
||||
|
|
@ -407,7 +519,7 @@ m.route(document.body, "/user/list", {
|
|||
"/user/list": {
|
||||
oninit: state.loadUsers,
|
||||
view: function() {
|
||||
return state.users.length > 0 ? state.users.map(function() {
|
||||
return state.users.length > 0 ? state.users.map(function(user) {
|
||||
return m("div", user.id)
|
||||
}) : "loading"
|
||||
}
|
||||
|
|
@ -433,7 +545,7 @@ m.route(document.body, "/user/list", {
|
|||
"/user/list": {
|
||||
onmatch: state.loadUsers,
|
||||
render: function() {
|
||||
return state.users.length > 0 ? state.users.map(function() {
|
||||
return state.users.length > 0 ? state.users.map(function(user) {
|
||||
return m("div", user.id)
|
||||
}) : "loading"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ nav a {border-left:1px solid #ddd;padding:0 10px;}
|
|||
nav a:first-child {border:0;padding-left:0;}
|
||||
main {margin-bottom:100px;}
|
||||
main section {margin-left:270px;}
|
||||
h1 {margin:0 0 15px;}
|
||||
h5 {font-style:italic;}
|
||||
h1 {font-size:24px;margin:0 0 15px;}
|
||||
h2 {font-size:22px;margin:30px 0 15px;}
|
||||
h3 {font-size:20px;margin:30px 0 15px;}
|
||||
h4 {font-size:18px;margin:15px 0 15px;}
|
||||
h5 {font-weight:bold;margin:15px 0 15px;}
|
||||
pre,code {background:#eee;font-family:monospace;}
|
||||
pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;}
|
||||
code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:3px;white-space:pre;}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,7 @@ The easist way to setup the test runner is to create an NPM script for it. Open
|
|||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"build": "bundle index.js --output app.js --watch",
|
||||
"test": "ospec"
|
||||
},
|
||||
"dependencies": {
|
||||
"mithril": "^1.0.0-rc.5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -48,3 +44,42 @@ Writing tests upfront requires specifications to be frozen. Upfront tests are a
|
|||
|
||||
Writing tests after the fact is a way to document the behavior of a system and avoid regressions. They are useful to ensure that obscure corner cases are not inadvertedly broken and that previously fixed bugs do not get re-introduced by unrelated changes.
|
||||
|
||||
---
|
||||
|
||||
### Unit testing
|
||||
|
||||
Unit testing is the practice of isolating a part of an application (typically a single module), and asserting that, given some inputs, it produces the expected outputs.
|
||||
|
||||
Testing a Mithril component is easy. Let's assume we have a simple component like this:
|
||||
|
||||
```javascript
|
||||
// MyComponent.js
|
||||
var m = require("mithril")
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
return m("div", "Hello world")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can then create a `tests/MyComponent.js` file and create a test for this component like this:
|
||||
|
||||
```javascript
|
||||
var MyComponent = require("MyComponent")
|
||||
|
||||
o.spec("MyComponent", function() {
|
||||
o("returns a div", function() {
|
||||
var vnode = MyComponent.view()
|
||||
|
||||
o(vnode.tag).equals("div")
|
||||
o(vnode.children.length).equals(1)
|
||||
o(vnode.children[0].tag).equals("#")
|
||||
o(vnode.children[0].children).equals("Hello world")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Typically, you wouldn't test the structure of the vnode tree so granularly, and you would instead only test non-trivial, dynamic aspects of the view. A tool that can help making testing easier with deep vnode trees is [Mithril Query](https://github.com/StephanHoyer/mithril-query).
|
||||
|
||||
Sometimes, you need to mock the dependencies of a module in order to test the module in isolation. [Mockery](https://github.com/mfncooper/mockery) is one tool that allows you to do that.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
- [Structure](#structure)
|
||||
- [Vnode types](#vnode-types)
|
||||
- [Monomorphic class](#monomorphic-class)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -100,3 +101,13 @@ Only element tag names and components can be the first argument of the `m()` fun
|
|||
The `mithril/render/vnode` module is used by Mithril to generate all vnodes. This ensures modern Javascript engines can optimize virtual dom diffing by always compiling vnodes to the same hidden class.
|
||||
|
||||
When creating libraries that emit vnodes, you should use this module instead of writing naked Javascript objects in order to ensure a high level of rendering performance.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
#### Avoid memoizing mutable vnodes
|
||||
|
||||
Vnodes are supposed to represent the state of the DOM at a certain point in time. Mithril's rendering engine assumes a reused vnode is unchanged, so modifying a vnode that was used in a previous render will result in undefined behavior.
|
||||
|
||||
It is possible to reuse vnodes to prevent a diff, but it's preferable to use the `onbeforeupdate` hook to make your intent clear to other developers (or your future self).
|
||||
1
index.js
1
index.js
|
|
@ -16,5 +16,6 @@ m.jsonp = requestService.jsonp
|
|||
m.parseQueryString = require("./querystring/parse")
|
||||
m.buildQueryString = require("./querystring/build")
|
||||
m.version = "bleeding-edge"
|
||||
m.vnode = require("./render/vnode")
|
||||
|
||||
module.exports = m
|
||||
|
|
|
|||
272
mithril.js
272
mithril.js
|
|
@ -4,7 +4,7 @@ function Vnode(tag, key, attrs0, children, text, dom) {
|
|||
return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined, skip: false}
|
||||
}
|
||||
Vnode.normalize = function(node) {
|
||||
if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
|
||||
if (node instanceof Array) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
|
||||
if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node, undefined, undefined)
|
||||
return node
|
||||
}
|
||||
|
|
@ -52,19 +52,19 @@ function hyperscript(selector) {
|
|||
break
|
||||
}
|
||||
}
|
||||
if (Array.isArray(children) && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children
|
||||
if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children
|
||||
else childList = children
|
||||
return Vnode(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined)
|
||||
}
|
||||
}
|
||||
var attrs, children, childrenIndex
|
||||
if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !Array.isArray(arguments[1])) {
|
||||
if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !(arguments[1] instanceof Array)) {
|
||||
attrs = arguments[1]
|
||||
childrenIndex = 2
|
||||
}
|
||||
else childrenIndex = 1
|
||||
if (arguments.length === childrenIndex + 1) {
|
||||
children = Array.isArray(arguments[childrenIndex]) ? arguments[childrenIndex] : [arguments[childrenIndex]]
|
||||
children = arguments[childrenIndex] instanceof Array ? arguments[childrenIndex] : [arguments[childrenIndex]]
|
||||
}
|
||||
else {
|
||||
children = []
|
||||
|
|
@ -82,8 +82,8 @@ hyperscript.fragment = function(attrs1, children) {
|
|||
}
|
||||
var m = hyperscript
|
||||
/** @constructor */
|
||||
var PromisePolyfill0 = function(executor) {
|
||||
if (!(this instanceof PromisePolyfill0)) throw new Error("Promise must be called with `new`")
|
||||
var PromisePolyfill = function(executor) {
|
||||
if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
|
||||
if (typeof executor !== "function") throw new TypeError("executor must be a function")
|
||||
var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
|
||||
var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
|
||||
|
|
@ -124,7 +124,7 @@ var PromisePolyfill0 = function(executor) {
|
|||
}
|
||||
executeOnce(executor)
|
||||
}
|
||||
PromisePolyfill0.prototype.then = function(onFulfilled, onRejection) {
|
||||
PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
|
||||
var self = this, instance = self._instance
|
||||
function handle(callback, list, next, state) {
|
||||
list.push(function(value) {
|
||||
|
|
@ -134,22 +134,22 @@ PromisePolyfill0.prototype.then = function(onFulfilled, onRejection) {
|
|||
if (typeof instance.retry === "function" && state === instance.state) instance.retry()
|
||||
}
|
||||
var resolveNext, rejectNext
|
||||
var promise = new PromisePolyfill0(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
|
||||
var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
|
||||
handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
|
||||
return promise
|
||||
}
|
||||
PromisePolyfill0.prototype.catch = function(onRejection) {
|
||||
PromisePolyfill.prototype.catch = function(onRejection) {
|
||||
return this.then(null, onRejection)
|
||||
}
|
||||
PromisePolyfill0.resolve = function(value) {
|
||||
if (value instanceof PromisePolyfill0) return value
|
||||
return new PromisePolyfill0(function(resolve) {resolve(value)})
|
||||
PromisePolyfill.resolve = function(value) {
|
||||
if (value instanceof PromisePolyfill) return value
|
||||
return new PromisePolyfill(function(resolve) {resolve(value)})
|
||||
}
|
||||
PromisePolyfill0.reject = function(value) {
|
||||
return new PromisePolyfill0(function(resolve, reject) {reject(value)})
|
||||
PromisePolyfill.reject = function(value) {
|
||||
return new PromisePolyfill(function(resolve, reject) {reject(value)})
|
||||
}
|
||||
PromisePolyfill0.all = function(list) {
|
||||
return new PromisePolyfill0(function(resolve, reject) {
|
||||
PromisePolyfill.all = function(list) {
|
||||
return new PromisePolyfill(function(resolve, reject) {
|
||||
var total = list.length, count = 0, values = []
|
||||
if (list.length === 0) resolve([])
|
||||
else for (var i = 0; i < list.length; i++) {
|
||||
|
|
@ -167,18 +167,21 @@ PromisePolyfill0.all = function(list) {
|
|||
}
|
||||
})
|
||||
}
|
||||
PromisePolyfill0.race = function(list) {
|
||||
return new PromisePolyfill0(function(resolve, reject) {
|
||||
PromisePolyfill.race = function(list) {
|
||||
return new PromisePolyfill(function(resolve, reject) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
list[i].then(resolve, reject)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (typeof Promise === "undefined") {
|
||||
if (typeof window !== "undefined") window.Promise = PromisePolyfill0
|
||||
else if (typeof global !== "undefined") global.Promise = PromisePolyfill0
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill
|
||||
var PromisePolyfill = window.Promise
|
||||
} else if (typeof global !== "undefined") {
|
||||
if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill
|
||||
var PromisePolyfill = global.Promise
|
||||
} else {
|
||||
}
|
||||
var PromisePolyfill = typeof Promise !== "undefined" ? Promise : PromisePolyfill0
|
||||
var buildQueryString = function(object) {
|
||||
if (Object.prototype.toString.call(object) !== "[object Object]") return ""
|
||||
var args = []
|
||||
|
|
@ -187,7 +190,7 @@ var buildQueryString = function(object) {
|
|||
}
|
||||
return args.join("&")
|
||||
function destructure(key0, value) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value instanceof Array) {
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
destructure(key0 + "[" + i + "]", value[i])
|
||||
}
|
||||
|
|
@ -251,6 +254,9 @@ var _8 = function($window, Promise) {
|
|||
xhr.setRequestHeader("Accept", "application/json, text/*")
|
||||
}
|
||||
if (args.withCredentials) xhr.withCredentials = args.withCredentials
|
||||
for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
|
||||
xhr.setRequestHeader(key, args.headers[key])
|
||||
}
|
||||
if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
|
|
@ -278,7 +284,6 @@ var _8 = function($window, Promise) {
|
|||
function jsonp(args, extra) {
|
||||
var finalize = finalizer()
|
||||
args = normalize(args, extra)
|
||||
|
||||
var promise0 = new Promise(function(resolve, reject) {
|
||||
var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
|
||||
var script = $window.document.createElement("script")
|
||||
|
|
@ -327,7 +332,7 @@ var _8 = function($window, Promise) {
|
|||
function extract(xhr) {return xhr.responseText}
|
||||
function cast(type0, data) {
|
||||
if (typeof type0 === "function") {
|
||||
if (Array.isArray(data)) {
|
||||
if (data instanceof Array) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
data[i] = new type0(data[i])
|
||||
}
|
||||
|
|
@ -427,7 +432,9 @@ var coreRenderer = function($window) {
|
|||
function createComponent(vnode, hooks, ns) {
|
||||
// For object literals since `Vnode()` always sets the `state` field.
|
||||
if (!vnode.state) vnode.state = {}
|
||||
assign(vnode.state, vnode.tag)
|
||||
var constructor = function() {}
|
||||
constructor.prototype = vnode.tag
|
||||
vnode.state = new constructor
|
||||
var view = vnode.tag.view
|
||||
if (view.reentrantLock != null) return $emptyFragment
|
||||
view.reentrantLock = true
|
||||
|
|
@ -452,83 +459,84 @@ var coreRenderer = function($window) {
|
|||
else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined)
|
||||
else if (vnodes == null) removeNodes(old, 0, old.length, vnodes)
|
||||
else {
|
||||
var isUnkeyed = false
|
||||
for (var i = 0; i < vnodes.length; i++) {
|
||||
if (vnodes[i] != null) {
|
||||
isUnkeyed = vnodes[i].key == null
|
||||
break
|
||||
if (old.length === vnodes.length) {
|
||||
var isUnkeyed = false
|
||||
for (var i = 0; i < vnodes.length; i++) {
|
||||
if (vnodes[i] != null && old[i] != null) {
|
||||
isUnkeyed = vnodes[i].key == null && old[i].key == null
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isUnkeyed) {
|
||||
for (var i = 0; i < old.length; i++) {
|
||||
if (old[i] === vnodes[i]) continue
|
||||
else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling))
|
||||
else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
|
||||
else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (old.length === vnodes.length && isUnkeyed) {
|
||||
for (var i = 0; i < old.length; i++) {
|
||||
if (old[i] === vnodes[i]) continue
|
||||
else if (old[i] == null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling))
|
||||
else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
|
||||
else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns)
|
||||
var recycling = isRecyclable(old, vnodes)
|
||||
if (recycling) old = old.concat(old.pool)
|
||||
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldStart], v = vnodes[start]
|
||||
if (o === v && !recycling) oldStart++, start++
|
||||
else if (o == null) oldStart++
|
||||
else if (v == null) start++
|
||||
else if (o.key === v.key) {
|
||||
oldStart++, start++
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
}
|
||||
}
|
||||
else {
|
||||
var recycling = isRecyclable(old, vnodes)
|
||||
if (recycling) old = old.concat(old.pool)
|
||||
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldStart], v = vnodes[start]
|
||||
if (o === v && !recycling) oldStart++, start++
|
||||
else if (o == null) oldStart++
|
||||
else {
|
||||
var o = old[oldEnd]
|
||||
if (o === v && !recycling) oldEnd--, start++
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) start++
|
||||
else if (o.key === v.key) {
|
||||
oldStart++, start++
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
}
|
||||
else {
|
||||
var o = old[oldEnd]
|
||||
if (o === v && !recycling) oldEnd--, start++
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) start++
|
||||
else if (o.key === v.key) {
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
|
||||
oldEnd--, start++
|
||||
}
|
||||
else break
|
||||
}
|
||||
}
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldEnd], v = vnodes[end]
|
||||
if (o === v && !recycling) oldEnd--, end--
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) end--
|
||||
else if (o.key === v.key) {
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
if (o.dom != null) nextSibling = o.dom
|
||||
oldEnd--, end--
|
||||
if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
|
||||
oldEnd--, start++
|
||||
}
|
||||
else {
|
||||
if (!map) map = getKeyMap(old, oldEnd)
|
||||
if (v != null) {
|
||||
var oldIndex = map[v.key]
|
||||
if (oldIndex != null) {
|
||||
var movable = old[oldIndex]
|
||||
updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
insertNode(parent, toFragment(movable), nextSibling)
|
||||
old[oldIndex].skip = true
|
||||
if (movable.dom != null) nextSibling = movable.dom
|
||||
}
|
||||
else {
|
||||
var dom = createNode(v, hooks, undefined)
|
||||
insertNode(parent, dom, nextSibling)
|
||||
nextSibling = dom
|
||||
}
|
||||
}
|
||||
end--
|
||||
}
|
||||
if (end < start) break
|
||||
else break
|
||||
}
|
||||
createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||
removeNodes(old, oldStart, oldEnd + 1, vnodes)
|
||||
}
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldEnd], v = vnodes[end]
|
||||
if (o === v && !recycling) oldEnd--, end--
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) end--
|
||||
else if (o.key === v.key) {
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
if (o.dom != null) nextSibling = o.dom
|
||||
oldEnd--, end--
|
||||
}
|
||||
else {
|
||||
if (!map) map = getKeyMap(old, oldEnd)
|
||||
if (v != null) {
|
||||
var oldIndex = map[v.key]
|
||||
if (oldIndex != null) {
|
||||
var movable = old[oldIndex]
|
||||
updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
insertNode(parent, toFragment(movable), nextSibling)
|
||||
old[oldIndex].skip = true
|
||||
if (movable.dom != null) nextSibling = movable.dom
|
||||
}
|
||||
else {
|
||||
var dom = createNode(v, hooks, undefined)
|
||||
insertNode(parent, dom, nextSibling)
|
||||
nextSibling = dom
|
||||
}
|
||||
}
|
||||
end--
|
||||
}
|
||||
if (end < start) break
|
||||
}
|
||||
createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||
removeNodes(old, oldStart, oldEnd + 1, vnodes)
|
||||
}
|
||||
}
|
||||
function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) {
|
||||
|
|
@ -691,24 +699,21 @@ var coreRenderer = function($window) {
|
|||
}
|
||||
}
|
||||
}
|
||||
function once(f) {
|
||||
var called = false
|
||||
return function() {
|
||||
if (!called) {
|
||||
called = true
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
function removeNode(vnode, context) {
|
||||
var expected = 1, called = 0
|
||||
if (vnode.attrs && vnode.attrs.onbeforeremove) {
|
||||
expected++
|
||||
vnode.attrs.onbeforeremove.call(vnode.state, vnode, once(continuation))
|
||||
var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode)
|
||||
if (result != null && typeof result.then === "function") {
|
||||
expected++
|
||||
result.then(continuation, continuation)
|
||||
}
|
||||
}
|
||||
if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) {
|
||||
expected++
|
||||
vnode.tag.onbeforeremove.call(vnode.state, vnode, once(continuation))
|
||||
var result = vnode.tag.onbeforeremove.call(vnode.state, vnode)
|
||||
if (result != null && typeof result.then === "function") {
|
||||
expected++
|
||||
result.then(continuation, continuation)
|
||||
}
|
||||
}
|
||||
continuation()
|
||||
function continuation() {
|
||||
|
|
@ -741,7 +746,7 @@ var coreRenderer = function($window) {
|
|||
if (vnode.instance != null) onremove(vnode.instance)
|
||||
else {
|
||||
var children = vnode.children
|
||||
if (Array.isArray(children)) {
|
||||
if (children instanceof Array) {
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var child = children[i]
|
||||
if (child != null) onremove(child)
|
||||
|
|
@ -757,14 +762,14 @@ var coreRenderer = function($window) {
|
|||
}
|
||||
function setAttr(vnode, key2, old, value, ns) {
|
||||
var element = vnode.dom
|
||||
if (key2 === "key" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return
|
||||
if (key2 === "key" || key2 === "is" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return
|
||||
var nsLastIndex = key2.indexOf(":")
|
||||
if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") {
|
||||
element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value)
|
||||
}
|
||||
else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value)
|
||||
else if (key2 === "style") updateStyle(element, old, value)
|
||||
else if (key2 in element && !isAttribute(key2) && ns === undefined) {
|
||||
else if (key2 in element && !isAttribute(key2) && ns === undefined && !isCustomElement(vnode)) {
|
||||
//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
|
||||
if (vnode.tag === "input" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return
|
||||
//setting select[value] to same value while having select open blinks select dropdown in Chrome
|
||||
|
|
@ -813,6 +818,9 @@ var coreRenderer = function($window) {
|
|||
function isAttribute(attr) {
|
||||
return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type"
|
||||
}
|
||||
function isCustomElement(vnode){
|
||||
return vnode.attrs.is || vnode.tag.indexOf("-") > -1
|
||||
}
|
||||
function hasIntegrationMethods(source) {
|
||||
return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove)
|
||||
}
|
||||
|
|
@ -874,16 +882,13 @@ var coreRenderer = function($window) {
|
|||
}
|
||||
return false
|
||||
}
|
||||
function assign(target, source) {
|
||||
Object.keys(source).forEach(function(k){target[k] = source[k]})
|
||||
}
|
||||
function render(dom, vnodes) {
|
||||
if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
|
||||
var hooks = []
|
||||
var active = $doc.activeElement
|
||||
// First time0 rendering into a node clears it out
|
||||
if (dom.vnodes == null) dom.textContent = ""
|
||||
if (!Array.isArray(vnodes)) vnodes = [vnodes]
|
||||
if (!(vnodes instanceof Array)) vnodes = [vnodes]
|
||||
updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined)
|
||||
dom.vnodes = vnodes
|
||||
for (var i = 0; i < hooks.length; i++) hooks[i]()
|
||||
|
|
@ -1041,9 +1046,11 @@ var coreRouter = function($window) {
|
|||
var hash = buildQueryString(hashData)
|
||||
if (hash) path += "#" + hash
|
||||
if (supportsPushState) {
|
||||
if (options && options.replace) $window.history.replaceState(null, null, router.prefix + path)
|
||||
else $window.history.pushState(null, null, router.prefix + path)
|
||||
var state = options ? options.state : null
|
||||
var title = options ? options.title : null
|
||||
$window.onpopstate()
|
||||
if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path)
|
||||
else $window.history.pushState(state, title, router.prefix + path)
|
||||
}
|
||||
else $window.location.href = router.prefix + path
|
||||
}
|
||||
|
|
@ -1053,6 +1060,10 @@ var coreRouter = function($window) {
|
|||
var params = {}
|
||||
var pathname = parsePath(path, params, params)
|
||||
|
||||
var state = $window.history.state
|
||||
if (state != null) {
|
||||
for (var k in state) params[k] = state[k]
|
||||
}
|
||||
for (var route0 in routes) {
|
||||
var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
|
||||
if (matcher.test(pathname)) {
|
||||
|
|
@ -1080,37 +1091,37 @@ var coreRouter = function($window) {
|
|||
var _20 = function($window, redrawService0) {
|
||||
var routeService = coreRouter($window)
|
||||
var identity = function(v) {return v}
|
||||
var render1, component, attrs3, currentPath, updatePending = false
|
||||
var render1, component, attrs3, currentPath, lastUpdate
|
||||
var route = function(root, defaultRoute, routes) {
|
||||
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
|
||||
var update = function(routeResolver, comp, params, path) {
|
||||
component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, updatePending = false
|
||||
render1 = (routeResolver.render || identity).bind(routeResolver)
|
||||
run1()
|
||||
}
|
||||
var run1 = function() {
|
||||
if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3)))
|
||||
}
|
||||
var bail = function() {
|
||||
routeService.setPath(defaultRoute)
|
||||
routeService.setPath(defaultRoute, null, {replace: true})
|
||||
}
|
||||
routeService.defineRoutes(routes, function(payload, params, path) {
|
||||
if (payload.view) update({}, payload, params, path)
|
||||
var update = lastUpdate = function(routeResolver, comp) {
|
||||
if (update !== lastUpdate) return
|
||||
component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, lastUpdate = null
|
||||
render1 = (routeResolver.render || identity).bind(routeResolver)
|
||||
run1()
|
||||
}
|
||||
if (payload.view) update({}, payload)
|
||||
else {
|
||||
if (payload.onmatch) {
|
||||
updatePending = true
|
||||
Promise.resolve(payload.onmatch(params, path)).then(function(resolved) {
|
||||
if (updatePending) update(payload, resolved, params, path)
|
||||
update(payload, resolved)
|
||||
}, bail)
|
||||
}
|
||||
else update(payload, "div", params, path)
|
||||
else update(payload, "div")
|
||||
}
|
||||
}, bail)
|
||||
redrawService0.subscribe(root, run1)
|
||||
}
|
||||
route.set = function(path, data, options) {
|
||||
if (updatePending) options = {replace: true}
|
||||
updatePending = false
|
||||
if (lastUpdate != null) options = {replace: true}
|
||||
lastUpdate = null
|
||||
routeService.setPath(path, data, options)
|
||||
}
|
||||
route.get = function() {return currentPath}
|
||||
|
|
@ -1141,7 +1152,8 @@ m.request = requestService.request
|
|||
m.jsonp = requestService.jsonp
|
||||
m.parseQueryString = parseQueryString
|
||||
m.buildQueryString = buildQueryString
|
||||
m.version = "1.0.0-rc.6"
|
||||
m.version = "1.0.0-rc.7"
|
||||
m.vnode = Vnode
|
||||
if (typeof module !== "undefined") module["exports"] = m
|
||||
else window.m = m
|
||||
}
|
||||
83
mithril.min.js
vendored
83
mithril.min.js
vendored
|
|
@ -1,41 +1,42 @@
|
|||
new function(){function w(a,c,h,d,g,m){return{tag:a,key:c,attrs:h,children:d,text:g,dom:m,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function z(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===K[a]){for(var c,h,d=[],g={};c=P.exec(a);){var m=c[1],l=c[2];""===m&&""!==l?h=l:"#"===m?g.id=l:"."===m?d.push(l):"["===c[3][0]&&((m=c[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),
|
||||
"class"===c[4]?d.push(m):g[c[4]]=m||!0)}0<d.length&&(g.className=d.join(" "));K[a]=function(a,c){var m=!1,b,p,d=a.className||a["class"],t;for(t in g)a[t]=g[t];void 0!==d&&(void 0!==a["class"]&&(a["class"]=void 0,a.className=d),void 0!==g.className&&(a.className=g.className+" "+d));for(t in a)if("key"!==t){m=!0;break}Array.isArray(c)&&1==c.length&&null!=c[0]&&"#"===c[0].tag?p=c[0].children:b=c;return w(h||"div",a.key,m?a:void 0,b,p,void 0)}}var n;null==arguments[1]||"object"===typeof arguments[1]&&
|
||||
void 0===arguments[1].tag&&!Array.isArray(arguments[1])?(n=arguments[1],d=2):d=1;if(arguments.length===d+1)c=Array.isArray(arguments[d])?arguments[d]:[arguments[d]];else for(c=[];d<arguments.length;d++)c.push(arguments[d]);return"string"===typeof a?K[a](n||{},w.normalizeChildren(c)):w(a,n&&n.key,n||{},w.normalizeChildren(c),void 0,void 0)}function Q(a){var c=0,h=null,d="function"===typeof requestAnimationFrame?requestAnimationFrame:setTimeout;return function(){var g=Date.now();0===c||16<=g-c?(c=g,
|
||||
a()):null===h&&(h=d(function(){h=null;a();c=Date.now()},16-(g-c)))}}w.normalize=function(a){return Array.isArray(a)?w("[",void 0,void 0,w.normalizeChildren(a),void 0,void 0):null!=a&&"object"!==typeof a?w("#",void 0,void 0,a,void 0,void 0):a};w.normalizeChildren=function(a){for(var c=0;c<a.length;c++)a[c]=w.normalize(a[c]);return a};var P=/(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g,K={};z.trust=function(a){null==a&&(a="");return w("<",void 0,void 0,a,void 0,void 0)};
|
||||
z.fragment=function(a,c){return w("[",a.key,a,w.normalizeChildren(c),void 0,void 0)};var v=function(a){function c(a,b){return function C(c){var l;try{if(!b||null==c||"object"!==typeof c&&"function"!==typeof c||"function"!==typeof(l=c.then))k(function(){b||0!==a.length||console.error("Possible unhandled promise rejection:",c);for(var d=0;d<a.length;d++)a[d](c);g.length=0;m.length=0;p.state=b;p.retry=function(){C(c)}});else{if(c===d)throw new TypeError("Promise can't be resolved w/ itself");h(l.bind(c))}}catch(G){n(G)}}}
|
||||
function h(a){function b(b){return function(a){0<c++||b(a)}}var c=0,d=b(n);try{a(b(l),d)}catch(t){d(t)}}if(!(this instanceof v))throw Error("Promise must be called with `new`");if("function"!==typeof a)throw new TypeError("executor must be a function");var d=this,g=[],m=[],l=c(g,!0),n=c(m,!1),p=d._instance={resolvers:g,rejectors:m},k="function"===typeof setImmediate?setImmediate:setTimeout;h(a)};v.prototype.then=function(a,c){function h(a,c,h,l){c.push(function(b){if("function"!==typeof a)h(b);else try{g(a(b))}catch(D){m&&
|
||||
m(D)}});"function"===typeof d.retry&&l===d.state&&d.retry()}var d=this._instance,g,m,l=new v(function(a,c){g=a;m=c});h(a,d.resolvers,g,!0);h(c,d.rejectors,m,!1);return l};v.prototype["catch"]=function(a){return this.then(null,a)};v.resolve=function(a){return a instanceof v?a:new v(function(c){c(a)})};v.reject=function(a){return new v(function(c,h){h(a)})};v.all=function(a){return new v(function(c,h){var d=a.length,g=0,m=[];if(0===a.length)c([]);else for(var l=0;l<a.length;l++)(function(l){function p(a){g++;
|
||||
m[l]=a;g===d&&c(m)}null==a[l]||"object"!==typeof a[l]&&"function"!==typeof a[l]||"function"!==typeof a[l].then?p(a[l]):a[l].then(p,h)})(l)})};v.race=function(a){return new v(function(c,h){for(var d=0;d<a.length;d++)a[d].then(c,h)})};"undefined"===typeof E&&("undefined"!==typeof window?window.Promise=v:"undefined"!==typeof global&&(global.Promise=v));var H="undefined"!==typeof E?E:v,I=function(a){function c(a,d){if(Array.isArray(d))for(var g=0;g<d.length;g++)c(a+"["+g+"]",d[g]);else if("[object Object]"===
|
||||
Object.prototype.toString.call(d))for(g in d)c(a+"["+g+"]",d[g]);else h.push(encodeURIComponent(a)+(null!=d&&""!==d?"="+encodeURIComponent(d):""))}if("[object Object]"!==Object.prototype.toString.call(a))return"";var h=[],d;for(d in a)c(d,a[d]);return h.join("&")},M=function(a,c){function h(){function b(){0===--a&&"function"===typeof r&&r()}var a=0;return function t(c){var d=c.then;c.then=function(){a++;var g=d.apply(c,arguments);g.then(b,function(c){b();if(0===a)throw c;});return t(g)};return c}}
|
||||
function d(b,a){if("string"===typeof b){var c=b;b=a||{};null==b.url&&(b.url=c)}return b}function g(b,a){if(null==a)return b;for(var c=b.match(/:[^\/]+/gi)||[],d=0;d<c.length;d++){var g=c[d].slice(1);null!=a[g]&&(b=b.replace(c[d],a[g]),delete a[g])}return b}function m(a,c){var b=I(c);if(""!==b){var d=0>a.indexOf("?")?"?":"&";a+=d+b}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a,c){if("function"===typeof a)if(Array.isArray(c))for(var b=
|
||||
0;b<c.length;b++)c[b]=new a(c[b]);else return new a(c);return c}var k=0,r;return{request:function(b,k){var r=h();b=d(b,k);var t=new c(function(c,d){null==b.method&&(b.method="GET");b.method=b.method.toUpperCase();var h="boolean"===typeof b.useBody?b.useBody:"GET"!==b.method&&"TRACE"!==b.method;"function"!==typeof b.serialize&&(b.serialize="undefined"!==typeof FormData&&b.data instanceof FormData?function(a){return a}:JSON.stringify);"function"!==typeof b.deserialize&&(b.deserialize=l);"function"!==
|
||||
typeof b.extract&&(b.extract=n);b.url=g(b.url,b.data);h?b.data=b.serialize(b.data):b.url=m(b.url,b.data);var k=new a.XMLHttpRequest;k.open(b.method,b.url,"boolean"===typeof b.async?b.async:!0,"string"===typeof b.user?b.user:void 0,"string"===typeof b.password?b.password:void 0);b.serialize===JSON.stringify&&h&&k.setRequestHeader("Content-Type","application/json; charset=utf-8");b.deserialize===l&&k.setRequestHeader("Accept","application/json, text/*");b.withCredentials&&(k.withCredentials=b.withCredentials);
|
||||
"function"===typeof b.config&&(k=b.config(k,b)||k);k.onreadystatechange=function(){if(4===k.readyState)try{var a=b.extract!==n?b.extract(k,b):b.deserialize(b.extract(k,b));if(200<=k.status&&300>k.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?t:r(t)},jsonp:function(b,l){var n=h();b=d(b,l);var t=new c(function(c,d){var h=b.callbackName||"_mithril_"+Math.round(1E16*
|
||||
Math.random())+"_"+k++,l=a.document.createElement("script");a[h]=function(d){l.parentNode.removeChild(l);c(p(b.type,d));delete a[h]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){r=a}}}(window,H),O=function(a){function c(e,f,a,b,c,d,g){for(;a<
|
||||
b;a++){var q=f[a];null!=q&&p(e,h(q,c,g),d)}}function h(e,f,a){var q=e.tag;null!=e.attrs&&L(e.attrs,e,f);if("string"===typeof q)switch(q){case "#":return e.dom=B.createTextNode(e.children);case "<":return d(e);case "[":var b=B.createDocumentFragment();null!=e.children&&(q=e.children,c(b,q,0,q.length,f,null,a));e.dom=b.firstChild;e.domSize=b.childNodes.length;return b;default:var g=e.tag;switch(e.tag){case "svg":a="http://www.w3.org/2000/svg";break;case "math":a="http://www.w3.org/1998/Math/MathML"}var p=
|
||||
(q=e.attrs)&&q.is,g=a?p?B.createElementNS(a,g,{is:p}):B.createElementNS(a,g):p?B.createElement(g,{is:p}):B.createElement(g);e.dom=g;if(null!=q)for(b in p=a,q)t(e,b,null,q[b],p);null!=e.attrs&&null!=e.attrs.contenteditable?k(e):(null!=e.text&&(""!==e.text?g.textContent=e.text:e.children=[w("#",void 0,void 0,e.text,void 0,void 0)]),null!=e.children&&(b=e.children,c(g,b,0,b.length,f,null,a),f=e.attrs,"select"===e.tag&&null!=f&&("value"in f&&t(e,"value",null,f.value,void 0),"selectedIndex"in f&&t(e,"selectedIndex",
|
||||
null,f.selectedIndex,void 0))));return g}else{e.state||(e.state={});R(e.state,e.tag);b=e.tag.view;if(null!=b.reentrantLock)e=E;else if(b.reentrantLock=!0,L(e.tag,e,f),e.instance=w.normalize(b.call(e.state,e)),b.reentrantLock=null,null!=e.instance){if(e.instance===e)throw Error("A view cannot return the vnode it received as arguments");f=h(e.instance,f,a);e.dom=e.instance.dom;e.domSize=null!=e.dom?e.instance.domSize:0;e=f}else e.domSize=0,e=E;return e}}function d(e){var f={caption:"table",thead:"table",
|
||||
tbody:"table",tfoot:"table",tr:"tbody",th:"tr",td:"tr",colgroup:"table",col:"colgroup"}[(e.children.match(/^\s*?<(\w+)/im)||[])[1]]||"div",f=B.createElement(f);f.innerHTML=e.children;e.dom=f.firstChild;e.domSize=f.childNodes.length;e=B.createDocumentFragment();for(var a;a=f.firstChild;)e.appendChild(a);return e}function g(e,f,a,b,d,g){if(f!==a&&(null!=f||null!=a))if(null==f)c(e,a,0,a.length,b,d,void 0);else if(null==a)r(f,0,f.length,a);else{for(var q=!1,k=0;k<a.length;k++)if(null!=a[k]){q=null==a[k].key;
|
||||
break}if(f.length===a.length&&q)for(k=0;k<f.length;k++)f[k]!==a[k]&&(null==f[k]?p(e,h(a[k],b,g),n(f,k+1,d)):null==a[k]?r(f,k,k+1,a):m(e,f[k],a[k],b,n(f,k+1,d),!1,g));else{a:{if(null!=f.pool&&Math.abs(f.pool.length-a.length)<=Math.abs(f.length-a.length)&&(q=a[0]&&a[0].children&&a[0].children.length||0,Math.abs((f.pool[0]&&f.pool[0].children&&f.pool[0].children.length||0)-q)<=Math.abs((f[0]&&f[0].children&&f[0].children.length||0)-q))){q=!0;break a}q=!1}q&&(f=f.concat(f.pool));for(var A=k=0,y=f.length-
|
||||
1,t=a.length-1,D;y>=k&&t>=A;){var x=f[k],u=a[A];if(x!==u||q)if(null==x)k++;else if(null==u)A++;else if(x.key===u.key)k++,A++,m(e,x,u,b,n(f,k,d),q,g),q&&x.tag===u.tag&&p(e,l(x),d);else if(x=f[y],x!==u||q)if(null==x)y--;else if(null==u)A++;else if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),(q||A<t)&&p(e,l(x),n(f,k,d)),y--,A++;else break;else y--,A++;else k++,A++}for(;y>=k&&t>=A;){x=f[y];u=a[t];if(x!==u||q)if(null==x)y--;else{if(null!=u)if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),q&&x.tag===u.tag&&p(e,l(x),
|
||||
d),null!=x.dom&&(d=x.dom),y--;else{if(!D){D=f;var x=y,C={},w;for(w=0;w<x;w++){var v=D[w];null!=v&&(v=v.key,null!=v&&(C[v]=w))}D=C}null!=u&&(x=D[u.key],null!=x?(C=f[x],m(e,C,u,b,n(f,y+1,d),q,g),p(e,l(C),d),f[x].skip=!0,null!=C.dom&&(d=C.dom)):(u=h(u,b,void 0),p(e,u,d),d=u))}t--}else y--,t--;if(t<A)break}c(e,a,A,t+1,b,d,g);r(f,k,y+1,a)}}}function m(a,f,b,c,u,y,n){var e=f.tag;if(e===b.tag){b.state=f.state;b.events=f.events;var q;var A;null!=b.attrs&&"function"===typeof b.attrs.onbeforeupdate&&(q=b.attrs.onbeforeupdate.call(b.state,
|
||||
b,f));"string"!==typeof b.tag&&"function"===typeof b.tag.onbeforeupdate&&(A=b.tag.onbeforeupdate.call(b.state,b,f));void 0===q&&void 0===A||q||A?q=!1:(b.dom=f.dom,b.domSize=f.domSize,b.instance=f.instance,q=!0);if(!q)if(null!=b.attrs&&z(b.attrs,b,c,y),"string"===typeof e)switch(e){case "#":f.children.toString()!==b.children.toString()&&(f.dom.nodeValue=b.children);b.dom=f.dom;break;case "<":f.children!==b.children?(l(f),p(a,d(b),u)):(b.dom=f.dom,b.domSize=f.domSize);break;case "[":g(a,f.children,
|
||||
b.children,c,u,n);f=0;c=b.children;b.dom=null;if(null!=c){for(var r=0;r<c.length;r++)a=c[r],null!=a&&null!=a.dom&&(null==b.dom&&(b.dom=a.dom),f+=a.domSize||1);1!==f&&(b.domSize=f)}break;default:a=n;u=b.dom=f.dom;switch(b.tag){case "svg":a="http://www.w3.org/2000/svg";break;case "math":a="http://www.w3.org/1998/Math/MathML"}"textarea"===b.tag&&(null==b.attrs&&(b.attrs={}),null!=b.text&&(b.attrs.value=b.text,b.text=void 0));y=f.attrs;n=b.attrs;e=a;if(null!=n)for(r in n)t(b,r,y&&y[r],n[r],e);if(null!=
|
||||
y)for(r in y)null!=n&&r in n||("className"===r&&(r="class"),"o"!==r[0]||"n"!==r[1]||v(r)?"key"!==r&&b.dom.removeAttribute(r):G(b,r,void 0));null!=b.attrs&&null!=b.attrs.contenteditable?k(b):null!=f.text&&null!=b.text&&""!==b.text?f.text.toString()!==b.text.toString()&&(f.dom.firstChild.nodeValue=b.text):(null!=f.text&&(f.children=[w("#",void 0,void 0,f.text,void 0,f.dom.firstChild)]),null!=b.text&&(b.children=[w("#",void 0,void 0,b.text,void 0,void 0)]),g(u,f.children,b.children,c,null,a))}else b.instance=
|
||||
w.normalize(b.tag.view.call(b.state,b)),z(b.tag,b,c,y),null!=b.instance?(null==f.instance?p(a,h(b.instance,c,n),u):m(a,f.instance,b.instance,c,u,y,n),b.dom=b.instance.dom,b.domSize=b.instance.domSize):null!=f.instance?(D(f.instance,null),b.dom=void 0,b.domSize=0):(b.dom=f.dom,b.domSize=f.domSize)}else D(f,null),p(a,h(b,c,n),u)}function l(b){var a=b.domSize;if(null!=a||null==b.dom){var e=B.createDocumentFragment();if(0<a){for(b=b.dom;--a;)e.appendChild(b.nextSibling);e.insertBefore(b,e.firstChild)}return e}return b.dom}
|
||||
function n(b,a,c){for(;a<b.length;a++)if(null!=b[a]&&null!=b[a].dom)return b[a].dom;return c}function p(b,a,c){c&&c.parentNode?b.insertBefore(a,c):b.appendChild(a)}function k(b){var a=b.children;if(null!=a&&1===a.length&&"<"===a[0].tag)a=a[0].children,b.dom.innerHTML!==a&&(b.dom.innerHTML=a);else if(null!=b.text||null!=a&&0!==a.length)throw Error("Child node of a contenteditable must be trusted");}function r(b,a,c,d){for(;a<c;a++){var e=b[a];null!=e&&(e.skip?e.skip=!1:D(e,d))}}function b(b){var a=
|
||||
!1;return function(){a||(a=!0,b())}}function D(a,f){function e(){if(++d===c&&(C(a),a.dom)){var b=a.domSize||1;if(1<b)for(var e=a.dom;--b;){var g=e.nextSibling,k=g.parentNode;null!=k&&k.removeChild(g)}b=a.dom;e=b.parentNode;null!=e&&e.removeChild(b);if(b=null!=f&&null==a.domSize)b=a.attrs,b=!(null!=b&&(b.oncreate||b.onupdate||b.onbeforeremove||b.onremove));b&&"string"===typeof a.tag&&(f.pool?f.pool.push(a):f.pool=[a])}}var c=1,d=0;a.attrs&&a.attrs.onbeforeremove&&(c++,a.attrs.onbeforeremove.call(a.state,
|
||||
a,b(e)));"string"!==typeof a.tag&&a.tag.onbeforeremove&&(c++,a.tag.onbeforeremove.call(a.state,a,b(e)));e()}function C(b){b.attrs&&b.attrs.onremove&&b.attrs.onremove.call(b.state,b);"string"!==typeof b.tag&&b.tag.onremove&&b.tag.onremove.call(b.state,b);if(null!=b.instance)C(b.instance);else if(b=b.children,Array.isArray(b))for(var a=0;a<b.length;a++){var e=b[a];null!=e&&C(e)}}function t(b,a,c,d,g){var e=b.dom;if("key"!==a&&(c!==d||"value"===a||"checked"===a||"selectedIndex"===a||"selected"===a&&
|
||||
b.dom===B.activeElement||"object"===typeof d)&&"undefined"!==typeof d&&!v(a)){var f=a.indexOf(":");if(-1<f&&"xlink"===a.substr(0,f))e.setAttributeNS("http://www.w3.org/1999/xlink",a.slice(f+1),d);else if("o"===a[0]&&"n"===a[1]&&"function"===typeof d)G(b,a,d);else if("style"===a)if(b=c,b===d&&(e.style.cssText="",b=null),null==d)e.style.cssText="";else if("string"===typeof d)e.style.cssText=d;else{"string"===typeof b&&(e.style.cssText="");for(var k in d)e.style[k]=d[k];if(null!=b&&"string"!==typeof b)for(k in b)k in
|
||||
d||(e.style[k]="")}else a in e&&"href"!==a&&"list"!==a&&"form"!==a&&"width"!==a&&"height"!==a&&void 0===g?"input"===b.tag&&"value"===a&&b.dom.value===d&&b.dom===B.activeElement||"select"===b.tag&&"value"===a&&b.dom.value===d&&b.dom===B.activeElement||"option"===b.tag&&"value"===a&&b.dom.value===d||(e[a]=d):"boolean"===typeof d?d?e.setAttribute(a,""):e.removeAttribute(a):e.setAttribute("className"===a?"class":a,d)}}function v(b){return"oninit"===b||"oncreate"===b||"onupdate"===b||"onremove"===b||"onbeforeremove"===
|
||||
b||"onbeforeupdate"===b}function G(b,a,c){var d=b.dom,e="function"!==typeof F?c:function(b){var a=c.call(d,b);F.call(d,b);return a};if(a in d)d[a]="function"===typeof c?e:null;else{var f=a.slice(2);void 0===b.events&&(b.events={});b.events[a]!==e&&(null!=b.events[a]&&d.removeEventListener(f,b.events[a],!1),"function"===typeof c&&(b.events[a]=e,d.addEventListener(f,b.events[a],!1)))}}function L(b,a,c){"function"===typeof b.oninit&&b.oninit.call(a.state,a);"function"===typeof b.oncreate&&c.push(b.oncreate.bind(a.state,
|
||||
a))}function z(b,a,c,d){d?L(b,a,c):"function"===typeof b.onupdate&&c.push(b.onupdate.bind(a.state,a))}function R(b,a){Object.keys(a).forEach(function(c){b[c]=a[c]})}var B=a.document,E=B.createDocumentFragment(),F;return{render:function(b,a){if(!b)throw Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var c=[],d=B.activeElement;null==b.vnodes&&(b.textContent="");Array.isArray(a)||(a=[a]);g(b,b.vnodes,w.normalizeChildren(a),c,null,void 0);b.vnodes=a;for(var e=
|
||||
0;e<c.length;e++)c[e]();B.activeElement!==d&&d.focus()},setEventCallback:function(b){return F=b}}},J=function(a){function c(a){a=d.indexOf(a);-1<a&&d.splice(a,2)}function h(){for(var a=1;a<d.length;a+=2)d[a]()}a=O(a);a.setEventCallback(function(a){!1!==a.redraw&&h()});var d=[];return{subscribe:function(a,h){c(a);d.push(a,Q(h))},unsubscribe:c,redraw:h,render:a.render}}(window);M.setCompletionCallback(J.redraw);z.mount=function(a){return function(c,h){if(null===h)a.render(c,[]),a.unsubscribe(c);else{if(null==
|
||||
h.view)throw Error("m.mount(element, component) expects a component, not a vnode");a.subscribe(c,function(){a.render(c,w(h))});a.redraw()}}}(J);var E=H,N=function(a){if(""===a||null==a)return{};"?"===a.charAt(0)&&(a=a.slice(1));a=a.split("&");for(var c={},h={},d=0;d<a.length;d++){var g=a[d].split("="),m=decodeURIComponent(g[0]),g=2===g.length?decodeURIComponent(g[1]):"";"true"===g?g=!0:"false"===g&&(g=!1);var l=m.split(/\]\[?|\[/),n=c;-1<m.indexOf("[")&&l.pop();for(var p=0;p<l.length;p++){var m=l[p],
|
||||
k=l[p+1],k=""==k||!isNaN(parseInt(k,10)),r=p===l.length-1;""===m&&(m=l.slice(0,p).join(),null==h[m]&&(h[m]=0),m=h[m]++);null==n[m]&&(n[m]=r?g:k?[]:{});n=n[m]}}return c},S=function(a){function c(c){var d=a.location[c].replace(/(?:%[a-f89][a-f0-9])+/gim,decodeURIComponent);"pathname"===c&&"/"!==d[0]&&(d="/"+d);return d}function h(a){return function(){null==l&&(l=m(function(){l=null;a()}))}}function d(a,c,d){var b=a.indexOf("?"),g=a.indexOf("#"),k=-1<b?b:-1<g?g:a.length;if(-1<b){var b=N(a.slice(b+1,
|
||||
-1<g?g:a.length)),h;for(h in b)c[h]=b[h]}if(-1<g)for(h in c=N(a.slice(g+1)),c)d[h]=c[h];return a.slice(0,k)}var g="function"===typeof a.history.pushState,m="function"===typeof setImmediate?setImmediate:setTimeout,l,n={prefix:"#!",getPath:function(){switch(n.prefix.charAt(0)){case "#":return c("hash").slice(n.prefix.length);case "?":return c("search").slice(n.prefix.length)+c("hash");default:return c("pathname").slice(n.prefix.length)+c("search")+c("hash")}},setPath:function(c,k,h){var b={},l={};c=
|
||||
d(c,b,l);if(null!=k){for(var m in k)b[m]=k[m];c=c.replace(/:([^\/]+)/g,function(a,c){delete b[c];return k[c]})}(m=I(b))&&(c+="?"+m);(l=I(l))&&(c+="#"+l);g?(h&&h.replace?a.history.replaceState(null,null,n.prefix+c):a.history.pushState(null,null,n.prefix+c),a.onpopstate()):a.location.href=n.prefix+c},defineRoutes:function(c,k,l){function b(){var a=n.getPath(),b={},g=d(a,b,b),h;for(h in c){var m=new RegExp("^"+h.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$");if(m.test(g)){g.replace(m,
|
||||
function(){for(var d=h.match(/:[^\/]+/g)||[],g=[].slice.call(arguments,1,-2),l=0;l<d.length;l++)b[d[l].replace(/:|\./g,"")]=decodeURIComponent(g[l]);k(c[h],b,a,h)});return}}l(a,b)}g?a.onpopstate=h(b):"#"===n.prefix.charAt(0)&&(a.onhashchange=b);b()}};return n};z.route=function(a,c){var h=S(a),d=function(a){return a},g,m,l,n,p=!1,k=function(a,b,k){if(null==a)throw Error("Ensure the DOM element that was passed to `m.route` is not undefined");var r=function(a,b,c,h){m=null!=b&&"function"===typeof b.view?
|
||||
b:"div";l=c;n=h;p=!1;g=(a.render||d).bind(a);t()},t=function(){null!=g&&c.render(a,g(w(m,l.key,l)))},v=function(){h.setPath(b)};h.defineRoutes(k,function(a,b,c){a.view?r({},a,b,c):a.onmatch?(p=!0,E.resolve(a.onmatch(b,c)).then(function(d){p&&r(a,d,b,c)},v)):r(a,"div",b,c)},v);c.subscribe(a,t)};k.set=function(a,b,c){p&&(c={replace:!0});p=!1;h.setPath(a,b,c)};k.get=function(){return n};k.prefix=function(a){h.prefix=a};k.link=function(a){a.dom.setAttribute("href",h.prefix+a.attrs.href);a.dom.onclick=
|
||||
function(a){a.ctrlKey||a.metaKey||a.shiftKey||2===a.which||(a.preventDefault(),a.redraw=!1,a=this.getAttribute("href"),0===a.indexOf(h.prefix)&&(a=a.slice(h.prefix.length)),k.set(a,void 0,void 0))}};return k}(window,J);z.withAttr=function(a,c,h){return function(d){return c.call(h||this,a in d.currentTarget?d.currentTarget[a]:d.currentTarget.getAttribute(a))}};H=O(window);z.render=H.render;z.redraw=J.redraw;z.request=M.request;z.jsonp=M.jsonp;z.parseQueryString=N;z.buildQueryString=I;z.version="1.0.0-rc.6";
|
||||
"undefined"!==typeof module?module.exports=z:window.m=z};
|
||||
new function(){function w(b,c,h,d,g,l){return{tag:b,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(b){if(null==b||"string"!==typeof b&&null==b.view)throw Error("The selector must be either a string or a component.");if("string"===typeof b&&void 0===G[b]){for(var c,h,d=[],g={};c=N.exec(b);){var l=c[1],m=c[2];""===l&&""!==m?h=m:"#"===l?g.id=m:"."===l?d.push(m):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),
|
||||
"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0<d.length&&(g.className=d.join(" "));G[b]=function(b,c){var d=!1,a,l,n=b.className||b["class"],z;for(z in g)b[z]=g[z];void 0!==n&&(void 0!==b["class"]&&(b["class"]=void 0,b.className=n),void 0!==g.className&&(b.className=g.className+" "+n));for(z in b)if("key"!==z){d=!0;break}c instanceof Array&&1==c.length&&null!=c[0]&&"#"===c[0].tag?l=c[0].children:a=c;return w(h||"div",b.key,d?b:void 0,a,l,void 0)}}var q;null!=arguments[1]&&("object"!==typeof arguments[1]||
|
||||
void 0!==arguments[1].tag||arguments[1]instanceof Array)?d=1:(q=arguments[1],d=2);if(arguments.length===d+1)c=arguments[d]instanceof Array?arguments[d]:[arguments[d]];else for(c=[];d<arguments.length;d++)c.push(arguments[d]);return"string"===typeof b?G[b](q||{},w.normalizeChildren(c)):w(b,q&&q.key,q||{},w.normalizeChildren(c),void 0,void 0)}function O(b){var c=0,h=null,d="function"===typeof requestAnimationFrame?requestAnimationFrame:setTimeout;return function(){var g=Date.now();0===c||16<=g-c?(c=
|
||||
g,b()):null===h&&(h=d(function(){h=null;b();c=Date.now()},16-(g-c)))}}w.normalize=function(b){return b instanceof Array?w("[",void 0,void 0,w.normalizeChildren(b),void 0,void 0):null!=b&&"object"!==typeof b?w("#",void 0,void 0,b,void 0,void 0):b};w.normalizeChildren=function(b){for(var c=0;c<b.length;c++)b[c]=w.normalize(b[c]);return b};var N=/(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g,G={};A.trust=function(b){null==b&&(b="");return w("<",void 0,void 0,b,void 0,
|
||||
void 0)};A.fragment=function(b,c){return w("[",b.key,b,w.normalizeChildren(c),void 0,void 0)};var x=function(b){function c(b,a){return function p(c){var m;try{if(!a||null==c||"object"!==typeof c&&"function"!==typeof c||"function"!==typeof(m=c.then))k(function(){a||0!==b.length||console.error("Possible unhandled promise rejection:",c);for(var d=0;d<b.length;d++)b[d](c);g.length=0;l.length=0;n.state=a;n.retry=function(){p(c)}});else{if(c===d)throw new TypeError("Promise can't be resolved w/ itself");
|
||||
h(m.bind(c))}}catch(P){q(P)}}}function h(b){function a(a){return function(b){0<c++||a(b)}}var c=0,d=a(q);try{b(a(m),d)}catch(z){d(z)}}if(!(this instanceof x))throw Error("Promise must be called with `new`");if("function"!==typeof b)throw new TypeError("executor must be a function");var d=this,g=[],l=[],m=c(g,!0),q=c(l,!1),n=d._instance={resolvers:g,rejectors:l},k="function"===typeof setImmediate?setImmediate:setTimeout;h(b)};x.prototype.then=function(b,c){function h(b,c,h,m){c.push(function(a){if("function"!==
|
||||
typeof b)h(a);else try{g(b(a))}catch(C){l&&l(C)}});"function"===typeof d.retry&&m===d.state&&d.retry()}var d=this._instance,g,l,m=new x(function(b,c){g=b;l=c});h(b,d.resolvers,g,!0);h(c,d.rejectors,l,!1);return m};x.prototype["catch"]=function(b){return this.then(null,b)};x.resolve=function(b){return b instanceof x?b:new x(function(c){c(b)})};x.reject=function(b){return new x(function(c,h){h(b)})};x.all=function(b){return new x(function(c,h){var d=b.length,g=0,l=[];if(0===b.length)c([]);else for(var m=
|
||||
0;m<b.length;m++)(function(m){function n(b){g++;l[m]=b;g===d&&c(l)}null==b[m]||"object"!==typeof b[m]&&"function"!==typeof b[m]||"function"!==typeof b[m].then?n(b[m]):b[m].then(n,h)})(m)})};x.race=function(b){return new x(function(c,h){for(var d=0;d<b.length;d++)b[d].then(c,h)})};"undefined"!==typeof window?("undefined"===typeof window.Promise&&(window.Promise=x),x=window.Promise):"undefined"!==typeof global&&("undefined"===typeof global.Promise&&(global.Promise=x),x=global.Promise);var D=function(b){function c(b,
|
||||
d){if(d instanceof Array)for(var g=0;g<d.length;g++)c(b+"["+g+"]",d[g]);else if("[object Object]"===Object.prototype.toString.call(d))for(g in d)c(b+"["+g+"]",d[g]);else h.push(encodeURIComponent(b)+(null!=d&&""!==d?"="+encodeURIComponent(d):""))}if("[object Object]"!==Object.prototype.toString.call(b))return"";var h=[],d;for(d in b)c(d,b[d]);return h.join("&")},I=function(b,c){function h(){function a(){0===--b&&"function"===typeof t&&t()}var b=0;return function z(c){var d=c.then;c.then=function(){b++;
|
||||
var g=d.apply(c,arguments);g.then(a,function(c){a();if(0===b)throw c;});return z(g)};return c}}function d(a,b){if("string"===typeof a){var c=a;a=b||{};null==a.url&&(a.url=c)}return a}function g(a,b){if(null==b)return a;for(var c=a.match(/:[^\/]+/gi)||[],d=0;d<c.length;d++){var g=c[d].slice(1);null!=b[g]&&(a=a.replace(c[d],b[g]),delete b[g])}return a}function l(a,b){var c=D(b);if(""!==c){var d=0>a.indexOf("?")?"?":"&";a+=d+c}return a}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b);
|
||||
}}function q(b){return b.responseText}function n(b,c){if("function"===typeof b)if(c instanceof Array)for(var a=0;a<c.length;a++)c[a]=new b(c[a]);else return new b(c);return c}var k=0,t;return{request:function(a,k){var t=h();a=d(a,k);var z=new c(function(c,d){null==a.method&&(a.method="GET");a.method=a.method.toUpperCase();var h="boolean"===typeof a.useBody?a.useBody:"GET"!==a.method&&"TRACE"!==a.method;"function"!==typeof a.serialize&&(a.serialize="undefined"!==typeof FormData&&a.data instanceof FormData?
|
||||
function(b){return b}:JSON.stringify);"function"!==typeof a.deserialize&&(a.deserialize=m);"function"!==typeof a.extract&&(a.extract=q);a.url=g(a.url,a.data);h?a.data=a.serialize(a.data):a.url=l(a.url,a.data);var k=new b.XMLHttpRequest;k.open(a.method,a.url,"boolean"===typeof a.async?a.async:!0,"string"===typeof a.user?a.user:void 0,"string"===typeof a.password?a.password:void 0);a.serialize===JSON.stringify&&h&&k.setRequestHeader("Content-Type","application/json; charset=utf-8");a.deserialize===
|
||||
m&&k.setRequestHeader("Accept","application/json, text/*");a.withCredentials&&(k.withCredentials=a.withCredentials);for(var t in a.headers)({}).hasOwnProperty.call(a.headers,t)&&k.setRequestHeader(t,a.headers[t]);"function"===typeof a.config&&(k=a.config(k,a)||k);k.onreadystatechange=function(){if(4===k.readyState)try{var b=a.extract!==q?a.extract(k,a):a.deserialize(a.extract(k,a));if(200<=k.status&&300>k.status||304===k.status)c(n(a.type,b));else{var g=Error(k.responseText),e;for(e in b)g[e]=b[e];
|
||||
d(g)}}catch(f){d(f)}};h&&null!=a.data?k.send(a.data):k.send()});return!0===a.background?z:t(z)},jsonp:function(a,m){var t=h();a=d(a,m);var q=new c(function(c,d){var h=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,m=b.document.createElement("script");b[h]=function(d){m.parentNode.removeChild(m);c(n(a.type,d));delete b[h]};m.onerror=function(){m.parentNode.removeChild(m);d(Error("JSONP request failed"));delete b[h]};null==a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey||
|
||||
"callback"]=h;m.src=l(a.url,a.data);b.document.documentElement.appendChild(m)});return!0===a.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,x),M=function(b){function c(e,f,b,a,c,d,g){for(;b<a;b++){var r=f[b];null!=r&&k(e,h(r,c,g),d)}}function h(e,f,b){var a=e.tag;null!=e.attrs&&H(e.attrs,e,f);if("string"===typeof a)switch(a){case "#":return e.dom=B.createTextNode(e.children);case "<":return d(e);case "[":var r=B.createDocumentFragment();null!=e.children&&(a=e.children,c(r,a,0,
|
||||
a.length,f,null,b));e.dom=r.firstChild;e.domSize=r.childNodes.length;return r;default:var h=e.tag;switch(e.tag){case "svg":b="http://www.w3.org/2000/svg";break;case "math":b="http://www.w3.org/1998/Math/MathML"}var k=(a=e.attrs)&&a.is,h=b?k?B.createElementNS(b,h,{is:k}):B.createElementNS(b,h):k?B.createElement(h,{is:k}):B.createElement(h);e.dom=h;if(null!=a)for(r in k=b,a)z(e,r,null,a[r],k);null!=e.attrs&&null!=e.attrs.contenteditable?t(e):(null!=e.text&&(""!==e.text?h.textContent=e.text:e.children=
|
||||
[w("#",void 0,void 0,e.text,void 0,void 0)]),null!=e.children&&(r=e.children,c(h,r,0,r.length,f,null,b),f=e.attrs,"select"===e.tag&&null!=f&&("value"in f&&z(e,"value",null,f.value,void 0),"selectedIndex"in f&&z(e,"selectedIndex",null,f.selectedIndex,void 0))));return h}else return g(e,f,b)}function d(e){var f={caption:"table",thead:"table",tbody:"table",tfoot:"table",tr:"tbody",th:"tr",td:"tr",colgroup:"table",col:"colgroup"}[(e.children.match(/^\s*?<(\w+)/im)||[])[1]]||"div",f=B.createElement(f);
|
||||
f.innerHTML=e.children;e.dom=f.firstChild;e.domSize=f.childNodes.length;e=B.createDocumentFragment();for(var b;b=f.firstChild;)e.appendChild(b);return e}function g(e,f,b){e.state||(e.state={});var a=function(){};a.prototype=e.tag;e.state=new a;a=e.tag.view;if(null!=a.reentrantLock)return L;a.reentrantLock=!0;H(e.tag,e,f);e.instance=w.normalize(a.call(e.state,e));a.reentrantLock=null;if(null!=e.instance){if(e.instance===e)throw Error("A view cannot return the vnode it received as arguments");f=h(e.instance,
|
||||
f,b);e.dom=e.instance.dom;e.domSize=null!=e.dom?e.instance.domSize:0;return f}e.domSize=0;return L}function l(e,f,b,d,g,l){if(f!==b&&(null!=f||null!=b))if(null==f)c(e,b,0,b.length,d,g,void 0);else if(null==b)a(f,0,f.length,b);else{if(f.length===b.length){for(var r=!1,u=0;u<b.length;u++)if(null!=b[u]&&null!=f[u]){r=null==b[u].key&&null==f[u].key;break}if(r){for(u=0;u<f.length;u++)f[u]!==b[u]&&(null==f[u]&&null!=b[u]?k(e,h(b[u],d,l),n(f,u+1,g)):null==b[u]?a(f,u,u+1,b):m(e,f[u],b[u],d,n(f,u+1,g),!1,
|
||||
l));return}}a:{if(null!=f.pool&&Math.abs(f.pool.length-b.length)<=Math.abs(f.length-b.length)&&(r=b[0]&&b[0].children&&b[0].children.length||0,Math.abs((f.pool[0]&&f.pool[0].children&&f.pool[0].children.length||0)-r)<=Math.abs((f[0]&&f[0].children&&f[0].children.length||0)-r))){r=!0;break a}r=!1}r&&(f=f.concat(f.pool));for(var t=u=0,v=f.length-1,z=b.length-1,C;v>=u&&z>=t;){var y=f[u],p=b[t];if(y!==p||r)if(null==y)u++;else if(null==p)t++;else if(y.key===p.key)u++,t++,m(e,y,p,d,n(f,u,g),r,l),r&&y.tag===
|
||||
p.tag&&k(e,q(y),g);else if(y=f[v],y!==p||r)if(null==y)v--;else if(null==p)t++;else if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),(r||t<z)&&k(e,q(y),n(f,u,g)),v--,t++;else break;else v--,t++;else u++,t++}for(;v>=u&&z>=t;){y=f[v];p=b[z];if(y!==p||r)if(null==y)v--;else{if(null!=p)if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!C){C=f;var y=v,E={},w;for(w=0;w<y;w++){var x=C[w];null!=x&&(x=x.key,null!=x&&(E[x]=w))}C=E}null!=p&&(y=C[p.key],null!=
|
||||
y?(E=f[y],m(e,E,p,d,n(f,v+1,g),r,l),k(e,q(E),g),f[y].skip=!0,null!=E.dom&&(g=E.dom)):(p=h(p,d,void 0),k(e,p,g),g=p))}z--}else v--,z--;if(z<t)break}c(e,b,t,z+1,d,g,l);a(f,u,v+1,b)}}function m(b,f,a,c,g,n,p){var e=f.tag;if(e===a.tag){a.state=f.state;a.events=f.events;var r;var u;null!=a.attrs&&"function"===typeof a.attrs.onbeforeupdate&&(r=a.attrs.onbeforeupdate.call(a.state,a,f));"string"!==typeof a.tag&&"function"===typeof a.tag.onbeforeupdate&&(u=a.tag.onbeforeupdate.call(a.state,a,f));void 0===
|
||||
r&&void 0===u||r||u?r=!1:(a.dom=f.dom,a.domSize=f.domSize,a.instance=f.instance,r=!0);if(!r)if(null!=a.attrs&&K(a.attrs,a,c,n),"string"===typeof e)switch(e){case "#":f.children.toString()!==a.children.toString()&&(f.dom.nodeValue=a.children);a.dom=f.dom;break;case "<":f.children!==a.children?(q(f),k(b,d(a),g)):(a.dom=f.dom,a.domSize=f.domSize);break;case "[":l(b,f.children,a.children,c,g,p);f=0;c=a.children;a.dom=null;if(null!=c){for(var v=0;v<c.length;v++)b=c[v],null!=b&&null!=b.dom&&(null==a.dom&&
|
||||
(a.dom=b.dom),f+=b.domSize||1);1!==f&&(a.domSize=f)}break;default:b=p;g=a.dom=f.dom;switch(a.tag){case "svg":b="http://www.w3.org/2000/svg";break;case "math":b="http://www.w3.org/1998/Math/MathML"}"textarea"===a.tag&&(null==a.attrs&&(a.attrs={}),null!=a.text&&(a.attrs.value=a.text,a.text=void 0));n=f.attrs;p=a.attrs;e=b;if(null!=p)for(v in p)z(a,v,n&&n[v],p[v],e);if(null!=n)for(v in n)null!=p&&v in p||("className"===v&&(v="class"),"o"!==v[0]||"n"!==v[1]||x(v)?"key"!==v&&a.dom.removeAttribute(v):A(a,
|
||||
v,void 0));null!=a.attrs&&null!=a.attrs.contenteditable?t(a):null!=f.text&&null!=a.text&&""!==a.text?f.text.toString()!==a.text.toString()&&(f.dom.firstChild.nodeValue=a.text):(null!=f.text&&(f.children=[w("#",void 0,void 0,f.text,void 0,f.dom.firstChild)]),null!=a.text&&(a.children=[w("#",void 0,void 0,a.text,void 0,void 0)]),l(g,f.children,a.children,c,null,b))}else a.instance=w.normalize(a.tag.view.call(a.state,a)),K(a.tag,a,c,n),null!=a.instance?(null==f.instance?k(b,h(a.instance,c,p),g):m(b,
|
||||
f.instance,a.instance,c,g,n,p),a.dom=a.instance.dom,a.domSize=a.instance.domSize):null!=f.instance?(C(f.instance,null),a.dom=void 0,a.domSize=0):(a.dom=f.dom,a.domSize=f.domSize)}else C(f,null),k(b,h(a,c,p),g)}function q(a){var b=a.domSize;if(null!=b||null==a.dom){var e=B.createDocumentFragment();if(0<b){for(a=a.dom;--b;)e.appendChild(a.nextSibling);e.insertBefore(a,e.firstChild)}return e}return a.dom}function n(a,b,c){for(;b<a.length;b++)if(null!=a[b]&&null!=a[b].dom)return a[b].dom;return c}function k(a,
|
||||
b,c){c&&c.parentNode?a.insertBefore(b,c):a.appendChild(b)}function t(a){var b=a.children;if(null!=b&&1===b.length&&"<"===b[0].tag)b=b[0].children,a.dom.innerHTML!==b&&(a.dom.innerHTML=b);else if(null!=a.text||null!=b&&0!==b.length)throw Error("Child node of a contenteditable must be trusted");}function a(a,b,c,d){for(;b<c;b++){var e=a[b];null!=e&&(e.skip?e.skip=!1:C(e,d))}}function C(a,b){function e(){if(++c===f&&(p(a),a.dom)){var e=a.domSize||1;if(1<e)for(var d=a.dom;--e;){var g=d.nextSibling,h=
|
||||
g.parentNode;null!=h&&h.removeChild(g)}e=a.dom;d=e.parentNode;null!=d&&d.removeChild(e);if(e=null!=b&&null==a.domSize)e=a.attrs,e=!(null!=e&&(e.oncreate||e.onupdate||e.onbeforeremove||e.onremove));e&&"string"===typeof a.tag&&(b.pool?b.pool.push(a):b.pool=[a])}}var f=1,c=0;if(a.attrs&&a.attrs.onbeforeremove){var d=a.attrs.onbeforeremove.call(a.state,a);null!=d&&"function"===typeof d.then&&(f++,d.then(e,e))}"string"!==typeof a.tag&&a.tag.onbeforeremove&&(d=a.tag.onbeforeremove.call(a.state,a),null!=
|
||||
d&&"function"===typeof d.then&&(f++,d.then(e,e)));e()}function p(a){a.attrs&&a.attrs.onremove&&a.attrs.onremove.call(a.state,a);"string"!==typeof a.tag&&a.tag.onremove&&a.tag.onremove.call(a.state,a);if(null!=a.instance)p(a.instance);else if(a=a.children,a instanceof Array)for(var b=0;b<a.length;b++){var e=a[b];null!=e&&p(e)}}function z(a,b,c,d,g){var e=a.dom;if("key"!==b&&"is"!==b&&(c!==d||"value"===b||"checked"===b||"selectedIndex"===b||"selected"===b&&a.dom===B.activeElement||"object"===typeof d)&&
|
||||
"undefined"!==typeof d&&!x(b)){var f=b.indexOf(":");if(-1<f&&"xlink"===b.substr(0,f))e.setAttributeNS("http://www.w3.org/1999/xlink",b.slice(f+1),d);else if("o"===b[0]&&"n"===b[1]&&"function"===typeof d)A(a,b,d);else if("style"===b)if(a=c,a===d&&(e.style.cssText="",a=null),null==d)e.style.cssText="";else if("string"===typeof d)e.style.cssText=d;else{"string"===typeof a&&(e.style.cssText="");for(var h in d)e.style[h]=d[h];if(null!=a&&"string"!==typeof a)for(h in a)h in d||(e.style[h]="")}else b in
|
||||
e&&"href"!==b&&"list"!==b&&"form"!==b&&"width"!==b&&"height"!==b&&void 0===g&&!(a.attrs.is||-1<a.tag.indexOf("-"))?"input"===a.tag&&"value"===b&&a.dom.value===d&&a.dom===B.activeElement||"select"===a.tag&&"value"===b&&a.dom.value===d&&a.dom===B.activeElement||"option"===a.tag&&"value"===b&&a.dom.value===d||(e[b]=d):"boolean"===typeof d?d?e.setAttribute(b,""):e.removeAttribute(b):e.setAttribute("className"===b?"class":b,d)}}function x(a){return"oninit"===a||"oncreate"===a||"onupdate"===a||"onremove"===
|
||||
a||"onbeforeremove"===a||"onbeforeupdate"===a}function A(a,b,c){var e=a.dom,d="function"!==typeof D?c:function(a){var b=c.call(e,a);D.call(e,a);return b};if(b in e)e[b]="function"===typeof c?d:null;else{var f=b.slice(2);void 0===a.events&&(a.events={});a.events[b]!==d&&(null!=a.events[b]&&e.removeEventListener(f,a.events[b],!1),"function"===typeof c&&(a.events[b]=d,e.addEventListener(f,a.events[b],!1)))}}function H(a,b,c){"function"===typeof a.oninit&&a.oninit.call(b.state,b);"function"===typeof a.oncreate&&
|
||||
c.push(a.oncreate.bind(b.state,b))}function K(a,b,c,d){d?H(a,b,c):"function"===typeof a.onupdate&&c.push(a.onupdate.bind(b.state,b))}var B=b.document,L=B.createDocumentFragment(),D;return{render:function(a,b){if(!a)throw Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var c=[],d=B.activeElement;null==a.vnodes&&(a.textContent="");b instanceof Array||(b=[b]);l(a,a.vnodes,w.normalizeChildren(b),c,null,void 0);a.vnodes=b;for(var e=0;e<c.length;e++)c[e]();B.activeElement!==
|
||||
d&&d.focus()},setEventCallback:function(a){return D=a}}},F=function(b){function c(b){b=d.indexOf(b);-1<b&&d.splice(b,2)}function h(){for(var b=1;b<d.length;b+=2)d[b]()}b=M(b);b.setEventCallback(function(b){!1!==b.redraw&&h()});var d=[];return{subscribe:function(b,h){c(b);d.push(b,O(h))},unsubscribe:c,redraw:h,render:b.render}}(window);I.setCompletionCallback(F.redraw);A.mount=function(b){return function(c,h){if(null===h)b.render(c,[]),b.unsubscribe(c);else{if(null==h.view)throw Error("m.mount(element, component) expects a component, not a vnode");
|
||||
b.subscribe(c,function(){b.render(c,w(h))});b.redraw()}}}(F);var Q=x,J=function(b){if(""===b||null==b)return{};"?"===b.charAt(0)&&(b=b.slice(1));b=b.split("&");for(var c={},h={},d=0;d<b.length;d++){var g=b[d].split("="),l=decodeURIComponent(g[0]),g=2===g.length?decodeURIComponent(g[1]):"";"true"===g?g=!0:"false"===g&&(g=!1);var m=l.split(/\]\[?|\[/),q=c;-1<l.indexOf("[")&&m.pop();for(var n=0;n<m.length;n++){var l=m[n],k=m[n+1],k=""==k||!isNaN(parseInt(k,10)),t=n===m.length-1;""===l&&(l=m.slice(0,
|
||||
n).join(),null==h[l]&&(h[l]=0),l=h[l]++);null==q[l]&&(q[l]=t?g:k?[]:{});q=q[l]}}return c},R=function(b){function c(c){var d=b.location[c].replace(/(?:%[a-f89][a-f0-9])+/gim,decodeURIComponent);"pathname"===c&&"/"!==d[0]&&(d="/"+d);return d}function h(b){return function(){null==m&&(m=l(function(){m=null;b()}))}}function d(b,c,d){var a=b.indexOf("?"),g=b.indexOf("#"),h=-1<a?a:-1<g?g:b.length;if(-1<a){var a=J(b.slice(a+1,-1<g?g:b.length)),k;for(k in a)c[k]=a[k]}if(-1<g)for(k in c=J(b.slice(g+1)),c)d[k]=
|
||||
c[k];return b.slice(0,h)}var g="function"===typeof b.history.pushState,l="function"===typeof setImmediate?setImmediate:setTimeout,m,q={prefix:"#!",getPath:function(){switch(q.prefix.charAt(0)){case "#":return c("hash").slice(q.prefix.length);case "?":return c("search").slice(q.prefix.length)+c("hash");default:return c("pathname").slice(q.prefix.length)+c("search")+c("hash")}},setPath:function(c,h,m){var a={},k={};c=d(c,a,k);if(null!=h){for(var l in h)a[l]=h[l];c=c.replace(/:([^\/]+)/g,function(b,
|
||||
c){delete a[c];return h[c]})}(l=D(a))&&(c+="?"+l);(k=D(k))&&(c+="#"+k);g?(k=m?m.state:null,l=m?m.title:null,b.onpopstate(),m&&m.replace?b.history.replaceState(k,l,q.prefix+c):b.history.pushState(k,l,q.prefix+c)):b.location.href=q.prefix+c},defineRoutes:function(c,k,m){function a(){var a=q.getPath(),g={},h=d(a,g,g),l=b.history.state;if(null!=l)for(var t in l)g[t]=l[t];for(var n in c)if(l=new RegExp("^"+n.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$"),l.test(h)){h.replace(l,
|
||||
function(){for(var b=n.match(/:[^\/]+/g)||[],d=[].slice.call(arguments,1,-2),h=0;h<b.length;h++)g[b[h].replace(/:|\./g,"")]=decodeURIComponent(d[h]);k(c[n],g,a,n)});return}m(a,g)}g?b.onpopstate=h(a):"#"===q.prefix.charAt(0)&&(b.onhashchange=a);a()}};return q};A.route=function(b,c){var h=R(b),d=function(b){return b},g,l,m,q,n,k=function(b,a,k){if(null==b)throw Error("Ensure the DOM element that was passed to `m.route` is not undefined");var p=function(){null!=g&&c.render(b,g(w(l,m.key,m)))},t=function(){h.setPath(a,
|
||||
null,{replace:!0})};h.defineRoutes(k,function(a,b,c){var h=n=function(a,k){h===n&&(l=null!=k&&"function"===typeof k.view?k:"div",m=b,q=c,n=null,g=(a.render||d).bind(a),p())};a.view?h({},a):a.onmatch?Q.resolve(a.onmatch(b,c)).then(function(b){h(a,b)},t):h(a,"div")},t);c.subscribe(b,p)};k.set=function(b,a,c){null!=n&&(c={replace:!0});n=null;h.setPath(b,a,c)};k.get=function(){return q};k.prefix=function(b){h.prefix=b};k.link=function(b){b.dom.setAttribute("href",h.prefix+b.attrs.href);b.dom.onclick=
|
||||
function(a){a.ctrlKey||a.metaKey||a.shiftKey||2===a.which||(a.preventDefault(),a.redraw=!1,a=this.getAttribute("href"),0===a.indexOf(h.prefix)&&(a=a.slice(h.prefix.length)),k.set(a,void 0,void 0))}};return k}(window,F);A.withAttr=function(b,c,h){return function(d){return c.call(h||this,b in d.currentTarget?d.currentTarget[b]:d.currentTarget.getAttribute(b))}};var S=M(window);A.render=S.render;A.redraw=F.redraw;A.request=I.request;A.jsonp=I.jsonp;A.parseQueryString=J;A.buildQueryString=D;A.version=
|
||||
"1.0.0-rc.7";A.vnode=w;"undefined"!==typeof module?module.exports=A:window.m=A};
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Noiseless testing framework
|
||||
|
||||
Version: 1.2
|
||||
Version: 1.2.2
|
||||
License: MIT
|
||||
|
||||
## About
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ function traverseDirectory(pathname, callback) {
|
|||
if (err) reject(err)
|
||||
var promises = []
|
||||
for (var i = 0; i < pathnames.length; i++) {
|
||||
if (pathnames[i] === "node_modules") continue
|
||||
pathnames[i] = path.join(pathname, pathnames[i])
|
||||
promises.push(traverseDirectory(pathnames[i], callback))
|
||||
}
|
||||
|
|
@ -31,7 +32,6 @@ function traverseDirectory(pathname, callback) {
|
|||
}
|
||||
|
||||
traverseDirectory(".", function(pathname, stat, children) {
|
||||
if (pathname.indexOf("node_modules") > -1) return
|
||||
if (pathname.match(/(?:^|\/)tests\/.*\.js$/)) {
|
||||
require(path.normalize(process.cwd()) + "/" + pathname)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,16 +135,18 @@ module.exports = new function init() {
|
|||
var aIsArgs = isArguments(a), bIsArgs = isArguments(b)
|
||||
if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) {
|
||||
for (var i in a) {
|
||||
if (!deepEqual(a[i], b[i])) return false
|
||||
if ((!(i in b)) || !deepEqual(a[i], b[i])) return false
|
||||
}
|
||||
for (var i in b) {
|
||||
if (!(i in a)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a.length === b.length && (Array.isArray(a) && Array.isArray(b) || aIsArgs && bIsArgs)) {
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) return false
|
||||
if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) {
|
||||
var aKeys = Object.getOwnPropertyNames(a), bKeys = Object.getOwnPropertyNames(b)
|
||||
if (aKeys.length !== bKeys.length) return false
|
||||
for (var i = 0; i < aKeys.length; i++) {
|
||||
if (!b.hasOwnProperty(aKeys[i]) || !deepEqual(a[aKeys[i]], b[aKeys[i]])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -185,7 +187,7 @@ module.exports = new function init() {
|
|||
results.push(result)
|
||||
}
|
||||
function serialize(value) {
|
||||
if (value === null || (typeof value === "object" && !Array.isArray(value)) || typeof value === "number") return String(value)
|
||||
if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value)
|
||||
else if (typeof value === "function") return value.name || "<anonymous function>"
|
||||
try {return JSON.stringify(value)} catch (e) {return String(value)}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ospec",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.2",
|
||||
"description": "Noiseless testing framework",
|
||||
"main": "ospec.js",
|
||||
"directories": {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ new function(o) {
|
|||
|
||||
o.spec("ospec", function() {
|
||||
o("skipped", function() {
|
||||
o(1).equals(1)
|
||||
o(true).equals(false)
|
||||
})
|
||||
o.only(".only()", function() {
|
||||
o(2).equals(2)
|
||||
|
|
@ -37,6 +37,36 @@ o.spec("ospec", function() {
|
|||
o({a: [1, 2], b: 3}).deepEquals({a: [1, 2], b: 3})
|
||||
o([{a: 1, b: 2}, {c: 3}]).deepEquals([{a: 1, b: 2}, {c: 3}])
|
||||
|
||||
var undef1 = {undef: void 0}
|
||||
var undef2 = {UNDEF: void 0}
|
||||
|
||||
o(undef1).notDeepEquals(undef2)
|
||||
o(undef1).notDeepEquals({})
|
||||
o({}).notDeepEquals(undef1)
|
||||
|
||||
var sparse1 = [void 1, void 2, void 3]
|
||||
delete sparse1[0]
|
||||
var sparse2 = [void 1, void 2, void 3]
|
||||
delete sparse2[1]
|
||||
|
||||
o(sparse1).notDeepEquals(sparse2)
|
||||
|
||||
var monkeypatch1 = [1, 2]
|
||||
monkeypatch1.field = 3
|
||||
var monkeypatch2 = [1, 2]
|
||||
monkeypatch2.field = 4
|
||||
|
||||
o(monkeypatch1).notDeepEquals([1, 2])
|
||||
o(monkeypatch1).notDeepEquals(monkeypatch2)
|
||||
|
||||
monkeypatch2.field = 3
|
||||
o(monkeypatch1).deepEquals(monkeypatch2)
|
||||
|
||||
monkeypatch1.undef = undefined
|
||||
monkeypatch2.UNDEF = undefined
|
||||
|
||||
o(monkeypatch1).notDeepEquals(monkeypatch2)
|
||||
|
||||
var values = ["a", "", 1, 0, true, false, null, undefined, Date(0), ["a"], [], function() {return arguments}.call(), new Uint8Array(), {a: 1}, {}]
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
for (var j = 0; j < values.length; j++) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mithril",
|
||||
"version": "1.0.0-rc.6",
|
||||
"version": "1.0.0-rc.7",
|
||||
"description": "A framework for building brilliant applications",
|
||||
"author": "Leo Horie",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -95,9 +95,12 @@ PromisePolyfill.race = function(list) {
|
|||
})
|
||||
}
|
||||
|
||||
if (typeof Promise === "undefined") {
|
||||
if (typeof window !== "undefined") window.Promise = PromisePolyfill
|
||||
else if (typeof global !== "undefined") global.Promise = PromisePolyfill
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill
|
||||
module.exports = window.Promise
|
||||
} else if (typeof global !== "undefined") {
|
||||
if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill
|
||||
module.exports = global.Promise
|
||||
} else {
|
||||
module.exports = PromisePolyfill
|
||||
}
|
||||
|
||||
module.exports = typeof Promise !== "undefined" ? Promise : PromisePolyfill
|
||||
177
render/render.js
177
render/render.js
|
|
@ -97,7 +97,9 @@ module.exports = function($window) {
|
|||
function createComponent(vnode, hooks, ns) {
|
||||
// For object literals since `Vnode()` always sets the `state` field.
|
||||
if (!vnode.state) vnode.state = {}
|
||||
assign(vnode.state, vnode.tag)
|
||||
var constructor = function() {}
|
||||
constructor.prototype = vnode.tag
|
||||
vnode.state = new constructor
|
||||
|
||||
var view = vnode.tag.view
|
||||
if (view.reentrantLock != null) return $emptyFragment
|
||||
|
|
@ -124,84 +126,85 @@ module.exports = function($window) {
|
|||
else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined)
|
||||
else if (vnodes == null) removeNodes(old, 0, old.length, vnodes)
|
||||
else {
|
||||
var isUnkeyed = false
|
||||
for (var i = 0; i < vnodes.length; i++) {
|
||||
if (vnodes[i] != null) {
|
||||
isUnkeyed = vnodes[i].key == null
|
||||
break
|
||||
if (old.length === vnodes.length) {
|
||||
var isUnkeyed = false
|
||||
for (var i = 0; i < vnodes.length; i++) {
|
||||
if (vnodes[i] != null && old[i] != null) {
|
||||
isUnkeyed = vnodes[i].key == null && old[i].key == null
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isUnkeyed) {
|
||||
for (var i = 0; i < old.length; i++) {
|
||||
if (old[i] === vnodes[i]) continue
|
||||
else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling))
|
||||
else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
|
||||
else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (old.length === vnodes.length && isUnkeyed) {
|
||||
for (var i = 0; i < old.length; i++) {
|
||||
if (old[i] === vnodes[i]) continue
|
||||
else if (old[i] == null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling))
|
||||
else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
|
||||
else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns)
|
||||
}
|
||||
}
|
||||
else {
|
||||
var recycling = isRecyclable(old, vnodes)
|
||||
if (recycling) old = old.concat(old.pool)
|
||||
var recycling = isRecyclable(old, vnodes)
|
||||
if (recycling) old = old.concat(old.pool)
|
||||
|
||||
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldStart], v = vnodes[start]
|
||||
if (o === v && !recycling) oldStart++, start++
|
||||
else if (o == null) oldStart++
|
||||
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldStart], v = vnodes[start]
|
||||
if (o === v && !recycling) oldStart++, start++
|
||||
else if (o == null) oldStart++
|
||||
else if (v == null) start++
|
||||
else if (o.key === v.key) {
|
||||
oldStart++, start++
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
}
|
||||
else {
|
||||
var o = old[oldEnd]
|
||||
if (o === v && !recycling) oldEnd--, start++
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) start++
|
||||
else if (o.key === v.key) {
|
||||
oldStart++, start++
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
}
|
||||
else {
|
||||
var o = old[oldEnd]
|
||||
if (o === v && !recycling) oldEnd--, start++
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) start++
|
||||
else if (o.key === v.key) {
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
|
||||
oldEnd--, start++
|
||||
}
|
||||
else break
|
||||
}
|
||||
}
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldEnd], v = vnodes[end]
|
||||
if (o === v && !recycling) oldEnd--, end--
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) end--
|
||||
else if (o.key === v.key) {
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
if (o.dom != null) nextSibling = o.dom
|
||||
oldEnd--, end--
|
||||
if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
|
||||
oldEnd--, start++
|
||||
}
|
||||
else {
|
||||
if (!map) map = getKeyMap(old, oldEnd)
|
||||
if (v != null) {
|
||||
var oldIndex = map[v.key]
|
||||
if (oldIndex != null) {
|
||||
var movable = old[oldIndex]
|
||||
updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
insertNode(parent, toFragment(movable), nextSibling)
|
||||
old[oldIndex].skip = true
|
||||
if (movable.dom != null) nextSibling = movable.dom
|
||||
}
|
||||
else {
|
||||
var dom = createNode(v, hooks, undefined)
|
||||
insertNode(parent, dom, nextSibling)
|
||||
nextSibling = dom
|
||||
}
|
||||
}
|
||||
end--
|
||||
}
|
||||
if (end < start) break
|
||||
else break
|
||||
}
|
||||
createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||
removeNodes(old, oldStart, oldEnd + 1, vnodes)
|
||||
}
|
||||
while (oldEnd >= oldStart && end >= start) {
|
||||
var o = old[oldEnd], v = vnodes[end]
|
||||
if (o === v && !recycling) oldEnd--, end--
|
||||
else if (o == null) oldEnd--
|
||||
else if (v == null) end--
|
||||
else if (o.key === v.key) {
|
||||
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
||||
if (o.dom != null) nextSibling = o.dom
|
||||
oldEnd--, end--
|
||||
}
|
||||
else {
|
||||
if (!map) map = getKeyMap(old, oldEnd)
|
||||
if (v != null) {
|
||||
var oldIndex = map[v.key]
|
||||
if (oldIndex != null) {
|
||||
var movable = old[oldIndex]
|
||||
updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
||||
insertNode(parent, toFragment(movable), nextSibling)
|
||||
old[oldIndex].skip = true
|
||||
if (movable.dom != null) nextSibling = movable.dom
|
||||
}
|
||||
else {
|
||||
var dom = createNode(v, hooks, undefined)
|
||||
insertNode(parent, dom, nextSibling)
|
||||
nextSibling = dom
|
||||
}
|
||||
}
|
||||
end--
|
||||
}
|
||||
if (end < start) break
|
||||
}
|
||||
createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
||||
removeNodes(old, oldStart, oldEnd + 1, vnodes)
|
||||
}
|
||||
}
|
||||
function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) {
|
||||
|
|
@ -367,24 +370,21 @@ module.exports = function($window) {
|
|||
}
|
||||
}
|
||||
}
|
||||
function once(f) {
|
||||
var called = false
|
||||
return function() {
|
||||
if (!called) {
|
||||
called = true
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
function removeNode(vnode, context) {
|
||||
var expected = 1, called = 0
|
||||
if (vnode.attrs && vnode.attrs.onbeforeremove) {
|
||||
expected++
|
||||
vnode.attrs.onbeforeremove.call(vnode.state, vnode, once(continuation))
|
||||
var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode)
|
||||
if (result != null && typeof result.then === "function") {
|
||||
expected++
|
||||
result.then(continuation, continuation)
|
||||
}
|
||||
}
|
||||
if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) {
|
||||
expected++
|
||||
vnode.tag.onbeforeremove.call(vnode.state, vnode, once(continuation))
|
||||
var result = vnode.tag.onbeforeremove.call(vnode.state, vnode)
|
||||
if (result != null && typeof result.then === "function") {
|
||||
expected++
|
||||
result.then(continuation, continuation)
|
||||
}
|
||||
}
|
||||
continuation()
|
||||
function continuation() {
|
||||
|
|
@ -434,14 +434,14 @@ module.exports = function($window) {
|
|||
}
|
||||
function setAttr(vnode, key, old, value, ns) {
|
||||
var element = vnode.dom
|
||||
if (key === "key" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return
|
||||
if (key === "key" || key === "is" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return
|
||||
var nsLastIndex = key.indexOf(":")
|
||||
if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") {
|
||||
element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value)
|
||||
}
|
||||
else if (key[0] === "o" && key[1] === "n" && typeof value === "function") updateEvent(vnode, key, value)
|
||||
else if (key === "style") updateStyle(element, old, value)
|
||||
else if (key in element && !isAttribute(key) && ns === undefined) {
|
||||
else if (key in element && !isAttribute(key) && ns === undefined && !isCustomElement(vnode)) {
|
||||
//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
|
||||
if (vnode.tag === "input" && key === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return
|
||||
//setting select[value] to same value while having select open blinks select dropdown in Chrome
|
||||
|
|
@ -490,6 +490,9 @@ module.exports = function($window) {
|
|||
function isAttribute(attr) {
|
||||
return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type"
|
||||
}
|
||||
function isCustomElement(vnode){
|
||||
return vnode.attrs.is || vnode.tag.indexOf("-") > -1
|
||||
}
|
||||
function hasIntegrationMethods(source) {
|
||||
return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove)
|
||||
}
|
||||
|
|
@ -555,10 +558,6 @@ module.exports = function($window) {
|
|||
return false
|
||||
}
|
||||
|
||||
function assign(target, source) {
|
||||
Object.keys(source).forEach(function(k){target[k] = source[k]})
|
||||
}
|
||||
|
||||
function render(dom, vnodes) {
|
||||
if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
|
||||
var hooks = []
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
<script src="../../render/fragment.js"></script>
|
||||
<script src="../../render/hyperscript.js"></script>
|
||||
<script src="../../render/render.js"></script>
|
||||
<script src="../../promise/promise.js"></script>
|
||||
<script src="test-hyperscript.js"></script>
|
||||
<script src="test-trust.js"></script>
|
||||
<script src="test-fragment.js"></script>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,43 @@ o.spec("attributes", function() {
|
|||
root = $window.document.body
|
||||
render = vdom($window).render
|
||||
})
|
||||
o.spec("customElements", function(){
|
||||
|
||||
o("when vnode is customElement, custom setAttribute called", function(){
|
||||
|
||||
var normal = [
|
||||
{ tag: "input", attrs: { value: 'hello' } },
|
||||
{ tag: "input", attrs: { value: 'hello' } },
|
||||
{ tag: "input", attrs: { value: 'hello' } }
|
||||
]
|
||||
|
||||
var custom = [
|
||||
{ tag: "custom-element", attrs: { custom: 'x' } },
|
||||
{ tag: "input", attrs: { is: 'something-special', custom: 'x' } },
|
||||
{ tag: "custom-element", attrs: { is: 'something-special', custom: 'x' } }
|
||||
]
|
||||
|
||||
var view = normal.concat(custom)
|
||||
|
||||
var f = $window.document.createElement
|
||||
var spy
|
||||
|
||||
$window.document.createElement = function(tag, is){
|
||||
var el = f(tag, is)
|
||||
if(!spy){
|
||||
spy = o.spy(el.setAttribute)
|
||||
}
|
||||
el.setAttribute = spy
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
render(root, view)
|
||||
|
||||
o(spy.callCount).equals( custom.length )
|
||||
})
|
||||
|
||||
})
|
||||
o.spec("input readonly", function() {
|
||||
o("when input readonly is true, attribute is present", function() {
|
||||
var a = {tag: "input", attrs: {readonly: true}}
|
||||
|
|
|
|||
|
|
@ -588,14 +588,12 @@ o.spec("component", function() {
|
|||
o("calls onbeforeremove", function() {
|
||||
var called = 0
|
||||
var component = {
|
||||
onbeforeremove: function(vnode, done) {
|
||||
onbeforeremove: function(vnode) {
|
||||
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"}
|
||||
|
|
@ -614,14 +612,12 @@ o.spec("component", function() {
|
|||
o("calls onbeforeremove when returning fragment", function() {
|
||||
var called = 0
|
||||
var component = {
|
||||
onbeforeremove: function(vnode, done) {
|
||||
onbeforeremove: function(vnode) {
|
||||
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"}]
|
||||
|
|
@ -672,6 +668,10 @@ o.spec("component", function() {
|
|||
function init(vnode) {
|
||||
o(vnode.state.data).deepEquals(data)
|
||||
o(vnode.state.data).equals(data)
|
||||
|
||||
//inherits state via prototype
|
||||
component.x = 1
|
||||
o(vnode.state.x).equals(1)
|
||||
}
|
||||
})
|
||||
o("state copy is shallow", function() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ var o = require("../../ospec/ospec")
|
|||
var callAsync = require("../../test-utils/callAsync")
|
||||
var domMock = require("../../test-utils/domMock")
|
||||
var vdom = require("../../render/render")
|
||||
var Promise = require("../../promise/promise")
|
||||
|
||||
o.spec("onbeforeremove", function() {
|
||||
var $window, root, render
|
||||
|
|
@ -43,17 +44,13 @@ o.spec("onbeforeremove", function() {
|
|||
render(root, [vnode])
|
||||
render(root, [])
|
||||
|
||||
function remove(node, complete) {
|
||||
function remove(node) {
|
||||
o(node).equals(vnode)
|
||||
o(this).equals(vnode.state)
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild).equals(vnode.dom)
|
||||
|
||||
callAsync(function() {
|
||||
o(root.childNodes.length).equals(1)
|
||||
|
||||
complete()
|
||||
|
||||
o(root.childNodes.length).equals(0)
|
||||
|
||||
done()
|
||||
|
|
@ -66,16 +63,12 @@ o.spec("onbeforeremove", function() {
|
|||
render(root, [vnode])
|
||||
render(root, [])
|
||||
|
||||
function remove(node, complete) {
|
||||
function remove(node) {
|
||||
o(node).equals(vnode)
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild).equals(vnode.dom)
|
||||
|
||||
callAsync(function() {
|
||||
o(root.childNodes.length).equals(1)
|
||||
|
||||
complete()
|
||||
|
||||
o(root.childNodes.length).equals(0)
|
||||
|
||||
done()
|
||||
|
|
@ -88,16 +81,12 @@ o.spec("onbeforeremove", function() {
|
|||
render(root, [vnode])
|
||||
render(root, [])
|
||||
|
||||
function remove(node, complete) {
|
||||
function remove(node) {
|
||||
o(node).equals(vnode)
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild).equals(vnode.dom)
|
||||
|
||||
callAsync(function() {
|
||||
o(root.childNodes.length).equals(1)
|
||||
|
||||
complete()
|
||||
|
||||
o(root.childNodes.length).equals(0)
|
||||
|
||||
done()
|
||||
|
|
@ -110,16 +99,12 @@ o.spec("onbeforeremove", function() {
|
|||
render(root, [vnode])
|
||||
render(root, [])
|
||||
|
||||
function remove(node, complete) {
|
||||
function remove(node) {
|
||||
o(node).equals(vnode)
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild).equals(vnode.dom)
|
||||
|
||||
callAsync(function() {
|
||||
o(root.childNodes.length).equals(1)
|
||||
|
||||
complete()
|
||||
|
||||
o(root.childNodes.length).equals(0)
|
||||
|
||||
done()
|
||||
|
|
@ -133,17 +118,12 @@ o.spec("onbeforeremove", function() {
|
|||
render(root, [vnode])
|
||||
render(root, [])
|
||||
|
||||
function remove(node, complete) {
|
||||
function remove(node) {
|
||||
o(node).equals(vnode)
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild).equals(vnode.dom)
|
||||
|
||||
callAsync(function() {
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(spy.callCount).equals(0)
|
||||
|
||||
complete()
|
||||
|
||||
o(root.childNodes.length).equals(0)
|
||||
o(spy.callCount).equals(1)
|
||||
|
||||
|
|
@ -161,7 +141,7 @@ o.spec("onbeforeremove", function() {
|
|||
o(vnode.dom.attributes["onbeforeremove"]).equals(undefined)
|
||||
})
|
||||
o("does not recycle when there's an onbeforeremove", function() {
|
||||
var remove = function(vnode, done) {done()}
|
||||
var remove = function(vnode) {}
|
||||
var vnode = {tag: "div", key: 1, attrs: {onbeforeremove: remove}}
|
||||
var updated = {tag: "div", key: 1, attrs: {onbeforeremove: remove}}
|
||||
|
||||
|
|
@ -171,26 +151,27 @@ o.spec("onbeforeremove", function() {
|
|||
|
||||
o(vnode.dom).notEquals(updated.dom)
|
||||
})
|
||||
o("does not leave elements out of order during removal", function() {
|
||||
var finish
|
||||
var remove = function(vnode, done) {finish = done}
|
||||
o("does not leave elements out of order during removal", function(done) {
|
||||
var remove = function(vnode) {return Promise.resolve()}
|
||||
var vnodes = [{tag: "div", key: 1, attrs: {onbeforeremove: remove}, text: "1"}, {tag: "div", key: 2, attrs: {onbeforeremove: remove}, text: "2"}]
|
||||
var updated = {tag: "div", key: 2, attrs: {onbeforeremove: remove}, text: "2"}
|
||||
|
||||
render(root, vnodes)
|
||||
render(root, updated)
|
||||
|
||||
|
||||
o(root.childNodes.length).equals(2)
|
||||
o(root.firstChild.firstChild.nodeValue).equals("1")
|
||||
|
||||
finish()
|
||||
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild.firstChild.nodeValue).equals("2")
|
||||
|
||||
callAsync(function() {
|
||||
o(root.childNodes.length).equals(1)
|
||||
o(root.firstChild.firstChild.nodeValue).equals("2")
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
o("finalizes the remove phase only once when `done()` is called synchronously from both attrs- and tag.onbeforeremove", function() {
|
||||
o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) {
|
||||
var onremove = o.spy()
|
||||
var onbeforeremove = function(vnode, done){done()}
|
||||
var onbeforeremove = function(){return Promise.resolve()}
|
||||
var component = {
|
||||
onbeforeremove: onbeforeremove,
|
||||
onremove: onremove,
|
||||
|
|
@ -198,30 +179,30 @@ o.spec("onbeforeremove", function() {
|
|||
}
|
||||
render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}])
|
||||
render(root, [])
|
||||
o(onremove.callCount).equals(2) // once for `tag`, once for `attrs`
|
||||
callAsync(function() {
|
||||
o(onremove.callCount).equals(2) // once for `tag`, once for `attrs`
|
||||
done()
|
||||
})
|
||||
})
|
||||
o("doesn't finalize prematurely if `done` is called twice in the `tag` hook", function(done) {
|
||||
var async = false
|
||||
o("awaits promise resolution before removing the node", function(done) {
|
||||
var view = o.spy()
|
||||
var onremove = o.spy()
|
||||
var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})}
|
||||
var component = {
|
||||
view: function() {},
|
||||
onbeforeremove: function(vnode, doneRemoving){
|
||||
doneRemoving()
|
||||
doneRemoving()
|
||||
},
|
||||
onremove: function() {
|
||||
o(async).equals(true)
|
||||
done()
|
||||
},
|
||||
onbeforeremove: onbeforeremove,
|
||||
onremove: onremove,
|
||||
view: view,
|
||||
}
|
||||
render(root, [{
|
||||
tag:component,
|
||||
attrs: {
|
||||
onbeforeremove: function(vnode, doneRemoving){
|
||||
callAsync(doneRemoving)
|
||||
}
|
||||
}
|
||||
}])
|
||||
render(root, [{tag: component}])
|
||||
render(root, [])
|
||||
async = true
|
||||
|
||||
callAsync(function(){
|
||||
o(onremove.callCount).equals(0)
|
||||
|
||||
callAsync(function() {
|
||||
o(onremove.callCount).equals(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -104,6 +104,15 @@ o.spec("updateNodes", function() {
|
|||
o(updated[0].dom.nodeValue).equals("a")
|
||||
o(updated[0].dom).equals(root.childNodes[0])
|
||||
})
|
||||
o("handles undefined to null noop", function() {
|
||||
var vnodes = [null, {tag: "div"}]
|
||||
var updated = [undefined, {tag: "div"}]
|
||||
|
||||
render(root, vnodes)
|
||||
render(root, updated)
|
||||
|
||||
o(root.childNodes.length).equals(1)
|
||||
})
|
||||
o("reverses els w/ even count", function() {
|
||||
var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}]
|
||||
var updated = [{tag: "s", key: 4}, {tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}]
|
||||
|
|
@ -871,7 +880,7 @@ o.spec("updateNodes", function() {
|
|||
o(onupdate.callCount).equals(0)
|
||||
})
|
||||
o("cached, keyed nodes skip diff", function () {
|
||||
var onupdate = o.spy();
|
||||
var onupdate = o.spy()
|
||||
var cached = {tag:"a", key:"a", attrs:{onupdate: onupdate}}
|
||||
|
||||
render(root, cached)
|
||||
|
|
@ -917,4 +926,14 @@ o.spec("updateNodes", function() {
|
|||
o(update.callCount).equals(2)
|
||||
o(remove.callCount).equals(0)
|
||||
})
|
||||
o("component is recreated if key changes to undefined", function () {
|
||||
var vnode = {tag: "b", key: 1}
|
||||
var updated = {tag: "b"}
|
||||
|
||||
render(root, vnode)
|
||||
var dom = vnode.dom
|
||||
render(root, updated)
|
||||
|
||||
o(vnode.dom).notEquals(updated.dom)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ module.exports = function($window, Promise) {
|
|||
}
|
||||
if (args.withCredentials) xhr.withCredentials = args.withCredentials
|
||||
|
||||
for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
|
||||
xhr.setRequestHeader(key, args.headers[key])
|
||||
}
|
||||
|
||||
if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
|
|
@ -93,7 +97,7 @@ module.exports = function($window, Promise) {
|
|||
function jsonp(args, extra) {
|
||||
var finalize = finalizer()
|
||||
args = normalize(args, extra)
|
||||
|
||||
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
|
||||
var script = $window.document.createElement("script")
|
||||
|
|
|
|||
|
|
@ -343,6 +343,43 @@ o.spec("xhr", function() {
|
|||
done()
|
||||
}, 20)
|
||||
})
|
||||
o("headers are set when header arg passed", function(done) {
|
||||
mock.$defineRoutes({
|
||||
"POST /item": function(request) {
|
||||
return {status: 200, responseText: ""}
|
||||
}
|
||||
})
|
||||
xhr({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done)
|
||||
|
||||
function config(xhr) {
|
||||
o(xhr.getRequestHeader("Custom-Header")).equals("Value")
|
||||
}
|
||||
})
|
||||
o("headers are with higher precedence than default headers", function(done) {
|
||||
mock.$defineRoutes({
|
||||
"POST /item": function(request) {
|
||||
return {status: 200, responseText: ""}
|
||||
}
|
||||
})
|
||||
xhr({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done)
|
||||
|
||||
function config(xhr) {
|
||||
o(xhr.getRequestHeader("Content-Type")).equals("Value")
|
||||
}
|
||||
})
|
||||
o("json headers are set to the correct default value", function(done) {
|
||||
mock.$defineRoutes({
|
||||
"POST /item": function(request) {
|
||||
return {status: 200, responseText: ""}
|
||||
}
|
||||
})
|
||||
xhr({method: "POST", url: "/item", config: config}).then(done)
|
||||
|
||||
function config(xhr) {
|
||||
o(xhr.getRequestHeader("Content-Type")).equals("application/json; charset=utf-8")
|
||||
o(xhr.getRequestHeader("Accept")).equals("application/json, text/*")
|
||||
}
|
||||
})
|
||||
})
|
||||
o.spec("failure", function() {
|
||||
o("rejects on server error", function(done) {
|
||||
|
|
|
|||
|
|
@ -67,9 +67,11 @@ module.exports = function($window) {
|
|||
if (hash) path += "#" + hash
|
||||
|
||||
if (supportsPushState) {
|
||||
if (options && options.replace) $window.history.replaceState(null, null, router.prefix + path)
|
||||
else $window.history.pushState(null, null, router.prefix + path)
|
||||
var state = options ? options.state : null
|
||||
var title = options ? options.title : null
|
||||
$window.onpopstate()
|
||||
if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path)
|
||||
else $window.history.pushState(state, title, router.prefix + path)
|
||||
}
|
||||
else $window.location.href = router.prefix + path
|
||||
}
|
||||
|
|
@ -79,6 +81,10 @@ module.exports = function($window) {
|
|||
var params = {}
|
||||
var pathname = parsePath(path, params, params)
|
||||
|
||||
var state = $window.history.state
|
||||
if (state != null) {
|
||||
for (var k in state) params[k] = state[k]
|
||||
}
|
||||
for (var route in routes) {
|
||||
var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
|
||||
|
||||
|
|
|
|||
|
|
@ -150,6 +150,18 @@ o.spec("Router.setPath", function() {
|
|||
|
||||
o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test")
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
o("state works", function(done) {
|
||||
$window.location.href = prefix + "/test"
|
||||
router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail)
|
||||
|
||||
callAsync(function() {
|
||||
router.setPath("/other", null, {state: {a: 1}})
|
||||
|
||||
o($window.history.state).deepEquals({a: 1})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -112,4 +112,5 @@ createStream.merge = merge
|
|||
createStream.combine = combine
|
||||
createStream.HALT = HALT
|
||||
|
||||
module.exports = createStream
|
||||
if (typeof module !== "undefined") module["exports"] = createStream
|
||||
else window.stream = createStream
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module.exports = function(options) {
|
|||
var search = ""
|
||||
var hash = ""
|
||||
|
||||
var past = [], future = []
|
||||
var past = [{url: getURL(), isNew: true, state: null, title: null}], future = []
|
||||
|
||||
function getURL() {
|
||||
if (protocol === "file:") return protocol + "//" + pathname + search + hash
|
||||
|
|
@ -42,7 +42,7 @@ module.exports = function(options) {
|
|||
if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"})
|
||||
}
|
||||
function popstate() {
|
||||
if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate"})
|
||||
if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate", state: $window.history.state})
|
||||
}
|
||||
function unload() {
|
||||
if (typeof $window.onunload === "function") $window.onunload({type: "unload"})
|
||||
|
|
@ -135,20 +135,22 @@ module.exports = function(options) {
|
|||
},
|
||||
}
|
||||
$window.history = {
|
||||
pushState: function(data, title, url) {
|
||||
past.push({url: getURL(), isNew: false})
|
||||
pushState: function(state, title, url) {
|
||||
past.push({url: getURL(), isNew: false, state: state, title: title})
|
||||
future = []
|
||||
setURL(url)
|
||||
},
|
||||
replaceState: function(data, title, url) {
|
||||
future = []
|
||||
replaceState: function(state, title, url) {
|
||||
var entry = past[past.length - 1]
|
||||
entry.state = state
|
||||
entry.title = title
|
||||
setURL(url)
|
||||
},
|
||||
back: function() {
|
||||
var entry = past.pop()
|
||||
if (entry != null) {
|
||||
if (past.length > 1) {
|
||||
var entry = past.pop()
|
||||
if (entry.isNew) unload()
|
||||
future.push({url: getURL(), isNew: false})
|
||||
future.push({url: getURL(), isNew: false, state: entry.state, title: entry.title})
|
||||
setURL(entry.url)
|
||||
if (!entry.isNew) popstate()
|
||||
}
|
||||
|
|
@ -157,11 +159,14 @@ module.exports = function(options) {
|
|||
var entry = future.pop()
|
||||
if (entry != null) {
|
||||
if (entry.isNew) unload()
|
||||
past.push({url: getURL(), isNew: false})
|
||||
past.push({url: getURL(), isNew: false, state: entry.state, title: entry.title})
|
||||
setURL(entry.url)
|
||||
if (!entry.isNew) popstate()
|
||||
}
|
||||
},
|
||||
get state() {
|
||||
return past.length === 0 ? null : past[past.length - 1].state
|
||||
},
|
||||
}
|
||||
$window.onpopstate = null,
|
||||
$window.onhashchange = null,
|
||||
|
|
|
|||
|
|
@ -417,6 +417,71 @@ o.spec("pushStateMock", function() {
|
|||
$window.history.pushState(null, null, "b")
|
||||
$window.history.back()
|
||||
})
|
||||
o("replaceState does not break forward history", function() {
|
||||
$window.onpopstate = o.spy()
|
||||
|
||||
$window.history.pushState(null, null, "b")
|
||||
$window.history.back()
|
||||
|
||||
o($window.onpopstate.callCount).equals(1)
|
||||
o($window.location.href).equals("http://localhost/")
|
||||
|
||||
$window.history.replaceState(null, null, "a")
|
||||
|
||||
o($window.location.href).equals("http://localhost/a")
|
||||
|
||||
$window.history.forward()
|
||||
|
||||
o($window.onpopstate.callCount).equals(2)
|
||||
o($window.location.href).equals("http://localhost/b")
|
||||
})
|
||||
o("pushstate retains state", function() {
|
||||
$window.onpopstate = o.spy()
|
||||
|
||||
$window.history.pushState({a: 1}, null, "#a")
|
||||
$window.history.pushState({b: 2}, null, "#b")
|
||||
|
||||
o($window.onpopstate.callCount).equals(0)
|
||||
|
||||
$window.history.back()
|
||||
|
||||
o($window.onpopstate.callCount).equals(1)
|
||||
o($window.onpopstate.args[0].type).equals("popstate")
|
||||
o($window.onpopstate.args[0].state).deepEquals({a: 1})
|
||||
|
||||
$window.history.back()
|
||||
|
||||
o($window.onpopstate.callCount).equals(2)
|
||||
o($window.onpopstate.args[0].type).equals("popstate")
|
||||
o($window.onpopstate.args[0].state).equals(null)
|
||||
|
||||
$window.history.forward()
|
||||
|
||||
o($window.onpopstate.callCount).equals(3)
|
||||
o($window.onpopstate.args[0].type).equals("popstate")
|
||||
o($window.onpopstate.args[0].state).deepEquals({a: 1})
|
||||
|
||||
$window.history.forward()
|
||||
|
||||
o($window.onpopstate.callCount).equals(4)
|
||||
o($window.onpopstate.args[0].type).equals("popstate")
|
||||
o($window.onpopstate.args[0].state).deepEquals({b: 2})
|
||||
})
|
||||
o("replacestate replaces state", function() {
|
||||
$window.onpopstate = o.spy(pop)
|
||||
|
||||
$window.history.replaceState({a: 1}, null, "a")
|
||||
|
||||
o($window.history.state).deepEquals({a: 1})
|
||||
|
||||
$window.history.pushState(null, null, "a")
|
||||
$window.history.back()
|
||||
|
||||
function pop(e) {
|
||||
o(e.state).deepEquals({a: 1})
|
||||
o($window.history.state).deepEquals({a: 1})
|
||||
}
|
||||
})
|
||||
})
|
||||
o.spec("onhashchance", function() {
|
||||
o("onhashchange triggers on location.href change", function() {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ module.exports = function() {
|
|||
var $window = {
|
||||
XMLHttpRequest: function XMLHttpRequest() {
|
||||
var args = {}
|
||||
this.setRequestHeader = function(header, value) {}
|
||||
var headers = {}
|
||||
this.setRequestHeader = function(header, value) {
|
||||
headers[header] = value
|
||||
}
|
||||
this.getRequestHeader = function(header) {
|
||||
return headers[header]
|
||||
}
|
||||
this.open = function(method, url, async, user, password) {
|
||||
var urlData = parseURL(url, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"})
|
||||
args.method = method
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue