diff --git a/docs/components.md b/docs/components.md index eea61ae7..d564c42f 100644 --- a/docs/components.md +++ b/docs/components.md @@ -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"}) //
Hello
``` -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. --- diff --git a/docs/css.md b/docs/css.md deleted file mode 100644 index ab4f3e2f..00000000 --- a/docs/css.md +++ /dev/null @@ -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, Hello) -// 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. `` vs `` (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`. diff --git a/docs/es6.md b/docs/es6.md index 33851e0d..c80c8ab5 100644 --- a/docs/es6.md +++ b/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", diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 79d8eb96..1b9a6eec 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -438,7 +438,7 @@ m("ul", users.map(function(u) { // -// ES6: +// ES6+: // m("ul", users.map(u => // m("li", u.name) // )) diff --git a/docs/jsx.md b/docs/jsx.md index 638948fe..e4d7c747 100644 --- a/docs/jsx.md +++ b/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 `
` is different from `
` (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: `
`. Also, unlike HTML, JSX is based on XML, so you can do things like `
` as equivalent to `
`, 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 `
`) -- static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs ``) +```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) =>
{item.name}
)} -
- - ) - } -} + 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 ( +
+ {type} + +
+ ) + } + + return { + oninit: init, + // To ensure the tag gets properly diffed on route change. + onbeforeupdate: init, + view: () => ( +
+

My ramblings about everything

+ +
+ {feed("Atom", "blog.atom.xml")} + {feed("RSS", "blog.rss.xml")} +
+ + {tag != null + ? + : ( +
+
Posts, sorted by most recent
+ +
+ ) + } + +
+ {posts.map((post) => ( + + + +
+
{post.title}
+
{post.preview}...
+
+ + +
+ ))} +
+
+ ) + } +} +``` --- diff --git a/docs/nav-guides.md b/docs/nav-guides.md index e8ac4234..3067ebe1 100644 --- a/docs/nav-guides.md +++ b/docs/nav-guides.md @@ -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) diff --git a/docs/promise.md b/docs/promise.md index 438c92f2..1fe8f97a 100644 --- a/docs/promise.md +++ b/docs/promise.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. diff --git a/docs/releasing.md b/docs/releasing.md index 6a40a475..f2d9b5b5 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -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` ``` diff --git a/docs/simple-application.md b/docs/simple-application.md index 4d558ff6..04c6727f 100644 --- a/docs/simple-application.md +++ b/docs/simple-application.md @@ -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 @@ -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 `