Fix docs (#2482)
* Fix #2414, address part of #1687 Also cleared the CSS up to be a lot more readable instead of smooshed into a single line. * Redo the testing docs page - Addresses another part of #1687 - Also, fix a few linter issues in the ospec binary * Add note about third-party cookies, tweak a line * Make the JSX comparison much more meaningful And let the code speak for itself. Don't fuel the flame wars any more than what they've already become. We should be *unopinionated*, and so I've updated those docs to remove the existing opinion. * Remove a bunch of outdated ES6 references * Remove the CSS page
This commit is contained in:
parent
61b087ea20
commit
20f0759103
11 changed files with 525 additions and 283 deletions
|
|
@ -6,8 +6,9 @@
|
|||
- [State](#state)
|
||||
- [Closure component state](#closure-component-state)
|
||||
- [POJO component state](#pojo-component-state)
|
||||
- [ES6 Classes](#es6-classes)
|
||||
- [Classes](#classes)
|
||||
- [Class component state](#class-component-state)
|
||||
- [Special attributes](#special-attributes)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
### Structure
|
||||
|
|
@ -115,7 +116,7 @@ Note that unlike many other frameworks, mutating component state does *not* trig
|
|||
|
||||
If a state change occurs that is not as a result of any of the above conditions (e.g. after a `setTimeout`), then you can use `m.redraw()` to trigger a redraw manually.
|
||||
|
||||
#### Closure Component State
|
||||
#### 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.
|
||||
|
||||
|
|
@ -183,7 +184,7 @@ A big advantage of closure components is that we don't need to worry about bindi
|
|||
|
||||
---
|
||||
|
||||
#### POJO Component State
|
||||
#### POJO component state
|
||||
|
||||
It is generally recommended that you use closures for managing component state. If, however, you have reason to manage state in a POJO, the state of a component can be accessed in three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
|
||||
|
||||
|
|
@ -247,55 +248,55 @@ 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 arrow functions, or if those are not supported, use `vnode.state`.
|
||||
|
||||
---
|
||||
|
||||
### ES6 classes
|
||||
### Classes
|
||||
|
||||
If it suits your needs (like in object-oriented projects), components can also be written using ES6 class syntax:
|
||||
If it suits your needs (like in object-oriented projects), components can also be written using classes:
|
||||
|
||||
```javascript
|
||||
class ES6ClassComponent {
|
||||
class ClassComponent {
|
||||
constructor(vnode) {
|
||||
this.kind = "ES6 class"
|
||||
this.kind = "class component"
|
||||
}
|
||||
view() {
|
||||
return m("div", `Hello from an ${this.kind}`)
|
||||
return m("div", `Hello from a ${this.kind}`)
|
||||
}
|
||||
oncreate() {
|
||||
console.log(`A ${this.kind} component was created`)
|
||||
console.log(`A ${this.kind} was created`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Component classes must define a `view()` method, detected via `.prototype.view`, to get the tree to render.
|
||||
Class components must define a `view()` method, detected via `.prototype.view`, to get the tree to render.
|
||||
|
||||
They can be consumed in the same way regular components can.
|
||||
|
||||
```javascript
|
||||
// EXAMPLE: via m.render
|
||||
m.render(document.body, m(ES6ClassComponent))
|
||||
m.render(document.body, m(ClassComponent))
|
||||
|
||||
// EXAMPLE: via m.mount
|
||||
m.mount(document.body, ES6ClassComponent)
|
||||
m.mount(document.body, ClassComponent)
|
||||
|
||||
// EXAMPLE: via m.route
|
||||
m.route(document.body, "/", {
|
||||
"/": ES6ClassComponent
|
||||
"/": ClassComponent
|
||||
})
|
||||
|
||||
// EXAMPLE: component composition
|
||||
class AnotherES6ClassComponent {
|
||||
class AnotherClassComponent {
|
||||
view() {
|
||||
return m("main", [
|
||||
m(ES6ClassComponent)
|
||||
m(ClassComponent)
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Class Component State
|
||||
#### Class component state
|
||||
|
||||
With classes, state can be managed by class instance properties and methods, and accessed via `this`:
|
||||
|
||||
|
|
@ -324,7 +325,7 @@ class ComponentWithState {
|
|||
}
|
||||
```
|
||||
|
||||
Note that we must wrap the event callbacks in arrow functions so that the `this` context is preserved correctly.
|
||||
Note that we must use arrow functions for the event handler callbacks so the `this` context can be referenced correctly.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -332,6 +333,15 @@ Note that we must wrap the event callbacks in arrow functions so that the `this`
|
|||
|
||||
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
|
||||
|
||||
---
|
||||
|
||||
### Special attributes
|
||||
|
||||
Mithril places special semantics on several property keys, so you should normally avoid using them in normal component attributes.
|
||||
|
||||
- [Lifecycle methods](lifecycle-methods.md): `oninit`, `oncreate`, `onbeforeupdate`, `onupdate`, `onbeforeremove`, and `onremove`
|
||||
- `key`, which is used to track identity in keyed fragments
|
||||
- `tag`, which is used to tell vnodes apart from normal attributes objects and other things that are non-vnode objects.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
94
docs/css.md
94
docs/css.md
|
|
@ -1,94 +0,0 @@
|
|||
# CSS
|
||||
|
||||
- [Vanilla CSS](#vanilla-css)
|
||||
- [Tachyons](#tachyons)
|
||||
- [CSS-in-JS](#css-in-js)
|
||||
|
||||
---
|
||||
|
||||
### Vanilla CSS
|
||||
|
||||
For various reasons, CSS has a bad reputation and often developers reach for complex tools in an attempt to make styling more manageable. In this section, we'll take a step back and cover some tips on writing plain CSS:
|
||||
|
||||
- **Avoid using the space operator** - The vast majority of CSS maintainability issues are due to CSS specificity issues. The space operator defines a descendant (e.g. `.a .b`) and at the same time, it increases the level of specificity for the CSS rules that apply to that selector, sometimes overriding styles unexpectedly.
|
||||
|
||||
Instead, it's preferable to share a namespace prefix in all class names that belong to a logical group of elements:
|
||||
|
||||
```css
|
||||
/* AVOID */
|
||||
.chat.container {/*...*/}
|
||||
.chat .item {/*...*/}
|
||||
.chat .avatar {/*...*/}
|
||||
.chat .text {/*...*/}
|
||||
|
||||
/* PREFER */
|
||||
.chat-container {/*...*/}
|
||||
.chat-item {/*...*/}
|
||||
.chat-avatar {/*...*/}
|
||||
.chat-text {/*...*/}
|
||||
```
|
||||
|
||||
- **Use only single-class selectors** - This convention goes hand-in-hand with the previous one: avoiding high specificity selectors such as `#foo` or `div.bar` help decrease the likelyhood of specificity conflicts.
|
||||
|
||||
```css
|
||||
/* AVOID */
|
||||
#home {}
|
||||
input.highlighted {}
|
||||
|
||||
/* PREFER */
|
||||
.home {}
|
||||
.input-highlighted {}
|
||||
```
|
||||
|
||||
- **Develop naming conventions** - You can reduce naming collisions by defining keywords for certain types of UI elements. This is particularly effective when brand names are involved:
|
||||
|
||||
```css
|
||||
/* AVOID */
|
||||
.twitter {} /* icon link in footer */
|
||||
.facebook {} /* icon link in footer */
|
||||
/* later... */
|
||||
.modal.twitter {} /* tweet modal */
|
||||
.modal.facebook {} /* share modal */
|
||||
|
||||
/* PREFER */
|
||||
.link-twitter {}
|
||||
.link-facebook {}
|
||||
/* later... */
|
||||
.modal-twitter {}
|
||||
.modal-facebook {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tachyons
|
||||
|
||||
[Tachyons](https://github.com/tachyons-css/tachyons) is a CSS framework, but the concept behind it can easily be used without the library itself.
|
||||
|
||||
The basic idea is that every class name must declare one and only one CSS rule. For example, `bw1` stands for `border-width:1px;`. To create a complex style, one simply combines class names representing each of the required CSS rules. For example, `.black.bg-dark-blue.br2` styles an element with blue background, black text and a 4px border-radius.
|
||||
|
||||
Since each class is small and atomic, it's essentially impossible to run into CSS conflicts.
|
||||
|
||||
As it turns out, the Tachyons convention fits extremely well with Mithril and JSX:
|
||||
|
||||
```jsx
|
||||
var Hero = ".black.bg-dark-blue.br2.pa3"
|
||||
|
||||
m.mount(document.body, <Hero>Hello</Hero>)
|
||||
// equivalent to `m(".black.bg-dark.br2.pa3", "Hello")`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CSS in JS
|
||||
|
||||
In plain CSS, all selectors live in the global scope and are prone to name collisions and specificity conflicts. CSS-in-JS aims to solve the issue of scoping in CSS, i.e. it groups related styles into non-global modules that are invisible to each other. CSS-in-JS is suitable for extremely large dev teams working on a single codebase, but it's not a good choice for most teams.
|
||||
|
||||
Nowadays there are [a lot of CSS-in-JS libraries with various degrees of robustness](https://github.com/MicheleBertoli/css-in-js).
|
||||
|
||||
The main problem with many of these libraries is that even though they require a non-trivial amount of transpiler tooling and configuration, they also require sacrificing code readability in order to work, e.g. `<a class={classnames(styles.button, styles.danger)}></a>` vs `<a class="button danger"></a>` (or `m("a.button.danger")` if we're using hyperscript).
|
||||
|
||||
Often sacrifices also need to be made at time of debugging, when mapping rendered CSS class names back to their source. Often all you get in browser developer tools is a class like `button_fvp6zc2gdj35evhsl73ffzq_0 danger_fgdl0s2a5fmle5g56rbuax71_0` with useless source maps (or worse, entirely cryptic class names).
|
||||
|
||||
Another common issue is lack of support for less basic CSS features such as `@keyframes` and `@font-face`.
|
||||
|
||||
If you are adamant about using a CSS-in-JS library, consider using [J2C](https://github.com/j2css/j2c), which works without configuration and implements `@keyframes` and `@font-face`.
|
||||
115
docs/es6.md
115
docs/es6.md
|
|
@ -1,71 +1,117 @@
|
|||
# ES6
|
||||
# ES6+ on legacy browsers
|
||||
|
||||
- [Setup](#setup)
|
||||
- [Using Babel with Webpack](#using-babel-with-webpack)
|
||||
|
||||
---
|
||||
|
||||
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, but it's fully compatible with ES6 and later as well. All modern browsers do support it natively, up to and even including native module syntax. (They don't support Node's magic module resolution, so you can't use `import * as _ from "lodash-es"` or similar. They just support relative and URL paths.) And so you can feel free to use [arrow functions for your closure components and classes for your class components](components.md).
|
||||
|
||||
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.
|
||||
But, if like many of us, you still need to support older browsers like Internet Explorer, you'll need to transpile that down to ES5, and this is what this page is all about, using [Babel](https://babeljs.io) to make modern ES6+ code work on older browsers.
|
||||
|
||||
---
|
||||
|
||||
### Setup
|
||||
|
||||
The simplest way to setup an ES6 compilation toolchain is via [Babel](https://babeljs.io/).
|
||||
First, if you haven't already, make sure you have [Node](https://nodejs.org/en/) installed. It comes with [npm](https://www.npmjs.com/) pre-bundled, something we'll need soon.
|
||||
|
||||
Babel requires NPM, which is automatically installed when you install [Node.js](https://nodejs.org/en/). Once NPM is installed, create a project folder and run this command:
|
||||
Once you've got that downloaded, open a terminal and run these commands:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
# Replace this with the actual path to your project. Quote it if it has spaces,
|
||||
# and single-quote it if you're on Linux/Mac and it contains a `$` anywhere.
|
||||
cd "/path/to/your/project"
|
||||
|
||||
# If you have a `package.json` there already, skip this command.
|
||||
npm init
|
||||
```
|
||||
|
||||
If you want to use Webpack and Babel together, [skip to the section below](#using-babel-with-webpack).
|
||||
Now, you can go one of a couple different routes:
|
||||
|
||||
To install Babel as a standalone tool, use this command:
|
||||
- [Use Babel standalone, with no bundler at all](#using-babel-standalone)
|
||||
- [Use Babel and bundle with Webpack](#using-babel-with-webpack)
|
||||
|
||||
#### Using Babel standalone
|
||||
|
||||
First, we need to install a couple dependencies we need.
|
||||
|
||||
- `@babel/cli` installs the core Babel logic as well as the `babel` command.
|
||||
- `@babel/preset-env` helps Babel know what to transpile and how to transpile them.
|
||||
|
||||
```bash
|
||||
npm install @babel/cli @babel/preset-env --save-dev
|
||||
```
|
||||
|
||||
Create a `.babelrc` file:
|
||||
Now, create a `.babelrc` file and set up with `@babel/preset-env`.
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
"presets": ["@babel/preset-env"],
|
||||
"sourceMaps": true
|
||||
}
|
||||
```
|
||||
|
||||
To run Babel as a standalone tool, run this from the command line:
|
||||
And finally, if you have *very* specific requirements on what you need to support, you may want to [configure Browserslist](https://github.com/browserslist/browserslist) so Babel (and other libraries) know what features to target.
|
||||
|
||||
*By default, if you don't configure anything, Browserslist uses a fairly sensible query: `> 0.5%, last 2 versions, Firefox ESR, not dead`. Unless you have very specific circumstances that require you to change this, like if you need to support IE 8 with a lot of polyfills, don't bother with this step.*
|
||||
|
||||
Whenever you want to compile your project, run this command, and everything will be compiled.
|
||||
|
||||
```bash
|
||||
babel src --out-dir bin --source-maps
|
||||
babel src --out-dir dist
|
||||
```
|
||||
|
||||
You may find it convenient to use an npm script so you're not having to remember this and typing it out every time. Add a `"build"` field to the `"scripts"` object in your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And now, the command is a little easier to type and remember.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### Using Babel with Webpack
|
||||
|
||||
If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps.
|
||||
If you want to use Webpack to bundle, it's a few more steps to set up. First, we need to install all the dependencies we need for both Babel and Webpack.
|
||||
|
||||
- `webpack` is the core Webpack code and `webpack-cli` gives you the `webpack` command.
|
||||
- `@babel/core` is the core Babel code, a peer dependency for `babel-loader`.
|
||||
- `babel-loader` lets you teach Webpack how to use Babel to transpile your files.
|
||||
- `@babel/preset-env` helps Babel know what to transpile and how to transpile them.
|
||||
|
||||
```bash
|
||||
npm install @babel/core babel-loader @babel/preset-env --save-dev
|
||||
npm install webpack webpack-cli @babel/core babel-loader @babel/preset-env --save-dev
|
||||
```
|
||||
|
||||
Create a `.babelrc` file:
|
||||
Now, create a `.babelrc` file and set up with `@babel/preset-env`.
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
"presets": ["@babel/preset-env"],
|
||||
"sourceMaps": true
|
||||
}
|
||||
```
|
||||
|
||||
Next, create a file called `webpack.config.js`
|
||||
Next, if you have *very* specific requirements on what you need to support, you may want to [configure Browserslist](https://github.com/browserslist/browserslist) so Babel (and other libraries) know what features to target.
|
||||
|
||||
*By default, if you don't configure anything, Browserslist uses a fairly sensible query: `> 0.5%, last 2 versions, Firefox ESR, not dead`. Unless you have very specific circumstances that require you to change this, like if you need to support IE 8 with a lot of polyfills, don't bother with this step.*
|
||||
|
||||
And finally, set up Webpack by creating a file called `webpack.config.js`.
|
||||
|
||||
```javascript
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.js',
|
||||
entry: path.resolve(__dirname, 'src/index.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, './bin'),
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'app.js',
|
||||
},
|
||||
module: {
|
||||
|
|
@ -78,32 +124,40 @@ module.exports = {
|
|||
}
|
||||
```
|
||||
|
||||
This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `bin/app.js`.
|
||||
This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `dist/app.js`.
|
||||
|
||||
To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`:
|
||||
Now, to run the bundler, you just run this command:
|
||||
|
||||
```bash
|
||||
webpack -d --watch
|
||||
```
|
||||
|
||||
You may find it convenient to use an npm script so you're not having to remember this and typing it out every time. Add a `"build"` field to the `"scripts"` object in your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can now then run the bundler by running this from the command line:
|
||||
And now, the command is a little easier to type and remember.
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Production build
|
||||
For production builds, you'll want to minify your scripts. Luckily, this is also pretty easy: it's just running Webpack with a different option.
|
||||
|
||||
To generate a minified file, open `package.json` and add a new npm script called `build`:
|
||||
```bash
|
||||
webpack -p
|
||||
```
|
||||
|
||||
You may want to also add this to your npm scripts, so you can build it quickly and easily.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p"
|
||||
|
|
@ -111,11 +165,16 @@ To generate a minified file, open `package.json` and add a new npm script called
|
|||
}
|
||||
```
|
||||
|
||||
You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/):
|
||||
And then running this is a little easier to remember.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
And of course, you can do this in automatic production build scripts, too. Here's how it might look if you're using [Heroku](https://www.heroku.com/), for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p",
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ m("ul", users.map(function(u) { // <ul>
|
|||
// <li>Mary</li>
|
||||
})) // </ul>
|
||||
|
||||
// ES6:
|
||||
// ES6+:
|
||||
// m("ul", users.map(u =>
|
||||
// m("li", u.name)
|
||||
// ))
|
||||
|
|
|
|||
186
docs/jsx.md
186
docs/jsx.md
|
|
@ -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 you or your team's preferences.
|
||||
|
||||
```jsx
|
||||
function MyComponent() {
|
||||
|
|
@ -185,64 +185,154 @@ You can use hooks in your production environment to run the production build scr
|
|||
|
||||
### JSX vs hyperscript
|
||||
|
||||
JSX is essentially a trade-off: it introduces a non-standard syntax that cannot be run without appropriate tooling, in order to allow a developer to write HTML code using curly braces. The main benefit of using JSX instead of regular HTML is that the JSX specification is much stricter and yields syntax errors when appropriate, whereas HTML is far too forgiving and can make syntax issues difficult to spot.
|
||||
JSX and hyperscript are two different syntaxes you can use for specifying vnodes, and they have different tradeoffs:
|
||||
|
||||
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>`. Also, unlike HTML, JSX is based on XML, so you can do things like `<div class="test" />` as equivalent to `<div class="test"></div>`, where in HTML you can only use the second.
|
||||
- JSX is much more approachable if you're coming from an HTML/XML background and are more comfortable specifying DOM elements with that kind of syntax. It is also slightly cleaner in many cases since it uses fewer punctuation and the attributes contain less visual noise, so many people find it much easier to read. And of course, many common editors provide autocomplete support for DOM elements in the same way they do for HTML. However, it requires an extra build step to use, editor support isn't as broad as it is with normal JS, and it's considerably more verbose. It's also a bit more verbose when dealing with a lot of dynamic content because you have to use interpolations for everything.
|
||||
|
||||
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 more approachable if you come from a backend JS background that doesn't involve much HTML or XML. It's more concise with less redundancy, and it provides a CSS-like sugar for static classes, IDs, and other attributes. It also can be used with no build step at all, although [you can add one if you wish](https://github.com/MithrilJS/mopt). And it's slightly easier to work with in the face of a lot of dynamic content, because you don't need to "interpolate" anything. However, the terseness does make it harder to read for some people, especially those less experienced and coming from a front end HTML/CSS/XML background, and I'm not aware of any plugins that auto-complete parts of hyperscript selectors like IDs, classes, and attributes.
|
||||
|
||||
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:
|
||||
You can see the tradeoffs come into play in more complex trees. For instance, consider this hyperscript tree, adapted from a real-world project by [@isiahmeadows](https://github.com/isiahmeadows/) with some alterations for clarity and readability:
|
||||
|
||||
- it does not require repeating the tag name in closing tags when children are present (e.g. `m("div", m("span"))` vs `<div><span /></div>`)
|
||||
- static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs `<a class="button"></a>`)
|
||||
```js
|
||||
function SummaryView() {
|
||||
let tag, posts
|
||||
|
||||
In addition, since hyperscript is plain JavaScript, it's often more natural to indent than JSX:
|
||||
function init({attrs}) {
|
||||
Model.sendView(attrs.tag != null)
|
||||
if (attrs.tag != null) {
|
||||
tag = attrs.tag.toLowerCase()
|
||||
posts = Model.getTag(tag)
|
||||
} else {
|
||||
tag = undefined
|
||||
posts = Model.posts
|
||||
}
|
||||
}
|
||||
|
||||
```jsx
|
||||
//JSX
|
||||
function BigComponent() {
|
||||
function activate() { /* ... */ }
|
||||
function deactivate() { /* ... */ }
|
||||
function update() { /* ... */ }
|
||||
return {
|
||||
view: ({attrs}) => (
|
||||
<>
|
||||
{attrs.items.map((item) => <div>{item.name}</div>)}
|
||||
<div
|
||||
ondragover={activate}
|
||||
ondragleave={deactivate}
|
||||
ondragend={deactivate}
|
||||
ondrop={update}
|
||||
onblur={deactivate}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
function feed(type, href) {
|
||||
return m(".feed", [
|
||||
type,
|
||||
m("a", {href}, m("img.feed-icon[src=./feed-icon-16.gif]")),
|
||||
])
|
||||
}
|
||||
|
||||
// hyperscript
|
||||
function BigComponent() {
|
||||
function activate() { /* ... */ }
|
||||
function deactivate() { /* ... */ }
|
||||
function update() { /* ... */ }
|
||||
return {
|
||||
view: ({attrs}) => [
|
||||
attrs.items.map((item) => m("div", item.name)),
|
||||
m("div", {
|
||||
ondragover: this.activate,
|
||||
ondragleave: this.deactivate,
|
||||
ondragend: this.deactivate,
|
||||
ondrop: this.update,
|
||||
onblur: this.deactivate,
|
||||
})
|
||||
]
|
||||
}
|
||||
return {
|
||||
oninit: init,
|
||||
// To ensure the tag gets properly diffed on route change.
|
||||
onbeforeupdate: init,
|
||||
view: () =>
|
||||
m(".blog-summary", [
|
||||
m("p", "My ramblings about everything"),
|
||||
|
||||
m(".feeds", [
|
||||
feed("Atom", "blog.atom.xml"),
|
||||
feed("RSS", "blog.rss.xml"),
|
||||
]),
|
||||
|
||||
tag != null
|
||||
? m(TagHeader, {len: posts.length, tag})
|
||||
: m(".summary-header", [
|
||||
m(".summary-title", "Posts, sorted by most recent."),
|
||||
m(TagSearch),
|
||||
]),
|
||||
|
||||
m(".blog-list", posts.map((post) =>
|
||||
m(m.route.Link, {
|
||||
class: "blog-entry",
|
||||
href: `/posts/${post.url}`,
|
||||
}, [
|
||||
m(".post-date", post.date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})),
|
||||
|
||||
m(".post-stub", [
|
||||
m(".post-title", post.title),
|
||||
m(".post-preview", post.preview, "..."),
|
||||
]),
|
||||
|
||||
m(TagList, {post, tag}),
|
||||
])
|
||||
)),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Here's the exact equivalent of the above code, using JSX instead. You can see how the two syntaxes differ just in this bit, and what tradeoffs apply.
|
||||
|
||||
Needless to say, since hyperscript is pure JavaScript, there's no need to run a compilation step to produce runnable code.
|
||||
```jsx
|
||||
function SummaryView() {
|
||||
let tag, posts
|
||||
|
||||
function init({attrs}) {
|
||||
Model.sendView(attrs.tag != null)
|
||||
if (attrs.tag != null) {
|
||||
tag = attrs.tag.toLowerCase()
|
||||
posts = Model.getTag(tag)
|
||||
} else {
|
||||
tag = undefined
|
||||
posts = Model.posts
|
||||
}
|
||||
}
|
||||
|
||||
function feed(type, href) {
|
||||
return (
|
||||
<div class="feed">
|
||||
{type}
|
||||
<a href={href}><img class="feed-icon" src="./feed-icon-16.gif" /></a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: init,
|
||||
// To ensure the tag gets properly diffed on route change.
|
||||
onbeforeupdate: init,
|
||||
view: () => (
|
||||
<div class="blog-summary">
|
||||
<p>My ramblings about everything</p>
|
||||
|
||||
<div class="feeds">
|
||||
{feed("Atom", "blog.atom.xml")}
|
||||
{feed("RSS", "blog.rss.xml")}
|
||||
</div>
|
||||
|
||||
{tag != null
|
||||
? <TagHeader len={posts.length} tag={tag} />
|
||||
: (
|
||||
<div class="summary-header">
|
||||
<div class="summary-title">Posts, sorted by most recent</div>
|
||||
<TagSearch />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="blog-list">
|
||||
{posts.map((post) => (
|
||||
<m.route.Link class="blog-entry" href={`/posts/${post.url}`}>
|
||||
<div class="post-date">
|
||||
{post.date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="post-stub">
|
||||
<div class="post-title">{post.title}</div>
|
||||
<div class="post-preview">{post.preview}...</div>
|
||||
</div>
|
||||
|
||||
<TagList post={post} tag={tag} />
|
||||
</m.route.Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
- [Getting Help](support.md)
|
||||
- Resources
|
||||
- [JSX](jsx.md)
|
||||
- [ES6](es6.md)
|
||||
- [CSS](css.md)
|
||||
- [ES6+ on legacy browsers](es6.md)
|
||||
- [Animation](animation.md)
|
||||
- [Testing](testing.md)
|
||||
- [Examples](examples.md)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
### Description
|
||||
|
||||
A [ES6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) polyfill.
|
||||
An [ES6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) polyfill.
|
||||
|
||||
A Promise is a mechanism for working with asynchronous computations.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ $ git pull --rebase mithriljs next
|
|||
2. Determine patch level of the change
|
||||
3. Update information in `docs/change-log.md` to match reality of the new version being prepared for release
|
||||
4. Replace all existing references to `mithril@next` to `mithril` if moving from a release candidate to stable.
|
||||
- Note: if making an initial release candidate, don't forget to move all the playground snippets to pull from `mithril@next`!
|
||||
5. Commit changes to `next`
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Simple application
|
||||
|
||||
Let's develop a simple application that covers some of the major aspects of Single Page Applications
|
||||
Let's develop a simple application that shows off how to do most of the major things you would need to deal with while using Mithril.
|
||||
|
||||
An interactive running example can be seen here [flems: Simple Application](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcICGAHLA6AVmCADQgBmAljEagNqgB2GAthGiAKqQBOAsgPZJoBIqVj8GiSewBuGbgAIuERQF4FwADoMFuhVAph4qBbQC6xbXv38MSADKHjCsgFcGCChIAUASg1W1nrcEPCu3DrMuCEAjq4QRt5aOkGprPAAFoImmiAA4gCiACq5limp1uFQOSAZ8PBYYKgA9M0hzAC0IUYd2BS4GSr8ANau2HjizM19za48YKWBFXoA7hSZAMIhQpIUGFBNCvDc8WXLAL6+S0G4mRAM3m4e8F4P3a5Q8P7Jy9bK3LgDEYFOp3p9cEgMPAMNdrJdrucytdYOEQpITMBEdcoLYkCYnp4fBQkN9YcFQuFItEIHEEvAkmS0qEsniFLlCiUSIyglUanUGk1Wu0unTelh+oNuCMxjhcJNpuLZvNmrkFABqBTEs6-XRrTbbe4vfaHY6nbnw8o3O4PAkvHxgr4BS3Lf5y1GGkEKB3mq6WrEMa5gDAyCD49yEh6k53ksIRBRRWLxRI-HXx5nZNkgAAKHE52p1vMz-MaLTaEE63XgYolQ1G4zl-CmMzmKjAKpA6qUPDd3DR8FwWu51kh0JMrpRvcN+d+eoyW2Qhr2BxMpog07hvrh2nOIERjBYbHQ-0cRhEJBA4kkhtk8i7KhP8E9Kd0EgoDHWY+7OLsD+nMgoEArGGzyvH4TrLCEsaRN4uS4C23AdEC8ClHeAJIbgzDYI84Z2g88FRqmXoUnGzAwZgcE8IhTgdOs5YocAGQhGQNTNMg6ztp28EDkgxAKBIsAhFCobxtE-CuIggJvsMiIKFxlDcEYAByB6dqqqoalxUAYEpB6bhUlx6bo5zbruxD7qw7D-AAYvw3BRIQ56XlI8A3oo1m2cwT7XK+77OLaoEyAwggQN8rrfkg3iBcFuBQscYDcb4-rWP+gHARGYHPkEkGUvGZFkB59FDhUEhgK4ABGzAfi4OGgSF4GERUEC4FgIQhpIAAiEBkBgHz0oZDV-N2QYhn4RWpMZ0bjbxtBjbluRaWVwgLdAKG5FZFAKY+TCsLkvjrhUpG5G+WDiQODAnfAtDwAAnlgECqIgAAe8BmLQWBabAEBZFAQjcKo62bQo20QGYhWTb8PkXSYUSzgAgvU3BkXIUDxCh-k+Mj8Shd2E59rg8k6awnqYxAlz7TqJOfioPZ4wT8DKTt4MbuTQSHSAy1QICGCLVAq0gPY2lbQeu0s9YbPHadEuXe9GCfd9v2qALwLA6DJD1QNkPidDuBwwjSP7Kjavow8JPY9TuOGlzhMQMTBuk3ts3JXbVMAhbkhW-TwtM3oZOzWzZXifAEi4AH9QSFdt33aVFXrKrvG5AAysGEAi9yZj9RNO57iAwPsAL11if2DliBIzmuQo+eF15lopUB1UgRjQVCARFTZSRZGYW+XMF+JKEzd7uhs0wMgYfcrh947ehszCauZQNjFdSYADkzRIUvosQx4gmINrUriU1BgMMMk9GfHnDzLts3pxvg9kZAEYoVFQhyhkVBIGi-XWOnCImdnufoPWYuF5S7XnQAmQuTUWpdQoI9bwS8ADES9fTgP3t4JA-AUSsHdmVQQ10z6rycGDawuQCFGFyBibkaJfppVwhlWabdoKV3ErxUix4nC+E-j7BE04SFsXgM0VAxJyHqyyvcah9d0pPzqnPVuxFGEYB7vAFh3h3J2V4lImKCMwAcPNNw7cvhTLmUPCAOUYBRDAKvNIdAOCkB4LOhdYgIdA4SA0PldEQU7L7AUAARgAGxYEegoAAaioSETAADcmFuAAHM3wmAAEwAAYAkTW0N3KuwAomxIYKgbxyTAk9SDpEjAj0OhrCQJkXJiTqkBPCRNUeDBXAaCyXExJCg2kAGZ8l1O0Gk+CVFgTACQh0Iw10YCoCCgwCAxSYmtPaT47pWA7BIDfNE1AiSekMAoioAZVZaKeWAGVWWwxol7wYHieB3UrkYHCTg7g1DvEBIUGAfgBgkAKHgUgL54TxA4m4KgeBHSgXhJWWAGW11UBlRxLAYYMzsnrPmY8x64SllfNWagAAHE87xABWWpT0qxCHENwKErwJkSGmfU-pwz9moCyCGRQwACUdCJbZUlEhUDuF+ofSlvStkcw0KC8FkLoWwpaTktpbS8XIvqVLDQdyHlPJeW8j5XykC3Nsr9LodgKBzFQB02pODSlgAoAAL3RQqnZRqQWGGFVCjBYr5DwslQs2pqKVkMDWXk7F0rwnlMqXkxJABSTZTiw46EOcc05YlzkAogPGjV9yVC5KVa84kqrvmWoQiSlZeqDXIt+bZAFQKOk2rBVpCFb4eUdHtTCuFcy2neuRe69FTafG+uZaykluFyVTNDaHIOOT6UqHlVGs5FyIAYsnZOupu4LDsykjQegOcDzsEqpkbgVBzxVHYMWQUsxzonIbFMddjEqAAAFvG4Cvb45op7N2cyATdO67AHLnDMOcIAA)
|
||||
*An interactive example of the end result can be seen [here](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcIB0sxhAGhADMBLGXVAbVADsBDAWwjRAFVIAnAWQHsloMAVrgKxu1ROOYA3WuwAEbCPIC8c4AB1qc7XKjEw8VHIoBdPJp27utJABl9huYQCu1BMTEAKAJRqLlnXYIeGd2LXoMIIBHZwgDTw0tAOTGeAALXiN1EABxAFEAFWzzJOTLUKgskDT4eAAHMFQAeiag+gBaIIN22jriDDSlbgBrZ166rG56Jt7iJucOMGL-Mp0Ad2J0gGEgvnFiWihGuXh2WJLVgF9vFYCMdIhqTxc3eA8nrucoeF9E1ctFOwMHoDHJVJ9vhgkLR4LRbpZrrdLiVbrBQkFxEZgMjblBrEgjC93F5iEhfvDAsFQuFIhAYnF4AkKSlghkCXJsvkivhmQEKlUavVGi02p0GT0+gMhqNxpNprN5osmtk5ABqOSki7-bQbba7R5vQ7HU7nXmI0p3B5PIlvLwQn5+C2rQFYdEGsFye1mm4WnHUW5gWhSCCE1zEp7kp2UkJhOQRaKxeJ-bVx1mZDkgAAKLG5Wu1-IzgoazVaEA6XXgEv6g3YIzGdQmonlfUVSjAypAaoUHFd7Ax8Awmt5lmhsKMLrRfYNef+urSO2QBoORyMJogM4RPoRmkuIGRNAYTHQgPsBkEwhAonEBuksm7SlP8A9ye0YmI1E2457eJsj5nUmICA1lDV53h8R1ViCGNwk8bIMAWJR2hBeBinvIFkIwehemeMNbSeBD2EjFNPSpWN6FgkBaHgjgkIcdpNjLVDgDSIJCCqJpkE2DsuwIwckDwOQxFgIIYRDONIm4ZxEGBd9hmROReJIdgDAAOUPLsVRVdVeKgWhVMPLcymuQztEuHc9zwA9GGYQEADFuHYCIhHwS8xAkeBb3kOyHPoZ9bjfD9HBtMCpGoXgIF+F0fyQTxQvCjAYVOMA+O8P1LAAoCQPDcCXwCKDqTjCjCB8pjhzKMQwGcAAjehPycXCwIiiDiLKCAMDqIJg3EAARCBCFoL5GRMlqAR7QNgx8MrkjMqNpoEigpsK7JdKq-gVugVDsls4hlKfOhGGybwNzKcjsnfOopMHagLvgCh4AATzqCBlEQAAPeATAoOpdNgCAMigPh2GUbbdrkfaIBMUrZv+AKbqMCI5wAQVqdgKJkKBYlQ4KvHR2JIp7Sd+wwJT9MYD1cYga5ju1CmvyUXsiZJ+A1IO6HN2pgJTpAdaoGBWhVqgTaQFsPS9sPQ6OcsLnzsumXbu+2hfv+wHlBF0Fwch-BmpG2GpPhjAkZRtHDkxrXsaeCn8fpwmDT50mIHJk3KaOxb0qdumgRt8Q7eZ8W2Z0KnFq5qqpPgMQMBD2oxDux7nsqmrNk1gTsgAZSDCAJd5Exhpm3d9xAcHmBF+7JIHZyRDcm90BkeRi9LvyLQy4D6tAnGwr4IiynysiKKw98+ZLqTUIW-3tC5ugpEwx5nGH12dC5uEtdykaWL6owAHImmQ9fJZhtwRMQfXayktq9GoYY59M5PWEWQ7Fuz3ex4oyBw1QuK+CwNJSCQDFhssbOkTmXzoXdAspy6uWvJIdA8ZS5tQ6n1Ygr1PDrwAMTrx9DAk+ngkDcDRIwb2VVeD3WvlvBwUNLDZFIQYbIWJeQYkBllPCOVFrdxgnXKSAlyIngcN4P+AckQzkoZxeATRUCkhodrPKjwGEt2yu-Jqy8u6kTYbQQe8BOGeG8o5AS8iEoozALws0AidzeAslZI8mBsAXivO5ZghCkDELkFdG6AkI6hzEM1YqmIwY+UOHIAAjAANjqK9OQAA1JQ0I6AAG5-BYXYAAc3fEYAATAABhCbE6gM1NAD3rsmeJSTqBGH8Rk0JA0w5ZLHrQV67QNhIHSCUtJzTMk7k0Lk-BzhmqFOSXINJfS5AAGYylZJydQaiiFkLNWQu0Aw90YBGDCtQCAVS4yyCKUYfp-TSmtKSHUGwSB3wJM2aM9p4yCK0W6AxXyyYqqK2GAk4+1B2QoP6m82gqzCHsBkf4kJcgwDcD0EgOQKCkBgtWaIPE7AjAoMGXC1ZhywAK3ukYKqeJYDDFWT04pAztm7O0PssFRyjAAA4-n+IAKz4pOBAd67Q+CiHYDCd4iyxArLaf6c5NEZnXNQBkYM8hkxvUrAyhyzKxBGFcIDM+7LslnL5gLZqiLkWovRZiuJ6zelbLkFS16pzOXOKks1L5Py-kAqBSCsFSBPkOUBp0GwxAFhGEGdSwhtSwDEAAF7ErkCaxCbqEX6BVb6tVWLNU4u1TsvV-hCWHOoMcgJZSAm6tWfUxpia0kAFJ9W5MjmHLQtz7mPMks8mFEBy02u+UoEpZrAWkkteC-wfr2D2sOU6oZ1LIUORhXCwZgakW6RRRqagMr2hotweqpI2LNkDKjas2NPqo3JupcK+lcAxV4VZcsnN4y80Sv5UoY1RankvIgCSs9Z7RkuUgDAcM5AQCBJSagfxe4zDc1kuQKgBdDzMFqukdgpAXIVGYEWYU8xroPLlE0P9LFSAAAF-EYEQ4E6DmxYO83AQ9J6zAwDCWIHUDylwTCXCAA)*
|
||||
|
||||
First let's create an entry point for the application. Create a file `index.html`:
|
||||
|
||||
|
|
@ -78,6 +78,8 @@ module.exports = User
|
|||
|
||||
Then we can add an `m.request` call to make an XHR request. For this tutorial, we'll make XHR calls to the [REM](http://rem-rest-api.herokuapp.com/) API, a mock REST API designed for rapid prototyping. This API returns a list of users from the `GET https://rem-rest-api.herokuapp.com/api/users` endpoint. Let's use `m.request` to make an XHR request and populate our data with the response of that endpoint.
|
||||
|
||||
*Note: third-party cookies may have to be enabled for the REM endpoint to work.*
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var m = require("mithril")
|
||||
|
|
@ -213,13 +215,7 @@ The `m.mount` call renders the specified component (`UserList`) into a DOM eleme
|
|||
|
||||
---
|
||||
|
||||
Right now, the list looks rather plain because we have not defined any styles.
|
||||
|
||||
There are many similar conventions and libraries that help organize application styles nowadays. Some, like [Bootstrap](http://getbootstrap.com/) dictate a specific set of HTML structures and semantically meaningful class names, which has the upside of providing low cognitive dissonance, but the downside of making customization more difficult. Others, like [Tachyons](http://tachyons.io/) provide a large number of self-describing, atomic class names at the cost of making the class names themselves non-semantic. "CSS-in-JS" is another type of CSS system that is growing in popularity, which basically consists of scoping CSS via transpilation tooling. CSS-in-JS libraries achieve maintainability by reducing the size of the problem space, but come at the cost of having high complexity.
|
||||
|
||||
Regardless of what CSS convention/library you choose, a good rule of thumb is to avoid the cascading aspect of CSS. To keep this tutorial simple, we'll just use plain CSS with overly explicit class names, so that the styles themselves provide the atomicity of Tachyons, and class name collisions are made unlikely through the verbosity of the class names. Plain CSS can be sufficient for low-complexity projects (e.g. 3 to 6 man-months of initial implementation time and few project phases).
|
||||
|
||||
To add styles, let's first create a file called `styles.css` and include it in the `index.html` file:
|
||||
Right now, the list looks rather plain because we have not defined any styles. So let's add a few of them. Let's first create a file called `styles.css` and include it in the `index.html` file:
|
||||
|
||||
```markup
|
||||
<!doctype html>
|
||||
|
|
@ -239,15 +235,27 @@ To add styles, let's first create a file called `styles.css` and include it in t
|
|||
Now we can style the `UserList` component:
|
||||
|
||||
```css
|
||||
.user-list {list-style:none;margin:0 0 10px;padding:0;}
|
||||
.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
|
||||
.user-list-item:hover {text-decoration:underline;}
|
||||
.user-list {
|
||||
list-style: none;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
background: #fafafa;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin: 0 0 1px;
|
||||
padding: 8px 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-list-item:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
```
|
||||
|
||||
The CSS above is written using a convention of keeping all styles for a rule in a single line, in alphabetical order. This convention is designed to take maximum advantage of screen real estate, and makes it easier to scan the CSS selectors (since they are always on the left side) and their logical grouping, and it enforces predictable and uniform placement of CSS rules for each selector.
|
||||
|
||||
Obviously you can use whatever spacing/indentation convention you prefer. The example above is just an illustration of a not-so-widespread convention that has strong rationales behind it, but deviates from the more widespread cosmetic-oriented spacing conventions.
|
||||
|
||||
Reloading the browser window now should display some styled elements.
|
||||
|
||||
---
|
||||
|
|
@ -339,20 +347,64 @@ module.exports = {
|
|||
}
|
||||
```
|
||||
|
||||
And let's add some styles to `styles.css`:
|
||||
And let's add some more styles to `styles.css`:
|
||||
|
||||
```css
|
||||
/* styles.css */
|
||||
body,.input,.button {font:normal 16px Verdana;margin:0;}
|
||||
body, .input, .button {
|
||||
font: normal 16px Verdana;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-list {list-style:none;margin:0 0 10px;padding:0;}
|
||||
.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
|
||||
.user-list-item:hover {text-decoration:underline;}
|
||||
.user-list {
|
||||
list-style: none;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.label {display:block;margin:0 0 5px;}
|
||||
.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;}
|
||||
.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;}
|
||||
.button:hover {background:#e8e8e8;}
|
||||
.user-list-item {
|
||||
background: #fafafa;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin: 0 0 1px;
|
||||
padding: 8px 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-list-item:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #eee;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
```
|
||||
|
||||
Right now, this component does nothing to respond to user events. Let's add some code to our `User` model in `src/models/User.js`. This is how the code is right now:
|
||||
|
|
@ -570,23 +622,73 @@ This component is fairly straightforward, it has a `<nav>` with a link to the li
|
|||
|
||||
Notice there's also a `<section>` element with `vnode.children` as children. `vnode` is a reference to the vnode that represents an instance of the Layout component (i.e. the vnode returned by a `m(Layout)` call). Therefore, `vnode.children` refer to any children of that vnode.
|
||||
|
||||
Let's add some styles:
|
||||
And let's update the styles once more:
|
||||
|
||||
```css
|
||||
/* styles.css */
|
||||
body,.input,.button {font:normal 16px Verdana;margin:0;}
|
||||
body, .input, .button {
|
||||
font: normal 16px Verdana;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.layout {margin:10px auto;max-width:1000px;}
|
||||
.menu {margin:0 0 30px;}
|
||||
.layout {
|
||||
margin: 10px auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.user-list {list-style:none;margin:0 0 10px;padding:0;}
|
||||
.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
|
||||
.user-list-item:hover {text-decoration:underline;}
|
||||
.menu {
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
|
||||
.label {display:block;margin:0 0 5px;}
|
||||
.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;}
|
||||
.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;}
|
||||
.button:hover {background:#e8e8e8;}
|
||||
.user-list {
|
||||
list-style: none;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
background: #fafafa;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin: 0 0 1px;
|
||||
padding: 8px 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-list-item:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #eee;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
```
|
||||
|
||||
Let's change the router in `src/index.js` to add our layout into the mix:
|
||||
|
|
|
|||
172
docs/testing.md
172
docs/testing.md
|
|
@ -1,103 +1,175 @@
|
|||
# Testing
|
||||
|
||||
Mithril comes with a testing framework called [ospec](https://github.com/MithrilJS/mithril.js/tree/master/ospec). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis).
|
||||
- [Setup](#setup)
|
||||
- [Best practices](#best-practices)
|
||||
- [Unit testing](#unit-testing)
|
||||
|
||||
The easist way to setup the test runner is to create an NPM script for it. Open your project's `package.json` file and edit the `test` line under the `scripts` section:
|
||||
---
|
||||
|
||||
### Setup
|
||||
|
||||
Testing Mithril applications is relatively easy. The easiest way to get started is with [ospec](https://mochajs.org/), [mithril-query](https://github.com/MithrilJS/mithril-query), and JSDOM. Installing those is pretty easy: open up a terminal and run this command.
|
||||
|
||||
```bash
|
||||
npm install --save-dev ospec mithril-query jsdom
|
||||
```
|
||||
|
||||
And getting them set up is also relatively easy and requires a few short steps:
|
||||
|
||||
1. Add a `"test": "mocha"` to your npm scripts in your `package.json` file. This will end up looking something like this, maybe with a few extra fields relevant to your project:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"test": "ospec"
|
||||
"test": "ospec --require ./test-setup.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember this is a JSON file, so object key names such as `"test"` must be inside of double quotes.
|
||||
|
||||
To setup a test suite, create a `tests` folder and inside of it, create a test file:
|
||||
2. Create a setup file, `test-setup.js`, that looks like this:
|
||||
|
||||
```javascript
|
||||
// file: tests/math-test.js
|
||||
var o = require("mithril/ospec/ospec")
|
||||
var o = require("ospec")
|
||||
var jsdom = require("jsdom")
|
||||
var dom = new jsdom.JSDOM("", {
|
||||
// So we can get `requestAnimationFrame`
|
||||
pretendToBeVisual: true,
|
||||
})
|
||||
|
||||
o.spec("math", function() {
|
||||
o("addition works", function() {
|
||||
o(1 + 2).equals(3)
|
||||
// Fill in the globals Mithril needs to operate. Also, the first two are often
|
||||
// useful to have just in tests.
|
||||
global.window = dom.window
|
||||
global.document = dom.window.document
|
||||
global.requestAnimationFrame = dom.window.requestAnimationFrame
|
||||
|
||||
// Require Mithril to make sure it loads properly.
|
||||
require("mithril")
|
||||
|
||||
// And now, make sure JSDOM ends when the tests end.
|
||||
o.after(function() {
|
||||
dom.window.close()
|
||||
})
|
||||
```
|
||||
|
||||
3. Create a component, say `src/my-component.js`, that looks like this:
|
||||
|
||||
```javascript
|
||||
function MyComponent() {
|
||||
return {
|
||||
view: function (vnode) {
|
||||
return m("div", vnode.attrs.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MyComponent
|
||||
```
|
||||
|
||||
4. And finally, create a test file, say `src/tests/my-component.js`, that looks like this:
|
||||
|
||||
```javascript
|
||||
var mq = require("mithril-query")
|
||||
var o = require("ospec")
|
||||
|
||||
var MyComponent = require("../my-component.js")
|
||||
|
||||
o.spec("MyComponent", function() {
|
||||
o("things are working", function() {
|
||||
var out = mq(MyComponent, {text: "What a wonderful day to be alive!"})
|
||||
|
||||
out.should.contain("day")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
Once you've got all that set up, in that same terminal you installed everything to, run this command.
|
||||
|
||||
```
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
---
|
||||
Provided you have everything set up properly, you should end up with output that looks something like this:
|
||||
|
||||
### Running mithril in a non-browser environment
|
||||
|
||||
Mithril has a few dependencies on globals that exist in all its supported browser environments but are missing in all non-browser environments. To work around this you can use the browser mocks that ship with the mithril npm package.
|
||||
|
||||
The simplest way to do this is ensure the following snippet of code runs **before** you include mithril itself in your project.
|
||||
|
||||
```js
|
||||
// Polyfill DOM env for mithril
|
||||
global.window = require("mithril/test-utils/browserMock.js")();
|
||||
global.document = window.document;
|
||||
```
|
||||
––––––
|
||||
All 1 assertions passed in 0ms
|
||||
```
|
||||
|
||||
Once that snippet has been run you can `require("mithril")` and it should be quite happy.
|
||||
|
||||
---
|
||||
|
||||
### Good testing practices
|
||||
### Best practices
|
||||
|
||||
Generally speaking, there are two ways to write tests: upfront and after the fact.
|
||||
Testing is relatively straightforward in most cases. Each test usually consists of three parts: set up state, run code, check results. But there are things you do need to keep in mind while you test, to ensure you get the best bang for your buck and to help save you a *lot* of time.
|
||||
|
||||
Writing tests upfront requires specifications to be frozen. Upfront tests are a great way of codifying the rules that a yet-to-be-implemented API must obey. However, writing tests upfront may not be a suitable strategy if you don't have a reasonable idea of what your project will look like, if the scope of the API is not well known or if it's likely to change (e.g. based on previous history at the company).
|
||||
1. First and foremost, you want to write your tests as early in the process as possible. You don't need to write your tests immediately, but you want to at the very least write your tests as you write your code. That way, if things aren't working as you thought they were, you're spending 5 minutes now, knowing exactly what's going on, instead of 5 days straight 6 months later when you're trying to release that amazing app idea that's now spectacularly broken. You don't want to be stuck in *that* rut.
|
||||
|
||||
Writing tests after the fact is a way to document the behavior of a system and avoid regressions. They are useful to ensure that obscure corner cases are not inadvertently broken and that previously fixed bugs do not get re-introduced by unrelated changes.
|
||||
1. Test the API, test the behavior, but don't test the implementation. If you need to test that an event is being fired if a particular action happens, that's all fine and dandy, and feel free to do that. But don't test the entire DOM structure in your test. You don't want to be stuck having to rewrite part of 5 different tests just because you added a simple style-related class. You also don't want to rewrite all your tests simply because you added a new instance method to an object.
|
||||
|
||||
1. Don't be afraid to repeat yourself, and only abstract when you're literally doing the same thing tens to hundreds of times in the same file or when you're explicitly generating tests. Normally, in code, it's a good idea to draw a line when you're repeating the same logic more than 2-3 times and abstract it into a function, but when you're testing, even though there's a lot of duplicate logic, that redundancy helps give you context when troubleshooting tests. Remember: tests are specifications, not normal code.
|
||||
|
||||
---
|
||||
|
||||
### Unit testing
|
||||
|
||||
Unit testing is the practice of isolating a part of an application (typically a single module), and asserting that, given some inputs, it produces the expected outputs.
|
||||
Unit testing isolates parts of your application, usually a single module but sometimes even a single function, and tests them as a single "unit". It checks that given a particular input and initial state, it produces the desired output and side effects. This all seems complicated, but I promise, it's not. Here's a couple unit tests for JavaScript's `+` operator, applied to numbers:
|
||||
|
||||
Testing a Mithril component is easy. Let's assume we have a simple component like this:
|
||||
```javascript
|
||||
o.spec("addition", function() {
|
||||
o("works with integers", function() {
|
||||
o(1 + 2).equals(3)
|
||||
})
|
||||
|
||||
o("works with floats", function() {
|
||||
// Yep, thanks IEEE-754 floating point for being weird.
|
||||
o(0.1 + 0.2).equals(0.30000000000000004)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Just like you can unit test simple stuff like that, you can unit test Mithril components, too. Suppose you have this component:
|
||||
|
||||
```javascript
|
||||
// MyComponent.js
|
||||
var m = require("mithril")
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
return m("div",
|
||||
m("p", "Hello World")
|
||||
)
|
||||
function MyComponent() {
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
vnode.attrs.type === "goodbye"
|
||||
? "Goodbye, world!"
|
||||
: "Hello, world!"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MyComponent
|
||||
```
|
||||
|
||||
We can then create a `tests/MyComponent.js` file and create a test for this component like this:
|
||||
You could easily create a few unit tests for that.
|
||||
|
||||
```javascript
|
||||
var MyComponent = require("MyComponent")
|
||||
```js
|
||||
var mq = require("mithril-query")
|
||||
var MyComponent = require("./MyComponent")
|
||||
|
||||
o.spec("MyComponent", function() {
|
||||
o("returns a div", function() {
|
||||
var vnode = MyComponent.view()
|
||||
|
||||
o(vnode.tag).equals("div")
|
||||
o(vnode.children.length).equals(1)
|
||||
o(vnode.children[0].tag).equals("p")
|
||||
o(vnode.children[0].children).equals("Hello world")
|
||||
o("says 'Hello, world!' when `type` is `hello`", function() {
|
||||
var out = mq(MyComponent, {type: "hello"})
|
||||
out.should.contain("Hello, world!")
|
||||
})
|
||||
|
||||
o("says 'Goodbye, world!' when `type` is `goodbye`", function() {
|
||||
var out = mq(MyComponent, {type: "goodbye"})
|
||||
out.should.contain("Goodbye, world!")
|
||||
})
|
||||
|
||||
o("says 'Hello, world!' when no `type` is given", function() {
|
||||
var out = mq(MyComponent)
|
||||
out.should.contain("Hello, world!")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Typically, you wouldn't test the structure of the vnode tree so granularly, and you would instead only test non-trivial, dynamic aspects of the view. A tool that can help making testing easier with deep vnode trees is [Mithril Query](https://github.com/StephanHoyer/mithril-query).
|
||||
|
||||
Sometimes, you need to mock the dependencies of a module in order to test the module in isolation. [Mockery](https://github.com/mfncooper/mockery) is one tool that allows you to do that.
|
||||
As mentioned before, tests are specifications. You can see from the tests how the component is supposed to work, and the component does it very effectively.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function parseArgs(argv) {
|
|||
var args = {}
|
||||
var name
|
||||
argv.forEach(function(arg) {
|
||||
if (/^--/.test(arg)) {
|
||||
if ((/^--/).test(arg)) {
|
||||
name = arg.substr(2)
|
||||
args[name] = args[name] || []
|
||||
} else {
|
||||
|
|
@ -24,12 +24,15 @@ function parseArgs(argv) {
|
|||
|
||||
var args = parseArgs(process.argv)
|
||||
var globList = args.globs && args.globs.length ? args.globs : ["**/tests/**/*.js"]
|
||||
var ignore = ["**/node_modules/**"].concat(args.ignore||[])
|
||||
var ignore = ["**/node_modules/**"].concat(args.ignore || [])
|
||||
var cwd = process.cwd()
|
||||
|
||||
args.require && args.require.forEach(function(module) {
|
||||
module && require(require.resolve(module, { basedir: cwd }))
|
||||
})
|
||||
if (args.require) {
|
||||
args.require.forEach(function(module) {
|
||||
// eslint-disable-next-line global-require
|
||||
if (module) require(require.resolve(module, {basedir: cwd}))
|
||||
})
|
||||
}
|
||||
|
||||
var pending = globList.length
|
||||
globList.forEach(function(globPattern) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue