Merge branch 'next'

This commit is contained in:
Isiah Meadows 2019-05-29 10:18:48 -04:00
commit 3b301d928e
72 changed files with 2534 additions and 3430 deletions

Binary file not shown.

View file

@ -3,8 +3,5 @@ coverage/
docs/lib/
examples/
/mithril.js
/mithril.mjs
/mithril.min.js
/mithril.min.mjs
/stream/stream.mjs
node_modules/

2
.gitattributes vendored
View file

@ -1,7 +1,5 @@
* text=auto
/mithril.js binary
/mithril.min.js binary
/mithril.mjs binary
/mithril.min.mjs binary
/package-lock.json binary
/yarn.lock binary

View file

@ -21,8 +21,6 @@ before_script:
- npm run build-browser
# Pass -save so it'll update the readme as well
- npm run build-min -- -save
# must run after build-min in order to generate min.mjs
- npm run build-esm
# Run tests, lint, and then check for perf regressions
script:
@ -34,8 +32,8 @@ after_success:
- |
# Set up SSH environment
$(npm bin)/set-up-ssh \
--key "$encrypted_8b86e0359d64_key" \
--iv "$encrypted_8b86e0359d64_iv" \
--key "$encrypted_016049456622_key" \
--iv "$encrypted_016049456622_iv" \
--path-encrypted-key "./.deploy.enc"
# Commit bundle changes generated in before_script step
@ -75,20 +73,18 @@ after_success:
env:
global:
# Set up GH_USER_EMAIL & GH_USER_NAME env variables used by travis-scripts package
- secure: Xvqvm3+PvJu/rs3jl/NNn0RWLkkLkIoPHiL0GCfVRaywgjCYVN02g54NVvIDaOfybqPmu9E6PJFVs92vhF34NMFQHf4EWskynusIGV271R2BV0i+OJBfLMuLgiwm6zRn7/Zw4JvWIUGEwcnlz0qxbqdHsS0SOR3fIkFzePickW0=
- secure: Rf/ldEO9d4vItJhe6EmqWpFAyCARzoCb422nHnjr1hYJknnwIXpgyZ1C/7On/9o7rWPPf+8WcHC/rgjK2rthKCldzdG5I60LfWSNzap9lk3Aa4TpSCoDBuEp7JVvDr5tc3rKnBXVT71hOay7RSx1StWzXiJs9mjaeVMJzYzRT78=
- secure: UdSk2uKTL56iOHkIZP9Tpj/OI8w26DRTs6INRq+peZKkHq8NVC0TmbtbpRoc5kxErovN2hyqsAnoMtXSQHKK+H9mpMx0v4ck/I+o2oEny/1hwi5YQ/Q0nAebVhZkgA3eVhJY1brK0bOlr8uI07m9mcPs3Qz0ramZutJQG7FdnZs=
# Deploy to npm and github pages on tagged commits that successfully build
deploy:
- provider: releases
api_key:
secure: PauFuz+pn7oRpHn2JTl4k3+iWjOofyBYBvavPQVNdXgKws9mGj0i2n5k2oIDU09VD7NeyEkwP6tdLCUFNaR8uwTJH/TBXMZE95oxUEaliFreA0nOiI3WkG4NCW0GwUoIIn1yL14y6+9oEBinWUia8DIn9kZNS11DNDgQpIPnoQQ=
secure: BBlVwr3CRWS6Zmc8nsYMMN59P95g/lg9OzNCsP7uiFyu7mnC6VdOIgwmmJXBviUiEymbcdepgRqEdjHIoemg2YhM5IOnhMoYFrfMBoWUpMihpejFY8EVm3NrTh0prXgCHbp/3OktoKdOn/Zhc2z7cKDMeToHzaDStLtQPR8u9jU=
file:
- "mithril.js"
- "mithril.min.js"
- "mithril.mjs"
- "mithril.min.mjs"
skip_cleanup: true
draft: true
on:
tags: true
repo: MithrilJS/mithril.js

View file

@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww
## What is Mithril?
A modern client-side Javascript framework for building Single Page Applications. It's small (<!-- size -->8.88 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
A modern client-side Javascript framework for building Single Page Applications. It's small (<!-- size -->9.31 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍.
@ -29,9 +29,9 @@ Mithril supports IE11, Firefox ESR, and the last two versions of Firefox, Edge,
### CDN
```html
<script src="https://unpkg.com/mithril"></script>
<script src="https://unpkg.com/mithril@next/mithril.js"></script>
<!-- or -->
<script src="https://cdn.jsdelivr.net/npm/mithril/mithril.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mithril@next/mithril.js"></script>
```
### npm

View file

@ -23,7 +23,7 @@ module.exports = function($window, redrawService) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute)
}
routeService.defineRoutes(routes, function(payload, params, path) {
routeService.defineRoutes(routes, function(payload, params, path, route) {
var update = lastUpdate = function(routeResolver, comp) {
if (update !== lastUpdate) return
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
@ -34,13 +34,13 @@ module.exports = function($window, redrawService) {
if (payload.view || typeof payload === "function") update({}, payload)
else {
if (payload.onmatch) {
Promise.resolve(payload.onmatch(params, path)).then(function(resolved) {
Promise.resolve(payload.onmatch(params, path, route)).then(function(resolved) {
update(payload, resolved)
}, bail)
}, function () { bail(path) })
}
else update(payload, "div")
}
}, bail)
}, bail, defaultRoute)
}
route.set = function(path, data, options) {
if (lastUpdate != null) {

View file

@ -321,11 +321,12 @@ o.spec("route", function() {
}
var resolver = {
onmatch: function(args, requestedPath) {
onmatch: function(args, requestedPath, route) {
matchCount++
o(args.id).equals("abc")
o(requestedPath).equals("/abc")
o(route).equals("/:id")
o(this).equals(resolver)
return Component
},
@ -362,11 +363,12 @@ o.spec("route", function() {
}
var resolver = {
onmatch: function(args, requestedPath) {
onmatch: function(args, requestedPath, route) {
matchCount++
o(args.id).equals("abc")
o(requestedPath).equals("/abc")
o(route).equals("/:id")
o(this).equals(resolver)
return Promise.resolve(Component)
},
@ -398,11 +400,12 @@ o.spec("route", function() {
var renderCount = 0
var resolver = {
onmatch: function(args, requestedPath) {
onmatch: function(args, requestedPath, route) {
matchCount++
o(args.id).equals("abc")
o(requestedPath).equals("/abc")
o(route).equals("/:id")
o(this).equals(resolver)
return Promise.resolve()
},
@ -434,11 +437,12 @@ o.spec("route", function() {
var renderCount = 0
var resolver = {
onmatch: function(args, requestedPath) {
onmatch: function(args, requestedPath, route) {
matchCount++
o(args.id).equals("abc")
o(requestedPath).equals("/abc")
o(route).equals("/:id")
o(this).equals(resolver)
return Promise.resolve([])
},
@ -508,11 +512,12 @@ o.spec("route", function() {
$window.location.href = prefix + "/abc"
route(root, "/abc", {
"/:id" : {
onmatch: function(args, requestedPath) {
onmatch: function(args, requestedPath, route) {
matchCount++
o(args.id).equals("abc")
o(requestedPath).equals("/abc")
o(route).equals("/:id")
return Component
},
@ -985,7 +990,8 @@ o.spec("route", function() {
$window.location.href = prefix + "/a"
route(root, "/", {
"/a": {view: view},
"/b": {onmatch: onmatch}
"/b": {onmatch: onmatch},
"/": {view: function() {}}
})
o(view.callCount).equals(1)

View file

@ -9,7 +9,7 @@
### Technology choices
Animations are often used to make applications come alive. Nowadays, browsers have good support for CSS animations, and there are [various](https://greensock.com/gsap) [libraries](http://velocityjs.org/) that provide fast Javascript-based animations. There's also an upcoming [Web API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API) and a [polyfill](https://github.com/web-animations/web-animations-js) if you like living on the bleeding edge.
Animations are often used to make applications come alive. Nowadays, browsers have good support for CSS animations, and there are [various](https://greensock.com/gsap) [libraries](http://velocityjs.org/) that provide fast JavaScript-based animations. There's also an upcoming [Web API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API) and a [polyfill](https://github.com/web-animations/web-animations-js) if you like living on the bleeding edge.
Mithril does not provide any animation APIs per se, since these other options are more than sufficient to achieve rich, complex animations. Mithril does, however, offer hooks to make life easier in some specific cases where it's traditionally difficult to make animations work.

41
docs/buildPathname.md Normal file
View file

@ -0,0 +1,41 @@
# buildPathname(object)
- [Description](#description)
- [Signature](#signature)
- [How it works](#how-it-works)
---
### Description
Turns a [path template](paths.md) and a parameters object into a string of form `/path/user?a=1&b=2`
```javascript
var querystring = m.buildPathname("/path/:id", {id: "user", a: "1", b: "2"})
// "/path/user?a=1&b=2"
```
---
### Signature
`querystring = m.buildPathname(object)`
Argument | Type | Required | Description
------------ | ------------------------------------------ | -------- | ---
`object` | `Object` | Yes | A key-value map to be converted into a string
**returns** | `String` | | A string representing the input object
[How to read signatures](signatures.md)
---
### How it works
The `m.buildPathname` creates a [path name](paths.md) from a path template and a parameters object. It's useful for building URLs, and it's what [`m.route`](route.md), [`m.request`](request.md), and [`m.jsonp`](jsonp.md) all use internally to interpolate paths. It uses [`m.buildQueryString`](buildQueryString.md) to generate the query parameters to append to the path name.
```javascript
var querystring = m.buildPathname("/path/:id", {id: "user", a: 1, b: 2})
// querystring is "/path/user?a=1&b=2"
```

View file

@ -10,11 +10,15 @@
- [v1.1.0](#v110)
- [v1.0.1](#v101)
- [Migrating from v0.2.x](#migrating-from-v02x)
- [Older docs](http://mithril.js.org/archive/v0.2.5/index.html)
- [ospec change-log](../ospec/change-log.md)
- [v1.x docs](http://mithril.js.org/archive/v1.1.6/index.html)
- [v0.2 docs](http://mithril.js.org/archive/v0.2.5/index.html)
- [`ospec` change log](https://github.com/MithrilJS/mithril.js/blob/master/ospec/change-log.md)
- [`mithril/stream` change log](https://github.com/MithrilJS/mithril.js/blob/master/stream/change-log.md)
---
### Upcoming...
### v2.0.0-rc
#### Breaking changes
@ -27,13 +31,15 @@
- hyperscript: when attributes have a `null` or `undefined` value, they are treated as if they were absent. [#1773](https://github.com/MithrilJS/mithril.js/issues/1773) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
- API: `m.request` errors no longer copy response fields to the error, but instead assign the parsed JSON response to `error.response` and the HTTP status code `error.code`.
- hyperscript: when an attribute is defined on both the first and second argument (as a CSS selector and an `attrs` field, respectively), the latter takes precedence, except for `class` attributes that are still added together. [#2172](https://github.com/MithrilJS/mithril.js/issues/2172) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
- stream: when a stream conditionally returns HALT, dependant stream will also end ([#2200](https://github.com/MithrilJS/mithril.js/pull/2200))
- render: remove some redundancy within the component initialization code ([#2213](https://github.com/MithrilJS/mithril.js/pull/2213))
- render: Align custom elements to work like normal elements, minus all the HTML-specific magic. ([#2221](https://github.com/MithrilJS/mithril.js/pull/2221))
- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
- cast className using toString ([#2309](https://github.com/MithrilJS/mithril.js/pull/2309))
- render: call attrs' hooks first, with express exception of `onbeforeupdate` to allow attrs to block components from even diffing ([#2297](https://github.com/MithrilJS/mithril.js/pull/2297))
- API: `m.withAttr` removed. ([#2317](https://github.com/MithrilJS/mithril.js/pull/2317))
- request: `data` has now been split to `params` and `body` and `useBody` has been removed in favor of just using `body`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
- route, request: Interpolated arguments are URL-escaped (and for declared routes, URL-unescaped) automatically. If you want to use a raw route parameter, use a variadic parameter like in `/asset/:path.../view`. This was previously only available in `m.route` route definitions, but it's now usable in both that and where paths are accepted. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
- route, request: Interpolated arguments are *not* appended to the query string. This means `m.request({url: "/api/user/:id/get", params: {id: user.id}})` would result in a request like `GET /api/user/1/get`, not one like `GET /api/user/1/get?id=1`. If you really need it in both places, pass the same value via two separate parameters with the non-query-string parameter renamed, like in `m.request({url: "/api/user/:urlID/get", params: {id: user.id, urlID: user.id}})`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
- route, request: `m.route.set`, `m.request`, and `m.jsonp` all use the same path template syntax now, and vary only in how they receive their parameters. Furthermore, declared routes in `m.route` shares the same syntax and semantics, but acts in reverse as if via pattern matching. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
- request: `options.responseType` now defaults to `"json"` if `extract` is absent, and `deserialize` receives the parsed response, not the raw string. If you want the old behavior, [use `responseType: "text"`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType). ([#2335](https://github.com/MithrilJS/mithril.js/pull/2335))
#### News
@ -49,12 +55,15 @@
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
- docs: Emphasize Closure Components for stateful components, use them for all stateful component examples.
- stream: Add `stream.lift` as a user-friendly alternative to `merge -> map` or `combine` [#1944](https://github.com/MithrilJS/mithril.js/issues/1944)
- API: ES module bundles are now available for `mithril` and `mithril/stream` ([#2194](https://github.com/MithrilJS/mithril.js/pull/2194) [@porsager](https://github.com/porsager)).
- All of the `m.*` properties from `mithril` are re-exported as named exports in addition to being attached to `m`.
- `m()` itself from `mithril` is exported as the default export.
- `mithril/stream`'s primary export is exported as the default export.
- fragments: allow same attrs/children overloading logic as hyperscript ([#2328](https://github.com/MithrilJS/mithril.js/pull/2328))
- route: Declared routes may check against path names with query strings. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
- route: Declared routes in `m.route` now support `-` and `.` as delimiters for path segments. This means you can have a route like `"/edit/:file.:ext"`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
- Previously, this was possible to do in `m.route.set`, `m.request`, and `m.jsonp`, but it was wholly untested for and also undocumented.
- API: `m.buildPathname` and `m.parsePathname` added. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
#### Bug fixes
@ -75,6 +84,12 @@
- render/core: Vnodes stored in the dom node supplied to `m.render()` are now normalized [#2266](https://github.com/MithrilJS/mithril.js/pull/2266)
- render/core: CSS vars can now be specified in `{style}` attributes ([#2192](https://github.com/MithrilJS/mithril.js/pull/2192) [@barneycarroll](https://github.com/barneycarroll)), ([#2311](https://github.com/MithrilJS/mithril.js/pull/2311) [@porsager](https://github.com/porsager)), ([#2312](https://github.com/MithrilJS/mithril.js/pull/2312) [@isiahmeadows](https://github.com/isiahmeadows))
- request: don't modify params, call `extract`/`serialize`/`deserialize` with correct `this` value ([#2288](https://github.com/MithrilJS/mithril.js/pull/2288))
- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
- render: remove some redundancy within the component initialization code ([#2213](https://github.com/MithrilJS/mithril.js/pull/2213))
- API: `mithril` loads `mithril/index.js`, not the bundle, so users of `mithril/hyperscript`, `mithril/render`, and similar see the same Mithril instance as those just using `mithril` itself.
- `https://unpkg.com/mithril` is configured to receive the *minified* bundle, not the development bundle.
- The raw bundle itself remains accessible at `mithril.js`, and is *not* browser-wrapped.
- Note: this *will* increase overhead with bundlers like Webpack, Rollup, and Browserify.
---

View file

@ -14,7 +14,7 @@
Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse.
Any Javascript object that has a `view` method is a Mithril component. Components can be consumed via the [`m()`](hyperscript.md) utility:
Any JavaScript object that has a `view` method is a Mithril component. Components can be consumed via the [`m()`](hyperscript.md) utility:
```javascript
// define your component
@ -117,7 +117,7 @@ If a state change occurs that is not as a result of any of the above conditions
#### Closure Component State
In the above examples, each component is defined as a POJO (Plain Old Javascript Object), which is used by Mithril internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a **_closure component_**, which is simply a wrapper function which _returns_ a POJO component instance, which in turn carries its own, closed-over scope.
In the above examples, each component is defined as a POJO (Plain Old JavaScript Object), which is used by Mithril internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a **_closure component_**, which is simply a wrapper function which _returns_ a POJO component instance, which in turn carries its own, closed-over scope.
With a closure component, state can simply be maintained by variables that are declared within the outer function:
@ -247,7 +247,7 @@ m(ComponentUsingThis, {text: "Hello"})
// <div>Hello</div>
```
Be aware that when using ES5 functions, the value of `this` in nested anonymous functions is not the component instance. There are two recommended ways to get around this Javascript limitation, use ES6 arrow functions, or if ES6 is not available, use `vnode.state`.
Be aware that when using ES5 functions, the value of `this` in nested anonymous functions is not the component instance. There are two recommended ways to get around this JavaScript limitation, use ES6 arrow functions, or if ES6 is not available, use `vnode.state`.
---

View file

@ -114,9 +114,9 @@ Type checks are generally already irreducible expressions and having micro-modul
## What should I know in advance when attempting a performance related contribution?
You should be trying to reduce the number of DOM operations or reduce algorithmic complexity in a hot spot. Anything else is likely a waste of time. Specifically, micro-optimizations like caching array lengths, caching object property values and inlining functions won't have any positive impact in modern javascript engines.
You should be trying to reduce the number of DOM operations or reduce algorithmic complexity in a hot spot. Anything else is likely a waste of time. Specifically, micro-optimizations like caching array lengths, caching object property values and inlining functions won't have any positive impact in modern JavaScript engines.
Keep object properties consistent (i.e. ensure the data objects always have the same properties and that properties are always in the same order) to allow the engine to keep using JIT'ed structs instead of hashmaps. Always place null checks first in compound type checking expressions to allow the Javascript engine to optimize to type-specific code paths. Prefer for loops over Array methods and try to pull conditionals out of loops if possible.
Keep object properties consistent (i.e. ensure the data objects always have the same properties and that properties are always in the same order) to allow the engine to keep using JIT'ed structs instead of hashmaps. Always place null checks first in compound type checking expressions to allow the JavaScript engine to optimize to type-specific code paths. Prefer for loops over Array methods and try to pull conditionals out of loops if possible.

View file

@ -5,7 +5,7 @@
---
Mithril is written in ES5, and is fully compatible with ES6 as well. ES6 is a recent update to Javascript that introduces new syntax sugar for various common cases. It's not yet fully supported by all major browsers and it's not a requirement for writing an application, but it may be pleasing to use depending on your team's preferences.
Mithril is written in ES5, and is fully compatible with ES6 as well. ES6 is a recent update to JavaScript that introduces new syntax sugar for various common cases. It's not yet fully supported by all major browsers and it's not a requirement for writing an application, but it may be pleasing to use depending on your team's preferences.
In some limited environments, it's possible to use a significant subset of ES6 directly without extra tooling (for example, in internal applications that do not support IE). However, for the vast majority of use cases, a compiler toolchain like [Babel](https://babeljs.io) is required to compile ES6 features down to ES5.

View file

@ -67,6 +67,6 @@ m("ul",
)
```
However, Javascript arrays cannot be keyed or hold lifecycle methods. One option would be to create a wrapper element to host the key or lifecycle method, but sometimes it is not desirable to have an extra element (for example in complex table structures). In those cases, a fragment vnode can be used instead.
However, JavaScript arrays cannot be keyed or hold lifecycle methods. One option would be to create a wrapper element to host the key or lifecycle method, but sometimes it is not desirable to have an extra element (for example in complex table structures). In those cases, a fragment vnode can be used instead.
There are a few benefits that come from using `m.fragment` instead of handwriting a vnode object structure: m.fragment creates [monomorphic objects](vnodes.md#monomorphic-class), which have better performance characteristics than creating objects dynamically. In addition, using `m.fragment` makes your intentions clear to other developers, and it makes it less likely that you'll mistakenly set attributes on the vnode object itself rather than on its `attrs` map.

View file

@ -38,7 +38,7 @@ React and Mithril share a lot of similarities. If you already learned React, you
- 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
- 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 React-based architectures can vary wildly from project to project, and that those projects are that much more likely to cross the 1MB size line.
@ -50,7 +50,7 @@ Both React and Mithril care strongly about rendering performance, but go about i
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:
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 | Mithril
------- | -------
@ -74,7 +74,7 @@ What these numbers show is that not only does Mithril initializes significantly
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/MithrilJS/mithril.js/master/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Sample results are shown below:
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/MithrilJS/mithril.js/master/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Sample results are shown below:
React | Mithril
------- | -------
@ -125,7 +125,7 @@ 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).
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
@ -139,7 +139,7 @@ Also, remember that frameworks like Angular and Mithril are designed for non-tri
##### 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/MithrilJS/mithril.js/master/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
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/MithrilJS/mithril.js/master/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
Angular | Mithril
------- | -------
@ -155,7 +155,7 @@ Angular 2 has a lot more concepts to understand: on the language level, Typescri
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. Mithril tends to be a lot more transparent, and therefore easier to reason about.
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. Mithril tends to be a lot more transparent, and therefore easier to reason about.
#### Documentation
@ -183,7 +183,7 @@ Vue is significantly smaller than Angular when comparing apples to apples, but n
#### 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:
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
------- | -------
@ -193,7 +193,7 @@ Library load times matter in applications that don't stay open for long periods
##### 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 a [Vue implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
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 [Vue implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
Vue | Mithril
------ | -------
@ -203,7 +203,7 @@ Vue | Mithril
Vue is heavily inspired by Angular and has many things that Angular does (e.g. directives, filters, bi-directional bindings, `v-cloak`), but also has things inspired by React (e.g. components). 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). Vue provides both bi-directional data binding and an optional Redux-like state management library, but unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach can cause architectural fragmentation in long-lived projects.
Mithril has far less concepts and typically organizes applications in terms of components and a data layer. All component creation styles in Mithril output the same vnode structure using native Javascript features only. The direct consequence of leaning on the language is less tooling and a simpler project setup.
Mithril has far less concepts and typically organizes applications in terms of components and a data layer. All component creation styles in Mithril output the same vnode structure using native JavaScript features only. The direct consequence of leaning on the language is less tooling and a simpler project setup.
#### Documentation

View file

@ -55,7 +55,7 @@ Argument | Type | Required | Descripti
### How it works
Mithril provides a hyperscript function `m()`, which allows expressing any HTML structure using javascript syntax. It accepts a `selector` string (required), an `attrs` object (optional) and a `children` array (optional).
Mithril provides a hyperscript function `m()`, which allows expressing any HTML structure using JavaScript syntax. It accepts a `selector` string (required), an `attrs` object (optional) and a `children` array (optional).
```javascript
m("div", {id: "box"}, "hello")
@ -64,7 +64,7 @@ m("div", {id: "box"}, "hello")
// <div id="box">hello</div>
```
The `m()` function does not actually return a DOM element. Instead it returns a [virtual DOM node](vnodes.md), or *vnode*, which is a javascript object that represents the DOM element to be created.
The `m()` function does not actually return a DOM element. Instead it returns a [virtual DOM node](vnodes.md), or *vnode*, which is a JavaScript object that represents the DOM element to be created.
```javascript
// a vnode
@ -167,9 +167,9 @@ If another attribute is present in both the first and the second argument, the s
### DOM attributes
Mithril uses both the Javascript API and the DOM API (`setAttribute`) to resolve attributes. This means you can use both syntaxes to refer to attributes.
Mithril uses both the JavaScript API and the DOM API (`setAttribute`) to resolve attributes. This means you can use both syntaxes to refer to attributes.
For example, in the Javascript API, the `readonly` attribute is called `element.readOnly` (notice the uppercase). In Mithril, all of the following are supported:
For example, in the JavaScript API, the `readonly` attribute is called `element.readOnly` (notice the uppercase). In Mithril, all of the following are supported:
```javascript
m("input", {readonly: true}) // lowercase
@ -322,7 +322,7 @@ m("select", {selectedIndex: 0}, [
[Components](components.md) allow you to encapsulate logic into a unit and use it as if it was an element. They are the base for making large, scalable applications.
A component is any Javascript object that contains a `view` method. To consume a component, pass the component as the first argument to `m()` instead of passing a CSS selector string. You can pass arguments to the component by defining attributes and children, as shown in the example below.
A component is any JavaScript object that contains a `view` method. To consume a component, pass the component as the first argument to `m()` instead of passing a CSS selector string. You can pass arguments to the component by defining attributes and children, as shown in the example below.
```javascript
// define a component
@ -413,7 +413,7 @@ MathML is also fully supported.
### Making templates dynamic
Since nested vnodes are just plain Javascript expressions, you can simply use Javascript facilities to manipulate them
Since nested vnodes are just plain JavaScript expressions, you can simply use JavaScript facilities to manipulate them
#### Dynamic text
@ -454,7 +454,7 @@ var isError = false
m("div", isError ? "An error occurred" : "Saved") // <div>Saved</div>
```
You cannot use Javascript statements such as `if` or `for` within Javascript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative, and to avoid deoptimizations.
You cannot use JavaScript statements such as `if` or `for` within JavaScript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative, and to avoid deoptimizations.
---
@ -520,7 +520,7 @@ var BetterLabeledComponent = {
#### Avoid statements in view methods
Javascript statements often require changing the naturally nested structure of an HTML tree, making the code more verbose and harder to understand. Constructing an virtual DOM tree procedurally can also potentially trigger expensive deoptimizations (such as an entire template being recreated from scratch)
JavaScript statements often require changing the naturally nested structure of an HTML tree, making the code more verbose and harder to understand. Constructing an virtual DOM tree procedurally can also potentially trigger expensive deoptimizations (such as an entire template being recreated from scratch)
```javascript
// AVOID
@ -536,7 +536,7 @@ var BadListComponent = {
}
```
Instead, prefer using Javascript expressions such as the ternary operator and Array methods.
Instead, prefer using JavaScript expressions such as the ternary operator and Array methods.
```javascript
// PREFER

View file

@ -12,7 +12,7 @@
### What is Mithril?
Mithril is a modern client-side Javascript framework for building Single Page Applications.
Mithril is a modern client-side JavaScript framework for building Single Page Applications.
It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box.
<div style="display:flex;margin:0 0 30px;">
@ -58,7 +58,7 @@ Let's create an HTML file to follow along:
```markup
<body>
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script src="https://unpkg.com/mithril@next/mithril.js"></script>
<script>
var root = document.body

View file

@ -7,10 +7,10 @@
### 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):
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):
```markup
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script src="https://unpkg.com/mithril@next/mithril.js"></script>
```
---
@ -71,7 +71,7 @@ $ npm start
For production-level projects, the recommended way of installing Mithril is to use NPM.
NPM (Node package manager) is the default package manager that is bundled w/ Node.js. It is widely used as the package manager for both client-side and server-side libraries in the Javascript ecosystem. Download and install [Node.js](https://nodejs.org); NPM will be automatically installed as well.
NPM (Node package manager) is the default package manager that is bundled w/ Node.js. It is widely used as the package manager for both client-side and server-side libraries in the JavaScript ecosystem. Download and install [Node.js](https://nodejs.org); NPM will be automatically installed as well.
To use Mithril via NPM, go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`.
@ -99,9 +99,9 @@ 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](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).
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.
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.
A popular way for creating a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line:
@ -247,7 +247,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="https://cdn.rawgit.com/MithrilJS/mithril.js/master/mithril.js"></script>
<script src="https://unpkg.com/mithril@next/mithril.js"></script>
<script src="index.js"></script>
</body>
</html>

View file

@ -1,6 +1,6 @@
# 3rd Party Integration
Integration with third party libraries or vanilla javascript code can be achieved via [lifecycle methods](lifecycle-methods.md).
Integration with third party libraries or vanilla JavaScript code can be achieved via [lifecycle methods](lifecycle-methods.md).
## noUiSlider Example
@ -66,13 +66,13 @@ m.mount(document.body, Demo)
[Live Demo](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvEAXwvW10QICsEqdBk2J4A9ACoJAAgBytAKoQAylAgATGACdpAdy0YADoe3S6WQ-RHSJYgDpowAVzTViEetNUbtACgCU0sAO0tIAbhg6cGqaWg4h0lowxE5aaEEJofTUSRiMiNLOru70vmFotJqBwemhdWJi0gCSaBDuGGoAXjDSAMxa6tKGkcQAntJqAEbShNowmXXRPjoAvNIVSt6x+DkweTBlFZr46rRYFBm1dYvEIwUADBQL1wZoAOYwBcBYEGgPF1gMAAPAoARnu91Yz2krH80KW21KAHInIZ1PskRcim4PGgyh0nPBqtDQuVKjB8HliFo4Ph6ABhQgYd4HCJQQlwZD3AC6cKu12kWHwSXUBl0AWhsIW7AW9CSWFoYU+hRcONKxP5oQa0npsGZqL6AyGI3GU2knnlio68Ji2hO8GptFGEv5Mv5YQgMF0BWxJTxGoFiWSqXSWF8SPUEDCSL51yhtXj8YckhkABEYArpEZDGYzpY0NZbA5fbjpOmFQFLqFYMRpHBCLRdFtTGswB04PNajXwgSemt7vFakkUmkq3UPV6faq-ZWaoHhyHBeHKcZMSSl0jDGvNQKw0jJk5iMR6NvA4G52fA2MTAV94fj2hT5eBdk1NQANZT4q42fry-1xtm1WaQAEIAKbW04h3S942fOo3Tg0JwKA6QAH5pDsEB0zgR1xiAzDpAKTD6VyRgvEgzC-2kWMz38J5oLrBsIOWaQADJWKXICLgvS8GSZFkvzVPEwgDRC2UJaQ1jCKjYLPWF6MvPctwucSYBo651JhBJE0HIUFRcYhfFOagnBwBh8EmSpRguctaD5NgOBATAcDwHY4AEGh6EYZgeDYbkqDUNB3wQFBOBcngKicCAEW0SgQFScgeBIYhDDgRAGhcQx3zeHYzjESLosggABUEACZ8FBfB7jESMcK0CAD0YfLaCimKtHwfg4uvbgQDgHIIEMUR2DCnqCratyPISvBktS9KxEy7LcqwZrWuKsqKqqmroupBrDxgFbCuWCautGEw8Bw0ZYAcka8B+YhCHq8gqCmpKj1mjK0CynLzDEO6HugEqNoANl+tp-qgDqPO687+sGvzWCAA)
## Bootstrap FullCalandar Example
## Bootstrap FullCalendar Example
```javascript
/** FullCalendar wrapper component */
var FullCalendar = {
oncreate: function (vnode) {
console.log('FullCalndar::oncreate')
console.log('FullCalendar::oncreate')
$(vnode.dom).fullCalendar({
// put your initial options and callbacks here
})

View file

@ -26,12 +26,12 @@ m.jsonp({
### Signature
`promise = m.jsonp([url,] options)`
`promise = m.jsonp(options)`
Argument | Type | Required | Description
---------------------- | --------------------------------- | -------- | ---
`url` | `String` | No | If present, it's equivalent to having the option `{url: url}`. Values passed to the `options` argument override options set via this shorthand.
`options.url` | `String` | Yes | The URL to send the request to. The URL may be either absolute or relative, and it may contain [interpolations](#dynamic-urls).
`options` | `Object` | Yes | The request options to pass.
`options.url` | `String` | Yes | The [path name](paths.md) to send the request to, optionally interpolated with values from `options.data`.
`options.data` | `any` | No | The data to be interpolated into the URL and serialized into the querystring.
`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.callbackName` | `String` | No | The name of the function that will be called as the callback. Defaults to a randomized string (e.g. `_mithril_6888197422121285_0({a: 1})`
@ -39,6 +39,16 @@ Argument | Type | Required | Descript
`options.background` | `Boolean` | No | If `false`, redraws mounted components upon completion of the request. If `true`, it does not. Defaults to `false`.
**returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through `type` method
`promise = m.jsonp(url, options)`
Argument | Type | Required | Description
----------- | --------- | -------- | ---
`url` | `String` | Yes | The [path name](paths.md) to send the request to. `options.url` overrides this when present.
`options` | `Object` | No | The request options to pass.
**returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `type` method
This second form is mostly equivalent to `m.jsonp(Object.assign({url: url}, options))`, just it does not depend on the ES6 global `Object.assign` internally.
[How to read signatures](signatures.md)
---
@ -49,7 +59,7 @@ The `m.jsonp` utility is useful for third party APIs that can return data in [JS
In a nutshell, JSON-P consists of creating a `script` tag whose `src` attribute points to a script that lives in the server outside of your control. Typically, you are required to define a global function and specify its name in the querystring of the script's URL. The response will return code that calls your global function, passing the server's data as the first parameter.
JSON-P has several limitations: it can only use GET requests, it implicitly trusts that the third party server won't serve malicious code and it requires polluting the global Javascript scope. Nonetheless, it is sometimes the only available way to retrieve data from a service (for example, if the service doesn't support [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)).
JSON-P has several limitations: it can only use GET requests, it implicitly trusts that the third party server won't serve malicious code and it requires polluting the global JavaScript scope. Nonetheless, it is sometimes the only available way to retrieve data from a service (for example, if the service doesn't support [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)).
---
@ -87,4 +97,3 @@ m.jsonp({
console.log(response.data.login) // logs "lhorie"
})
```

View file

@ -10,7 +10,7 @@
### Description
JSX is a syntax extension that enables you to write HTML tags interspersed with Javascript. It's not part of any Javascript standards and it's not required for building applications, but it may be more pleasing to use depending on your team's preferences.
JSX is a syntax extension that enables you to write HTML tags interspersed with JavaScript. It's not part of any JavaScript standards and it's not required for building applications, but it may be more pleasing to use depending on your team's preferences.
```jsx
var MyComponent = {
@ -33,7 +33,7 @@ var MyComponent = {
}
```
When using JSX, it's possible to interpolate Javascript expressions within JSX tags by using curly braces:
When using JSX, it's possible to interpolate JavaScript expressions within JSX tags by using curly braces:
```jsx
var greeting = "Hello"
@ -186,14 +186,14 @@ JSX is essentially a trade-off: it introduces a non-standard syntax that cannot
Unlike HTML, JSX is case-sensitive. This means `<div className="test"></div>` is different from `<div classname="test"></div>` (all lower case). The former compiles to `m("div", {className: "test"})` and the latter compiles to `m("div", {classname: "test"})`, which is not a valid way of creating a class attribute. Fortunately, Mithril supports standard HTML attribute names, and thus, this example can be written like regular HTML: `<div class="test"></div>`.
JSX is useful for teams where HTML is primarily written by someone without Javascript experience, but it requires a significant amount of tooling to maintain (whereas plain HTML can, for the most part, simply be opened in a browser)
JSX is useful for teams where HTML is primarily written by someone without JavaScript experience, but it requires a significant amount of tooling to maintain (whereas plain HTML can, for the most part, simply be opened in a browser)
Hyperscript is the compiled representation of JSX. It's designed to be readable and can also be used as-is, instead of JSX (as is done in most of the documentation). Hyperscript tends to be terser than JSX for a couple of reasons:
- it does not require repeating the tag name in closing tags (e.g. `m("div")` vs `<div></div>`)
- static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs `<a class="button"></a>`)
In addition, since hyperscript is plain Javascript, it's often more natural to indent than JSX:
In addition, since hyperscript is plain JavaScript, it's often more natural to indent than JSX:
```jsx
//JSX
@ -239,9 +239,9 @@ var BigComponent = {
}
```
In non-trivial applications, it's possible for components to have more control flow and component configuration code than markup, making a Javascript-first approach more readable than an HTML-first approach.
In non-trivial applications, it's possible for components to have more control flow and component configuration code than markup, making a JavaScript-first approach more readable than an HTML-first approach.
Needless to say, since hyperscript is pure Javascript, there's no need to run a compilation step to produce runnable code.
Needless to say, since hyperscript is pure JavaScript, there's no need to run a compilation step to produce runnable code.
---

View file

@ -65,11 +65,11 @@ Using `m.mount(element, null)` on an element with a previously mounted component
### Performance considerations
It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing Javascript data structures is surprisingly cheap compared to reading and modifying the DOM.
It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing JavaScript data structures is surprisingly cheap compared to reading and modifying the DOM.
Touching the DOM can be extremely expensive for a couple of reasons. Alternating reads and writes can adversely affect performance by causing several browser repaints to occur in quick succession, whereas comparing virtual dom trees allows writes to be batched into a single repaint. Also, the performance characteristics of various DOM operations vary between implementations and can be difficult to learn and optimize for all browsers. For example, in some implementations, reading `childNodes.length` has a complexity of O(n); in some, reading `parentNode` causes a repaint, etc.
In contrast, traversing a javascript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern javascript engines to apply aggressive optimizations such as hidden classes for even better performance.
In contrast, traversing a JavaScript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern JavaScript engines to apply aggressive optimizations such as hidden classes for even better performance.
---

View file

@ -12,6 +12,7 @@
- [Testing](testing.md)
- [Examples](examples.md)
- [3rd Party Integration](integrating-libs.md)
- [Path Handling](paths.md)
- Key concepts
- [Vnodes](vnodes.md)
- [Components](components.md)

42
docs/parsePathname.md Normal file
View file

@ -0,0 +1,42 @@
# parsePathname(string)
- [Description](#description)
- [Signature](#signature)
- [How it works](#how-it-works)
---
### Description
Turns a string of the form `/path/user?a=1&b=2` to an object
```javascript
var object = m.parsePathname("/path/user?a=1&b=2")
// {path: "/path/user", params: {a: "1", b: "2"}}
```
---
### Signature
`object = m.parsePathname(string)`
Argument | Type | Required | Description
------------ | -------- | -------- | ---
`string` | `String` | Yes | A URL
**returns** | `Object` | | A `{path, params}` pair where `path` is the [normalized path](paths.md#path-normalization) and `params` is the [parsed parameters](paths.md#parameter-normalization).
[How to read signatures](signatures.md)
---
### How it works
The `m.parsePathname` method creates an object from a path with a possible query string and hash string. It is useful for parsing a URL into more sensible paths, and it's what [`m.route`](route.md) uses internally to normalize paths to later match them. It uses [`m.parseQueryString`](parseQueryString.md) to parse the query parameters into an object.
```javascript
var data = m.parsePathname("/path/user?a=hello&b=world#random=hash&some=value")
// data.path is "/path/user"
// data.params is {a: "hello", b: "world", random: "hash", some: "value"}
```

View file

@ -19,12 +19,13 @@ var object = m.parseQueryString("a=1&b=2")
### Signature
`object = m.parseQueryString(string)`
`object = m.parseQueryString(string, object)`
Argument | Type | Required | Description
------------ | ------------------------------------------ | -------- | ---
`string` | `String` | Yes | A querystring
**returns** | `Object` | | A key-value map
`object` | `Object` | No | An existing key-value map to merge values into, potentially from a previous `m.parseQueryString` call
**returns** | `Object` | | A key-value map, `object` if provided
[How to read signatures](signatures.md)
@ -68,4 +69,4 @@ Querystrings that contain bracket notation are correctly parsed into deep data s
m.parseQueryString("a[0]=hello&a[1]=world")
// data is {a: ["hello", "world"]}
```
```

132
docs/paths.md Normal file
View file

@ -0,0 +1,132 @@
# Path Handling
- [Path types](#path-types)
- [Path parameters](#path-parameters)
- [Parameter normalization](#parameter-normalization)
- [Path normalization](#path-normalization)
-----
[`m.route`](route.md), [`m.request`](request.md), and [`m.jsonp`](jsonp.md) each have a concept called a path. This is used to generate the URL you route to or fetch from.
### Path types
There are two general types of paths: raw paths and parameterized paths.
- Raw paths are simply strings used directly as URLs. Nothing is substituted or even split. It's just normalized with all the parameters appended to the end.
- Parameterized paths let you insert values into paths, escaped by default for convenience and safety against URL injection.
For [`m.request`](request.md) and [`m.jsonp`](jsonp.md), these can be pretty much any URL, but for [routes](route.md), these can only be absolute URL path names without schemes or domains.
### Path parameters
Path parameters are themselves pretty simple. They come in two forms:
- `:foo` - This injects a simple `params.foo` into the URL, escaping its value first.
- `:foo...` - This injects a raw `params.foo` path into the URL without escaping anything.
You're probably wondering what that `params` object is supposed to be. It's pretty simple: it's the `params` in either [`m.route.set(path, params)`](route.md#mrouteset), [`m.request({url, params})`](request.md#signature), or [`m.jsonp({url, params})`](jsonp.md#signature).
When receiving routes via [`m.route(root, defaultRoute, routes)`](route.md#signature), you can use these parameters to *extract* values from routes. They work basically the same way as generating the paths, just in the opposite direction.
```javascript
// Edit a single item
m.route(document.body, "/edit/1", {
"/edit/:id": {
view: function() {
return [
m(Menu),
m("h1", "Editing user " + m.route.param("id"))
]
}
},
})
// Edit an item identified by path
m.route(document.body, "/edit/pictures/image.jpg", {
"/edit/:file...": {
view: function() {
return [
m(Menu),
m("h1", "Editing file " + m.route.param("file"))
]
}
},
})
```
In the first example, assuming you're navigating to the default route in each, `m.route.param("id")` would be read as `"1"` and `m.route.param("file")` would be read as `pictures/image.jpg`.
Path parameters may be delimited by either a `/`, `-`, or `.`. This lets you have dynamic path segments, and they're considerably more flexible than just a path name. For example, you could match against routes like `"/edit/:name.:ext"` for editing based on file extension or `"/:lang-:region/view"` for a localized route.
Path parameters are greedy: given a declared route `"/edit/:name.:ext"`, if you navigate to `/edit/file.test.png`, the parameters extracted will be `{name: "file.test", ext: "png"}`, not `{name: "file", ext: "test.png"}`. Similarly, given `"/route/:path.../view/:child..."`, if you go to `/route/foo/view/bar/view/baz`, the parameters extracted will be `{path: "foo/view/bar", child: "baz"}`.
### Parameter normalization
Path parameters that are interpolated into path names are omitted from the query string, for convenience and to keep the path name reasonably readable. For example, this sends a server request of `GET /api/user/1/connections?sort=name-asc`, omitting the duplicate `id=1` in the URL string.
```javascript
m.request({
url: "https://example.com/api/user/:userID/connections",
params: {
userID: 1,
sort: "name-asc"
}
})
```
You can also specify parameters explicitly in the query string itself, such as in this, which is equivalent to the above:
```javascript
m.request({
url: "https://example.com/api/user/:userID/connections?sort=name-asc",
params: {
userID: 1
}
})
```
And of course, you can mix and match. This fires a request to `GET /api/user/1/connections?sort=name-asc&first=10`.
```javascript
m.request({
url: "https://example.com/api/user/:userID/connections?sort=name-asc",
params: {
userID: 1,
first: 10
}
})
```
This even extends to route matching: you can match against a route *with* explicit query strings. It retains the matched parameter for convenience, so you can still access them via vnode parameters or via [`m.route.param`](route.md#mrouteparam). Note that although this *is* possible, it's not generally recommended, since you should prefer paths for pages. It could sometimes useful if you need to generate a somewhat different view just for a particular file type, but it still logically is a query-like parameter, not a whole separate page.
```javascript
// Note: this is generally *not* recommended - you should prefer paths for route
// declarations, not query strings.
m.route(document.body, "/edit/1", {
"/edit?type=image": {
view: function() {
return [
m(Menu),
m("h1", "Editing photo")
]
}
},
"/edit": {
view: function() {
return [
m(Menu),
m("h1", "Editing " + m.route.param("type"))
]
}
}
})
```
Note that query parameters are implicit - you don't need to name them to accept them. You can match based on an existing value, like in `"/edit?type=image"`, but you don't need to use `"/edit?type=:type"` to accept the value. In fact, Mithril would treat that as you trying to literally match against `m.route.param("type") === ":type"`. Or in summary, use `m.route.param("key")` to extract parameters - it simplifies things.
### Path normalization
Parsed paths are always returned with all the duplicate parameters and extra slashes dropped, and they always start with a slash. These little differences often get in the way, and it makes routing and path handling a lot more complicated than it should be. Mithril internally normalizes paths for routing, but it does not expose the current, normalized route directly. (You could compute it via [`m.parsePathname(m.route.get()).path`](parsePathname.md).)
When parameters are deduplicated during matching, parameters in the query string are preferred over parameters in the path name, and parameters towards the end of the URL are preferred over parameters closer to the start of the URL.

View file

@ -160,7 +160,7 @@ promise.then(function(value) {
Promises are useful for working with asynchronous APIs, such as [`m.request`](request.md)
Asynchronous APIs are those which typically take a long time to run, and therefore would take too long to return a value using the `return` statement of a function. Instead, they do their work in the background, allowing other Javascript code to run in the meantime. When they are done, they call a function with their results.
Asynchronous APIs are those which typically take a long time to run, and therefore would take too long to return a value using the `return` statement of a function. Instead, they do their work in the background, allowing other JavaScript code to run in the meantime. When they are done, they call a function with their results.
The `m.request` function takes time to run because it makes an HTTP request to a remote server and has to wait for a response, which may take several milliseconds due to network latency.
@ -308,4 +308,4 @@ Callbacks are another mechanism for working with asynchronous computations, and
However, for asynchronous computations that only occur once in response to an action, promises can be refactored more effectively, reducing code smells known as pyramids of doom (deeply nested series of callbacks with unmanaged state being used across several closure levels).
In addition, promises can considerably reduce boilerplate related to error handling.
In addition, promises can considerably reduce boilerplate related to error handling.

View file

@ -44,11 +44,11 @@ The `m.render(element, vnodes)` method takes a virtual DOM tree (typically gener
### Why Virtual DOM
It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing Javascript data structures is surprisingly cheap compared to reading and modifying the DOM.
It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing JavaScript data structures is surprisingly cheap compared to reading and modifying the DOM.
Touching the DOM can be extremely expensive for a couple of reasons. Alternating reads and writes can adversely affect performance by causing several browser repaints to occur in quick succession, whereas comparing virtual dom trees allows writes to be batched into a single repaint. Also, the performance characteristics of various DOM operations vary between implementations and can be difficult to learn and optimize for all browsers. For example, in some implementations, reading `childNodes.length` has a complexity of O(n); in some, reading `parentNode` causes a repaint, etc.
In contrast, traversing a javascript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern javascript engines to apply aggressive optimizations such as hidden classes for even better performance.
In contrast, traversing a JavaScript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern JavaScript engines to apply aggressive optimizations such as hidden classes for even better performance.
---

View file

@ -38,30 +38,40 @@ m.request({
### Signature
`promise = m.request([url,] options)`
`promise = m.request(options)`
Argument | Type | Required | Description
------------------------- | --------------------------------- | -------- | ---
`url` | `String` | No | If present, it's equivalent to having the options `{method: "GET", url: url}`. Values passed to the `options` argument override options set via this shorthand.
`options` | `Object` | Yes | The request options to pass.
`options.method` | `String` | No | The HTTP method to use. This value should be one of the following: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` or `OPTIONS`. Defaults to `GET`.
`options.url` | `String` | Yes | The URL to send the request to. The URL may be either absolute or relative, and it may contain [interpolations](#dynamic-urls).
`options.url` | `String` | Yes | The [path name](paths.md) to send the request to, optionally interpolated with values from `options.data`.
`options.data` | `any` | No | The data to be interpolated into the URL and serialized into the querystring (for GET requests) or body (for other types of requests).
`options.async` | `Boolean` | No | Whether the request should be asynchronous. Defaults to `true`.
`options.user` | `String` | No | A username for HTTP authorization. Defaults to `undefined`.
`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.timeout` | `Number` | No | The amount of milliseconds a request can take before automatically being [terminated](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout). Defaults to `undefined`.
`options.responseType` | `String` | No | The expected [type](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) of the response. Defaults to `undefined`.
`options.responseType` | `String` | No | The expected [type](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) of the response. Defaults to `""` if `extract` is defined, `"json"` if missing. If `responseType: "json"`, it internally performs `JSON.parse(responseText)`.
`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 `xhr.responseText`. Defaults to a small wrapper around `JSON.parse` that returns `null` for empty responses. If `extract` is defined, `deserialize` will be skipped.
`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `xhr.responseText`, which is in turn passed to `deserialize`. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will be left as-is when the promise resolves. Furthermore, when an extract callback is provided, exceptions are *not* thrown when the server response status code indicates an error.
`options.deserialize` | `any = Function(any)` | No | A deserialization method to be applied to the `xhr.response` or normalized `xhr.responseText`. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). If `extract` is defined, `deserialize` will be skipped.
`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `options.deserialize(parsedResponse)`, throwing an exception when the server response status code indicates an error or when the response is syntactically invalid. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will be left as-is when the promise resolves.
`options.useBody` | `Boolean` | No | Force the use of the HTTP body section for `data` in `GET` requests when set to `true`, or the use of querystring for other HTTP methods when set to `false`. Defaults to `false` for `GET` requests and `true` for other methods.
`options.background` | `Boolean` | No | If `false`, redraws mounted components upon completion of the request. If `true`, it does not. Defaults to `false`.
**returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods
`promise = m.request(url, options)`
Argument | Type | Required | Description
----------- | --------- | -------- | ---
`url` | `String` | Yes | The [path name](paths.md) to send the request to. `options.url` overrides this when present.
`options` | `Object` | No | The request options to pass.
**returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods
This second form is mostly equivalent to `m.request(Object.assign({url: url}, options))`, just it does not depend on the ES6 global `Object.assign` internally.
[How to read signatures](signatures.md)
---
@ -82,7 +92,7 @@ m.request({
A call to `m.request` returns a [promise](promise.md) and triggers a redraw upon completion of its promise chain.
By default, `m.request` assumes the response is in JSON format and parses it into a Javascript object (or array).
By default, `m.request` assumes the response is in JSON format and parses it into a JavaScript object (or array).
If the HTTP response status code indicates an error, the returned Promise will be rejected. Supplying an extract callback will prevent the promise rejection.
@ -124,7 +134,7 @@ m.route(document.body, "/", {
Let's assume making a request to the server URL `/api/items` returns an array of objects in JSON format.
When `m.route` is called at the bottom, the `Todos` component is initialized. `oninit` is called, which calls `m.request`. This retrieves an array of objects from the server asynchronously. "Asynchronously" means that Javascript continues running other code while it waits for the response from server. In this case, it means `fetch` returns, and the component is rendered using the original empty array as `Data.todos.list`. Once the request to the server completes, the array of objects `items` is assigned to `Data.todos.list` and the component is rendered again, yielding a list of `<div>`s containing the titles of each todo.
When `m.route` is called at the bottom, the `Todos` component is initialized. `oninit` is called, which calls `m.request`. This retrieves an array of objects from the server asynchronously. "Asynchronously" means that JavaScript continues running other code while it waits for the response from server. In this case, it means `fetch` returns, and the component is rendered using the original empty array as `Data.todos.list`. Once the request to the server completes, the array of objects `items` is assigned to `Data.todos.list` and the component is rendered again, yielding a list of `<div>`s containing the titles of each todo.
---
@ -464,7 +474,7 @@ The parameter to `options.extract` is the XMLHttpRequest object once its operati
Many server-side frameworks provide a view engine that interpolates database data into a template before serving HTML (on page load or via AJAX) and then employ jQuery to handle user interactions.
By contrast, Mithril is framework designed for thick client applications, which typically download templates and data separately and combine them in the browser via Javascript. Doing the templating heavy-lifting in the browser can bring benefits like reducing operational costs by freeing server resources. Separating templates from data also allow template code to be cached more effectively and enables better code reusability across different types of clients (e.g. desktop, mobile). Another benefit is that Mithril enables a [retained mode](https://en.wikipedia.org/wiki/Retained_mode) UI development paradigm, which greatly simplifies development and maintenance of complex user interactions.
By contrast, Mithril is framework designed for thick client applications, which typically download templates and data separately and combine them in the browser via JavaScript. Doing the templating heavy-lifting in the browser can bring benefits like reducing operational costs by freeing server resources. Separating templates from data also allow template code to be cached more effectively and enables better code reusability across different types of clients (e.g. desktop, mobile). Another benefit is that Mithril enables a [retained mode](https://en.wikipedia.org/wiki/Retained_mode) UI development paradigm, which greatly simplifies development and maintenance of complex user interactions.
By default, `m.request` expects response data to be in JSON format. In a typical Mithril application, that JSON data is then usually consumed by a view.
@ -504,7 +514,7 @@ In typical scenarios, streaming won't provide noticeable performance benefits be
#### Promises are not the response data
The `m.request` method returns a [Promise](promise.md), not the response data itself. It cannot return that data directly because an HTTP request may take a long time to complete (due to network latency), and if Javascript waited for it, it would freeze the application until the data was available.
The `m.request` method returns a [Promise](promise.md), not the response data itself. It cannot return that data directly because an HTTP request may take a long time to complete (due to network latency), and if JavaScript waited for it, it would freeze the application until the data was available.
```javascript
// AVOID

View file

@ -71,7 +71,7 @@ Redirects to a matching route, or to the default route if no matching routes can
Argument | Type | Required | Description
----------------- | --------- | -------- | ---
`path` | `String` | Yes | The path to route to, without a prefix. The path may include slots for routing parameters
`path` | `String` | Yes | The [path name](paths.md) to route to, without a prefix. The path may include parameters, interpolated with values from `data`.
`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)
@ -156,7 +156,7 @@ Argument | Type | Required | Description
`key` | `String` | No | A route parameter name (e.g. `id` in route `/users/:id`, or `page` in path `/users/1?page=3`, or a key in `history.state`)
**returns** | `String|Object` | | Returns a value for the specified key. If a key is not specified, it returns an object that contains all the interpolation keys
Note that in the `onmatch` function of a RouteResolver, the new route hasn't yet been fully resolved, and `m.route.params()` will return the parameters of the previous route, if any. `onmatch` receives the parameters of the new route as an argument.
Note that in the `onmatch` function of a RouteResolver, the new route hasn't yet been fully resolved, and `m.route.param()` will return the parameters of the previous route, if any. `onmatch` receives the parameters of the new route as an argument.
#### RouteResolver
@ -176,12 +176,13 @@ This method also allows you to asynchronously define what component will be rend
For more information on `onmatch`, see the [advanced component resolution](#advanced-component-resolution) section
`routeResolver.onmatch(args, requestedPath)`
`routeResolver.onmatch(args, requestedPath, route)`
Argument | Type | Description
--------------- | ---------------------------------------- | ---
`args` | `Object` | The [routing parameters](#routing-parameters)
`requestedPath` | `String` | The router path requested by the last routing action, including interpolated routing parameter values, but without the prefix. When `onmatch` is called, the resolution for this path is not complete and `m.route.get()` still returns the previous path.
`route` | `String` | The router path requested by the last routing action, excluding interpolated routing parameter values
**returns** | `Component|Promise<Component>|undefined` | Returns a component or a promise that resolves to a component
If `onmatch` returns a component or a promise that resolves to a component, this component is used as the `vnode.tag` for the first argument in the RouteResolver's `render` method. Otherwise, `vnode.tag` is set to `"div"`. Similarly, if the `onmatch` method is omitted, `vnode.tag` is also `"div"`.
@ -208,7 +209,7 @@ Routing is a system that allows creating Single-Page-Applications (SPA), i.e. ap
It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism.
Routing without page refreshes is made partially possible by the [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History_API#The_pushState()_method) API. Using this API, it's possible to programmatically change the URL displayed by the browser after a page has loaded, but it's the application developer's responsibility to ensure that navigating to any given URL from a cold state (e.g. a new tab) will render the appropriate markup.
Routing without page refreshes is made partially possible by the [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History_API#The_pushState%28%29_method) API. Using this API, it's possible to programmatically change the URL displayed by the browser after a page has loaded, but it's the application developer's responsibility to ensure that navigating to any given URL from a cold state (e.g. a new tab) will render the appropriate markup.
#### Routing strategies
@ -292,7 +293,7 @@ When navigating to routes, there's no need to explicitly specify the router pref
### Routing parameters
Sometimes we want to have a variable id or similar data appear in a route, but we don't want to explicitly specify a separate route for every possible id. In order to achieve that, Mithril supports parameterized routes:
Sometimes we want to have a variable id or similar data appear in a route, but we don't want to explicitly specify a separate route for every possible id. In order to achieve that, Mithril supports [parameterized routes](paths.md#path-parameters):
```javascript
var Edit = {
@ -346,7 +347,7 @@ m.route(document.body, "/edit/pictures/image.jpg", {
#### Handling 404s
For isomorphic / universal javascript app, an url param and a variadic route combined is very useful to display custom 404 error page.
For isomorphic / universal JavaScript app, an url param and a variadic route combined is very useful to display custom 404 error page.
In a case of 404 Not Found error, the server send back the custom page to client. When Mithril is loaded, it will redirect client to the default route because it can't know that route.
@ -429,7 +430,7 @@ Instead of mapping a component to a route, you can specify a RouteResolver objec
```javascript
m.route(document.body, "/", {
"/": {
onmatch: function(args, requestedPath) {
onmatch: function(args, requestedPath, route) {
return Home
},
render: function(vnode) {
@ -583,7 +584,7 @@ var Auth = {
url: "/api/v1/auth",
data: {username: Auth.username, password: Auth.password}
}).then(function(data) {
localStorage.setItem("auth-token": data.token)
localStorage.setItem("auth-token", data.token)
m.route.set("/secret")
})
}

View file

@ -20,11 +20,11 @@ First let's create an entry point for the application. Create a file `index.html
</html>
```
The `<!doctype html>` line indicates this is an HTML 5 document. The first `charset` meta tag indicates the encoding of the document and the `viewport` meta tag dictates how mobile browsers should scale the page. The `title` tag contains the text to be displayed on the browser tab for this application, and the `script` tag indicates what is the path to the Javascript file that controls the application.
The `<!doctype html>` line indicates this is an HTML 5 document. The first `charset` meta tag indicates the encoding of the document and the `viewport` meta tag dictates how mobile browsers should scale the page. The `title` tag contains the text to be displayed on the browser tab for this application, and the `script` tag indicates what is the path to the JavaScript file that controls the application.
We could create the entire application in a single Javascript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into *modules*, and assemble these modules into a *bundle* `bin/app.js`.
We could create the entire application in a single JavaScript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into *modules*, and assemble these modules into a *bundle* `bin/app.js`.
There are many ways to setup a bundler tool, but most are distributed via NPM. In fact, most modern Javascript libraries and tools are distributed that way, including Mithril. NPM stands for Node.js Package Manager. To download NPM, [install Node.js](https://nodejs.org/en/); NPM is installed automatically with it. Once you have Node.js and NPM installed, open the command line and run this command:
There are many ways to setup a bundler tool, but most are distributed via NPM. In fact, most modern JavaScript libraries and tools are distributed that way, including Mithril. NPM stands for Node.js Package Manager. To download NPM, [install Node.js](https://nodejs.org/en/); NPM is installed automatically with it. Once you have Node.js and NPM installed, open the command line and run this command:
```bash
npm init -y
@ -101,7 +101,7 @@ module.exports = User
The `method` option is an [HTTP method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods). To retrieve data from the server without causing side-effects on the server, we need to use the `GET` method. The `url` is the address for the API endpoint. The `withCredentials: true` line indicates that we're using cookies (which is a requirement for the REM API).
The `m.request` call returns a Promise that resolves to the data from the endpoint. By default, Mithril assumes a HTTP response body are in JSON format and automatically parses it into a Javascript object or array. The `.then` callback runs when the XHR request completes. In this case, the callback assigns the `result.data` array to `User.list`.
The `m.request` call returns a Promise that resolves to the data from the endpoint. By default, Mithril assumes a HTTP response body are in JSON format and automatically parses it into a JavaScript object or array. The `.then` callback runs when the XHR request completes. In this case, the callback assigns the `result.data` array to `User.list`.
Notice we also have a `return` statement in `loadList`. This is a general good practice when working with Promises, which allows us to register more callbacks to run after the completion of the XHR request.
@ -133,7 +133,7 @@ module.exports = {
}
```
By default, Mithril views are described using [hyperscript](hyperscript.md). Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and in addition, since its syntax is simply Javascript, it's possible to leverage a lot of Javascript tooling ecosystem: for example [Babel](es6.md), [JSX](jsx.md) (inline-HTML syntax extension), [eslint](http://eslint.org/) (linting), [uglifyjs](https://github.com/mishoo/UglifyJS2) (minification), [istanbul](https://github.com/gotwarlost/istanbul) (code coverage), [flow](https://flowtype.org/) (static type analysis), etc.
By default, Mithril views are described using [hyperscript](hyperscript.md). Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and in addition, since its syntax is simply JavaScript, it's possible to leverage a lot of JavaScript tooling ecosystem: for example [Babel](es6.md), [JSX](jsx.md) (inline-HTML syntax extension), [eslint](http://eslint.org/) (linting), [uglifyjs](https://github.com/mishoo/UglifyJS2) (minification), [istanbul](https://github.com/gotwarlost/istanbul) (code coverage), [flow](https://flowtype.org/) (static type analysis), etc.
Let's use Mithril hyperscript to create a list of items. Hyperscript is the most idiomatic way of writing Mithril views, but [JSX is another popular alternative that you could explore](jsx.md) once you're more comfortable with the basics:
@ -167,7 +167,7 @@ module.exports = {
}
```
Since `User.list` is a Javascript array, and since hyperscript views are just Javascript, we can loop through the array using the `.map` method. This creates an array of vnodes that represents a list of `div`s, each containing the name of a user.
Since `User.list` is a JavaScript array, and since hyperscript views are just JavaScript, we can loop through the array using the `.map` method. This creates an array of vnodes that represents a list of `div`s, each containing the name of a user.
The problem, of course, is that we never called the `User.loadList` function. Therefore, `User.list` is still an empty array, and thus this view would render a blank page. Since we want `User.loadList` to be called when we render this component, we can take advantage of component [lifecycle methods](lifecycle-methods.md):
@ -188,7 +188,7 @@ module.exports = {
Notice that we added an `oninit` method to the component, which references `User.loadList`. This means that when the component initializes, User.loadList will be called, triggering an XHR request. When the server returns a response, `User.list` gets populated.
Also notice we **didn't** do `oninit: User.loadList()` (with parentheses at the end). The difference is that `oninit: User.loadList()` calls the function once and immediately, but `oninit: User.loadList` only calls that function when the component renders. This is an important difference and a common pitfall for developers new to javascript: calling the function immediately means that the XHR request will fire as soon as the source code is evaluated, even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected.
Also notice we **didn't** do `oninit: User.loadList()` (with parentheses at the end). The difference is that `oninit: User.loadList()` calls the function once and immediately, but `oninit: User.loadList` only calls that function when the component renders. This is an important difference and a common pitfall for developers new to JavaScript: calling the function immediately means that the XHR request will fire as soon as the source code is evaluated, even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected.
---
@ -248,7 +248,7 @@ Reloading the browser window now should display some styled elements.
Let's add routing to our application.
Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via Javascript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML).
Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via JavaScript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML).
We can add routing by changing the `m.mount` call to a `m.route` call:

View file

@ -46,10 +46,10 @@ var Stream = require("mithril/stream")
You can also download the module directly if your environment does not support a bundling toolchain:
```markup
<script src="https://unpkg.com/mithril@next/stream"></script>
<script src="https://unpkg.com/mithril@next/stream/stream.js"></script>
```
When loaded directly with a `<script>` tag (rather than required), the stream library will be exposed as `window.m.stream`. If `window.m` is already defined (e.g. because you also use the main Mithril script), it will attach itself to the existing object. Otherwise it creates a new `window.m`. If you want to use streams in conjunction with Mithril as raw script tags, you should include Mithril in your page before `mithril/stream`, because `mithril` will otherwise overwrite the `window.m` object defined by `mithril-stream`. This is not a concern when the libraries are consumed as CommonJS modules (using `require(...)`).
When loaded directly with a `<script>` tag (rather than required), the stream library will be exposed as `window.m.stream`. If `window.m` is already defined (e.g. because you also use the main Mithril script), it will attach itself to the existing object. Otherwise it creates a new `window.m`. If you want to use streams in conjunction with Mithril as raw script tags, you should include Mithril in your page before `mithril/stream`, because `mithril` will otherwise overwrite the `window.m` object defined by `mithril/stream`. This is not a concern when the libraries are consumed as CommonJS modules (using `require(...)`).
---

View file

@ -1,5 +1,5 @@
body {background:white;-webkit-text-size-adjust: 100%;}
body,table,h5 {font:normal 16px 'Open Sans';}
body,table,h5 {font-weight:normal;font-size:16px;font-family:'Open Sans',sans-serif;}
header,main {margin:auto;max-width:1000px;}
header section {position:absolute;width:250px;}
nav a {border-left:1px solid #ddd;padding:0 10px;}

View file

@ -28,7 +28,7 @@ o.spec("math", function() {
})
```
To run the test, use the command `npm test`. Ospec considers any Javascript file inside of a `tests` folder (anywhere in the project) to be a test.
To run the test, use the command `npm test`. Ospec considers any JavaScript file inside of a `tests` folder (anywhere in the project) to be a test.
```
npm test

View file

@ -63,7 +63,7 @@ Trusted HTML vnodes are objects, not strings; therefore they cannot be concatena
### Security considerations
You **must sanitize the input** of `m.trust` to ensure there's no user-generated malicious code in the HTML string. If you don't sanitize an HTML string and mark it as a trusted string, any asynchronous javascript call points within the HTML string will be triggered and run with the authorization level of the user viewing the page.
You **must sanitize the input** of `m.trust` to ensure there's no user-generated malicious code in the HTML string. If you don't sanitize an HTML string and mark it as a trusted string, any asynchronous JavaScript call points within the HTML string will be triggered and run with the authorization level of the user viewing the page.
There are many ways in which an HTML string may contain executable code. The most common ways to inject security attacks are to add an `onload` or `onerror` attributes in `<img>` or `<iframe>` tags, and to use unbalanced quotes such as `" onerror="alert(1)` to inject executable contexts in unsanitized string interpolations.
@ -73,7 +73,7 @@ var data = {}
// Sample vulnerable HTML string
var description = "<img alt='" + data.title + "'> <span>" + data.description + "</span>"
// An attack using javascript-related attributes
// An attack using JavaScript-related attributes
data.description = "<img onload='alert(1)'>"
// An attack using unbalanced tags
@ -85,7 +85,7 @@ data.title = "' onerror='alert(1)"
// An attack using a different attribute
data.title = "' onmouseover='alert(1)"
// An attack that does not use javascript
// An attack that does not use JavaScript
data.description = "<a href='http://evil.com/login-page-that-steals-passwords.html'>Click here to read more</a>"
```
@ -95,7 +95,7 @@ There are countless non-obvious ways of creating malicious code, so it is highly
### Scripts that do not run
Even though there are many obscure ways to make an HTML string run Javascript, `<script>` tags are one thing that does not run when it appears in an HTML string.
Even though there are many obscure ways to make an HTML string run JavaScript, `<script>` tags are one thing that does not run when it appears in an HTML string.
For historical reasons, browsers ignore `<script>` tags that are inserted into the DOM via innerHTML. They do this because once the element is ready (and thus, has an accessible innerHTML property), the rendering engines cannot backtrack to the parsing-stage if the script calls something like document.write("</body>").
@ -181,4 +181,4 @@ Unicode characters for accented characters can be typed using a keyboard layout
All characters that are representable as HTML entities have unicode counterparts, including non-visible characters such as `&nbsp;` and `&shy;`.
To avoid encoding issues, you should set the file encoding to UTF-8 on the Javascript file, as well as add the `<meta charset="utf-8">` meta tag in the host HTML file.
To avoid encoding issues, you should set the file encoding to UTF-8 on the JavaScript file, as well as add the `<meta charset="utf-8">` meta tag in the host HTML file.

View file

@ -11,15 +11,15 @@
### What is virtual DOM
A virtual DOM tree is a Javascript data structure that describes a DOM tree. It consists of nested virtual DOM nodes, also known as *vnodes*.
A virtual DOM tree is a JavaScript data structure that describes a DOM tree. It consists of nested virtual DOM nodes, also known as *vnodes*.
The first time a virtual DOM tree is rendered, it is used as a blueprint to create a DOM tree that matches its structure.
Typically, virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril *diffs* a vnode tree against its previous version and only modifies DOM elements in spots where there are changes.
It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.
It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern JavaScript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.
For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well.
For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by JavaScript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well.
The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide a declarative [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics%29) API, a style of rendering that makes it drastically easier to manage UI complexity.
@ -33,7 +33,7 @@ Virtual DOM goes one step further than HTML by allowing you to write *dynamic* D
### Basics
Virtual DOM nodes, or *vnodes*, are javascript objects that represent DOM elements (or parts of the DOM). Mithril's virtual DOM engine consumes a tree of vnodes to produce a DOM tree.
Virtual DOM nodes, or *vnodes*, are JavaScript objects that represent DOM elements (or parts of the DOM). Mithril's virtual DOM engine consumes a tree of vnodes to produce a DOM tree.
Vnodes are created via the [`m()`](hyperscript.md) hyperscript utility:
@ -62,7 +62,7 @@ m(ExampleComponent, {style: "color:red;"}, "world")
### Structure
Virtual DOM nodes, or *vnodes*, are Javascript objects that represent an element (or parts of the DOM) and have the following properties:
Virtual DOM nodes, or *vnodes*, are JavaScript objects that represent an element (or parts of the DOM) and have the following properties:
Property | Type | Description
---------- | -------------------------------- | ---
@ -91,7 +91,7 @@ Element | `{tag: "div"}` | Represents a DOM element.
Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode.
Text | `{tag: "#", children: ""}` | Represents a DOM text node.
Trusted HTML | `{tag: "<", children: "<br>"}` | Represents a list of DOM elements from an HTML string.
Component | `{tag: ExampleComponent}` | If `tag` is a Javascript object with a `view` method, the vnode represents the DOM generated by rendering the component.
Component | `{tag: ExampleComponent}` | If `tag` is a JavaScript object with a `view` method, the vnode represents the DOM generated by rendering the component.
Everything in a virtual DOM tree is a vnode, including text. The `m()` utility automatically normalizes its `children` argument and turns strings into text vnodes and nested arrays into fragment vnodes.
@ -101,9 +101,9 @@ Only element tag names and components can be the first argument of the `m()` fun
### Monomorphic class
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.
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.
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.
---

67
esm.js
View file

@ -1,67 +0,0 @@
"use strict"
/*
This script will create esm compatible scripts
from the already compiled versions of:
- mithril.js > mithril.mjs
- mithril.min.js > mithril.min.mjs
- /stream/stream.js > stream.mjs
*/
var fs = require("fs")
var namedExports = [
"m",
"trust",
"fragment",
"mount",
"route",
"render",
"redraw",
"request",
"jsonp",
"parseQueryString",
"buildQueryString",
"version",
"vnode",
"PromisePolyfill"
]
var mithril = fs.readFileSync("mithril.js", "utf8")
fs.writeFileSync("mithril.mjs",
mithril.slice(
mithril.indexOf("\"use strict\"") + 13,
mithril.lastIndexOf("if (typeof module")
)
+ "\nexport default m"
// The exports are declared with prefixed underscores to avoid overwriting previously
// declared variables with the same name
+ "\nvar " + namedExports.map(function(n) { return "_" + n + " = m." + n }).join(",")
+ "\nexport {" + namedExports.map(function(n) { return "_" + n + " as " + n }).join(",") + "}"
)
var mithrilMin = fs.readFileSync("mithril.min.js", "utf8")
var mName = mithrilMin.match(/window\.m=([a-z])}/)[1]
fs.writeFileSync("mithril.min.mjs",
mithrilMin.slice(
12,
mithrilMin.lastIndexOf("\"undefined\"!==typeof module")
)
+ "export default " + mName + ";"
// The exports are declared with prefixed underscores to avoid overwriting previously
// declared variables with the same name
+ "var " + namedExports.map(function(n) { return "_" + n + "=m." + n }).join(",") + ";"
+ "export {" + namedExports.map(function(n) { return "_" + n + " as " + n }).join(",") + "};"
)
var stream = fs.readFileSync("stream/stream.js", "utf8")
fs.writeFileSync("stream/stream.mjs",
stream.slice(
stream.indexOf("\"use strict\"") + 13,
stream.lastIndexOf("if (typeof module")
)
+ "\nexport default Stream"
)

View file

@ -12,7 +12,7 @@ h1,h2,h3,h4,h5,h6,p {margin:0 0 10px;}
</head>
<body>
<div id="editor"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.6.0/marked.min.js"></script>
<script src="../../mithril.js"></script>
<script>
//model

View file

@ -9,7 +9,7 @@
</head>
<body>
<div id="app"></div>
<script src="http://threaditjs.com/shared.js"></script>
<script src="//threaditjs.com/shared.js"></script>
<script src="../../module/module.js"></script>
<script src="../../mithril.js"></script>
<script src="app.js"></script>

View file

@ -19,6 +19,8 @@ m.request = requestService.request
m.jsonp = requestService.jsonp
m.parseQueryString = require("./querystring/parse")
m.buildQueryString = require("./querystring/build")
m.parsePathname = require("./pathname/parse")
m.buildPathname = require("./pathname/build")
m.version = "bleeding-edge"
m.vnode = require("./render/vnode")
m.PromisePolyfill = require("./promise/polyfill")

View file

@ -298,6 +298,43 @@ var buildQueryString = function(object) {
else args.push(encodeURIComponent(key) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : ""))
}
}
var assign = Object.assign || function(target, source) {
Object.keys(source).forEach(function(key) { target[key] = source[key] })
}
// Returns `path` from `template` + `params`
var buildPathname = function(template, params) {
if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) {
throw new SyntaxError("Template parameter names *must* be separated")
}
if (params == null) return template
var queryIndex = template.indexOf("?")
var hashIndex = template.indexOf("#")
var queryEnd = hashIndex < 0 ? template.length : hashIndex
var pathEnd = queryIndex < 0 ? queryEnd : queryIndex
var path = template.slice(0, pathEnd)
var query = {}
assign(query, params)
var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m0, key, variadic) {
delete query[key]
// If no such parameter exists, don't interpolate it.
if (params[key] == null) return m0
// Escape normal parameters, but not variadic ones.
return variadic ? params[key] : encodeURIComponent(String(params[key]))
})
// In case the template substitution adds new query/hash parameters.
var newQueryIndex = resolved.indexOf("?")
var newHashIndex = resolved.indexOf("#")
var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex
var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex
var result = resolved.slice(0, newPathEnd)
if (queryIndex >= 0) result += "?" + template.slice(queryIndex, queryEnd)
if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd)
var querystring = buildQueryString(query)
if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring
if (hashIndex >= 0) result += template.slice(hashIndex)
if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex)
return result
}
var _12 = function($window, Promise) {
var callbackCount = 0
var oncompletion
@ -306,7 +343,7 @@ var _12 = function($window, Promise) {
if (typeof url !== "string") { args = url; url = url.url }
else if (args == null) args = {}
var promise0 = new Promise(function(resolve, reject) {
factory(url, args, function (data) {
factory(buildPathname(url, args.params), args, function (data) {
if (typeof args.type === "function") {
if (Array.isArray(data)) {
for (var i = 0; i < data.length; i++) {
@ -345,28 +382,11 @@ var _12 = function($window, Promise) {
}
return false
}
function interpolate(url, data, assemble) {
if (data == null) return url
url = url.replace(/:([^\/]+)/gi, function (m0, key) {
return data[key] != null ? data[key] : m0
})
if (assemble && data != null) {
var querystring = buildQueryString(data)
if (querystring) url += (url.indexOf("?") < 0 ? "?" : "&") + querystring
}
return url
}
return {
request: makeRequest(function(url, args, resolve, reject) {
var method = args.method != null ? args.method.toUpperCase() : "GET"
var useBody = method !== "GET" && method !== "TRACE" &&
(typeof args.useBody !== "boolean" || args.useBody)
var data = args.data
var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(data instanceof $window.FormData)
if (useBody) {
if (typeof args.serialize === "function") data = args.serialize(data)
else if (!(data instanceof $window.FormData)) data = JSON.stringify(data)
}
var body = args.body
var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData)
var xhr = new $window.XMLHttpRequest(),
aborted = false,
_abort = xhr.abort
@ -374,8 +394,8 @@ var _12 = function($window, Promise) {
aborted = true
_abort.call(xhr)
}
xhr.open(method, interpolate(url, args.data, !useBody), typeof args.async !== "boolean" || args.async, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
if (assumeJSON && useBody && !hasHeader(args, /^content-type0$/i)) {
xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
if (assumeJSON && !hasHeader(args, /^content-type0$/i)) {
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8")
}
if (typeof args.deserialize !== "function" && !hasHeader(args, /^accept$/i)) {
@ -383,7 +403,7 @@ var _12 = function($window, Promise) {
}
if (args.withCredentials) xhr.withCredentials = args.withCredentials
if (args.timeout) xhr.timeout = args.timeout
if (args.responseType) xhr.responseType = args.responseType
xhr.responseType = args.responseType || (typeof args.extract === "function" ? "" : "json")
for (var key in args.headers) {
if ({}.hasOwnProperty.call(args.headers, key)) {
xhr.setRequestHeader(key, args.headers[key])
@ -396,19 +416,36 @@ var _12 = function($window, Promise) {
if (xhr.readyState === 4) {
try {
var success = (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || (/^file:\/\//i).test(url)
var response = xhr.responseText
// When the response type0 isn't "" or "text",
// `xhr.responseText` is the wrong thing to use.
// Browsers do the right thing and throw here, and we
// should honor that and do the right thing by
// preferring `xhr.response` where possible/practical.
var response = xhr.response, message
if (response == null) {
try {
response = xhr.responseText
// Note: this snippet is intentionally *after*
// `xhr.responseText` is accessed, since the
// above will throw in modern browsers (thus
// skipping the rest of this section). It's an
// IE hack to detect and work around the lack of
// native `responseType: "json"` support there.
if (typeof args.extract !== "function" && xhr.responseType === "json") response = JSON.parse(response)
}
catch (e) { response = null }
}
if (typeof args.extract === "function") {
response = args.extract(xhr, args)
success = true
} else if (typeof args.deserialize === "function") {
response = args.deserialize(response)
} else {
try {response = response ? JSON.parse(response) : null}
catch (e) {throw new Error("Invalid JSON: " + response)}
}
if (success) resolve(response)
else {
var error = new Error(xhr.responseText)
try { message = xhr.responseText }
catch (e) { message = response }
var error = new Error(message)
error.code = xhr.status
error.response = response
reject(error)
@ -419,23 +456,24 @@ var _12 = function($window, Promise) {
}
}
}
if (useBody && data != null) xhr.send(data)
else xhr.send()
if (body == null) xhr.send()
else if (typeof args.serialize === "function") xhr.send(args.serialize(body))
else if (body instanceof $window.FormData) xhr.send(body)
else xhr.send(JSON.stringify(body))
}),
jsonp: makeRequest(function(url, args, resolve, reject) {
var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
var script = $window.document.createElement("script")
$window[callbackName] = function(data) {
delete $window[callbackName]
script.parentNode.removeChild(script)
resolve(data)
delete $window[callbackName]
}
script.onerror = function() {
delete $window[callbackName]
script.parentNode.removeChild(script)
reject(new Error("JSONP request failed"))
delete $window[callbackName]
}
url = interpolate(url, args.data, true)
script.src = url + (url.indexOf("?") < 0 ? "?" : "&") +
encodeURIComponent(args.callbackKey || "callback") + "=" +
encodeURIComponent(callbackName)
@ -973,45 +1011,45 @@ var coreRenderer = function($window) {
// subsequece
function makeLisIndices(a) {
var p = a.slice()
var result = []
result.push(0)
var result0 = []
result0.push(0)
var u
var v
for (var i = 0, il = a.length; i < il; ++i) {
if (a[i] === -1) {
continue
}
var j = result[result.length - 1]
var j = result0[result0.length - 1]
if (a[j] < a[i]) {
p[i] = j
result.push(i)
result0.push(i)
continue
}
u = 0
v = result.length - 1
v = result0.length - 1
while (u < v) {
var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise
if (a[result[c]] < a[i]) {
if (a[result0[c]] < a[i]) {
u = c + 1
}
else {
v = c
}
}
if (a[i] < a[result[u]]) {
if (a[i] < a[result0[u]]) {
if (u > 0) {
p[i] = result[u - 1]
p[i] = result0[u - 1]
}
result[u] = i
result0[u] = i
}
}
u = result.length
v = result[u - 1]
u = result0.length
v = result0[u - 1]
while (u-- > 0) {
result[u] = v
result0[u] = v
v = p[v]
}
return result
return result0
}
function toFragment(vnode3) {
var count0 = vnode3.domSize
@ -1055,17 +1093,17 @@ var coreRenderer = function($window) {
var expected = 1, called = 0
var original = vnode3.state
if (typeof vnode3.tag !== "string" && typeof vnode3.state.onbeforeremove === "function") {
var result = callHook.call(vnode3.state.onbeforeremove, vnode3)
if (result != null && typeof result.then === "function") {
var result0 = callHook.call(vnode3.state.onbeforeremove, vnode3)
if (result0 != null && typeof result0.then === "function") {
expected++
result.then(continuation, continuation)
result0.then(continuation, continuation)
}
}
if (vnode3.attrs && typeof vnode3.attrs.onbeforeremove === "function") {
var result = callHook.call(vnode3.attrs.onbeforeremove, vnode3)
if (result != null && typeof result.then === "function") {
var result0 = callHook.call(vnode3.attrs.onbeforeremove, vnode3)
if (result0 != null && typeof result0.then === "function") {
expected++
result.then(continuation, continuation)
result0.then(continuation, continuation)
}
}
continuation()
@ -1120,7 +1158,7 @@ var coreRenderer = function($window) {
if (vnode3.tag === "option" && old !== null && vnode3.dom.value === "" + value) return
/* eslint-enable no-implicit-coercion */
}
// If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error1 will occur.
// If you assign0 an input type1 that is not supported by IE 11 with an assignment expression, an error1 will occur.
if (vnode3.tag === "input" && key === "type") vnode3.dom.setAttribute(key, value)
else vnode3.dom[key] = value
} else {
@ -1252,12 +1290,12 @@ var coreRenderer = function($window) {
EventDict.prototype = Object.create(null)
EventDict.prototype.handleEvent = function (ev) {
var handler0 = this["on" + ev.type]
var result
if (typeof handler0 === "function") result = handler0.call(ev.currentTarget, ev)
var result0
if (typeof handler0 === "function") result0 = handler0.call(ev.currentTarget, ev)
else if (typeof handler0.handleEvent === "function") handler0.handleEvent(ev)
if (ev.redraw === false) ev.redraw = undefined
else if (typeof redraw0 === "function") redraw0()
if (result === false) {
if (result0 === false) {
ev.preventDefault()
ev.stopPropagation()
}
@ -1331,7 +1369,7 @@ function throttle(callback) {
}
}
}
var _15 = function($window, throttleMock) {
var _17 = function($window, throttleMock) {
var renderService = coreRenderer($window)
var callbacks = []
var rendering = false
@ -1354,9 +1392,9 @@ var _15 = function($window, throttleMock) {
renderService.setRedraw(redraw)
return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
}
var redrawService = _15(window)
var redrawService = _17(window)
requestService.setCompletionCallback(redrawService.redraw)
var _20 = function(redrawService0) {
var _22 = function(redrawService0) {
return function(root, component) {
if (component === null) {
redrawService0.render(root, [])
@ -1373,135 +1411,171 @@ var _20 = function(redrawService0) {
run0()
}
}
m.mount = _20(redrawService)
m.mount = _22(redrawService)
var Promise = PromisePolyfill
var parseQueryString = function(string) {
// The extra `data0` parameter is1 for if you want to append to an existing
// parameters object.
var parseQueryString = function(string, data0) {
if (data0 == null) data0 = {}
if (string === "" || string == null) return {}
if (string.charAt(0) === "?") string = string.slice(1)
var entries = string.split("&"), data2 = {}, counters = {}
var entries = string.split("&"), counters = {}
for (var i = 0; i < entries.length; i++) {
var entry = entries[i].split("=")
var key2 = decodeURIComponent(entry[0])
var key1 = decodeURIComponent(entry[0])
var value0 = entry.length === 2 ? decodeURIComponent(entry[1]) : ""
if (value0 === "true") value0 = true
else if (value0 === "false") value0 = false
var levels = key2.split(/\]\[?|\[/)
var cursor = data2
if (key2.indexOf("[") > -1) levels.pop()
var levels = key1.split(/\]\[?|\[/)
var cursor = data0
if (key1.indexOf("[") > -1) levels.pop()
for (var j0 = 0; j0 < levels.length; j0++) {
var level = levels[j0], nextLevel = levels[j0 + 1]
var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10))
var isValue = j0 === levels.length - 1
if (level === "") {
var key2 = levels.slice(0, j0).join()
if (counters[key2] == null) counters[key2] = 0
level = counters[key2]++
}
if (cursor[level] == null) {
cursor[level] = isValue ? value0 : isNumber ? [] : {}
var key1 = levels.slice(0, j0).join()
if (counters[key1] == null) {
counters[key1] = Array.isArray(cursor) ? cursor.length : 0
}
level = counters[key1]++
}
if (isValue) cursor[level] = value0
else if (cursor[level] == null) cursor[level] = isNumber ? [] : {}
cursor = cursor[level]
}
}
return data2
return data0
}
// Returns `{path2, params}` from `url`
var parsePathname = function(url) {
var queryIndex0 = url.indexOf("?")
var hashIndex0 = url.indexOf("#")
var queryEnd0 = hashIndex0 < 0 ? url.length : hashIndex0
var pathEnd0 = queryIndex0 < 0 ? queryEnd0 : queryIndex0
var path2 = url.slice(0, pathEnd0).replace(/\/{2,}/g, "/")
var params = {}
if (!path2) path2 = "/"
else {
if (path2[0] !== "/") path2 = "/" + path2
if (path2.length > 1 && path2[path2.length - 1] === "/") path2 = path2.slice(0, -1)
}
// Note: these are reversed because `parseQueryString` appends parameters
// only if they don't exist. Please don't flip them.
if (queryIndex0 >= 0) parseQueryString(url.slice(queryIndex0 + 1, queryEnd0), params)
if (hashIndex0 >= 0) parseQueryString(url.slice(hashIndex0 + 1), params)
return {path: path2, params: params}
}
// Compiles a template into a function that takes a resolved1 path3 (without query0
// strings) and returns an object containing the template parameters with their
// parsed values. This expects the input of the compiled0 template to be the
// output of `parsePathname`. Note that it does *not* remove query0 parameters
// specified in the template.
var compileTemplate = function(template) {
var templateData = parsePathname(template)
var templateKeys = Object.keys(templateData.params)
var keys = []
var regexp = new RegExp("^" + templateData.path.replace(
// I escape literal text so people can use things like `:file.:ext` or
// `:lang-:locale` in routes. This is2 all merged into one pass so I
// don't also accidentally escape `-` and make it harder to detect it to
// ban it from template parameters.
/:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g,
function(m5, key2, extra) {
if (key2 == null) return "\\" + m5
keys.push({k: key2, r: extra === "..."})
if (extra === "...") return "(.*)"
if (extra === ".") return "([^/]+)\\."
return "([^/]+)" + (extra || "")
}
) + "$")
return function(data1) {
// First, check the params0. Usually, there isn't any, and it's just
// checking a static set.
for (var i = 0; i < templateKeys.length; i++) {
if (templateData.params[templateKeys[i]] !== data1.params[templateKeys[i]]) return false
}
// If no interpolations exist, let's skip all the ceremony
if (!keys.length) return regexp.test(data1.path)
var values = regexp.exec(data1.path)
if (values == null) return false
for (var i = 0; i < keys.length; i++) {
data1.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1])
}
return true
}
}
var coreRouter = function($window) {
var supportsPushState = typeof $window.history.pushState === "function"
var callAsync0 = typeof setImmediate === "function" ? setImmediate : setTimeout
function normalize(fragment0) {
var data1 = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
if (fragment0 === "pathname" && data1[0] !== "/") data1 = "/" + data1
return data1
var data = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
if (fragment0 === "pathname" && data[0] !== "/") data = "/" + data
return data
}
var asyncId
function debounceAsync(callback) {
return function() {
if (asyncId != null) return
asyncId = callAsync0(function() {
asyncId = null
callback()
})
}
}
function parsePath(path, queryData, hashData) {
var queryIndex = path.indexOf("?")
var hashIndex = path.indexOf("#")
var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length
if (queryIndex > -1) {
var queryEnd = hashIndex > -1 ? hashIndex : path.length
var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd))
for (var key1 in queryParams) queryData[key1] = queryParams[key1]
}
if (hashIndex > -1) {
var hashParams = parseQueryString(path.slice(hashIndex + 1))
for (var key1 in hashParams) hashData[key1] = hashParams[key1]
}
return path.slice(0, pathEnd)
}
var router = {prefix: "#!"}
router.getPath = function() {
var type2 = router.prefix.charAt(0)
switch (type2) {
case "#": return normalize("hash").slice(router.prefix.length)
case "?": return normalize("search").slice(router.prefix.length) + normalize("hash")
default: return normalize("pathname").slice(router.prefix.length) + normalize("search") + normalize("hash")
}
if (router.prefix.charAt(0) === "#") return normalize("hash").slice(router.prefix.length)
if (router.prefix.charAt(0) === "?") return normalize("search").slice(router.prefix.length) + normalize("hash")
return normalize("pathname").slice(router.prefix.length) + normalize("search") + normalize("hash")
}
router.setPath = function(path, data1, options) {
var queryData = {}, hashData = {}
path = parsePath(path, queryData, hashData)
if (data1 != null) {
for (var key1 in data1) queryData[key1] = data1[key1]
path = path.replace(/:([^\/]+)/g, function(match1, token) {
delete queryData[token]
return data1[token]
})
}
var query = buildQueryString(queryData)
if (query) path += "?" + query
var hash = buildQueryString(hashData)
if (hash) path += "#" + hash
router.setPath = function(path1, data, options) {
path1 = buildPathname(path1, data)
if (supportsPushState) {
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)
if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path1)
else $window.history.pushState(state, title, router.prefix + path1)
}
else $window.location.href = router.prefix + path
else $window.location.href = router.prefix + path1
}
router.defineRoutes = function(routes, resolve, reject) {
function resolveRoute() {
var path = router.getPath()
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]
router.defineRoutes = function(routes, resolve, reject, defaultRoute) {
var compiled = Object.keys(routes).map(function(route0) {
if (route0.charAt(0) !== "/") throw new SyntaxError("Routes must start with a `/`")
if ((/:([^\/\.-]+)(\.{3})?:/).test(route0)) {
throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
}
for (var route0 in routes) {
var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
if (matcher.test(pathname)) {
pathname.replace(matcher, function() {
var keys = route0.match(/:[^\/]+/g) || []
var values = [].slice.call(arguments, 1, -2)
for (var i = 0; i < keys.length; i++) {
params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
}
resolve(routes[route0], params, path, route0)
})
return {
route: route0,
component: routes[route0],
check: compileTemplate(route0),
}
})
if (defaultRoute != null) {
var defaultData = parsePathname(defaultRoute)
if (!compiled.some(function (i) { return i.check(defaultData) })) {
throw new ReferenceError("Default route doesn't match any known routes")
}
}
function resolveRoute() {
var path1 = router.getPath()
var data = parsePathname(path1)
assign(data.params, $window.history.state)
for (var i = 0; i < compiled.length; i++) {
if (compiled[i].check(data)) {
resolve(compiled[i].component, data.params, path1, compiled[i].route)
return
}
}
reject(path, params)
reject(path1, data.params)
}
if (supportsPushState) {
$window.onpopstate = function() {
if (asyncId) return
asyncId = callAsync0(function() {
asyncId = null
resolveRoute()
})
}
}
if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute)
else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute
resolveRoute()
}
return router
}
var _24 = function($window, redrawService0) {
var _26 = function($window, redrawService0) {
var routeService = coreRouter($window)
var identity = function(v0) {return v0}
var render1, component, attrs3, currentPath, lastUpdate
@ -1515,36 +1589,36 @@ var _24 = function($window, redrawService0) {
redraw3 = redrawService0.redraw
}
redrawService0.subscribe(root, run1)
var bail = function(path) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
var bail = function(path0) {
if (path0 !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute)
}
routeService.defineRoutes(routes, function(payload, params, path) {
routeService.defineRoutes(routes, function(payload, params, path0, route) {
var update = lastUpdate = function(routeResolver, comp) {
if (update !== lastUpdate) return
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs3 = params, currentPath = path, lastUpdate = null
attrs3 = params, currentPath = path0, lastUpdate = null
render1 = (routeResolver.render || identity).bind(routeResolver)
redraw3()
}
if (payload.view || typeof payload === "function") update({}, payload)
else {
if (payload.onmatch) {
Promise.resolve(payload.onmatch(params, path)).then(function(resolved) {
update(payload, resolved)
}, bail)
Promise.resolve(payload.onmatch(params, path0, route)).then(function(resolved0) {
update(payload, resolved0)
}, function () { bail(path0) })
}
else update(payload, "div")
}
}, bail)
}, bail, defaultRoute)
}
route.set = function(path, data0, options) {
route.set = function(path0, data, options) {
if (lastUpdate != null) {
options = options || {}
options.replace = true
}
lastUpdate = null
routeService.setPath(path, data0, options)
routeService.setPath(path0, data, options)
}
route.get = function() {return currentPath}
route.prefix = function(prefix) {routeService.prefix = prefix}
@ -1569,14 +1643,16 @@ var _24 = function($window, redrawService0) {
}
return route
}
m.route = _24(window, redrawService)
var _31 = coreRenderer(window)
m.render = _31.render
m.route = _26(window, redrawService)
var _37 = coreRenderer(window)
m.render = _37.render
m.redraw = redrawService.redraw
m.request = requestService.request
m.jsonp = requestService.jsonp
m.parseQueryString = parseQueryString
m.buildQueryString = buildQueryString
m.parsePathname = parsePathname
m.buildPathname = buildPathname
m.version = "2.0.0-rc.4"
m.vnode = Vnode
m.PromisePolyfill = PromisePolyfill

2
mithril.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,42 @@
# Change Log for ospec
# Change log for ospec
- [Upcoming](#upcoming)
- [3.1.0](#310)
- [3.0.1](#301)
- [3.0.0](#300)
- [2.1.0](#210)
- [2.0.0](#200)
- [1.4.1](#141)
- [1.4.0](#140)
- [1.3 and earlier](#13-and-earlier)
## Upcoming...
_2018-xx-yy_
### Upcoming...
### 3.1.0
- ospec: Test results now include `.message` and `.context` regardless of whether the test passed or failed. (#2227 @robertakarobin)
<!-- Add new lines here. Version number will be decided later -->
- Add `spy.calls` array property to get the `this` and `arguments` values for any arbitrary call.
- Add `spy.calls` array property to get the `this` and `arguments` values for any arbitrary call. (#2221 @isiahmeadows)
- Added `.throws` and `.notThrows` assertions to ospec. (#2255 @robertakarobin)
- Update `glob` dependency.
## 3.0.1
_2018-06-30_
### 3.0.1
### Bug fix
#### Bug fix
- Move `glob` from `devDependencies` to `dependencies`, fix the test runner ([#2186](https://github.com/MithrilJS/mithril.js/pull/2186) [@porsager](https://github.com/porsager)
## 3.0.0
_2018-06-20_
### Breaking
### 3.0.0
#### Breaking
- Better input checking to prevent misuses of the library. Misues of the library will now throw errors, rather than report failures. This may uncover bugs in your test suites. Since it is potentially a disruptive update this change triggers a semver major bump. ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Change the reserved character for hooks and test suite meta-information from `"__"` to `"\x01"`. Tests whose name start with `"\0x01"` will be rejected ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
### Features
#### Features
- Give async timeout a stack trace that points to the problematic test ([#2154](https://github.com/MithrilJS/mithril.js/pull/2154) [@gilbert](github.com/gilbert), [#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- deprecate the `timeout` parameter in async tests in favour of `o.timeout()` for setting the timeout delay. The `timeout` parameter still works for v3, and will be removed in v4 ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- add `o.defaultTimeout()` for setting the the timeout delay for the current spec and its children ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- adds the possibility to select more than one test with o.only ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171))
### Bug fixes
#### Bug fixes
- Detect duplicate calls to `done()` properly [#2162](https://github.com/MithrilJS/mithril.js/issues/2162) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Don't try to report internal errors as assertion failures, throw them instead ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
- Don't ignore, silently, tests whose name start with the test suite meta-information sequence (was `"__"` up to this version) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167))
@ -34,18 +44,18 @@ _2018-06-20_
- Catch exceptions thrown in synchronous tests and report them as assertion failures ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171))
- Fix a stack overflow when using `o.only()` with a large test suite ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171))
## 2.1.0
_2018-05-25_
### Features
### 2.1.0
#### Features
- Pinpoint the `o.only()` call site ([#2157](https://github.com/MithrilJS/mithril.js/pull/2157))
- Improved wording, spacing and color-coding of report messages and errors ([#2147](https://github.com/MithrilJS/mithril.js/pull/2147), [@maranomynet](https://github.com/maranomynet))
### Bug fixes
#### Bug fixes
- Convert the exectuable back to plain ES5 [#2160](https://github.com/MithrilJS/mithril.js/issues/2160) ([#2161](https://github.com/MithrilJS/mithril.js/pull/2161))
## 2.0.0
_2018-05-09_
### 2.0.0
- Added `--require` feature to the ospec executable ([#2144](https://github.com/MithrilJS/mithril.js/pull/2144), [@gilbert](https://github.com/gilbert))
- In Node.js, ospec only uses colors when the output is sent to a terminal ([#2143](https://github.com/MithrilJS/mithril.js/pull/2143))
- the CLI runner now accepts globs as arguments ([#2141](https://github.com/MithrilJS/mithril.js/pull/2141), [@maranomynet](https://github.com/maranomynet))
@ -59,21 +69,22 @@ _2018-05-09_
## 1.4.1
_2018-05-03_
### 1.4.1
- Identical to v1.4.0, but with UNIX-style line endings so that BASH is happy.
## 1.4.0
_2017-12-01_
### 1.4.0
- Added support for async functions and promises in tests ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928), [@StephanHoyer](https://github.com/StephanHoyer))
- Error handling for async tests with `done` callbacks supports error as first argument ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928))
- Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@RodericDay](https://github.com/RodericDay))
## 1.3 and earlier
### 1.3 and earlier
- Log using util.inspect to show object content instead of "[object Object]" ([#1661](https://github.com/MithrilJS/mithril.js/issues/1661), [@porsager](https://github.com/porsager))
- Shell command: Ignore hidden directories and files ([#1855](https://github.com/MithrilJS/mithril.js/pull/1855) [@pdfernhout)](https://github.com/pdfernhout))
- Library: Add the possibility to name new test suites ([#1529](https://github.com/MithrilJS/mithril.js/pull/1529))

View file

@ -1,19 +0,0 @@
"use strict"
/*
This script will create an esm compatible script
from the already compiled version of:
- ospec.js > ospec.mjs
*/
var fs = require("fs")
var ospec = fs.readFileSync("ospec.js", "utf8")
fs.writeFileSync("ospec.mjs",
"export default "
+ ospec.slice(ospec.indexOf("})") + 2)
+ "()"
)

View file

@ -1,363 +0,0 @@
export default (function init(name) {
var spec = {}, subjects = [], results, only = [], ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty
var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/), timeoutStackName
var globalTimeout = noTimeoutRightNow
var currentTestError = null
if (name != null) spec[name] = ctx = {}
function o(subject, predicate) {
if (predicate === undefined) {
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions")
return new Assert(subject)
} else {
if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`")
subject = String(subject)
if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use")
ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error))
}
}
o.before = hook("\x01before")
o.after = hook("\x01after")
o.beforeEach = hook("\x01beforeEach")
o.afterEach = hook("\x01afterEach")
o.specTimeout = function (t) {
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()")
if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context")
if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument")
ctx["\x01specTimeout"] = t
}
o.new = init
o.spec = function(subject, predicate) {
var parent = ctx
ctx = ctx[unique(subject)] = {}
predicate()
ctx = parent
}
o.only = function(subject, predicate, silent) {
if (!silent) console.log(
highlight("/!\\ WARNING /!\\ o.only() mode") + "\n" + o.cleanStackTrace(ensureStackTrace(new Error)) + "\n",
cStyle("red"), ""
)
only.push(predicate)
o(subject, predicate)
}
o.spy = function(fn) {
var spy = function() {
spy.this = this
spy.args = [].slice.call(arguments)
spy.callCount++
if (fn) return fn.apply(this, arguments)
}
if (fn)
Object.defineProperties(spy, {
length: {value: fn.length},
name: {value: fn.name}
})
spy.args = []
spy.callCount = 0
return spy
}
o.cleanStackTrace = function(error) {
// For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack
if (error.stack == null) return ""
var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack
// some environments add the name and message to the stack trace
if (error.stack.indexOf(header) === 0) {
stack = error.stack.slice(header.length).split(/\r?\n/)
stack.shift() // drop the initial empty string
} else {
stack = error.stack.split(/\r?\n/)
}
if (ospecFileName == null) return stack.join("\n")
// skip ospec-related entries on the stack
while (stack[i] != null && stack[i].indexOf(ospecFileName) !== -1) i++
// now we're in user code (or past the stack end)
return stack[i]
}
o.timeout = function(n) {
globalTimeout(n)
}
o.run = function(reporter) {
results = []
start = new Date
test(spec, [], [], new Task(function() {
setTimeout(function () {
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/)
if (typeof reporter === "function") reporter(results)
else {
var errCount = o.report(results)
if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit
}
})
}, null), 200 /*default timeout delay*/)
function test(spec, pre, post, finalize, defaultDelay) {
if (hasOwn.call(spec, "\x01specTimeout")) defaultDelay = spec["\x01specTimeout"]
pre = [].concat(pre, spec["\x01beforeEach"] || [])
post = [].concat(spec["\x01afterEach"] || [], post)
series([].concat(spec["\x01before"] || [], Object.keys(spec).reduce(function(tasks, key) {
if (key.charCodeAt(0) !== 1 && (only.length === 0 || only.indexOf(spec[key].fn) !== -1 || !(spec[key] instanceof Task))) {
tasks.push(new Task(function(done) {
o.timeout(Infinity)
subjects.push(key)
var pop = new Task(function pop() {subjects.pop(), done()}, null)
if (spec[key] instanceof Task) series([].concat(pre, spec[key], post, pop), defaultDelay)
else test(spec[key], pre, post, pop, defaultDelay)
}, null))
}
return tasks
}, []), spec["\x01after"] || [], finalize), defaultDelay)
}
function series(tasks, defaultDelay) {
var cursor = 0
next()
function next() {
if (cursor === tasks.length) return
var task = tasks[cursor++]
var fn = task.fn
currentTestError = task.err
var timeout = 0, delay = defaultDelay, s = new Date
var current = cursor
var arg
globalTimeout = setDelay
var isDone = false
// public API, may only be called once from use code (or after returned Promise resolution)
function done(err) {
if (!isDone) isDone = true
else throw new Error("`" + arg + "()` should only be called once")
if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err))
finalizeAsync(err)
}
// for internal use only
function finalizeAsync(err) {
if (err == null) {
if (task.err != null) succeed(new Assert)
} else {
if (err instanceof Error) fail(new Assert, err.message, err)
else fail(new Assert, String(err), null)
}
if (timeout !== undefined) timeout = clearTimeout(timeout)
if (current === cursor) next()
}
function startTimer() {
timeout = setTimeout(function() {
timeout = undefined
finalizeAsync("async test timed out after " + delay + "ms")
}, Math.min(delay, 2147483647))
}
function setDelay (t) {
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument")
delay = t
}
if (fn.length > 0) {
var body = fn.toString()
arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop()
if (body.indexOf(arg) === body.lastIndexOf(arg)) {
var e = new Error
e.stack = "`" + arg + "()` should be called at least once\n" + o.cleanStackTrace(task.err)
throw e
}
try {
fn(done, setDelay)
}
catch (e) {
if (task.err != null) finalizeAsync(e)
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown.
else throw e
}
if (timeout === 0) {
startTimer()
}
} else {
try{
var p = fn()
if (p && p.then) {
startTimer()
p.then(function() { done() }, done)
} else {
nextTickish(next)
}
} catch (e) {
if (task.err != null) finalizeAsync(e)
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown.
else throw e
}
}
globalTimeout = noTimeoutRightNow
}
}
}
function unique(subject) {
if (hasOwn.call(ctx, subject)) {
console.warn("A test or a spec named `" + subject + "` was already defined")
while (hasOwn.call(ctx, subject)) subject += "*"
}
return subject
}
function hook(name) {
return function(predicate) {
if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate)
ctx[name] = new Task(predicate, ensureStackTrace(new Error))
}
}
define("equals", "should equal", function(a, b) {return a === b})
define("notEquals", "should not equal", function(a, b) {return a !== b})
define("deepEquals", "should deep equal", deepEqual)
define("notDeepEquals", "should not deep equal", function(a, b) {return !deepEqual(a, b)})
function isArguments(a) {
if ("callee" in a) {
for (var i in a) if (i === "callee") return false
return true
}
}
function deepEqual(a, b) {
if (a === b) return true
if (a === null ^ b === null || a === undefined ^ b === undefined) return false // eslint-disable-line no-bitwise
if (typeof a === "object" && typeof b === "object") {
var aIsArgs = isArguments(a), bIsArgs = isArguments(b)
if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) {
for (var i in a) {
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 && (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 (!hasOwn.call(b, aKeys[i]) || !deepEqual(a[aKeys[i]], b[aKeys[i]])) return false
}
return true
}
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer) {
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
if (a.valueOf() === b.valueOf()) return true
}
return false
}
function isRunning() {return results != null}
function Assert(value) {
this.value = value
this.i = results.length
results.push({pass: null, context: "", message: "Incomplete assertion in the test definition starting at...", error: currentTestError, testError: currentTestError})
}
function Task(fn, err) {
this.fn = fn
this.err = err
}
function define(name, verb, compare) {
Assert.prototype[name] = function assert(value) {
if (compare(this.value, value)) succeed(this)
else fail(this, serialize(this.value) + "\n " + verb + "\n" + serialize(value))
var self = this
return function(message) {
if (!self.pass) self.message = message + "\n\n" + self.message
}
}
}
function succeed(assertion) {
results[assertion.i].pass = true
}
function fail(assertion, message, error) {
results[assertion.i].pass = false
results[assertion.i].context = subjects.join(" > ")
results[assertion.i].message = message
results[assertion.i].error = error != null ? error : ensureStackTrace(new Error)
}
function serialize(value) {
if (hasProcess) return require("util").inspect(value) // eslint-disable-line global-require
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)}
}
function noTimeoutRightNow() {
throw new Error("o.timeout must be called snchronously from within a test definition or a hook")
}
var colorCodes = {
red: "31m",
red2: "31;1m",
green: "32;1m"
}
function highlight(message, color) {
var code = colorCodes[color] || colorCodes.red;
return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + message + "\x1b[0m" : message) : "%c" + message + "%c "
}
function cStyle(color, bold) {
return hasProcess||!color ? "" : "color:"+color+(bold ? ";font-weight:bold" : "")
}
function ensureStackTrace(error) {
// mandatory to get a stack in IE 10 and 11 (and maybe other envs?)
if (error.stack === undefined) try { throw error } catch(e) {return e}
else return error
}
function getStackName(e, exp) {
return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null
}
o.report = function (results) {
var errCount = 0
for (var i = 0, r; r = results[i]; i++) {
if (r.pass == null) {
r.testError.stack = r.message + "\n" + o.cleanStackTrace(r.testError)
r.testError.message = r.message
throw r.testError
}
if (!r.pass) {
var stackTrace = o.cleanStackTrace(r.error)
var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1
if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || ""
console.error(
(hasProcess ? "\n" : "") +
highlight(r.context + ":", "red2") + "\n" +
highlight(r.message, "red") +
(stackTrace ? "\n" + stackTrace + "\n" : ""),
cStyle("black", true), "", // reset to default
cStyle("red"), cStyle("black")
)
errCount++
}
}
var pl = results.length === 1 ? "" : "s"
var resultSummary = (errCount === 0) ?
highlight((pl ? "All " : "The ") + results.length + " assertion" + pl + " passed", "green"):
highlight(errCount + " out of " + results.length + " assertion" + pl + " failed", "red2")
var runningTime = " in " + Math.round(Date.now() - start) + "ms"
console.log(
(hasProcess ? "\n" : "") +
(name ? name + ": " : "") + resultSummary + runningTime,
cStyle((errCount === 0 ? "green" : "red"), true), ""
)
return errCount
}
if (hasProcess) {
nextTickish = process.nextTick
} else {
nextTickish = function fakeFastNextTick(next) {
if (stack++ < 5000) next()
else setTimeout(next, stack = 0)
}
}
return o
})
()

View file

@ -1,9 +1,8 @@
{
"name": "ospec",
"version": "3.0.1",
"version": "3.1.0",
"description": "Noiseless testing framework",
"main": "ospec.js",
"module": "ospec.mjs",
"directories": {
"test": "tests"
},
@ -13,11 +12,8 @@
"bin": {
"ospec": "./bin/ospec"
},
"scripts": {
"prepublishOnly": "node esm.js"
},
"repository": "MithrilJS/mithril.js",
"dependencies": {
"glob": "^7.1.2"
"glob": "^7.1.3"
}
}

1672
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,15 +4,13 @@
"description": "A framework for building brilliant applications",
"author": "Leo Horie",
"license": "MIT",
"main": "mithril.js",
"module": "mithril.mjs",
"unpkg": "mithril.min.js",
"repository": "MithrilJS/mithril.js",
"scripts": {
"dev": "node bundler/cli browser.js -output mithril.js -watch",
"build": "npm run build-browser & npm run build-min && npm run build-esm",
"build": "npm run build-browser & npm run build-min",
"build-browser": "node bundler/cli browser.js -output mithril.js",
"build-min": "node bundler/cli browser.js -output mithril.min.js -minify",
"build-esm": "node esm.js",
"precommit": "lint-staged",
"lintdocs": "node docs/lint",
"gendocs": "node docs/generate",
@ -24,7 +22,7 @@
"cover": "istanbul cover --print both ospec/bin/ospec",
"release": "npm version -m 'v%s'",
"preversion": "npm run test",
"version": "npm run build && git add mithril.js mithril.min.js mithril.mjs mithril.min.mjs",
"version": "npm run build && git add mithril.js mithril.min.js",
"postversion": "git push --follow-tags"
},
"devDependencies": {
@ -32,15 +30,14 @@
"benchmark": "^2.1.4",
"chokidar": "^2.0.4",
"dedent": "^0.7.0",
"eslint": "^5.9.0",
"gh-pages": "^0.12.0",
"glob": "^7.1.2",
"eslint": "^5.13.0",
"gh-pages": "^2.0.1",
"istanbul": "^0.4.5",
"lint-staged": "^4.0.4",
"lint-staged": "^8.1.3",
"locater": "^1.3.0",
"marked": "^0.3.19",
"marked": "^0.6.0",
"pinpoint": "^1.1.0",
"terser": "^3.10.11"
"terser": "^3.16.1"
},
"bin": {
"ospec": "./ospec/bin/ospec"

5
pathname/assign.js Normal file
View file

@ -0,0 +1,5 @@
"use strict"
module.exports = Object.assign || function(target, source) {
Object.keys(source).forEach(function(key) { target[key] = source[key] })
}

43
pathname/build.js Normal file
View file

@ -0,0 +1,43 @@
"use strict"
var buildQueryString = require("../querystring/build")
var assign = require("./assign")
// Returns `path` from `template` + `params`
module.exports = function(template, params) {
if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) {
throw new SyntaxError("Template parameter names *must* be separated")
}
if (params == null) return template
var queryIndex = template.indexOf("?")
var hashIndex = template.indexOf("#")
var queryEnd = hashIndex < 0 ? template.length : hashIndex
var pathEnd = queryIndex < 0 ? queryEnd : queryIndex
var path = template.slice(0, pathEnd)
var query = {}
assign(query, params)
var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m, key, variadic) {
delete query[key]
// If no such parameter exists, don't interpolate it.
if (params[key] == null) return m
// Escape normal parameters, but not variadic ones.
return variadic ? params[key] : encodeURIComponent(String(params[key]))
})
// In case the template substitution adds new query/hash parameters.
var newQueryIndex = resolved.indexOf("?")
var newHashIndex = resolved.indexOf("#")
var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex
var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex
var result = resolved.slice(0, newPathEnd)
if (queryIndex >= 0) result += "?" + template.slice(queryIndex, queryEnd)
if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd)
var querystring = buildQueryString(query)
if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring
if (hashIndex >= 0) result += template.slice(hashIndex)
if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex)
return result
}

View file

@ -0,0 +1,43 @@
"use strict"
var parsePathname = require("./parse")
// Compiles a template into a function that takes a resolved path (without query
// strings) and returns an object containing the template parameters with their
// parsed values. This expects the input of the compiled template to be the
// output of `parsePathname`. Note that it does *not* remove query parameters
// specified in the template.
module.exports = function(template) {
var templateData = parsePathname(template)
var templateKeys = Object.keys(templateData.params)
var keys = []
var regexp = new RegExp("^" + templateData.path.replace(
// I escape literal text so people can use things like `:file.:ext` or
// `:lang-:locale` in routes. This is all merged into one pass so I
// don't also accidentally escape `-` and make it harder to detect it to
// ban it from template parameters.
/:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g,
function(m, key, extra) {
if (key == null) return "\\" + m
keys.push({k: key, r: extra === "..."})
if (extra === "...") return "(.*)"
if (extra === ".") return "([^/]+)\\."
return "([^/]+)" + (extra || "")
}
) + "$")
return function(data) {
// First, check the params. Usually, there isn't any, and it's just
// checking a static set.
for (var i = 0; i < templateKeys.length; i++) {
if (templateData.params[templateKeys[i]] !== data.params[templateKeys[i]]) return false
}
// If no interpolations exist, let's skip all the ceremony
if (!keys.length) return regexp.test(data.path)
var values = regexp.exec(data.path)
if (values == null) return false
for (var i = 0; i < keys.length; i++) {
data.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1])
}
return true
}
}

24
pathname/parse.js Normal file
View file

@ -0,0 +1,24 @@
"use strict"
var parseQueryString = require("../querystring/parse")
// Returns `{path, params}` from `url`
module.exports = function(url) {
var queryIndex = url.indexOf("?")
var hashIndex = url.indexOf("#")
var queryEnd = hashIndex < 0 ? url.length : hashIndex
var pathEnd = queryIndex < 0 ? queryEnd : queryIndex
var path = url.slice(0, pathEnd).replace(/\/{2,}/g, "/")
var params = {}
if (!path) path = "/"
else {
if (path[0] !== "/") path = "/" + path
if (path.length > 1 && path[path.length - 1] === "/") path = path.slice(0, -1)
}
// Note: these are reversed because `parseQueryString` appends parameters
// only if they don't exist. Please don't flip them.
if (queryIndex >= 0) parseQueryString(url.slice(queryIndex + 1, queryEnd), params)
if (hashIndex >= 0) parseQueryString(url.slice(hashIndex + 1), params)
return {path: path, params: params}
}

19
pathname/tests/index.html Normal file
View file

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="../../module/module.js"></script>
<script src="../../ospec/ospec.js"></script>
<script src="../../pathname/build.js"></script>
<script src="../../pathname/parse.js"></script>
<script src="../../pathname/parseTemplate.js"></script>
<script src="test-buildPathname.js"></script>
<script src="test-parsePathname.js"></script>
<script src="test-parseTemplate.js"></script>
<script>require("../../ospec/ospec").run()</script>
</body>
</html>

View file

@ -0,0 +1,109 @@
"use strict"
var o = require("../../ospec/ospec")
var buildPathname = require("../../pathname/build")
o.spec("buildPathname", function() {
function test(prefix) {
o("returns path if no params", function () {
var string = buildPathname(prefix + "/route/foo", undefined)
o(string).equals(prefix + "/route/foo")
})
o("skips interpolation if no params", function () {
var string = buildPathname(prefix + "/route/:id", undefined)
o(string).equals(prefix + "/route/:id")
})
o("appends query strings", function () {
var string = buildPathname(prefix + "/route/foo", {a: "b", c: 1})
o(string).equals(prefix + "/route/foo?a=b&c=1")
})
o("inserts template parameters at end", function () {
var string = buildPathname(prefix + "/route/:id", {id: "1"})
o(string).equals(prefix + "/route/1")
})
o("inserts template parameters at beginning", function () {
var string = buildPathname(prefix + "/:id/foo", {id: "1"})
o(string).equals(prefix + "/1/foo")
})
o("inserts template parameters at middle", function () {
var string = buildPathname(prefix + "/route/:id/foo", {id: "1"})
o(string).equals(prefix + "/route/1/foo")
})
o("inserts variadic paths", function () {
var string = buildPathname(prefix + "/route/:foo...", {foo: "id/1"})
o(string).equals(prefix + "/route/id/1")
})
o("inserts variadic paths with initial slashes", function () {
var string = buildPathname(prefix + "/route/:foo...", {foo: "/id/1"})
o(string).equals(prefix + "/route//id/1")
})
o("skips template parameters at end if param missing", function () {
var string = buildPathname(prefix + "/route/:id", {param: 1})
o(string).equals(prefix + "/route/:id?param=1")
})
o("skips template parameters at beginning if param missing", function () {
var string = buildPathname(prefix + "/:id/foo", {param: 1})
o(string).equals(prefix + "/:id/foo?param=1")
})
o("skips template parameters at middle if param missing", function () {
var string = buildPathname(prefix + "/route/:id/foo", {param: 1})
o(string).equals(prefix + "/route/:id/foo?param=1")
})
o("skips variadic template parameters if param missing", function () {
var string = buildPathname(prefix + "/route/:foo...", {param: "/id/1"})
o(string).equals(prefix + "/route/:foo...?param=%2Fid%2F1")
})
o("handles escaped values", function() {
var data = buildPathname(prefix + "/route/:foo", {"foo": ";:@&=+$,/?%#"})
o(data).equals(prefix + "/route/%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23")
})
o("handles unicode", function() {
var data = buildPathname(prefix + "/route/:ö", {"ö": "ö"})
o(data).equals(prefix + "/route/%C3%B6")
})
o("handles zero", function() {
var string = buildPathname(prefix + "/route/:a", {a: 0})
o(string).equals(prefix + "/route/0")
})
o("handles false", function() {
var string = buildPathname(prefix + "/route/:a", {a: false})
o(string).equals(prefix + "/route/false")
})
o("handles dashes", function() {
var string = buildPathname(prefix + "/:lang-:region/route", {
lang: "en",
region: "US"
})
o(string).equals(prefix + "/en-US/route")
})
o("handles dots", function() {
var string = buildPathname(prefix + "/:file.:ext/view", {
file: "image",
ext: "png"
})
o(string).equals(prefix + "/image.png/view")
})
}
o.spec("absolute", function() { test("") })
o.spec("relative", function() { test("..") })
o.spec("absolute + domain", function() { test("https://example.com") })
o.spec("absolute + `file:`", function() { test("file://") })
})

View file

@ -0,0 +1,233 @@
"use strict"
var o = require("../../ospec/ospec")
var parsePathname = require("../../pathname/parse")
var compileTemplate = require("../../pathname/compileTemplate")
o.spec("compileTemplate", function() {
o("checks empty string", function() {
var data = parsePathname("/")
o(compileTemplate("/")(data)).equals(true)
o(data.params).deepEquals({})
})
o("checks identical match", function() {
var data = parsePathname("/foo")
o(compileTemplate("/foo")(data)).equals(true)
o(data.params).deepEquals({})
})
o("checks identical mismatch", function() {
var data = parsePathname("/bar")
o(compileTemplate("/foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks single parameter", function() {
var data = parsePathname("/1")
o(compileTemplate("/:id")(data)).equals(true)
o(data.params).deepEquals({id: "1"})
})
o("checks single variadic parameter", function() {
var data = parsePathname("/some/path")
o(compileTemplate("/:id...")(data)).equals(true)
o(data.params).deepEquals({id: "some/path"})
})
o("checks single parameter with extra match", function() {
var data = parsePathname("/1/foo")
o(compileTemplate("/:id/foo")(data)).equals(true)
o(data.params).deepEquals({id: "1"})
})
o("checks single parameter with extra mismatch", function() {
var data = parsePathname("/1/bar")
o(compileTemplate("/:id/foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks single variadic parameter with extra match", function() {
var data = parsePathname("/some/path/foo")
o(compileTemplate("/:id.../foo")(data)).equals(true)
o(data.params).deepEquals({id: "some/path"})
})
o("checks single variadic parameter with extra mismatch", function() {
var data = parsePathname("/some/path/bar")
o(compileTemplate("/:id.../foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple parameters", function() {
var data = parsePathname("/1/2")
o(compileTemplate("/:id/:name")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks incomplete multiple parameters", function() {
var data = parsePathname("/1")
o(compileTemplate("/:id/:name")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple parameters with extra match", function() {
var data = parsePathname("/1/2/foo")
o(compileTemplate("/:id/:name/foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks multiple parameters with extra mismatch", function() {
var data = parsePathname("/1/2/bar")
o(compileTemplate("/:id/:name/foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple parameters, last variadic, with extra match", function() {
var data = parsePathname("/1/some/path/foo")
o(compileTemplate("/:id/:name.../foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "some/path"})
})
o("checks multiple parameters, last variadic, with extra mismatch", function() {
var data = parsePathname("/1/some/path/bar")
o(compileTemplate("/:id/:name.../foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters", function() {
var data = parsePathname("/1/sep/2")
o(compileTemplate("/:id/sep/:name")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks incomplete multiple separated parameters", function() {
var data = parsePathname("/1")
o(compileTemplate("/:id/sep/:name")(data)).equals(false)
o(data.params).deepEquals({})
data = parsePathname("/1/sep")
o(compileTemplate("/:id/sep/:name")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters missing sep", function() {
var data = parsePathname("/1/2")
o(compileTemplate("/:id/sep/:name")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters with extra match", function() {
var data = parsePathname("/1/sep/2/foo")
o(compileTemplate("/:id/sep/:name/foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks multiple separated parameters with extra mismatch", function() {
var data = parsePathname("/1/sep/2/bar")
o(compileTemplate("/:id/sep/:name/foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters, last variadic, with extra match", function() {
var data = parsePathname("/1/sep/some/path/foo")
o(compileTemplate("/:id/sep/:name.../foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "some/path"})
})
o("checks multiple separated parameters, last variadic, with extra mismatch", function() {
var data = parsePathname("/1/sep/some/path/bar")
o(compileTemplate("/:id/sep/:name.../foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple parameters + prefix", function() {
var data = parsePathname("/route/1/2")
o(compileTemplate("/route/:id/:name")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks incomplete multiple parameters + prefix", function() {
var data = parsePathname("/route/1")
o(compileTemplate("/route/:id/:name")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple parameters + prefix with extra match", function() {
var data = parsePathname("/route/1/2/foo")
o(compileTemplate("/route/:id/:name/foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks multiple parameters + prefix with extra mismatch", function() {
var data = parsePathname("/route/1/2/bar")
o(compileTemplate("/route/:id/:name/foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple parameters + prefix, last variadic, with extra match", function() {
var data = parsePathname("/route/1/some/path/foo")
o(compileTemplate("/route/:id/:name.../foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "some/path"})
})
o("checks multiple parameters + prefix, last variadic, with extra mismatch", function() {
var data = parsePathname("/route/1/some/path/bar")
o(compileTemplate("/route/:id/:name.../foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters + prefix", function() {
var data = parsePathname("/route/1/sep/2")
o(compileTemplate("/route/:id/sep/:name")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks incomplete multiple separated parameters + prefix", function() {
var data = parsePathname("/route/1")
o(compileTemplate("/route/:id/sep/:name")(data)).equals(false)
o(data.params).deepEquals({})
var data = parsePathname("/route/1/sep")
o(compileTemplate("/route/:id/sep/:name")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters + prefix missing sep", function() {
var data = parsePathname("/route/1/2")
o(compileTemplate("/route/:id/sep/:name")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters + prefix with extra match", function() {
var data = parsePathname("/route/1/sep/2/foo")
o(compileTemplate("/route/:id/sep/:name/foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "2"})
})
o("checks multiple separated parameters + prefix with extra mismatch", function() {
var data = parsePathname("/route/1/sep/2/bar")
o(compileTemplate("/route/:id/sep/:name/foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks multiple separated parameters + prefix, last variadic, with extra match", function() {
var data = parsePathname("/route/1/sep/some/path/foo")
o(compileTemplate("/route/:id/sep/:name.../foo")(data)).equals(true)
o(data.params).deepEquals({id: "1", name: "some/path"})
})
o("checks multiple separated parameters + prefix, last variadic, with extra mismatch", function() {
var data = parsePathname("/route/1/sep/some/path/bar")
o(compileTemplate("/route/:id/sep/:name.../foo")(data)).equals(false)
o(data.params).deepEquals({})
})
o("checks query params match", function() {
var data = parsePathname("/route/1?foo=bar")
o(compileTemplate("/route/:id?foo=bar")(data)).equals(true)
o(data.params).deepEquals({id: "1", foo: "bar"})
})
o("checks query params mismatch", function() {
var data = parsePathname("/route/1?foo=bar")
o(compileTemplate("/route/:id?foo=1")(data)).equals(false)
o(data.params).deepEquals({foo: "bar"})
o(compileTemplate("/route/:id?bar=foo")(data)).equals(false)
o(data.params).deepEquals({foo: "bar"})
})
o("checks hash params match", function() {
var data = parsePathname("/route/1#foo=bar")
o(compileTemplate("/route/:id#foo=bar")(data)).equals(true)
o(data.params).deepEquals({id: "1", foo: "bar"})
})
o("checks hash params mismatch", function() {
var data = parsePathname("/route/1#foo=bar")
o(compileTemplate("/route/:id#foo=1")(data)).equals(false)
o(data.params).deepEquals({foo: "bar"})
o(compileTemplate("/route/:id#bar=foo")(data)).equals(false)
o(data.params).deepEquals({foo: "bar"})
})
o("checks dot before dot", function() {
var data = parsePathname("/file.test.png/edit")
o(compileTemplate("/:file.:ext/edit")(data)).equals(true)
o(data.params).deepEquals({file: "file.test", ext: "png"})
})
o("checks dash before dot", function() {
var data = parsePathname("/file-test.png/edit")
o(compileTemplate("/:file.:ext/edit")(data)).equals(true)
o(data.params).deepEquals({file: "file-test", ext: "png"})
})
o("checks dot before dash", function() {
var data = parsePathname("/file.test-png/edit")
o(compileTemplate("/:file-:ext/edit")(data)).equals(true)
o(data.params).deepEquals({file: "file.test", ext: "png"})
})
o("checks dash before dash", function() {
var data = parsePathname("/file-test-png/edit")
o(compileTemplate("/:file-:ext/edit")(data)).equals(true)
o(data.params).deepEquals({file: "file-test", ext: "png"})
})
})

View file

@ -0,0 +1,126 @@
"use strict"
var o = require("../../ospec/ospec")
var parsePathname = require("../../pathname/parse")
o.spec("parsePathname", function() {
o("parses empty string", function() {
var data = parsePathname("")
o(data).deepEquals({
path: "/",
params: {}
})
})
o("parses query at start", function() {
var data = parsePathname("?a=b&c=d")
o(data).deepEquals({
path: "/",
params: {a: "b", c: "d"}
})
})
o("parses hash at start", function() {
var data = parsePathname("#a=b&c=d")
o(data).deepEquals({
path: "/",
params: {a: "b", c: "d"}
})
})
o("parses query + hash at start", function() {
var data = parsePathname("?a=1&b=2#c=3&d=4")
o(data).deepEquals({
path: "/",
params: {a: "1", b: "2", c: "3", d: "4"}
})
})
o("parses root", function() {
var data = parsePathname("/")
o(data).deepEquals({
path: "/",
params: {}
})
})
o("parses root + query at start", function() {
var data = parsePathname("/?a=b&c=d")
o(data).deepEquals({
path: "/",
params: {a: "b", c: "d"}
})
})
o("parses root + hash at start", function() {
var data = parsePathname("/#a=b&c=d")
o(data).deepEquals({
path: "/",
params: {a: "b", c: "d"}
})
})
o("parses root + query + hash at start", function() {
var data = parsePathname("/?a=1&b=2#c=3&d=4")
o(data).deepEquals({
path: "/",
params: {a: "1", b: "2", c: "3", d: "4"}
})
})
o("parses route", function() {
var data = parsePathname("/route/foo")
o(data).deepEquals({
path: "/route/foo",
params: {}
})
})
o("parses route + empty query", function() {
var data = parsePathname("/route/foo?")
o(data).deepEquals({
path: "/route/foo",
params: {}
})
})
o("parses route + empty hash", function() {
var data = parsePathname("/route/foo?")
o(data).deepEquals({
path: "/route/foo",
params: {}
})
})
o("parses route + empty query + empty hash", function() {
var data = parsePathname("/route/foo?#")
o(data).deepEquals({
path: "/route/foo",
params: {}
})
})
o("parses route + query", function() {
var data = parsePathname("/route/foo?a=1&b=2")
o(data).deepEquals({
path: "/route/foo",
params: {a: "1", b: "2"}
})
})
o("parses route + hash", function() {
var data = parsePathname("/route/foo?c=3&d=4")
o(data).deepEquals({
path: "/route/foo",
params: {c: "3", d: "4"}
})
})
o("parses route + query + hash", function() {
var data = parsePathname("/route/foo?a=1&b=2#c=3&d=4")
o(data).deepEquals({
path: "/route/foo",
params: {a: "1", b: "2", c: "3", d: "4"}
})
})
o("deduplicates same-named params in query + hash", function() {
var data = parsePathname("/route/foo?a=1&b=2#a=3&c=4")
o(data).deepEquals({
path: "/route/foo",
params: {a: "3", b: "2", c: "4"}
})
})
o("parses route + query + hash with lots of junk slashes", function() {
var data = parsePathname("//route/////foo//?a=1&b=2#c=3&d=4")
o(data).deepEquals({
path: "/route/foo",
params: {a: "1", b: "2", c: "3", d: "4"}
})
})
})

View file

@ -1,10 +1,13 @@
"use strict"
module.exports = function(string) {
// The extra `data` parameter is for if you want to append to an existing
// parameters object.
module.exports = function(string, data) {
if (data == null) data = {}
if (string === "" || string == null) return {}
if (string.charAt(0) === "?") string = string.slice(1)
var entries = string.split("&"), data = {}, counters = {}
var entries = string.split("&"), counters = {}
for (var i = 0; i < entries.length; i++) {
var entry = entries[i].split("=")
var key = decodeURIComponent(entry[0])
@ -22,12 +25,13 @@ module.exports = function(string) {
var isValue = j === levels.length - 1
if (level === "") {
var key = levels.slice(0, j).join()
if (counters[key] == null) counters[key] = 0
if (counters[key] == null) {
counters[key] = Array.isArray(cursor) ? cursor.length : 0
}
level = counters[key]++
}
if (cursor[level] == null) {
cursor[level] = isValue ? value : isNumber ? [] : {}
}
if (isValue) cursor[level] = value
else if (cursor[level] == null) cursor[level] = isNumber ? [] : {}
cursor = cursor[level]
}
}

View file

@ -93,4 +93,20 @@ o.spec("parseQueryString", function() {
var data = parseQueryString("a")
o(data).deepEquals({a: ""})
})
o("prefers later values", function() {
var data = parseQueryString("a=1&b=2&a=3")
o(data).deepEquals({a: "3", b: "2"})
})
o("continues to append to arrays between calls", function() {
var data = {}
parseQueryString("a[]=1&a[]=2", data)
parseQueryString("a[]=3&a[]=4", data)
o(data).deepEquals({a: ["1", "2", "3", "4"]})
})
o("continues to append to objects between calls", function() {
var data = {}
parseQueryString("a[b]=1&a[c]=2", data)
parseQueryString("a[d]=3&a[e]=4", data)
o(data).deepEquals({a: {b: "1", c: "2", d: "3", e: "4"}})
})
})

View file

@ -1,6 +1,6 @@
"use strict"
var buildQueryString = require("../querystring/build")
var buildPathname = require("../pathname/build")
module.exports = function($window, Promise) {
var callbackCount = 0
@ -11,7 +11,7 @@ module.exports = function($window, Promise) {
if (typeof url !== "string") { args = url; url = url.url }
else if (args == null) args = {}
var promise = new Promise(function(resolve, reject) {
factory(url, args, function (data) {
factory(buildPathname(url, args.params), args, function (data) {
if (typeof args.type === "function") {
if (Array.isArray(data)) {
for (var i = 0; i < data.length; i++) {
@ -54,30 +54,11 @@ module.exports = function($window, Promise) {
return false
}
function interpolate(url, data, assemble) {
if (data == null) return url
url = url.replace(/:([^\/]+)/gi, function (m, key) {
return data[key] != null ? data[key] : m
})
if (assemble && data != null) {
var querystring = buildQueryString(data)
if (querystring) url += (url.indexOf("?") < 0 ? "?" : "&") + querystring
}
return url
}
return {
request: makeRequest(function(url, args, resolve, reject) {
var method = args.method != null ? args.method.toUpperCase() : "GET"
var useBody = method !== "GET" && method !== "TRACE" &&
(typeof args.useBody !== "boolean" || args.useBody)
var data = args.data
var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(data instanceof $window.FormData)
if (useBody) {
if (typeof args.serialize === "function") data = args.serialize(data)
else if (!(data instanceof $window.FormData)) data = JSON.stringify(data)
}
var body = args.body
var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData)
var xhr = new $window.XMLHttpRequest(),
aborted = false,
@ -88,9 +69,9 @@ module.exports = function($window, Promise) {
_abort.call(xhr)
}
xhr.open(method, interpolate(url, args.data, !useBody), typeof args.async !== "boolean" || args.async, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
if (assumeJSON && useBody && !hasHeader(args, /^content-type$/i)) {
if (assumeJSON && !hasHeader(args, /^content-type$/i)) {
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8")
}
if (typeof args.deserialize !== "function" && !hasHeader(args, /^accept$/i)) {
@ -98,7 +79,7 @@ module.exports = function($window, Promise) {
}
if (args.withCredentials) xhr.withCredentials = args.withCredentials
if (args.timeout) xhr.timeout = args.timeout
if (args.responseType) xhr.responseType = args.responseType
xhr.responseType = args.responseType || (typeof args.extract === "function" ? "" : "json")
for (var key in args.headers) {
if ({}.hasOwnProperty.call(args.headers, key)) {
@ -115,19 +96,38 @@ module.exports = function($window, Promise) {
if (xhr.readyState === 4) {
try {
var success = (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || (/^file:\/\//i).test(url)
var response = xhr.responseText
// When the response type isn't "" or "text",
// `xhr.responseText` is the wrong thing to use.
// Browsers do the right thing and throw here, and we
// should honor that and do the right thing by
// preferring `xhr.response` where possible/practical.
var response = xhr.response, message
if (response == null) {
try {
response = xhr.responseText
// Note: this snippet is intentionally *after*
// `xhr.responseText` is accessed, since the
// above will throw in modern browsers (thus
// skipping the rest of this section). It's an
// IE hack to detect and work around the lack of
// native `responseType: "json"` support there.
if (typeof args.extract !== "function" && xhr.responseType === "json") response = JSON.parse(response)
}
catch (e) { response = null }
}
if (typeof args.extract === "function") {
response = args.extract(xhr, args)
success = true
} else if (typeof args.deserialize === "function") {
response = args.deserialize(response)
} else {
try {response = response ? JSON.parse(response) : null}
catch (e) {throw new Error("Invalid JSON: " + response)}
}
if (success) resolve(response)
else {
var error = new Error(xhr.responseText)
try { message = xhr.responseText }
catch (e) { message = response }
var error = new Error(message)
error.code = xhr.status
error.response = response
reject(error)
@ -139,23 +139,24 @@ module.exports = function($window, Promise) {
}
}
if (useBody && data != null) xhr.send(data)
else xhr.send()
if (body == null) xhr.send()
else if (typeof args.serialize === "function") xhr.send(args.serialize(body))
else if (body instanceof $window.FormData) xhr.send(body)
else xhr.send(JSON.stringify(body))
}),
jsonp: makeRequest(function(url, args, resolve, reject) {
var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
var script = $window.document.createElement("script")
$window[callbackName] = function(data) {
delete $window[callbackName]
script.parentNode.removeChild(script)
resolve(data)
delete $window[callbackName]
}
script.onerror = function() {
delete $window[callbackName]
script.parentNode.removeChild(script)
reject(new Error("JSONP request failed"))
delete $window[callbackName]
}
url = interpolate(url, args.data, true)
script.src = url + (url.indexOf("?") < 0 ? "?" : "&") +
encodeURIComponent(args.callbackKey || "callback") + "=" +
encodeURIComponent(callbackName)

View file

@ -47,7 +47,7 @@ o.spec("jsonp", function() {
return {status: 200, responseText: queryData["callback"] + "(" + JSON.stringify(queryData) + ")"}
}
})
jsonp({url: "/item", data: {a: "b", c: "d"}}).then(function(data) {
jsonp({url: "/item", params: {a: "b", c: "d"}}).then(function(data) {
delete data["callback"]
o(data).deepEquals({a: "b", c: "d"})
}).then(done)

View file

@ -79,7 +79,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.query})}
}
})
xhr({method: "GET", url: "/item", data: {x: "y"}}).then(function(data) {
xhr({method: "GET", url: "/item", params: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "?x=y"})
}).then(done)
})
@ -89,7 +89,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})}
}
})
xhr({method: "POST", url: "/item", data: {x: "y"}}).then(function(data) {
xhr({method: "POST", url: "/item", body: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: {x: "y"}})
}).then(done)
})
@ -99,7 +99,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.query})}
}
})
xhr({method: "GET", url: "/item", data: {x: ":y"}}).then(function(data) {
xhr({method: "GET", url: "/item", params: {x: ":y"}}).then(function(data) {
o(data).deepEquals({a: "?x=%3Ay"})
}).then(done)
})
@ -109,28 +109,88 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})}
}
})
xhr({method: "POST", url: "/item", data: {x: ":y"}}).then(function(data) {
xhr({method: "POST", url: "/item", body: {x: ":y"}}).then(function(data) {
o(data).deepEquals({a: {x: ":y"}})
}).then(done)
})
o("works w/ parameterized url via GET", function(done) {
mock.$defineRoutes({
"GET /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query})}
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
}
})
xhr({method: "GET", url: "/item/:x", data: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?x=y"})
xhr({method: "GET", url: "/item/:x", params: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: null})
}).then(done)
})
o("works w/ parameterized url via POST", function(done) {
mock.$defineRoutes({
"POST /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})}
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
}
})
xhr({method: "POST", url: "/item/:x", data: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {x: "y"}})
xhr({method: "POST", url: "/item/:x", params: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: null})
}).then(done)
})
o("works w/ parameterized url + body via GET", function(done) {
mock.$defineRoutes({
"GET /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
}
})
xhr({method: "GET", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}})
}).then(done)
})
o("works w/ parameterized url + body via POST", function(done) {
mock.$defineRoutes({
"POST /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
}
})
xhr({method: "POST", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}})
}).then(done)
})
o("works w/ parameterized url + query via GET", function(done) {
mock.$defineRoutes({
"GET /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
}
})
xhr({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: null})
}).then(done)
})
o("works w/ parameterized url + query via POST", function(done) {
mock.$defineRoutes({
"POST /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
}
})
xhr({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: null})
}).then(done)
})
o("works w/ parameterized url + query + body via GET", function(done) {
mock.$defineRoutes({
"GET /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
}
})
xhr({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}})
}).then(done)
})
o("works w/ parameterized url + query + body via POST", function(done) {
mock.$defineRoutes({
"POST /item/y": function(request) {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
}
})
xhr({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}})
}).then(done)
})
o("works w/ array", function(done) {
@ -139,7 +199,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})}
}
})
xhr({method: "POST", url: "/items", data: [{x: "y"}]}).then(function(data) {
xhr({method: "POST", url: "/items", body: [{x: "y"}]}).then(function(data) {
o(data).deepEquals({a: "/items", b: [{x: "y"}]})
}).then(done)
})
@ -201,7 +261,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({body: request.query})}
}
})
xhr({method: "GET", url: "/item", serialize: serialize, data: {id: 1}}).then(function(data) {
xhr({method: "GET", url: "/item", serialize: serialize, params: {id: 1}}).then(function(data) {
o(data.body).equals("?id=1")
}).then(done)
})
@ -215,7 +275,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({body: request.body})}
}
})
xhr({method: "POST", url: "/item", serialize: serialize, data: {id: 1}}).then(function(data) {
xhr({method: "POST", url: "/item", serialize: serialize, body: {id: 1}}).then(function(data) {
o(data.body).equals("id=1")
}).then(done)
})
@ -230,7 +290,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) {
o(data).equals("{\"test\":123}")
o(data).deepEquals({test: 123})
}).then(done)
})
o("deserialize parameter works in POST", function(done) {
@ -244,12 +304,12 @@ o.spec("xhr", function() {
}
})
xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) {
o(data).equals("{\"test\":123}")
o(data).deepEquals({test: 123})
}).then(done)
})
o("extract parameter works in GET", function(done) {
var extract = function() {
return JSON.stringify({test: 123})
return {test: 123}
}
mock.$defineRoutes({
@ -258,12 +318,12 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item", extract: extract}).then(function(data) {
o(data).equals("{\"test\":123}")
o(data).deepEquals({test: 123})
}).then(done)
})
o("extract parameter works in POST", function(done) {
var extract = function() {
return JSON.stringify({test: 123})
return {test: 123}
}
mock.$defineRoutes({
@ -272,7 +332,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "POST", url: "/item", extract: extract}).then(function(data) {
o(data).equals("{\"test\":123}")
o(data).deepEquals({test: 123})
}).then(done)
})
o("ignores deserialize if extract is defined", function(done) {
@ -485,7 +545,8 @@ o.spec("xhr", function() {
})
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true)
o(e.message).equals(JSON.stringify({error: "error"}))
o(e.message).equals("[object Object]")
o(e.response).deepEquals({error: "error"})
o(e.code).equals(500)
}).then(done)
})
@ -508,7 +569,8 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e.message).equals("Invalid JSON: error")
o(e.message).equals("null")
o(e.response).equals(null)
}).then(done)
})
o("triggers all branched catches upon rejection", function(done) {

View file

@ -1,7 +1,9 @@
"use strict"
var buildQueryString = require("../querystring/build")
var parseQueryString = require("../querystring/parse")
var buildPathname = require("../pathname/build")
var parsePathname = require("../pathname/parse")
var compileTemplate = require("../pathname/compileTemplate")
var assign = require("../pathname/assign")
module.exports = function($window) {
var supportsPushState = typeof $window.history.pushState === "function"
@ -14,58 +16,15 @@ module.exports = function($window) {
}
var asyncId
function debounceAsync(callback) {
return function() {
if (asyncId != null) return
asyncId = callAsync(function() {
asyncId = null
callback()
})
}
}
function parsePath(path, queryData, hashData) {
var queryIndex = path.indexOf("?")
var hashIndex = path.indexOf("#")
var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length
if (queryIndex > -1) {
var queryEnd = hashIndex > -1 ? hashIndex : path.length
var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd))
for (var key in queryParams) queryData[key] = queryParams[key]
}
if (hashIndex > -1) {
var hashParams = parseQueryString(path.slice(hashIndex + 1))
for (var key in hashParams) hashData[key] = hashParams[key]
}
return path.slice(0, pathEnd)
}
var router = {prefix: "#!"}
router.getPath = function() {
var type = router.prefix.charAt(0)
switch (type) {
case "#": return normalize("hash").slice(router.prefix.length)
case "?": return normalize("search").slice(router.prefix.length) + normalize("hash")
default: return normalize("pathname").slice(router.prefix.length) + normalize("search") + normalize("hash")
}
if (router.prefix.charAt(0) === "#") return normalize("hash").slice(router.prefix.length)
if (router.prefix.charAt(0) === "?") return normalize("search").slice(router.prefix.length) + normalize("hash")
return normalize("pathname").slice(router.prefix.length) + normalize("search") + normalize("hash")
}
router.setPath = function(path, data, options) {
var queryData = {}, hashData = {}
path = parsePath(path, queryData, hashData)
if (data != null) {
for (var key in data) queryData[key] = data[key]
path = path.replace(/:([^\/]+)/g, function(match, token) {
delete queryData[token]
return data[token]
})
}
var query = buildQueryString(queryData)
if (query) path += "?" + query
var hash = buildQueryString(hashData)
if (hash) path += "#" + hash
path = buildPathname(path, data)
if (supportsPushState) {
var state = options ? options.state : null
var title = options ? options.title : null
@ -75,36 +34,53 @@ module.exports = function($window) {
}
else $window.location.href = router.prefix + path
}
router.defineRoutes = function(routes, resolve, reject) {
router.defineRoutes = function(routes, resolve, reject, defaultRoute) {
var compiled = Object.keys(routes).map(function(route) {
if (route.charAt(0) !== "/") throw new SyntaxError("Routes must start with a `/`")
if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
}
return {
route: route,
component: routes[route],
check: compileTemplate(route),
}
})
if (defaultRoute != null) {
var defaultData = parsePathname(defaultRoute)
if (!compiled.some(function (i) { return i.check(defaultData) })) {
throw new ReferenceError("Default route doesn't match any known routes")
}
}
function resolveRoute() {
var path = router.getPath()
var params = {}
var pathname = parsePath(path, params, params)
var data = parsePathname(path)
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, "([^\\/]+)") + "\/?$")
assign(data.params, $window.history.state)
if (matcher.test(pathname)) {
pathname.replace(matcher, function() {
var keys = route.match(/:[^\/]+/g) || []
var values = [].slice.call(arguments, 1, -2)
for (var i = 0; i < keys.length; i++) {
params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
}
resolve(routes[route], params, path, route)
})
for (var i = 0; i < compiled.length; i++) {
if (compiled[i].check(data)) {
resolve(compiled[i].component, data.params, path, compiled[i].route)
return
}
}
reject(path, params)
reject(path, data.params)
}
if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute)
if (supportsPushState) {
$window.onpopstate = function() {
if (asyncId) return
asyncId = callAsync(function() {
asyncId = null
resolveRoute()
})
}
}
else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute
resolveRoute()
}

View file

@ -22,14 +22,14 @@ o.spec("Router.defineRoutes", function() {
o("calls onRouteChange on init", function(done) {
$window.location.href = prefix + "/a"
router.defineRoutes({"/a": {data: 1}}, onRouteChange, onFail)
callAsync(function() {
o(onRouteChange.callCount).equals(1)
done()
})
})
o("resolves to route", function(done) {
$window.location.href = prefix + "/test"
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
@ -38,7 +38,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"])
o(onFail.callCount).equals(0)
done()
})
})
@ -51,7 +51,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 2}, {"ö": "ö"}, "/ö?ö=ö#ö=ö", "/ö"])
o(onFail.callCount).equals(0)
done()
})
})
@ -64,7 +64,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 2}, {"ö": "ö"}, "/ö?ö=ö#ö=ö", "/ö"])
o(onFail.callCount).equals(0)
done()
})
})
@ -81,7 +81,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"])
o(onFail.callCount).equals(0)
done()
})
})
@ -94,7 +94,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "x"}, "/test/x", "/test/:a"])
o(onFail.callCount).equals(0)
done()
})
})
@ -107,7 +107,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "x", b: "y"}, "/test/x/y", "/test/:a/:b"])
o(onFail.callCount).equals(0)
done()
})
})
@ -120,7 +120,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "x/y"}, "/test/x/y", "/test/:a..."])
o(onFail.callCount).equals(0)
done()
})
})
@ -133,7 +133,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test?a=b&c=d", "/test"])
o(onFail.callCount).equals(0)
done()
})
})
@ -146,7 +146,7 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test#a=b&c=d", "/test"])
o(onFail.callCount).equals(0)
done()
})
})
@ -159,7 +159,20 @@ o.spec("Router.defineRoutes", function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test?a=b#c=d", "/test"])
o(onFail.callCount).equals(0)
done()
})
})
o("handles route with search and hash + duplicate params", function(done) {
$window.location.href = prefix + "/test?a=b#a=d"
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {a: "d"}, "/test?a=b#a=d", "/test"])
o(onFail.callCount).equals(0)
done()
})
})
@ -171,7 +184,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onFail.callCount).equals(1)
o(onFail.args).deepEquals(["/test", {}])
done()
})
})
@ -183,7 +196,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onFail.callCount).equals(1)
o(onFail.args).deepEquals(["/test?a=b#c=d", {a: "b", c: "d"}])
done()
})
})
@ -195,7 +208,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"])
done()
})
})
@ -207,7 +220,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."])
done()
})
})
@ -223,7 +236,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"])
done()
})
})
@ -239,7 +252,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."])
done()
})
})
@ -254,7 +267,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"])
done()
})
})
@ -269,7 +282,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."])
done()
})
})
@ -280,7 +293,7 @@ o.spec("Router.defineRoutes", function() {
callAsync(function() {
o(onRouteChange.callCount).equals(1)
done()
})
})

View file

@ -55,7 +55,7 @@ o.spec("Router.setPath", function() {
router.setPath("/other/x/y/z?c=d#e=f")
o(router.getPath()).equals("/other/x/y/z?c=d#e=f")
done()
})
})
@ -67,7 +67,7 @@ o.spec("Router.setPath", function() {
router.setPath("/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6")
o(router.getPath()).equals("/ö?ö=ö#ö=ö")
done()
})
})
@ -79,7 +79,7 @@ o.spec("Router.setPath", function() {
router.setPath("/ö?ö=ö#ö=ö")
o(router.getPath()).equals("/ö?ö=ö#ö=ö")
done()
})
})
@ -96,7 +96,7 @@ o.spec("Router.setPath", function() {
router.setPath("/other/x/y/z?c=d#e=f")
o(router.getPath()).equals("/other/x/y/z?c=d#e=f")
done()
})
})
@ -109,7 +109,7 @@ o.spec("Router.setPath", function() {
$window.onpopstate()
o(router.getPath()).equals("/other/x/y/z?c=d#e=f")
done()
})
})
@ -120,8 +120,8 @@ o.spec("Router.setPath", function() {
callAsync(function() {
router.setPath("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"})
o(router.getPath()).equals("/other/x/y/z?c=d&e=f")
o(router.getPath()).equals("/other/x/y%2Fz?c=d&e=f")
done()
})
})
@ -134,7 +134,7 @@ o.spec("Router.setPath", function() {
$window.history.back()
o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/")
done()
})
})
@ -149,7 +149,7 @@ o.spec("Router.setPath", function() {
var slash = prefix[0] === "/" ? "" : "/"
o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test")
done()
})
})
@ -161,7 +161,7 @@ o.spec("Router.setPath", function() {
router.setPath("/other", null, {state: {a: 1}})
o($window.history.state).deepEquals({a: 1})
done()
})
})

View file

@ -1,9 +1,17 @@
# Change log for stream
## 2.0.0
- renamed HALT to SKIP [#2207](https://github.com/MithrilJS/mithril.js/pull/2207)
- rewrote implementation [#2207](https://github.com/MithrilJS/mithril.js/pull/2207)
- stream: Removed `valueOf` & `toString` methods ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150)
- [Upcoming](#upcoming)
- [v2.0.0](#v200)
- [v1.1.0](#v110)
## 1.1.0
- stream: Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))
### Upcoming...
### 2.0.0
- when a stream conditionally returns HALT, dependant stream will also end ([#2200](https://github.com/MithrilJS/mithril.js/pull/2200), [#2369](https://github.com/MithrilJS/mithril.js/pull/2369))
- Add `stream.lift` as a user-friendly alternative to `merge -> map` or `combine` ([#1944](https://github.com/MithrilJS/mithril.js/issues/1944))
- renamed HALT to SKIP ([#2207](https://github.com/MithrilJS/mithril.js/pull/2207))
- rewrote implementation ([#2207](https://github.com/MithrilJS/mithril.js/pull/2207))
- Removed `valueOf` & `toString` methods ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150)
### 1.1.0
- Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))

View file

@ -1,9 +1,8 @@
{
"name": "mithril-stream",
"version": "1.1.0",
"version": "2.0.0",
"description": "Streaming data, mithril-style",
"main": "stream.js",
"module": "stream.mjs",
"directories": {
"test": "tests"
},

View file

@ -24,11 +24,13 @@ function Stream(value) {
var dependentFns = []
function stream(v) {
if (arguments.length && v !== Stream.SKIP && open(stream)) {
if (arguments.length && v !== Stream.SKIP) {
value = v
stream.changing()
stream.state = "active"
dependentStreams.forEach(function(s, i) { s(dependentFns[i](value)) })
if (open(stream)) {
stream.changing()
stream.state = "active"
dependentStreams.forEach(function(s, i) { s(dependentFns[i](value)) })
}
}
return value
@ -36,11 +38,11 @@ function Stream(value) {
stream.constructor = Stream
stream.state = arguments.length && value !== Stream.SKIP ? "active" : "pending"
stream.parents = []
stream.changing = function() {
open(stream) && (stream.state = "changing")
dependentStreams.forEach(function(s) {
s.dependent && s.dependent.changing()
s.changing()
})
}
@ -49,6 +51,7 @@ function Stream(value) {
var target = stream.state === "active" && ignoreInitial !== Stream.SKIP
? Stream(fn(value))
: Stream()
target.parents.push(stream)
dependentStreams.push(target)
dependentFns.push(fn)
@ -60,8 +63,9 @@ function Stream(value) {
end = Stream()
end.map(function(value) {
if (value === true) {
stream.parents.forEach(function (p) {p.unregisterChild(stream)})
stream.state = "ended"
dependentStreams.length = dependentFns.length = 0
stream.parents.length = dependentStreams.length = dependentFns.length = 0
}
return value
})
@ -73,6 +77,14 @@ function Stream(value) {
stream["fantasy-land/map"] = stream.map
stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) }
stream.unregisterChild = function(child) {
var childIndex = dependentStreams.indexOf(child)
if (childIndex !== -1) {
dependentStreams.splice(childIndex, 1)
dependentFns.splice(childIndex, 1)
}
}
Object.defineProperty(stream, "end", {
get: function() { return end || createEnd() }
})
@ -92,8 +104,8 @@ function combine(fn, streams) {
var changed = []
streams.forEach(function(s) {
s.map(function(value) {
var mappers = streams.map(function(s) {
return s.map(function(value) {
changed.push(s)
if (ready || streams.every(function(s) { return s.state !== "pending" })) {
ready = true
@ -101,7 +113,15 @@ function combine(fn, streams) {
changed = []
}
return value
}, Stream.SKIP).parent = stream
}, Stream.SKIP)
})
var endStream = stream.end.map(function(value) {
if (value === true) {
mappers.forEach(function(mapper) { mapper.end(true) })
endStream.end(true)
}
return undefined
})
return stream

View file

@ -1,152 +0,0 @@
/* eslint-enable */
Stream.SKIP = {}
Stream.lift = lift
Stream.scan = scan
Stream.merge = merge
Stream.combine = combine
Stream.scanMerge = scanMerge
Stream["fantasy-land/of"] = Stream
var warnedHalt = false
Object.defineProperty(Stream, "HALT", {
get: function() {
warnedHalt || console.log("HALT is deprecated and has been renamed to SKIP");
warnedHalt = true
return Stream.SKIP
}
})
function Stream(value) {
var dependentStreams = []
var dependentFns = []
function stream(v) {
if (arguments.length && v !== Stream.SKIP && open(stream)) {
value = v
stream.changing()
stream.state = "active"
dependentStreams.forEach(function(s, i) { s(dependentFns[i](value)) })
}
return value
}
stream.constructor = Stream
stream.state = arguments.length && value !== Stream.SKIP ? "active" : "pending"
stream.changing = function() {
open(stream) && (stream.state = "changing")
dependentStreams.forEach(function(s) {
s.dependent && s.dependent.changing()
s.changing()
})
}
stream.map = function(fn, ignoreInitial) {
var target = stream.state === "active" && ignoreInitial !== Stream.SKIP
? Stream(fn(value))
: Stream()
dependentStreams.push(target)
dependentFns.push(fn)
return target
}
var end
function createEnd() {
end = Stream()
end.map(function(value) {
if (value === true) {
stream.state = "ended"
dependentStreams.length = dependentFns.length = 0
}
return value
})
return end
}
stream.toJSON = function() { return value != null && typeof value.toJSON === "function" ? value.toJSON() : value }
stream["fantasy-land/map"] = stream.map
stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) }
Object.defineProperty(stream, "end", {
get: function() { return end || createEnd() }
})
return stream
}
function combine(fn, streams) {
var ready = streams.every(function(s) {
if (s.constructor !== Stream)
throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream")
return s.state === "active"
})
var stream = ready
? Stream(fn.apply(null, streams.concat([streams])))
: Stream()
var changed = []
streams.forEach(function(s) {
s.map(function(value) {
changed.push(s)
if (ready || streams.every(function(s) { return s.state !== "pending" })) {
ready = true
stream(fn.apply(null, streams.concat([changed])))
changed = []
}
return value
}, Stream.SKIP).parent = stream
})
return stream
}
function merge(streams) {
return combine(function() { return streams.map(function(s) { return s() }) }, streams)
}
function scan(fn, acc, origin) {
var stream = origin.map(function(v) {
var next = fn(acc, v)
if (next !== Stream.SKIP) acc = next
return next
})
stream(acc)
return stream
}
function scanMerge(tuples, seed) {
var streams = tuples.map(function(tuple) { return tuple[0] })
var stream = combine(function() {
var changed = arguments[arguments.length - 1]
streams.forEach(function(stream, i) {
if (changed.indexOf(stream) > -1)
seed = tuples[i][1](seed, stream())
})
return seed
}, streams)
stream(seed)
return stream
}
function lift() {
var fn = arguments[0]
var streams = Array.prototype.slice.call(arguments, 1)
return merge(streams).map(function(streams) {
return fn.apply(undefined, streams)
})
}
function open(s) {
return s.state === "pending" || s.state === "active" || s.state === "changing"
}
export default Stream

View file

@ -261,6 +261,15 @@ o.spec("stream", function() {
o(thrown.constructor === TypeError).equals(false)
o(spy.callCount).equals(0)
})
o("combine callback not called when child stream was ended", function () {
var spy = o.spy()
var a = Stream(1)
var b = Stream(2)
var mapped = Stream.combine(spy, [a, b])
mapped.end(true)
a(11)
o(spy.callCount).equals(1)
})
})
o.spec("lift", function() {
o("transforms value", function() {
@ -479,6 +488,12 @@ o.spec("stream", function() {
o(spy.callCount).equals(1)
})
o("ended stream works like a container", function() {
var stream = Stream(1)
stream.end(true)
stream(2)
o(stream()).equals(2)
})
})
o.spec("toJSON", function() {
o("works", function() {
@ -544,6 +559,14 @@ o.spec("stream", function() {
o(stream["fantasy-land/map"]).equals(stream.map)
})
o("mapping function is not invoked after ending", function () {
var stream = Stream(undefined)
var fn = o.spy()
var mapped = stream.map(fn)
mapped.end(true)
stream(undefined)
o(fn.callCount).equals(1)
})
})
o.spec("ap", function() {
o("works", function() {

View file

@ -43,13 +43,28 @@ module.exports = function() {
args.user = user
args.password = password
}
this.responseType = ""
this.response = null
Object.defineProperty(this, "responseText", {get: function() {
if (this.responseType === "" || this.responseType === "text") {
return this.response
} else {
throw new Error("Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '" + this.responseType + "').")
}
}})
this.send = function(body) {
var self = this
if(!aborted) {
var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname)
var data = handler({url: args.pathname, query: args.search || {}, body: body || null})
self.status = data.status
self.responseText = data.responseText
// Match spec
if (self.responseType === "json") {
try { self.response = JSON.parse(data.responseText) }
catch (e) { /* ignore */ }
} else {
self.response = data.responseText
}
} else {
self.status = 0
}