Compare commits
68 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00cf6c6b67 | ||
|
|
c9f185c6c9 | ||
|
|
b5857a7cd0 | ||
|
|
e15602ba49 | ||
|
|
27d94544a2 | ||
|
|
cfa890f685 | ||
|
|
f2cd99027b | ||
|
|
e5f897f8a2 | ||
|
|
0b8dd459dd | ||
|
|
a644d9f7ad | ||
|
|
d093e1422b | ||
|
|
542946fd74 | ||
|
|
08f5ccfeda | ||
|
|
abd7f86888 | ||
|
|
33e1562fc1 | ||
|
|
61ad84c1d9 | ||
|
|
806cb99ead | ||
|
|
3fea28e24d | ||
|
|
455cccb3b5 | ||
|
|
30d84f99e4 | ||
|
|
20dbbe816d | ||
|
|
745dfb614a | ||
|
|
9d7cf4d474 | ||
|
|
24b1ed5175 | ||
|
|
a0aae8f846 | ||
|
|
e000cd0803 | ||
|
|
d32d59413e | ||
|
|
53a4c2f3b4 | ||
|
|
36c503a9d7 | ||
|
|
a4bf63d899 | ||
|
|
b8f5a10022 | ||
|
|
700b9f85a1 | ||
|
|
bf842cec3f | ||
|
|
a4670fd092 | ||
|
|
05caaaf8f1 | ||
|
|
ac4ff1b67e | ||
|
|
84ca0d862d | ||
|
|
436634b1fe | ||
|
|
dcb992071f | ||
|
|
8673cd987d | ||
|
|
57c331c6b4 | ||
|
|
a4db1a1116 | ||
|
|
3e3c8cf047 | ||
|
|
8865d09457 | ||
|
|
8505916ebc | ||
|
|
2a7f31acb8 | ||
|
|
593562ab44 | ||
|
|
5af0693fea | ||
|
|
0d095d1373 | ||
|
|
3a633ce99c | ||
|
|
1b0899b469 | ||
|
|
2b687a31ea | ||
|
|
5351b9b973 | ||
|
|
123ccc1d0d | ||
|
|
8e7ef427e0 | ||
|
|
5488953436 | ||
|
|
ccdb138bb8 | ||
|
|
2fcaec7d8b | ||
|
|
9a426162f8 | ||
|
|
3fd24e3c73 | ||
|
|
f1fcaa6041 | ||
|
|
7d7d645046 | ||
|
|
9ba52b3a05 | ||
|
|
811c216cb4 | ||
|
|
a621d9d411 | ||
|
|
a28e91e15b | ||
|
|
6c1c5cf106 | ||
|
|
274cb2fff2 |
|
|
@ -10,6 +10,6 @@ trim_trailing_whitespace = true
|
|||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
|
||||
[{package.json,.travis.yml}]
|
||||
[{package.json,.travis.yml,.github/**/*.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
|
|||
|
|
@ -7,11 +7,6 @@
|
|||
/.eslintcache
|
||||
|
||||
# These are artifacts from various scripts
|
||||
/dist
|
||||
/archive
|
||||
/mithril.js
|
||||
/mithril.min.js
|
||||
/stream/stream.min.js
|
||||
|
||||
# And the examples are ignored (for now).
|
||||
/examples
|
||||
|
|
|
|||
2
.gitattributes
vendored
|
|
@ -1,8 +1,6 @@
|
|||
* text=auto
|
||||
/mithril.js binary
|
||||
/mithril.min.js binary
|
||||
/package-lock.json binary
|
||||
/yarn.lock binary
|
||||
|
||||
# Assets
|
||||
*.png binary
|
||||
|
|
|
|||
3
.github/CODEOWNERS
vendored
|
|
@ -1 +1,2 @@
|
|||
* @dead-claudia @StephanHoyer
|
||||
* @MithrilJS/Committers
|
||||
/.github/ @MithrilJS/Admins
|
||||
|
|
|
|||
26
.github/ISSUE_TEMPLATE/0-docs.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: '📃 Documentation Issue'
|
||||
description: Report an issue with Mithril.js's documentation
|
||||
assignees: dead-claudia
|
||||
labels:
|
||||
- 'Area: Documentation'
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Offending URL
|
||||
description: Provide a link to the page with the issue
|
||||
placeholder: https://mithril.js.org/
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue description
|
||||
description: What is the precise issue with it. Please be specific.
|
||||
validations:
|
||||
required: true
|
||||
117
.github/ISSUE_TEMPLATE/1-core.yml
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
name: '🐛 Framework Bug'
|
||||
description: Report a bug in Mithril.js core
|
||||
assignees: dead-claudia
|
||||
labels:
|
||||
- 'Area: Core'
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mithril.js Version
|
||||
description: |
|
||||
Provide the exact version of Mithril.js you're experiencing these issues with. This
|
||||
matters, even if it's really old like version 0.1.0. Do note that bugs in older
|
||||
versions are commonly fixed in newer versions, so you should try to test it
|
||||
against the latest version if you can.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Browser and OS
|
||||
description: |
|
||||
Provide the name and version of both the browser and operating system you're
|
||||
experiencing these issues with. If it's multiple, feel free to list multiple.
|
||||
This matters, even if it's super ancient.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Project
|
||||
description: |
|
||||
(Optional) Provide a link to your project, if it happens to be open source or if
|
||||
you created a repo somewhere that we can look into further. If it's spread across
|
||||
multiple repos or projects, feel free to list them all.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Code
|
||||
description: |
|
||||
What did you try? What code is causing the unexpected behavior? Make sure to
|
||||
try to reduce your code as best as you can while still reproducing the bug, so
|
||||
we can more accurately determine the cause. Ideally, it should just be a bunch
|
||||
of Mithril.js calls with virtually no logic at all, but it's sufficient to just
|
||||
remove unrelated network calls, attributes, and the like.
|
||||
|
||||
In addition, make sure the bug still persists with the latest version of
|
||||
Mithril. If it's an older version, the bug may have already been fixed.
|
||||
|
||||
If you'd prefer, replace this code block with a link to a code playground like
|
||||
any of these:
|
||||
|
||||
- Flems <https://flems.io/mithril> (stores everything in URL hash)
|
||||
- JSFiddle <https://jsfiddle.net>
|
||||
- CodePen <https://codepen.io>
|
||||
- JSBin <https://jsbin.com>
|
||||
- Plunker <https://plnkr.co>
|
||||
- Glitch <https://glitch.com> (supports backend)
|
||||
- CodeSandbox <https://codesandbox.io> (supports backend)
|
||||
|
||||
Or if it's a remote development project on your own server, feel free to provide
|
||||
that if it's serving unminified code we can look at.
|
||||
|
||||
If it's a closed-source repo, it's okay to censor names and pull out irrelevant
|
||||
logic - we'd rather not sign NDAs just to see the code you're having trouble
|
||||
with. We do still need code of some kind that triggers the bug you're running
|
||||
into.
|
||||
render: javascript
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
What steps need to be taken to reproduce this behavior? Please include things
|
||||
like specific data that need typed in, specific buttons that need clicked, and
|
||||
so on.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: |
|
||||
What did you expect to happen?
|
||||
|
||||
- An alert to pop up?
|
||||
- A specific thing to be logged?
|
||||
|
||||
Please be very specific here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Observed Behavior
|
||||
description: |
|
||||
What actually happened?
|
||||
|
||||
- The alert never showed?
|
||||
- The wrong thing was logged?
|
||||
|
||||
Please be very specific here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context
|
||||
description: |
|
||||
(Optional) How is this issue affecting you? What are you trying to do? Providing
|
||||
us context helps us reach a solution that best fits your particular needs.
|
||||
117
.github/ISSUE_TEMPLATE/2-stream.yml
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
name: '🌊 Mithril.js Streams bug'
|
||||
description: Report an issue with Mithril.js's Streams module
|
||||
assignees: dead-claudia
|
||||
labels:
|
||||
- 'Area: Stream'
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mithril.js Version
|
||||
description: |
|
||||
Provide the exact version of Mithril.js you're experiencing these issues with. This
|
||||
matters, even if it's really old like version 0.1.0. Do note that bugs in older
|
||||
versions are commonly fixed in newer versions, so you should try to test it
|
||||
against the latest version if you can.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Browser and OS
|
||||
description: |
|
||||
Provide the name and version of both the browser and operating system you're
|
||||
experiencing these issues with. If it's multiple, feel free to list multiple.
|
||||
This matters, even if it's super ancient.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Project
|
||||
description: |
|
||||
(Optional) Provide a link to your project, if it happens to be open source or if
|
||||
you created a repo somewhere that we can look into further. If it's spread across
|
||||
multiple repos or projects, feel free to list them all.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Code
|
||||
description: |
|
||||
What did you try? What code is causing the unexpected behavior? Make sure to
|
||||
try to reduce your code as best as you can while still reproducing the bug, so
|
||||
we can more accurately determine the cause. Ideally, it should just be a bunch
|
||||
of Mithril.js calls with virtually no logic at all, but it's sufficient to just
|
||||
remove unrelated network calls, attributes, and the like.
|
||||
|
||||
In addition, make sure the bug still persists with the latest version of
|
||||
Mithril. If it's an older version, the bug may have already been fixed.
|
||||
|
||||
If you'd prefer, replace this code block with a link to a code playground like
|
||||
any of these:
|
||||
|
||||
- Flems <https://flems.io/mithril> (stores everything in URL hash)
|
||||
- JSFiddle <https://jsfiddle.net>
|
||||
- CodePen <https://codepen.io>
|
||||
- JSBin <https://jsbin.com>
|
||||
- Plunker <https://plnkr.co>
|
||||
- Glitch <https://glitch.com> (supports backend)
|
||||
- CodeSandbox <https://codesandbox.io> (supports backend)
|
||||
|
||||
Or if it's a remote development project on your own server, feel free to provide
|
||||
that if it's serving unminified code we can look at.
|
||||
|
||||
If it's a closed-source repo, it's okay to censor names and pull out irrelevant
|
||||
logic - we'd rather not sign NDAs just to see the code you're having trouble
|
||||
with. We do still need code of some kind that triggers the bug you're running
|
||||
into.
|
||||
render: javascript
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
What steps need to be taken to reproduce this behavior? Please include things
|
||||
like specific data that need typed in, specific buttons that need clicked, and
|
||||
so on.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: |
|
||||
What did you expect to happen?
|
||||
|
||||
- An alert to pop up?
|
||||
- A specific thing to be logged?
|
||||
|
||||
Please be very specific here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Observed Behavior
|
||||
description: |
|
||||
What actually happened?
|
||||
|
||||
- The alert never showed?
|
||||
- The wrong thing was logged?
|
||||
|
||||
Please be very specific here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context
|
||||
description: |
|
||||
(Optional) How is this issue affecting you? What are you trying to do? Providing
|
||||
us context helps us reach a solution that best fits your particular needs.
|
||||
102
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -1,102 +0,0 @@
|
|||
---
|
||||
name: "\U0001F41B Bug"
|
||||
about: Report a bug in Mithril.js
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: dead-claudia
|
||||
|
||||
---
|
||||
|
||||
<!-- Provide a general summary of your issue in the "Title" above -->
|
||||
<!--
|
||||
Provide the exact version of Mithril.js you're experiencing these issues with. This
|
||||
matters, even if it's really old like version 0.1.0. Do note that bugs in older
|
||||
versions are commonly fixed in newer versions, so you should try to test it
|
||||
against the latest version if you can.
|
||||
-->
|
||||
**Mithril.js version:**
|
||||
|
||||
<!--
|
||||
Provide the name and version of both the browser and operating system you're
|
||||
experiencing these issues with. If it's multiple, feel free to list multiple.
|
||||
This matters, even if it's super ancient like IE 6 on Windows XP.
|
||||
-->
|
||||
**Browser and OS:**
|
||||
|
||||
<!--
|
||||
Optional: Provide a link to your project, if it happens to be open source or if
|
||||
you created a repo somewhere that we can look into further. If it's multiple
|
||||
projects, feel free to list them all.
|
||||
-->
|
||||
**Project:**
|
||||
|
||||
## Code
|
||||
<!--
|
||||
What did you try? What code is causing the unexpected behavior? Make sure to
|
||||
try to reduce your code as best as you can while still reproducing the bug, so
|
||||
we can more accurately determine the cause. Ideally, it should just be a bunch
|
||||
of Mithril.js calls with virtually no logic at all, but it's sufficient to just
|
||||
remove unrelated network calls, attributes, and the like.
|
||||
|
||||
In addition, make sure the bug still persists with the latest version of
|
||||
Mithril. If it's an older version, the bug may have already been fixed.
|
||||
|
||||
If you'd prefer, replace this code block with a link to a code playground like
|
||||
any of these:
|
||||
|
||||
- Flems <https://flems.io/mithril> (stores everything in URL hash)
|
||||
- JSFiddle <https://jsfiddle.net>
|
||||
- CodePen <https://codepen.io>
|
||||
- JSBin <https://jsbin.com>
|
||||
- Plunker <https://plnkr.co>
|
||||
- Glitch <https://glitch.com> (supports backend)
|
||||
- CodeSandbox <https://codesandbox.io> (supports backend)
|
||||
|
||||
Or if it's a remote development project on your own server, feel free to provide
|
||||
that if it's serving unminified code we can look at.
|
||||
|
||||
If it's a closed-source repo, it's okay to censor names and pull out irrelevant
|
||||
logic - we'd rather not sign NDAs just to see the code you're having trouble
|
||||
with. We do still need code of some kind that triggers the bug you're running
|
||||
into.
|
||||
-->
|
||||
```javascript
|
||||
// Code
|
||||
```
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--
|
||||
What steps need to be taken to reproduce this behavior? Please include things
|
||||
like specific data that need typed in, specific buttons that need clicked, and
|
||||
so on.
|
||||
-->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Expected Behavior
|
||||
<!--
|
||||
What did you expect to happen?
|
||||
|
||||
- An alert to pop up?
|
||||
- A specific thing to be logged?
|
||||
|
||||
Please be very specific here.
|
||||
-->
|
||||
|
||||
## Current Behavior
|
||||
<!--
|
||||
What actually happened?
|
||||
|
||||
- The alert never showed?
|
||||
- The wrong thing was logged?
|
||||
|
||||
Please be very specific here.
|
||||
-->
|
||||
|
||||
## Context
|
||||
<!--
|
||||
Optional: How is this issue affecting you? What are you trying to do? Providing
|
||||
us context helps us reach a solution that best fits your particular needs.
|
||||
-->
|
||||
18
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: ℹ Questions, Ideas, and Discussions
|
||||
url: https://github.com/MithrilJS/mithril.js/discussions
|
||||
about: |
|
||||
Got a question on how to use Mithril.js? Have a fancy idea of how we could do better? Check
|
||||
out our discussions forum!
|
||||
- name: ℹ Show and Tell
|
||||
url: https://github.com/MithrilJS/mithril.js/discussions/new?category=show-and-tell
|
||||
about: |
|
||||
Got something to show off? Made something so cool, you just have to tell the world? Let us
|
||||
know here in our discussions forum!
|
||||
- name: 🚀 Feature Request or Enhancement
|
||||
url: https://github.com/MithrilJS/mithril.js/discussions/new?category=ideas
|
||||
about: Got a feature request? Let us know here! We do those in our discussion forum.
|
||||
- name: 💻 Zulip Chat
|
||||
url: https://mithril.zulipchat.com/
|
||||
about: Not sure about something? Just want to hang out? Come over to our Zulip chat!
|
||||
72
.github/ISSUE_TEMPLATE/feature-or-enhancement.md
vendored
|
|
@ -1,72 +0,0 @@
|
|||
---
|
||||
name: "\U0001F680 Feature or Enhancement"
|
||||
about: Suggest an idea or feature for Mithril.js
|
||||
title: ''
|
||||
labels: 'Type: Enhancement'
|
||||
assignees: dead-claudia
|
||||
|
||||
---
|
||||
|
||||
<!-- Provide a general summary of your suggestion in the "Title" above -->
|
||||
<!--
|
||||
Optional: Provide the exact version of Mithril.js you're experiencing issues with.
|
||||
This could matter, even if it's really old like version 0.1.0. Do note that bugs
|
||||
in older versions are commonly fixed in newer versions and that newer versions
|
||||
are much more actively maintained than older versions, so it's unlikely we'll
|
||||
add new features to older versions like 0.1.x.
|
||||
-->
|
||||
**Mithril.js version:**
|
||||
|
||||
<!--
|
||||
Optional: Provide the name and version of both the platform (Chrome, Node, etc.)
|
||||
and operating system you're running Mithril.js on. If it's multiple, feel free to
|
||||
list multiple. This could matter, even if it's super ancient like IE 6 on
|
||||
Windows XP.
|
||||
-->
|
||||
**Platform and OS:**
|
||||
|
||||
<!--
|
||||
Optional: Provide a link to your project, if it happens to be open source or if
|
||||
you created a repo somewhere that we can look into further. If it's multiple
|
||||
projects, feel free to list them all.
|
||||
-->
|
||||
**Project:**
|
||||
|
||||
<!-- Required -->
|
||||
**Is this something you're interested in implementing yourself?**
|
||||
|
||||
### Description
|
||||
<!--
|
||||
What exactly are you suggesting? Is it a particular missing feature? An odd
|
||||
design choice you think could be improved? This doesn't need to be a concrete,
|
||||
fully-fledged proposal, but it does need to be clear - it's hard to act on
|
||||
suggestions that are too vague or generic.
|
||||
-->
|
||||
|
||||
### Why
|
||||
<!--
|
||||
Why is this important to you? How would you use it? We need to know what
|
||||
problems it would solve in the real world and what benefits it would bring, for
|
||||
both you and other potential users, so we know how we should prioritize it and
|
||||
so we can see if a better solution might exist.
|
||||
-->
|
||||
|
||||
### Possible Implementation
|
||||
<!--
|
||||
Optional: How might this be implemented? This is optional, but it helps us put
|
||||
the size and cost of the feature into perspective. Simpler features to implement
|
||||
can often be justified by just being helpful, but big, complex features could
|
||||
require a massive benefit to pay for their size, scale, and complexity.
|
||||
|
||||
(This is why the discussion on a context API similar to React's got so
|
||||
contentious - it's right on that line where it could go either way on the
|
||||
cost/benefit ratio for us.)
|
||||
-->
|
||||
|
||||
### Open Questions
|
||||
<!--
|
||||
Optional: What things still need discussed? If there are certain details you
|
||||
aren't sure about, this could help inform discussion. Open questions like these
|
||||
are precisely what shaped our sync vs async redraw API to be what they are for
|
||||
v2.
|
||||
-->
|
||||
82
.github/ISSUE_TEMPLATE/question.md
vendored
|
|
@ -1,82 +0,0 @@
|
|||
---
|
||||
name: "\U0001F64B♀️ Question"
|
||||
about: Ask a question about Mithril.js
|
||||
title: ''
|
||||
labels: 'Type: Question'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Provide a general summary of your question in the "Title" above -->
|
||||
<!--
|
||||
Provide the exact version of Mithril.js you're experiencing these issues with. This
|
||||
matters, even if it's really old like version 0.1.0.
|
||||
-->
|
||||
**Mithril.js version:**
|
||||
|
||||
<!--
|
||||
Provide the name and version of both the browser and operating system you're
|
||||
experiencing these issues with. If it's multiple, feel free to list multiple.
|
||||
This matters, even if it's super ancient like IE 6 on Windows XP.
|
||||
-->
|
||||
**Browser and OS:**
|
||||
|
||||
<!--
|
||||
Optional: Provide a link to your project, if it happens to be open source or if
|
||||
you created a repo somewhere that we can look into further. If it's multiple
|
||||
projects, feel free to list them all.
|
||||
-->
|
||||
**Project:**
|
||||
|
||||
## Code
|
||||
<!--
|
||||
What did you try? Please be specific here. If you'd prefer, replace this code
|
||||
block with a link to a code playground like any of these:
|
||||
|
||||
- Flems <https://flems.io/mithril> (stores everything in URL hash)
|
||||
- JSFiddle <https://jsfiddle.net>
|
||||
- CodePen <https://codepen.io>
|
||||
- JSBin <https://jsbin.com>
|
||||
- Plunker <https://plnkr.co>
|
||||
- Glitch <https://glitch.com> (supports backend)
|
||||
- CodeSandbox <https://codesandbox.io> (supports backend)
|
||||
|
||||
Or if it's a remote development project on your own server, feel free to provide
|
||||
that if it's serving unminified code we can look at.
|
||||
-->
|
||||
```javascript
|
||||
// Code
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
<!--
|
||||
What did you expect to happen?
|
||||
|
||||
- An alert to pop up?
|
||||
- A specific thing to be logged?
|
||||
-->
|
||||
|
||||
## Current Behavior
|
||||
<!--
|
||||
What actually happened?
|
||||
|
||||
- The alert never showed?
|
||||
- The wrong thing was logged?
|
||||
-->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--
|
||||
What steps need to be taken to reproduce this behavior? Please include things
|
||||
like specific data that need typed in, specific buttons that need clicked, and
|
||||
so on.
|
||||
-->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context
|
||||
<!--
|
||||
How is this issue affecting you? What are you trying to do? Providing us context
|
||||
helps us reach a conclusion that best fits your particular needs.
|
||||
-->
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -17,15 +17,12 @@
|
|||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Documentation change
|
||||
|
||||
## Checklist:
|
||||
## Checklist
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
- [ ] I have read the **CONTRIBUTING** document.
|
||||
- [ ] I have added tests to cover my changes.
|
||||
- [ ] All new and existing tests passed.
|
||||
- [ ] I have updated `docs/changelog.md`
|
||||
- [ ] My change requires a documentation update, and I've opened a pull request to update it already:
|
||||
- [ ] I have read https://mithril.js.org/contributing.html.
|
||||
|
|
|
|||
29
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
security:
|
||||
applies-to: security-updates
|
||||
patterns: ['*']
|
||||
normal:
|
||||
applies-to: version-updates
|
||||
patterns: ['*']
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
security:
|
||||
applies-to: security-updates
|
||||
patterns: ['*']
|
||||
normal:
|
||||
applies-to: version-updates
|
||||
patterns: ['*']
|
||||
16
.github/workflows/auto-merge.yml
vendored
|
|
@ -1,16 +0,0 @@
|
|||
name: auto-merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [ next ]
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ahmadnassri/action-dependabot-auto-merge@v2
|
||||
with:
|
||||
target: patch
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
10
.github/workflows/issue-create.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
name: Ping triage on issue create
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
jobs:
|
||||
notify_triage:
|
||||
uses: MithrilJS/infra/.github/workflows/notify-triage.yml@main
|
||||
secrets: inherit
|
||||
30
.github/workflows/lint-docs.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: lint-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ next ]
|
||||
pull_request:
|
||||
branches: [ next ]
|
||||
|
||||
jobs:
|
||||
lint-docs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run lint:docs
|
||||
30
.github/workflows/lint-js.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: lint-js
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ next ]
|
||||
pull_request:
|
||||
branches: [ next ]
|
||||
|
||||
jobs:
|
||||
lint-js:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run lint:js
|
||||
43
.github/workflows/merge.yml
vendored
|
|
@ -1,43 +0,0 @@
|
|||
name: merge
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: prr:deploy
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'next'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx pr-release merge --target master --source next --commit --force --clean --changelog ./docs/recent-changes.md --compact --minimize-semver-change
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
# The following will publish the release to npm
|
||||
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
name: Setup NPM Auth
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: npm publish
|
||||
name: Publish
|
||||
11
.github/workflows/pr-create-release.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
name: Warn on opening a PR to `release`
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
branches: [release]
|
||||
permissions:
|
||||
issues: write
|
||||
jobs:
|
||||
comment:
|
||||
uses: MithrilJS/infra/.github/workflows/reject-pr.yml@main
|
||||
secrets: inherit
|
||||
38
.github/workflows/pr.yml
vendored
|
|
@ -1,38 +0,0 @@
|
|||
name: pr
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ next ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: prr:pre-release
|
||||
|
||||
jobs:
|
||||
pr:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx pr-release pr --verbose --target master --source next --compact --verbose --minimize-semver-change
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# The following will publish a prerelease to npm
|
||||
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
name: Setup NPM Auth
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: npx pr-release infer-prerelease --preid=next --target master --source next --verbose --publish --minimize-semver-change
|
||||
name: Publish
|
||||
19
.github/workflows/push-master.yml
vendored
|
|
@ -1,19 +0,0 @@
|
|||
name: 'Push `master`'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
branches: ['master']
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
await github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `⚠⚠⚠ Hey @${context.actor}, did you mean to open this against \`next\`? ⚠⚠⚠`
|
||||
})
|
||||
33
.github/workflows/push-release.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Create release when pushing to `release`
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: merge-release
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx pr-release merge --target release --source main --commit --force --clean --changelog ./docs/recent-changes.md --compact --minimize-semver-change
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- run: bash scripts/set-versioned-branch.sh release
|
||||
# The following will publish the release to npm
|
||||
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
name: Setup NPM Auth
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: npm publish
|
||||
name: Publish
|
||||
19
.github/workflows/rollback.yml
vendored
|
|
@ -7,25 +7,16 @@ concurrency: prr:deploy
|
|||
|
||||
jobs:
|
||||
pr:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
ref: 'next'
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx pr-release rollback --verbose --target master --source next --verbose --ignore 'package*' --ignore docs/changelog.md --ignore docs/recent-changes.md
|
||||
- run: npx pr-release rollback --verbose --target release --source main --verbose --ignore 'package*' --ignore docs/changelog.md --ignore docs/recent-changes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- run: bash scripts/set-versioned-branch.sh release
|
||||
|
|
|
|||
30
.github/workflows/test-js.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: test-js
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ next ]
|
||||
pull_request:
|
||||
branches: [ next ]
|
||||
|
||||
jobs:
|
||||
test-js:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14, 16, 18]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run test:js
|
||||
41
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
name: Test and maybe release
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
uses: MithrilJS/infra/.github/workflows/run-tests.yml@main
|
||||
with:
|
||||
test-node: true
|
||||
all-versions: true
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
publish-prerelease:
|
||||
needs: run-tests
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
concurrency: prr:pre-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx pr-release pr --verbose --target release --source main --compact --verbose --minimize-semver-change
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# The following will publish a prerelease to npm
|
||||
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
name: Setup NPM Auth
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: npx pr-release infer-prerelease --preid=next --target release --source main --verbose --publish --minimize-semver-change
|
||||
name: Publish
|
||||
5
.gitignore
vendored
|
|
@ -5,8 +5,3 @@
|
|||
/.vscode
|
||||
/.DS_Store
|
||||
/.eslintcache
|
||||
/.lint-docs-cache
|
||||
|
||||
# These are artifacts from various scripts
|
||||
/dist
|
||||
/archive
|
||||
|
|
|
|||
|
|
@ -20,6 +20,3 @@ tests/
|
|||
# from changes to it in patch releases. Let's force people to finally stop using
|
||||
# them.
|
||||
/test-utils/
|
||||
|
||||
# Exclude archive of previous docs (see #2552)
|
||||
/archive/
|
||||
|
|
|
|||
26
README.md
|
|
@ -1,13 +1,21 @@
|
|||
# VNDB Fork of Mithril.js
|
||||
|
||||
This is a shallow fork of [mithril.js](https://mithril.js.org/) with some
|
||||
functionality removed to save a few bytes. In particular:
|
||||
|
||||
- No routing
|
||||
- No XHR wrapper
|
||||
- No URL helper utilities
|
||||
- 'throw' errors instead of console.error() (nothing to do with size, just makes it easier to capture errors)
|
||||
|
||||
There's room for further optimization, but that easily runs into diminishing
|
||||
returns. This'll do for now.
|
||||
|
||||
(Original README below)
|
||||
|
||||
# Mithril.js
|
||||
|
||||
[](https://www.npmjs.com/package/mithril)
|
||||
[](https://github.com/MithrilJS/mithril.js/blob/next/LICENSE)
|
||||
[](https://www.npmjs.com/package/mithril)
|
||||
[](https://www.npmjs.com/package/mithril)
|
||||
[](https://opencollective.com/mithriljs)
|
||||
[](https://mithril.zulipchat.com/)
|
||||
|
||||
- [What is Mithril.js?](#what-is-mithriljs?)
|
||||
- [What is Mithril.js?](#what-is-mithriljs)
|
||||
- [Installation](#installation)
|
||||
- [Documentation](#documentation)
|
||||
- [Getting Help](#getting-help)
|
||||
|
|
@ -15,7 +23,7 @@
|
|||
|
||||
## What is Mithril.js?
|
||||
|
||||
A modern client-side JavaScript framework for building Single Page Applications. It's small (<!-- size -->9.17 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
|
||||
A modern client-side JavaScript framework for building Single Page Applications. It's small (<!-- size -->5.88 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
|
||||
|
||||
Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ module.exports = function(render, schedule, console) {
|
|||
|
||||
function sync() {
|
||||
for (offset = 0; offset < subscriptions.length; offset += 2) {
|
||||
try { render(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw) }
|
||||
catch (e) { console.error(e) }
|
||||
render(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw)
|
||||
}
|
||||
offset = -1
|
||||
}
|
||||
|
|
|
|||
BIN
assets/16.png
|
Before Width: | Height: | Size: 703 B |
BIN
assets/32.png
|
Before Width: | Height: | Size: 1.3 KiB |
BIN
assets/48.png
|
Before Width: | Height: | Size: 1.9 KiB |
101
assets/README.md
|
|
@ -1,101 +0,0 @@
|
|||
This contains general base image assets for docs and such.
|
||||
|
||||
- `logo.svg` is the logo source file, the base asset. It's an Inkscape SVG file, complete with relevant metadata.
|
||||
- `github-logo.png` is the logo export used for the GitHub profile picture.
|
||||
- `gitter-logo.png` is the logo export used for the Gitter avatar picture.
|
||||
- `open-collective-logo.png` is the logo export used for Open Collective. Unlike the GitHub and Gitter logos, this one has a transparent background.
|
||||
- `16.png`, `32.png`, and `48.png` are the logo exports used for the favicon. They are generated separately by Inkscape as ImageMagick's output is horrendously blurry. (I don't get how such a powerful image manipulation library can be so bad at rasterizing an SVG file, but apparently it's possible.)
|
||||
|
||||
In the docs, some of the logos are stored there directly rather than being first generated here.
|
||||
|
||||
- `docs/logo.svg` is just the `logo.svg` here, but size-optimized with metadata and Inkscape-specific stuff such stripped. I generated this from Inkscape as it's easier than installing npm modules and there's little point in installing a whole development dependency just to minify one (already small) SVG file.
|
||||
- `docs/favicon.ico` is `16.png`, `32.png`, and `48.png` from here combined into a single `.ico` file.
|
||||
|
||||
The other favicon file, `docs/favicon.png`, is simply copied from `32.png` directly.
|
||||
|
||||
### Generating
|
||||
|
||||
If you want to generate all these yourself from the originals and replicate everything for yourself:
|
||||
|
||||
1. Open `assets/logo.svg` in Inkscape.
|
||||
|
||||
1. Save an optimized SVG copy to `docs/logo.svg` with the following settings (irrelevant ones omitted):
|
||||
|
||||
- "Options" tab:
|
||||
- Shorten color values: checked
|
||||
- Convert CSS attributes to XML attributes: checked
|
||||
- Collapse groups: checked
|
||||
- Create groups for similar attributes: checked
|
||||
- Keep editor data: unchecked
|
||||
- Keep unreferenced definitions: unchecked
|
||||
- Work around renderer bugs: checked
|
||||
- "SVG Output" tab:
|
||||
- Remove the XML declaration: unchecked
|
||||
- Remove metadata: checked
|
||||
- Remove comments: checked
|
||||
- Embed raster images: checked
|
||||
- Enable viewboxing: unchecked
|
||||
- Format output with line-breaks and indentation: unchecked
|
||||
- Strip the "xml:space" attribute from the root SVG element: checked
|
||||
- "IDs" tab:
|
||||
- Remove unused IDs: checked
|
||||
|
||||
1. Export as a PNG to `assets/github-logo.png` with the following settings:
|
||||
|
||||
- Export area: page
|
||||
- Image width: 500 pixels
|
||||
- Image height: 500 pixels
|
||||
|
||||
1. Export as a PNG to `assets/gitter-logo.png` with the following settings:
|
||||
|
||||
- Export area: page
|
||||
- Image width: 96 pixels
|
||||
- Image height: 96 pixels
|
||||
|
||||
1. Export as a PNG to `assets/open-collective-logo.png` with the following settings:
|
||||
|
||||
- Export area: page
|
||||
- Image width: 256 pixels
|
||||
- Image height: 256 pixels
|
||||
|
||||
1. Export as a PNG to `assets/16.png` with the following settings:
|
||||
|
||||
- Export area: page
|
||||
- Image width: 16 pixels
|
||||
- Image height: 16 pixels
|
||||
|
||||
1. Export as a PNG to `assets/32.png` with the following settings:
|
||||
|
||||
- Export area: page
|
||||
- Image width: 32 pixels
|
||||
- Image height: 32 pixels
|
||||
|
||||
1. Export as a PNG to `assets/48.png` with the following settings:
|
||||
|
||||
- Export area: page
|
||||
- Image width: 48 pixels
|
||||
- Image height: 48 pixels
|
||||
|
||||
1. Run the following ImageMagick commands from the repo's root:
|
||||
|
||||
```sh
|
||||
magick convert assets/github-logo.png -background white -flatten assets/github-logo.png
|
||||
magick convert assets/gitter-logo.png -background white -flatten assets/gitter-logo.png
|
||||
magick convert -background none assets/16.png assets/32.png assets/48.png docs/favicon.ico
|
||||
```
|
||||
|
||||
1. Verify the icon has the expected sizes contained within it by running the following ImageMagick command from the root:
|
||||
|
||||
```sh
|
||||
magick identify docs/favicon.ico
|
||||
```
|
||||
|
||||
This should print something along the lines of this:
|
||||
|
||||
```
|
||||
docs/favicon.ico[0] ICO 16x16 16x16+0+0 8-bit sRGB 0.000u 0:00.000
|
||||
docs/favicon.ico[1] ICO 32x32 32x32+0+0 8-bit sRGB 0.000u 0:00.006
|
||||
docs/favicon.ico[2] ICO 48x48 48x48+0+0 8-bit sRGB 15086B 0.000u 0:00.008
|
||||
```
|
||||
|
||||
1. Copy `assets/32.png` to `docs/favicon.png`.
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new 0 0 38.044 38.044"
|
||||
version="1.1"
|
||||
viewBox="0 0 38.044 38.044"
|
||||
id="svg4"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
inkscape:export-filename="C:\Users\impin\projects\mithril.js\assets\github-logo.png"
|
||||
inkscape:export-xdpi="1261.7"
|
||||
inkscape:export-ydpi="1261.7">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Leo Horie</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:contributor>
|
||||
<cc:Agent>
|
||||
<dc:title />
|
||||
</cc:Agent>
|
||||
</dc:contributor>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1368"
|
||||
inkscape:window-height="850"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="16.822627"
|
||||
inkscape:cx="9.6794233"
|
||||
inkscape:cy="19.021999"
|
||||
inkscape:window-x="-6"
|
||||
inkscape:window-y="-6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4"
|
||||
inkscape:document-rotation="0" />
|
||||
<path
|
||||
d="m31.716 24.543c-0.017 7.03-5.742 12.745-12.776 12.745-7.045 0-12.777-5.732-12.777-12.777 0-0.022 4e-3 -0.043 4e-3 -0.065-3.69-2.243-6.167-6.29-6.167-10.914 0-7.046 5.731-12.777 12.777-12.777 2.268 0 4.395 0.601 6.244 1.642 1.849-1.041 3.977-1.642 6.245-1.642 7.046 0 12.777 5.732 12.777 12.777 0 4.691-2.548 8.789-6.327 11.011zm-12.695-19.461c-2.312 1.713-3.906 4.341-4.22 7.352 1.3-0.448 2.689-0.702 4.139-0.702 1.514 0 2.96 0.278 4.307 0.764-0.298-3.037-1.898-5.689-4.226-7.414zm-10.504 18.063c1.303 0.579 2.743 0.909 4.26 0.909 1.475 0 2.879-0.307 4.154-0.858-2.114-1.826-3.629-4.325-4.195-7.167-2.263 1.662-3.838 4.2-4.219 7.116zm10.423-9.157c-1.457 0-2.846 0.298-4.109 0.837 0.361 2.928 1.929 5.482 4.19 7.157 2.243-1.662 3.802-4.187 4.18-7.085-1.304-0.581-2.744-0.909-4.261-0.909zm2.171 9.209c1.275 0.55 2.679 0.858 4.154 0.858 1.457 0 2.846-0.298 4.11-0.837-0.356-2.885-1.883-5.404-4.089-7.082-0.582 2.799-2.087 5.257-4.175 7.061zm-2.171 11.836c5.432 0 9.915-4.137 10.466-9.425-1.3 0.447-2.689 0.702-4.14 0.702-2.268 0-4.396-0.601-6.245-1.642-1.848 1.041-3.975 1.642-6.244 1.642-1.514 0-2.96-0.278-4.307-0.763 0.523 5.317 5.018 9.486 10.47 9.486zm-6.163-32.024c-5.803 0-10.523 4.72-10.523 10.523 0 3.418 1.645 6.451 4.177 8.375 0.744-3.581 2.999-6.607 6.059-8.408 0.011-3.847 1.735-7.293 4.442-9.631-1.276-0.552-2.679-0.859-4.155-0.859zm12.489 0c-1.475 0-2.879 0.307-4.154 0.858 2.715 2.345 4.444 5.804 4.444 9.664 0 0.022-4e-3 0.044-4e-3 0.065 3.007 1.829 5.209 4.852 5.918 8.416 2.613-1.917 4.319-4.999 4.319-8.48-1e-3 -5.802-4.721-10.523-10.523-10.523z"
|
||||
fill="#010002"
|
||||
id="path2" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
BIN
docs/16.png
|
Before Width: | Height: | Size: 1.5 KiB |
BIN
docs/32.png
|
Before Width: | Height: | Size: 4.1 KiB |
BIN
docs/48.png
|
Before Width: | Height: | Size: 8.1 KiB |
|
|
@ -1 +0,0 @@
|
|||
mithril.js.org
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<!--meta-description
|
||||
Approaches you can use to animate your Mithril.js-based apps, including technology and performance suggestions
|
||||
-->
|
||||
|
||||
# Animations
|
||||
|
||||
- [Technology choices](#technology-choices)
|
||||
- [Animation on element creation](#animation-on-element-creation)
|
||||
- [Animation on element removal](#animation-on-element-removal)
|
||||
- [Performance](#performance)
|
||||
|
||||
---
|
||||
|
||||
### Technology choices
|
||||
|
||||
Animations are often used to make applications come alive. Nowadays, browsers have good support for CSS animations, and there are [various](https://greensock.com/gsap) [libraries](https://github.com/julianshapiro/velocity) that provide fast JavaScript-based animations. There's also an upcoming [Web API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API) and a [polyfill](https://github.com/web-animations/web-animations-js) if you like living on the bleeding edge.
|
||||
|
||||
Mithril.js does not provide any animation APIs per se, since these other options are more than sufficient to achieve rich, complex animations. Mithril.js does, however, offer hooks to make life easier in some specific cases where it's traditionally difficult to make animations work.
|
||||
|
||||
---
|
||||
|
||||
### Animation on element creation
|
||||
|
||||
Animating an element via CSS when the element is created couldn't be simpler. Just add an animation to a CSS class normally:
|
||||
|
||||
```css
|
||||
.fancy {animation:fade-in 0.5s;}
|
||||
@keyframes fade-in {
|
||||
from {opacity:0;}
|
||||
to {opacity:1;}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
var FancyComponent = {
|
||||
view: function() {
|
||||
return m(".fancy", "Hello world")
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, FancyComponent)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Animation on element removal
|
||||
|
||||
The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril.js offers the [`onbeforeremove`](lifecycle-methods.md#onbeforeremove) hook that allows us to defer the removal of an element.
|
||||
|
||||
Let's create an `exit` animation that fades `opacity` from 1 to 0.
|
||||
|
||||
```css
|
||||
.exit {animation:fade-out 0.5s;}
|
||||
@keyframes fade-out {
|
||||
from {opacity:1;}
|
||||
to {opacity:0;}
|
||||
}
|
||||
```
|
||||
|
||||
Now let's create a contrived component that shows and hides the `FancyComponent` we created in the previous section:
|
||||
|
||||
```javascript
|
||||
var on = true
|
||||
|
||||
var Toggler = {
|
||||
view: function() {
|
||||
return [
|
||||
m("button", {onclick: function() {on = !on}}, "Toggle"),
|
||||
on ? m(FancyComponent) : null,
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next, let's modify `FancyComponent` so that it fades out when removed:
|
||||
|
||||
```javascript
|
||||
var FancyComponent = {
|
||||
onbeforeremove: function(vnode) {
|
||||
vnode.dom.classList.add("exit")
|
||||
return new Promise(function(resolve) {
|
||||
vnode.dom.addEventListener("animationend", resolve)
|
||||
})
|
||||
},
|
||||
view: function() {
|
||||
return m(".fancy", "Hello world")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`vnode.dom` points to the root DOM element of the component (`<div class="fancy">`). We use the classList API here to add an `exit` class to `<div class="fancy">`.
|
||||
|
||||
Then we return a Promise that resolves when the `animationend` event fires. When we return a promise from `onbeforeremove`, Mithril.js waits until the promise is resolved and only then it removes the element. In this case, it waits for the exit animation to finish.
|
||||
|
||||
We can verify that both the enter and exit animations work by mounting the `Toggler` component:
|
||||
|
||||
```javascript
|
||||
m.mount(document.body, Toggler)
|
||||
```
|
||||
|
||||
Note that the `onbeforeremove` hook only fires on the element that loses its `parentNode` when an element gets detached from the DOM. This behavior is by design and exists to prevent a potential jarring user experience where every conceivable exit animation on the page would run on a route change. If your exit animation is not running, make sure to attach the `onbeforeremove` handler as high up the tree as it makes sense to ensure that your animation code is called.
|
||||
|
||||
---
|
||||
|
||||
### Performance
|
||||
|
||||
When creating animations, it's recommended that you only use the `opacity` and `transform` CSS rules, since these can be hardware-accelerated by modern browsers and yield better performance than animating `top`, `left`, `width`, and `height`.
|
||||
|
||||
It's also recommended that you avoid the `box-shadow` rule and selectors like `:nth-child`, since these are also resource intensive options. If you want to animate a `box-shadow`, consider [putting the `box-shadow` rule on a pseudo element, and animate that element's opacity instead](https://tobiasahlin.com/blog/how-to-animate-box-shadow/). Other things that can be expensive include large or dynamically scaled images and overlapping elements with different `position` values (e.g. an absolute positioned element over a fixed element).
|
||||
140
docs/api.md
|
|
@ -1,140 +0,0 @@
|
|||
<!--meta-description
|
||||
An API cheatsheet for Mithril.js
|
||||
-->
|
||||
|
||||
# API
|
||||
|
||||
### Cheatsheet
|
||||
|
||||
Here are examples for the most commonly used methods. If a method is not listed below, it's meant for advanced usage.
|
||||
|
||||
#### m(selector, attrs, children) - [docs](hyperscript.md)
|
||||
|
||||
```javascript
|
||||
m("div.class#id", {title: "title"}, ["children"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.mount(element, component) - [docs](mount.md)
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
count: 0,
|
||||
inc: function() {state.count++}
|
||||
}
|
||||
|
||||
var Counter = {
|
||||
view: function() {
|
||||
return m("div", {onclick: state.inc}, state.count)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Counter)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.route(root, defaultRoute, routes) - [docs](route.md)
|
||||
|
||||
```javascript
|
||||
var Home = {
|
||||
view: function() {
|
||||
return "Welcome"
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/home", {
|
||||
"/home": Home, // defines `https://example.com/#!/home`
|
||||
})
|
||||
```
|
||||
|
||||
#### m.route.set(path) - [docs](route.md#mrouteset)
|
||||
|
||||
```javascript
|
||||
m.route.set("/home")
|
||||
```
|
||||
|
||||
#### m.route.get() - [docs](route.md#mrouteget)
|
||||
|
||||
```javascript
|
||||
var currentRoute = m.route.get()
|
||||
```
|
||||
|
||||
#### m.route.prefix = prefix - [docs](route.md#mrouteprefix)
|
||||
|
||||
Invoke this before `m.route()` to change the routing prefix.
|
||||
|
||||
```javascript
|
||||
m.route.prefix = "#!"
|
||||
```
|
||||
|
||||
#### m(m.route.Link, ...) - [docs](route.md#mroutelink)
|
||||
|
||||
```javascript
|
||||
m(m.route.Link, {href: "/Home"}, "Go to home page")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.request(options) - [docs](request.md)
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "PUT",
|
||||
url: "/api/v1/users/:id",
|
||||
params: {id: 1, name: "test"}
|
||||
})
|
||||
.then(function(result) {
|
||||
console.log(result)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.parseQueryString(querystring) - [docs](parseQueryString.md)
|
||||
|
||||
```javascript
|
||||
var object = m.parseQueryString("a=1&b=2")
|
||||
// {a: "1", b: "2"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.buildQueryString(object) - [docs](buildQueryString.md)
|
||||
|
||||
```javascript
|
||||
var querystring = m.buildQueryString({a: "1", b: "2"})
|
||||
// "a=1&b=2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.trust(htmlString) - [docs](trust.md)
|
||||
|
||||
```javascript
|
||||
m.render(document.body, m.trust("<h1>Hello</h1>"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.redraw() - [docs](redraw.md)
|
||||
|
||||
```javascript
|
||||
var count = 0
|
||||
function inc() {
|
||||
setInterval(function() {
|
||||
count++
|
||||
m.redraw()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
var Counter = {
|
||||
oninit: inc,
|
||||
view: function() {
|
||||
return m("div", count)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Counter)
|
||||
```
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
<!--meta-description
|
||||
The Mithril.js auto-redraw system re-renders your app after some functions complete. Here, we describe the idiomatic Mithril.js patterns that trigger those redraws.
|
||||
-->
|
||||
|
||||
# The auto-redraw system
|
||||
|
||||
Mithril.js implements a virtual DOM diffing system for fast rendering, and in addition, it offers various mechanisms to gain granular control over the rendering of an application.
|
||||
|
||||
When used idiomatically, Mithril.js employs an auto-redraw system that synchronizes the DOM whenever changes are made in the data layer. The auto-redraw system becomes enabled when you call `m.mount` or `m.route` (but it stays disabled if your app is bootstrapped solely via `m.render` calls).
|
||||
|
||||
The auto-redraw system simply consists of triggering a re-render function behind the scenes after certain functions complete.
|
||||
|
||||
### After event handlers
|
||||
|
||||
Mithril.js automatically redraws after DOM event handlers that are defined in a Mithril.js view:
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("div", {onclick: doSomething})
|
||||
}
|
||||
}
|
||||
|
||||
function doSomething() {
|
||||
// a redraw happens synchronously after this function runs
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent)
|
||||
```
|
||||
|
||||
You can disable an auto-redraw for specific events by setting `e.redraw` to `false`.
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("div", {onclick: doSomething})
|
||||
}
|
||||
}
|
||||
|
||||
function doSomething(e) {
|
||||
e.redraw = false
|
||||
// no longer triggers a redraw when the div is clicked
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent)
|
||||
```
|
||||
|
||||
|
||||
### After m.request
|
||||
|
||||
Mithril.js automatically redraws after [`m.request`](request.md) completes:
|
||||
|
||||
```javascript
|
||||
m.request("/api/v1/users").then(function() {
|
||||
// a redraw happens after this function runs
|
||||
})
|
||||
```
|
||||
|
||||
You can disable an auto-redraw for a specific request by setting the `background` option to true:
|
||||
|
||||
```javascript
|
||||
m.request("/api/v1/users", {background: true}).then(function() {
|
||||
// does not trigger a redraw
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### After route changes
|
||||
|
||||
Mithril.js automatically redraws after [`m.route.set()`](route.md#mrouteset) calls and after route changes via links using [`m.route.Link`](route.md#mroutelink).
|
||||
|
||||
```javascript
|
||||
var RoutedComponent = {
|
||||
view: function() {
|
||||
return [
|
||||
// a redraw happens asynchronously after the route changes
|
||||
m(m.route.Link, {href: "/"}),
|
||||
m("div", {
|
||||
onclick: function() {
|
||||
m.route.set("/")
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": RoutedComponent,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### When Mithril.js does not redraw
|
||||
|
||||
Mithril.js does not redraw after `setTimeout`, `setInterval`, `requestAnimationFrame`, raw `Promise` resolutions and 3rd party library event handlers (e.g. Socket.io callbacks). In those cases, you must manually call [`m.redraw()`](redraw.md).
|
||||
|
||||
Mithril.js also does not redraw after lifecycle methods. Parts of the UI may be redrawn after an `oninit` handler, but other parts of the UI may already have been redrawn when a given `oninit` handler fires. Handlers like `oncreate` and `onupdate` fire after the UI has been redrawn.
|
||||
|
||||
If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw.
|
||||
|
||||
```javascript
|
||||
function StableComponent() {
|
||||
var height = 0
|
||||
|
||||
return {
|
||||
oncreate: function(vnode) {
|
||||
height = vnode.dom.offsetHeight
|
||||
m.redraw()
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "This component is " + height + "px tall")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mithril.js does not auto-redraw vnode trees that are rendered via `m.render`. This means redraws do not occur after event changes and `m.request` calls for templates that were rendered via `m.render`. Thus, if your architecture requires manual control over when rendering occurs (as can sometimes be the case when using libraries like Redux), you should use `m.render` instead of `m.mount`.
|
||||
|
||||
Remember that `m.render` expects a vnode tree, and `m.mount` expects a component:
|
||||
|
||||
```javascript
|
||||
// wrap the component in a m() call for m.render
|
||||
m.render(document.body, m(MyComponent))
|
||||
|
||||
// don't wrap the component for m.mount
|
||||
m.mount(document.body, MyComponent)
|
||||
```
|
||||
|
||||
Mithril.js may also avoid auto-redrawing if the frequency of requested redraws is higher than one animation frame (typically around 16ms). This means, for example, that when using fast-firing events like `onresize` or `onscroll`, Mithril.js will automatically throttle the number of redraws to avoid lag.
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.buildPathname(), which creates URLs from path templates and query objects
|
||||
-->
|
||||
# buildPathname(object)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Turns a [path template](paths.md) and a parameters object into a string of form `/path/user?a=1&b=2`
|
||||
|
||||
```javascript
|
||||
var pathname = m.buildPathname("/path/:id", {id: "user", a: "1", b: "2"})
|
||||
// "/path/user?a=1&b=2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`pathname = m.buildPathname(object)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------------ | -------- | ---
|
||||
`path` | `String` | Yes | A URL path
|
||||
`query ` | `Object` | Yes | A key-value map to be converted into a string
|
||||
**returns** | `String` | | A string representing the URL with the query string
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.buildPathname` creates a [path name](paths.md) from a path template and a parameters object. It's useful for building URLs, and it's what [`m.route`](route.md) and [`m.request`](request.md) use internally to interpolate paths. It uses [`m.buildQueryString`](buildQueryString.md) to generate the query parameters to append to the path name.
|
||||
|
||||
```javascript
|
||||
var pathname = m.buildPathname("/path/:id", {id: "user", a: 1, b: 2})
|
||||
|
||||
// pathname is "/path/user?a=1&b=2"
|
||||
```
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.buildQueryString(), which converts an object like {a: "1", b: "2"} into a string like "a=1&b=2"
|
||||
-->
|
||||
|
||||
# buildQueryString(object)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Turns an object into a string of form `a=1&b=2`
|
||||
|
||||
```javascript
|
||||
var querystring = m.buildQueryString({a: "1", b: "2"})
|
||||
// "a=1&b=2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`querystring = m.buildQueryString(object)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------------ | -------- | ---
|
||||
`query` | `Object` | Yes | A key-value map to be converted into a string
|
||||
**returns** | `String` | | A string representing the input object
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.buildQueryString` creates a querystring from an object. It's useful for manipulating URLs
|
||||
|
||||
```javascript
|
||||
var querystring = m.buildQueryString({a: 1, b: 2})
|
||||
|
||||
// querystring is "a=1&b=2"
|
||||
```
|
||||
|
||||
#### Deep data structures
|
||||
|
||||
Deep data structures are serialized in a way that is understood by popular web application servers such as PHP, Rails and ExpressJS
|
||||
|
||||
```javascript
|
||||
var querystring = m.buildQueryString({a: ["hello", "world"]})
|
||||
|
||||
// querystring is "a[0]=hello&a[1]=world"
|
||||
```
|
||||
|
||||
151
docs/censor.md
|
|
@ -1,151 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.censor(), which helps cloning vnodes
|
||||
-->
|
||||
|
||||
# censor(object, extra)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#signature)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Returns a shallow-cloned object with lifecycle attributes and any given custom attributes omitted.
|
||||
|
||||
```javascript
|
||||
var attrs = {one: "two", enabled: false, oninit: function() {}}
|
||||
var censored = m.censor(attrs, ["enabled"])
|
||||
// {one: "two"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`censored = m.censor(object, extra)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------------ | -------- | ---
|
||||
`object` | `Object` | Yes | A key-value map to be converted into a string
|
||||
`extra` | `Array<String>` | No | Additional properties to omit.
|
||||
**returns** | `Object` | | The original object if no properties to omit existed on it, a shallow-cloned object with the removed properties otherwise.
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
Ordinarily, you don't need this method, and you'll just want to specify the attributes you want. But sometimes, it's more convenient to send all attributes you don't know to another element. This is often perfectly reasonable, but it can lead you into a major trap with lifecycle methods getting called twice.
|
||||
|
||||
```javascript
|
||||
function SomePage() {
|
||||
return {
|
||||
view: function() {
|
||||
return m(SomeFancyView, {
|
||||
oncreate: function() {
|
||||
sendViewHit(m.route.get(), "some fancy view")
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function SomeFancyView() {
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.attrs, [ // !!!
|
||||
// ...
|
||||
])
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This looks benign, but this creates a problem: you're sending two hits each time this view is navigated. This is where `m.censor` come in: it lets you strip that `oncreate` from the attributes so it only gets called once and so the caller can remain sane and rest assured they aren't dealing with super weird bugs because of it.
|
||||
|
||||
```javascript
|
||||
// Fixed
|
||||
function SomeFancyView() {
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("div", m.censor(vnode.attrs), [
|
||||
// ...
|
||||
])
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also run into similar issues with keys:
|
||||
|
||||
```javascript
|
||||
function SomePage() {
|
||||
return {
|
||||
view: function() {
|
||||
return m(Layout, {
|
||||
pageTitle: "Some Page",
|
||||
key: someKey,
|
||||
}, [
|
||||
// ...
|
||||
])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function Layout() {
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m("header", [
|
||||
m("h1", "My beautiful web app"),
|
||||
m("nav"),
|
||||
]),
|
||||
m(".body", vnode.attrs, [ // !!!
|
||||
m("h2", vnode.attrs.pageTitle),
|
||||
vnode.children,
|
||||
])
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This would end up [throwing an error](keys.md#key-restrictions) because here's what Mithril.js sees when creating the `Layout` vnode:
|
||||
|
||||
```javascript
|
||||
return [
|
||||
m("header", [
|
||||
m("h1", "My beautiful web app"),
|
||||
m("nav"),
|
||||
]),
|
||||
m(".body", {pageTitle: "Some Page", key: someKey}, [
|
||||
m("h2", "Some Page"),
|
||||
[/* ... */],
|
||||
])
|
||||
]
|
||||
```
|
||||
|
||||
You wouldn't likely catch that at first glance, especially in much more real-world scenarios where there might be indirection and/or other issues. To correct this, you similarly have to censor out the `key:` attribute. You can also censor out the custom `pageTitle` attribute, too, since it doesn't provide any real value being in the DOM.
|
||||
|
||||
```javascript
|
||||
// Fixed
|
||||
function Layout() {
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m("header", [
|
||||
m("h1", "My beautiful web app"),
|
||||
m("nav"),
|
||||
]),
|
||||
m(".body", m.censor(vnode.attrs, ["pageTitle"]), [
|
||||
m("h2", vnode.attrs.pageTitle),
|
||||
vnode.children,
|
||||
])
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
<!--meta-description
|
||||
Official change log for Mithril.js
|
||||
-->
|
||||
# Change log
|
||||
|
||||
- [v2.0.4](#v204)
|
||||
- [v2.0.3](#v203)
|
||||
- [v2.0.1](#v201)
|
||||
- [v2.0.0](#v200)
|
||||
- [Migrating from v1.x](migration-v1x.md)
|
||||
- [Migrating from v0.2.x](migration-v02x.md)
|
||||
- [v1.x changelog](https://mithril.js.org/archive/v1.1.7/change-log.html)
|
||||
- [v1.x docs](https://mithril.js.org/archive/v1.1.7/index.html)
|
||||
- [v0.2 docs](https://mithril.js.org/archive/v0.2.5/index.html)
|
||||
- [`ospec` change log](https://github.com/MithrilJS/ospec/blob/master/changelog.md)
|
||||
- [`mithril-stream` change log](#mithril-stream-change-log)
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
### Upcoming...
|
||||
|
||||
*Note for later: release as semver-minor.*
|
||||
|
||||
PSA: changes to [`mithril/stream`](stream.md) are now specified in this changelog. I've also moved the old stream changelog into this file [here](#mithril-stream-change-log).
|
||||
|
||||
- Added `m.Fragment = "["` for an easier time with JSX setups. ([#2744](https://github.com/MithrilJS/mithril.js/pull/2744) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Reduced package download size substantially by removing the archive of previous releases' documentation. ([#2561](https://github.com/MithrilJS/mithril.js/pull/2561) [@cztomsik](https://github.com/cztomsik))
|
||||
- Improved error messages in multiple places. ([#2536](https://github.com/MithrilJS/mithril.js/pull/2536) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- The redraw reentrancy check was moved from `m.mount` to `m.render` and its error message was updated accordingly. ([#2536](https://github.com/MithrilJS/mithril.js/pull/2536) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- This is unlikely to break people because if you were to do it with `m.render` directly before now, you'd corrupt Mithril.js' internal representation and internal errors could occur as a result. Now, it just warns you.
|
||||
- For a better debugging experience with `m.route` route resolvers, errors on `onmatch` in the default route are left unhandled and errors in `onmatch` in other routes are logged to the console before redirecting. ([#2536](https://github.com/MithrilJS/mithril.js/pull/2536) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Bug fix with `m.redraw` where if you removed a root that was previously visited in the current redraw pass, it would lose its place and skip the next root.
|
||||
- Add `params:` attribute to `m.route.Link`. ([#2537](https://github.com/MithrilJS/mithril.js/pull/2537) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Add `m.censor`. ([#2538](https://github.com/MithrilJS/mithril.js/pull/2538) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Re-add stream bundles. ([#2539](https://github.com/MithrilJS/mithril.js/pull/2539) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Remove extra isLifecycleMethod call from removeAttr. ([#2594](https://github.com/MithrilJS/mithril.js/pull/2594) [@ZeikJT](https://github.com/zeikjt))
|
||||
- Fix issue where ending a stream in the middle of a stream callback would result in erroneous parent stream state for the rest of that emit. ([#2603](https://github.com/MithrilJS/mithril.js/pull/2603) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Fix issue where new redraw handlers weren't copied over on update. ([#2578](https://github.com/MithrilJS/mithril.js/pull/2578) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Make changes to file inputs gracefully handled, and don't break if the current value and old value mismatch (and the new value isn't empty), but instead just log an error. ([#2578](https://github.com/MithrilJS/mithril.js/pull/2578) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- This mainly exists just to kick the can down the road - this is the only case I'm aware of where the DOM itself would be responsible for throwing an error. A proper fix to the greater issue of error handling is much more complex, and I'd rather not block users any longer over this one specific issue.
|
||||
- Allow Mithril.js to be loaded in non-browser environments without modification. ([#2633](https://github.com/MithrilJS/mithril.js/pull/2633) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Work around a bundler bug that corrupted RegExps [#2647](https://github.com/MithrilJS/mithril.js/issues/) ([#2655](https://github.com/MithrilJS/mithril.js/pull/2655))
|
||||
- Adapt handling of no content (204) responses to match XHR Spec ([#2624](https://github.com/MithrilJS/mithril.js/pull/2641)) [@Evoke-PHP](https://github.com/Evoke-PHP)
|
||||
- Add `URLSearchParams` support to `m.request` ([#2695](https://github.com/MithrilJS/mithril.js/pull/2695) [@Coteh](https://github.com/Coteh))
|
||||
- Standardise vnode text representation ([#2670](https://github.com/MithrilJS/mithril.js/pull/2670)) [@barneycarroll](https://github.com/barneycarroll)
|
||||
- API: Invalid escapes in routes are now safely handled. [@StephanHoyer](https://github.com/StephanHoyer) based on older [fix](https://github.com/MithrilJS/mithril.js/pull/2061) by [@dead-claudia](https://github.com/dead-claudia)
|
||||
|
||||
Important note: if you were using any of these undocumented tools, they are no longer available as of this release. This is not considered a breaking change as they were written for internal usage and as of v2 are all 100% unsupported in userland.
|
||||
|
||||
- Mithril.js' internal bundler, previously available at `mithril/bundler`
|
||||
- Prefer using a dedicated bundler like Webpack or Rollup instead.
|
||||
- Mithril.js' CommonJS sham polyfill, previously available at `mithril/module`
|
||||
- Prefer using native `import`/`export` and/or Budo instead.
|
||||
- Mithril.js' internal test mocks, previously available at `mithril/test-utils`
|
||||
- Prefer using JSDOM or similar instead.
|
||||
|
||||
I'd like to apologize for missing these deprecations in the initial 2.0.0 change log. This was a major policy change we had been communicating the entire time and we should've let you all know this there in the change log as well.
|
||||
|
||||
-->
|
||||
|
||||
### v2.0.4
|
||||
_2019-08-18_
|
||||
|
||||
- Fix double-rendering of trusted content within `contenteditable` elements ([#2516](https://github.com/MithrilJS/mithril.js/pull/2516) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Fix error on `m.trust` updating ([#2516](https://github.com/MithrilJS/mithril.js/pull/2516) [@dead-claudia](https://github.com/dead-claudia))
|
||||
|
||||
### v2.0.3
|
||||
_2019-07-28_
|
||||
|
||||
- Ensure vnodes are removed correctly in the face of `onbeforeremove` resolving after new nodes are added ([#2492](https://github.com/MithrilJS/mithril.js/pull/2492) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Fix prototype pollution vulnerability in `m.parseQueryString` ([#2494](https://github.com/MithrilJS/mithril.js/pull/2494) [@dead-claudia](https://github.com/dead-claudia))
|
||||
|
||||
*v2.0.2 was skipped as it had a critical flaw and was immediately unpublished.*
|
||||
|
||||
### v2.0.1
|
||||
_2019-07-24_
|
||||
|
||||
Same as v2.0.0, but with a publish that didn't have a botched upload.
|
||||
|
||||
### v2.0.0
|
||||
_2019-07-24_
|
||||
|
||||
#### Breaking changes
|
||||
|
||||
- API: Component vnode `children` are not normalized into vnodes on ingestion; normalization only happens if and when they are ingested by the view ([#2155](https://github.com/MithrilJS/mithril.js/pull/2155/) (thanks to [@magikstm](https://github.com/magikstm) for related optimization [#2064](https://github.com/MithrilJS/mithril.js/pull/2064)))
|
||||
- API: `m.redraw()` is always asynchronous ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
|
||||
- API: `m.mount()` will only render its own root when called, it will not trigger a `redraw()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
|
||||
- API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook.
|
||||
- API: `m.request` will no longer reject the Promise on server errors (eg. status >= 400) if the caller supplies an `extract` callback. This gives applications more control over handling server responses.
|
||||
- hyperscript: when attributes have a `null` or `undefined` value, they are treated as if they were absent. [#1773](https://github.com/MithrilJS/mithril.js/issues/1773) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
|
||||
- API: `m.request` errors no longer copy response fields to the error, but instead assign the parsed JSON response to `error.response` and the HTTP status code `error.code`.
|
||||
- hyperscript: when an attribute is defined on both the first and second argument (as a CSS selector and an `attrs` field, respectively), the latter takes precedence, except for `class` attributes that are still added together. [#2172](https://github.com/MithrilJS/mithril.js/issues/2172) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
|
||||
- cast className using toString ([#2309](https://github.com/MithrilJS/mithril.js/pull/2309))
|
||||
- render: call attrs' hooks last, with express exception of `onbeforeupdate` to allow attrs to block components from even diffing ([#2297](https://github.com/MithrilJS/mithril.js/pull/2297))
|
||||
- API: `m.withAttr` removed. ([#2317](https://github.com/MithrilJS/mithril.js/pull/2317))
|
||||
- request: `data` has now been split to `params` and `body` and `useBody` has been removed in favor of just using `body`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- route, request: Interpolated arguments are URL-escaped (and for declared routes, URL-unescaped) automatically. If you want to use a raw route parameter, use a variadic parameter like in `/asset/:path.../view`. This was previously only available in `m.route` route definitions, but it's now usable in both that and where paths are accepted. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- route, request: Interpolated arguments are *not* appended to the query string. This means `m.request({url: "/api/user/:id/get", params: {id: user.id}})` would result in a request like `GET /api/user/1/get`, not one like `GET /api/user/1/get?id=1`. If you really need it in both places, pass the same value via two separate parameters with the non-query-string parameter renamed, like in `m.request({url: "/api/user/:urlID/get", params: {id: user.id, urlID: user.id}})`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- route, request: `m.route.set`, `m.request`, and `m.jsonp` all use the same path template syntax now, and vary only in how they receive their parameters. Furthermore, declared routes in `m.route` shares the same syntax and semantics, but acts in reverse as if via pattern matching. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- request: `options.responseType` now defaults to `"json"` if `extract` is absent, and `deserialize` receives the parsed response, not the raw string. If you want the old behavior, [use `responseType: "text"`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType). ([#2335](https://github.com/MithrilJS/mithril.js/pull/2335))
|
||||
- request: set `Content-Type: application/json; charset=utf-8` for all XHR methods by default, provided they have a body that's `!= null` ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361), [#2421](https://github.com/MithrilJS/mithril.js/pull/2421))
|
||||
- This can cause CORS issues when issuing `GET` with bodies, but you can address them through configuring CORS appropriately.
|
||||
- Previously, it was only set for all non-`GET` methods and only when `useBody: true` was passed (the default), and it was always set for them. Now it's automatically omitted when no body is present, so the hole is slightly broadened.
|
||||
- route: query parameters in hash strings are no longer supported ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- It's technically invalid in hashes, so I'd rather push people to keep in line with spec.
|
||||
- render: validate all elements are either keyed or unkeyed, and treat `null`/`undefined`/booleans as strictly unkeyed ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Gives a nice little perf boost with keyed fragments.
|
||||
- Minor, but imperceptible impact (within the margin of error) with unkeyed fragments.
|
||||
- Also makes the model a lot more consistent - all values are either keyed or unkeyed.
|
||||
- vnodes: normalize boolean children to `null`/`undefined` at the vnode level, always stringify non-object children that aren't holes ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Previously, `true` was equivalent to `"true"` and `false` was equivalent to `""`.
|
||||
- Previously, numeric children weren't coerced. Now, they are.
|
||||
- Unlikely to break most components, but it *could* break some users.
|
||||
- This increases consistency with how booleans are handled with children, so it should be more intuitive.
|
||||
- route: `key` parameter for routes now only works globally for components ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- Previously, it worked for route resolvers, too.
|
||||
- This lets you ensure global layouts used in `render` still render by diff.
|
||||
- redraw: `mithril/redraw` now just exposes the `m.redraw` callback ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- The `.schedule`, `.unschedule`, and `.render` properties of the former `redrawService` are all removed.
|
||||
- If you want to know how to work around it, look at the call to `mount` in Mithril.js' source for `m.route`. That should help you in finding ways around the removed feature. (It doesn't take that much more code.)
|
||||
- api: `m.version` has been removed. If you really need the version for whatever reason, just read the `version` field of `mithril/package.json` directly. ([#2466](https://github.com/MithrilJS/mithril.js/pull/2466) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- route: `m.route.prefix(...)` is now `m.route.prefix = ...`. ([#2469](https://github.com/MithrilJS/mithril.js/pull/2469) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- This is a fully fledged property, so you can not only write to it, but you can also read from it.
|
||||
- This aligns better with user intuition.
|
||||
- route: `m.route.link` function removed in favor of `m.route.Link` component. ([#2469](https://github.com/MithrilJS/mithril.js/pull/2469) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- An optional `options` object is accepted as an attribute. This was initially targeting the old `m.route.link` function and was transferred to this. ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930))
|
||||
- The new component handles many more edge cases around user interaction, including accessibility.
|
||||
- Link navigation can be disabled and cancelled.
|
||||
- Link targets can be trivially changed.
|
||||
|
||||
#### News
|
||||
|
||||
- Mithril.js now only officially supports IE11, Firefox ESR, and the last two versions of Chrome/FF/Edge/Safari. ([#2296](https://github.com/MithrilJS/mithril.js/pull/2296))
|
||||
- API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
|
||||
- API: Event handlers may also be objects with `handleEvent` methods ([#1949](https://github.com/MithrilJS/mithril.js/pull/1949), [#2222](https://github.com/MithrilJS/mithril.js/pull/2222)).
|
||||
- API: `m.request` better error message on JSON parse error - ([#2195](https://github.com/MithrilJS/mithril.js/pull/2195), [@codeclown](https://github.com/codeclown))
|
||||
- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966))
|
||||
- API: `m.request` supports `responseType` as attr - ([#2193](https://github.com/MithrilJS/mithril.js/pull/2193))
|
||||
- Mocks: add limited support for the DOMParser API ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
|
||||
- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
|
||||
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
|
||||
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
|
||||
- docs: Emphasize Closure Components for stateful components, use them for all stateful component examples.
|
||||
- API: ES module bundles are now available for `mithril` and `mithril/stream` ([#2194](https://github.com/MithrilJS/mithril.js/pull/2194) [@porsager](https://github.com/porsager)).
|
||||
- All of the `m.*` properties from `mithril` are re-exported as named exports in addition to being attached to `m`.
|
||||
- `m()` itself from `mithril` is exported as the default export.
|
||||
- `mithril/stream`'s primary export is exported as the default export.
|
||||
- fragments: allow same attrs/children overloading logic as hyperscript ([#2328](https://github.com/MithrilJS/mithril.js/pull/2328))
|
||||
- route: Declared routes may check against path names with query strings. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- route: Declared routes in `m.route` now support `-` and `.` as delimiters for path segments. This means you can have a route like `"/edit/:file.:ext"`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- Previously, this was possible to do in `m.route.set`, `m.request`, and `m.jsonp`, but it was wholly untested for and also undocumented.
|
||||
- API: `m.buildPathname` and `m.parsePathname` added. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||
- route: Use `m.mount(root, null)` to unsubscribe and clean up after a `m.route(root, ...)` call. ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453))
|
||||
- render: new `redraw` parameter exposed any time a child event handler is used ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- route: `m.route.SKIP` can be returned from route resolvers to skip to the next route ([#2469](https://github.com/MithrilJS/mithril.js/pull/2469) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- API: Full DOM no longer required to execute `require("mithril")`. You just need to set the necessary globals to *something*, even if `null` or `undefined`, so they can be properly used. ([#2469](https://github.com/MithrilJS/mithril.js/pull/2469) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- This enables isomorphic use of `m.route.Link` and `m.route.prefix`.
|
||||
- This enables isomorphic use of `m.request`, provided the `background: true` option is set and that an `XMLHttpRequest` polyfill is included as necessary.
|
||||
- Note that methods requiring DOM operations will still throw errors, such as `m.render(...)`, `m.redraw()`, and `m.route(...)`.
|
||||
- render: Align custom elements to work like normal elements, minus all the HTML-specific magic. ([#2221](https://github.com/MithrilJS/mithril.js/pull/2221))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- API: `m.route.set()` causes all mount points to be redrawn ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
|
||||
- ospec: Fix assertion definitions breaking on comparison failure
|
||||
- render/attrs: Using style objects in hyperscript calls will now properly diff style properties from one render to another as opposed to re-writing all element style properties every render.
|
||||
- render/attrs All vnodes attributes are properly removed when absent or set to `null` or `undefined` [#1804](https://github.com/MithrilJS/mithril.js/issues/1804) [#2082](https://github.com/MithrilJS/mithril.js/issues/2082) ([#1865](https://github.com/MithrilJS/mithril.js/pull/1865), [#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
|
||||
- render/core: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) ([#1918](https://github.com/MithrilJS/mithril.js/pull/1918) [@robinchew](https://github.com/robinchew), [#2052](https://github.com/MithrilJS/mithril.js/pull/2052))
|
||||
- render/core: fix various updateNodes/removeNodes issues when the pool and fragments are involved [#1990](https://github.com/MithrilJS/mithril.js/issues/1990), [#1991](https://github.com/MithrilJS/mithril.js/issues/1991), [#2003](https://github.com/MithrilJS/mithril.js/issues/2003), [#2021](https://github.com/MithrilJS/mithril.js/pull/2021)
|
||||
- render/core: fix crashes when the keyed vnodes with the same `key` had different `tag` values [#2128](https://github.com/MithrilJS/mithril.js/issues/2128) [@JacksonJN](https://github.com/JacksonJN) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
|
||||
- render/core: fix cached nodes behavior in some keyed diff scenarios [#2132](https://github.com/MithrilJS/mithril.js/issues/2132) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
|
||||
- render/events: `addEventListener` and `removeEventListener` are always used to manage event subscriptions, preventing external interference.
|
||||
- render/events: Event listeners allocate less memory, swap at low cost, and are properly diffed now when rendered via `m.mount()`/`m.redraw()`.
|
||||
- render/events: `Object.prototype` properties can no longer interfere with event listener calls.
|
||||
- render/events: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed.
|
||||
- render/hooks: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992)
|
||||
- docs: tweaks: ([#2104](https://github.com/MithrilJS/mithril.js/pull/2104) [@mikeyb](https://github.com/mikeyb), [#2205](https://github.com/MithrilJS/mithril.js/pull/2205), [@cavemansspa](https://github.com/cavemansspa), [#2250](https://github.com/MithrilJS/mithril.js/pull/2250) [@dead-claudia](https://github.com/dead-claudia), [#2265](https://github.com/MithrilJS/mithril.js/pull/2265), [@dead-claudia](https://github.com/dead-claudia))
|
||||
- render/core: avoid touching `Object.prototype.__proto__` setter with `key: "__proto__"` in certain situations ([#2251](https://github.com/MithrilJS/mithril.js/pull/2251))
|
||||
- render/core: Vnodes stored in the dom node supplied to `m.render()` are now normalized [#2266](https://github.com/MithrilJS/mithril.js/pull/2266)
|
||||
- render/core: CSS vars can now be specified in `{style}` attributes ([#2192](https://github.com/MithrilJS/mithril.js/pull/2192) [@barneycarroll](https://github.com/barneycarroll)), ([#2311](https://github.com/MithrilJS/mithril.js/pull/2311) [@porsager](https://github.com/porsager)), ([#2312](https://github.com/MithrilJS/mithril.js/pull/2312) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- request: don't modify params, call `extract`/`serialize`/`deserialize` with correct `this` value ([#2288](https://github.com/MithrilJS/mithril.js/pull/2288))
|
||||
- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
|
||||
- render: remove some redundancy within the component initialization code ([#2213](https://github.com/MithrilJS/mithril.js/pull/2213))
|
||||
- API: `mithril` loads `mithril/index.js`, not the bundle, so users of `mithril/hyperscript`, `mithril/render`, and similar see the same Mithril instance as those just using `mithril` itself.
|
||||
- `https://unpkg.com/mithril` is configured to receive the *minified* bundle, not the development bundle.
|
||||
- The raw bundle itself remains accessible at `mithril.js`, and is *not* browser-wrapped.
|
||||
- Note: this *will* increase overhead with bundlers like Webpack, Rollup, and Browserify.
|
||||
- request: autoredraw support fixed for `async`/`await` in Chrome ([#2428](https://github.com/MithrilJS/mithril.js/pull/2428) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- render: fix when attrs change with `onbeforeupdate` returning false, then remaining the same on next redraw ([#2447](https://github.com/MithrilJS/mithril.js/pull/2447) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- render: fix internal error when `onbeforeupdate` returns false and then true with new child tree ([#2447](https://github.com/MithrilJS/mithril.js/pull/2447) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- route: arbitrary prefixes are properly supported now, including odd prefixes like `?#` and invalid prefixes like `#foo#bar` ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- request: correct IE workaround for response type non-support ([#2449](https://github.com/MithrilJS/mithril.js/pull/2449) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- render: correct `contenteditable` check to also check for `contentEditable` property name ([#2450](https://github.com/MithrilJS/mithril.js/pull/2450) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- docs: clarify valid key usage ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- route: don't pollute globals ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453) [@dead-claudia](https://github.com/dead-claudia))
|
||||
- request: track xhr replacements correctly ([#2455](https://github.com/MithrilJS/mithril.js/pull/2455) [@dead-claudia](https://github.com/dead-claudia))
|
||||
|
||||
## `mithril-stream` change log
|
||||
|
||||
Formerly, `mithril/stream` was available standalone as the package `mithril-stream`, but this package has been deprecated and is no longer updated. The changelog for that package prior to being merged back into Mithril.js proper is below.
|
||||
|
||||
### 2.0.0
|
||||
_2019-02-07_
|
||||
|
||||
- when a stream conditionally returns HALT, dependant stream will also end ([#2200](https://github.com/MithrilJS/mithril.js/pull/2200), [#2369](https://github.com/MithrilJS/mithril.js/pull/2369))
|
||||
- Add `stream.lift` as a user-friendly alternative to `merge -> map` or `combine` ([#1944](https://github.com/MithrilJS/mithril.js/issues/1944))
|
||||
- renamed HALT to SKIP ([#2207](https://github.com/MithrilJS/mithril.js/pull/2207))
|
||||
- rewrote implementation ([#2207](https://github.com/MithrilJS/mithril.js/pull/2207))
|
||||
- Removed `valueOf` & `toString` methods ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150))
|
||||
- fixed `stream.end` propagation ([#2369](https://github.com/MithrilJS/mithril.js/pull/2369))
|
||||
|
||||
### 1.1.0
|
||||
_2017-07-13_
|
||||
|
||||
- Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))
|
||||
|
|
@ -1,673 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on the structure, lifecycle methods, state management, and syntactic variants of components in Mithril.js
|
||||
-->
|
||||
|
||||
# Components
|
||||
|
||||
- [Structure](#structure)
|
||||
- [Lifecycle methods](#lifecycle-methods)
|
||||
- [Passing data to components](#passing-data-to-components)
|
||||
- [State](#state)
|
||||
- [Closure component state](#closure-component-state)
|
||||
- [POJO component state](#pojo-component-state)
|
||||
- [Classes](#classes)
|
||||
- [Class component state](#class-component-state)
|
||||
- [Special attributes](#special-attributes)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
### Structure
|
||||
|
||||
Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse.
|
||||
|
||||
Any JavaScript object that has a `view` method is a Mithril.js component. Components can be consumed via the [`m()`](hyperscript.md) utility:
|
||||
|
||||
```javascript
|
||||
// define your component
|
||||
var Example = {
|
||||
view: function(vnode) {
|
||||
return m("div", "Hello")
|
||||
}
|
||||
}
|
||||
|
||||
// consume your component
|
||||
m(Example)
|
||||
|
||||
// equivalent HTML
|
||||
// <div>Hello</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Lifecycle methods
|
||||
|
||||
Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes. Note that `vnode` is passed as an argument to each lifecycle method, as well as to `view` (with the _previous_ vnode passed additionally to `onbeforeupdate`):
|
||||
|
||||
```javascript
|
||||
var ComponentWithHooks = {
|
||||
oninit: function(vnode) {
|
||||
console.log("initialized")
|
||||
},
|
||||
oncreate: function(vnode) {
|
||||
console.log("DOM created")
|
||||
},
|
||||
onbeforeupdate: function(newVnode, oldVnode) {
|
||||
return true
|
||||
},
|
||||
onupdate: function(vnode) {
|
||||
console.log("DOM updated")
|
||||
},
|
||||
onbeforeremove: function(vnode) {
|
||||
console.log("exit animation can start")
|
||||
return new Promise(function(resolve) {
|
||||
// call after animation completes
|
||||
resolve()
|
||||
})
|
||||
},
|
||||
onremove: function(vnode) {
|
||||
console.log("removing DOM element")
|
||||
},
|
||||
view: function(vnode) {
|
||||
return "hello"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.
|
||||
|
||||
```javascript
|
||||
function initialize(vnode) {
|
||||
console.log("initialized as vnode")
|
||||
}
|
||||
|
||||
m(ComponentWithHooks, {oninit: initialize})
|
||||
```
|
||||
|
||||
Lifecycle methods in vnodes do not override component methods, nor vice versa. Component lifecycle methods are always run after the vnode's corresponding method.
|
||||
|
||||
Take care not to use lifecycle method names for your own callback function names in vnodes.
|
||||
|
||||
To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md).
|
||||
|
||||
---
|
||||
|
||||
### Passing data to components
|
||||
|
||||
Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function:
|
||||
|
||||
```javascript
|
||||
m(Example, {name: "Floyd"})
|
||||
```
|
||||
|
||||
This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`:
|
||||
|
||||
```javascript
|
||||
var Example = {
|
||||
view: function (vnode) {
|
||||
return m("div", "Hello, " + vnode.attrs.name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: Lifecycle methods can also be defined in the `attrs` object, so you should avoid using their names for your own callbacks as they would also be invoked by Mithril.js itself. Use them in `attrs` only when you specifically wish to use them as lifecycle methods.
|
||||
|
||||
---
|
||||
|
||||
### State
|
||||
|
||||
Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns.
|
||||
|
||||
Note that unlike many other frameworks, mutating component state does *not* trigger [redraws](autoredraw.md) or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by [m.request](request.md) complete or when the browser navigates to different routes. Mithril.js' component state mechanisms simply exist as a convenience for applications.
|
||||
|
||||
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
|
||||
|
||||
In the above examples, each component is defined as a POJO (Plain Old JavaScript Object), which is used by Mithril.js internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a **_closure component_**, which is simply a wrapper function which _returns_ a POJO component instance, which in turn carries its own, closed-over scope.
|
||||
|
||||
With a closure component, state can simply be maintained by variables that are declared within the outer function:
|
||||
|
||||
```javascript
|
||||
function ComponentWithState(initialVnode) {
|
||||
// Component state variable, unique to each instance
|
||||
var count = 0
|
||||
|
||||
// POJO component instance: any object with a
|
||||
// view function which returns a vnode
|
||||
return {
|
||||
oninit: function(vnode){
|
||||
console.log("init a closure component")
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("div",
|
||||
m("p", "Count: " + count),
|
||||
m("button", {
|
||||
onclick: function() {
|
||||
count += 1
|
||||
}
|
||||
}, "Increment count")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Any functions declared within the closure also have access to its state variables.
|
||||
|
||||
```javascript
|
||||
function ComponentWithState(initialVnode) {
|
||||
var count = 0
|
||||
|
||||
function increment() {
|
||||
count += 1
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
count -= 1
|
||||
}
|
||||
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("div",
|
||||
m("p", "Count: " + count),
|
||||
m("button", {
|
||||
onclick: increment
|
||||
}, "Increment"),
|
||||
m("button", {
|
||||
onclick: decrement
|
||||
}, "Decrement")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Closure components are consumed in the same way as POJOs, e.g. `m(ComponentWithState, { passedData: ... })`.
|
||||
|
||||
A big advantage of closure components is that we don't need to worry about binding `this` when attaching event handler callbacks. In fact `this` is never used at all and we never have to think about `this` context ambiguities.
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 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.
|
||||
|
||||
#### At initialization
|
||||
|
||||
For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple "blueprint" state initialization.
|
||||
|
||||
In the example below, `data` becomes a property of the `ComponentWithInitialState` component's `vnode.state` object.
|
||||
|
||||
```javascript
|
||||
var ComponentWithInitialState = {
|
||||
data: "Initial content",
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.state.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentWithInitialState)
|
||||
|
||||
// Equivalent HTML
|
||||
// <div>Initial content</div>
|
||||
```
|
||||
|
||||
#### Via vnode.state
|
||||
|
||||
As you can see, state can also be accessed via the `vnode.state` property, which is available to all lifecycle methods as well as the `view` method of a component.
|
||||
|
||||
```javascript
|
||||
var ComponentWithDynamicState = {
|
||||
oninit: function(vnode) {
|
||||
vnode.state.data = vnode.attrs.text
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.state.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentWithDynamicState, {text: "Hello"})
|
||||
|
||||
// Equivalent HTML
|
||||
// <div>Hello</div>
|
||||
```
|
||||
|
||||
#### Via the this keyword
|
||||
|
||||
State can also be accessed via the `this` keyword, which is available to all lifecycle methods as well as the `view` method of a component.
|
||||
|
||||
```javascript
|
||||
var ComponentUsingThis = {
|
||||
oninit: function(vnode) {
|
||||
this.data = vnode.attrs.text
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("div", this.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentUsingThis, {text: "Hello"})
|
||||
|
||||
// Equivalent HTML
|
||||
// <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 arrow functions, or if those are not supported, use `vnode.state`.
|
||||
|
||||
---
|
||||
|
||||
### Classes
|
||||
|
||||
If it suits your needs (like in object-oriented projects), components can also be written using classes:
|
||||
|
||||
```javascript
|
||||
class ClassComponent {
|
||||
constructor(vnode) {
|
||||
this.kind = "class component"
|
||||
}
|
||||
view() {
|
||||
return m("div", `Hello from a ${this.kind}`)
|
||||
}
|
||||
oncreate() {
|
||||
console.log(`A ${this.kind} was created`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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(ClassComponent))
|
||||
|
||||
// EXAMPLE: via m.mount
|
||||
m.mount(document.body, ClassComponent)
|
||||
|
||||
// EXAMPLE: via m.route
|
||||
m.route(document.body, "/", {
|
||||
"/": ClassComponent
|
||||
})
|
||||
|
||||
// EXAMPLE: component composition
|
||||
class AnotherClassComponent {
|
||||
view() {
|
||||
return m("main", [
|
||||
m(ClassComponent)
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Class component state
|
||||
|
||||
With classes, state can be managed by class instance properties and methods, and accessed via `this`:
|
||||
|
||||
```javascript
|
||||
class ComponentWithState {
|
||||
constructor(vnode) {
|
||||
this.count = 0
|
||||
}
|
||||
increment() {
|
||||
this.count += 1
|
||||
}
|
||||
decrement() {
|
||||
this.count -= 1
|
||||
}
|
||||
view() {
|
||||
return m("div",
|
||||
m("p", "Count: " + count),
|
||||
m("button", {
|
||||
onclick: () => {this.increment()}
|
||||
}, "Increment"),
|
||||
m("button", {
|
||||
onclick: () => {this.decrement()}
|
||||
}, "Decrement")
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that we must use arrow functions for the event handler callbacks so the `this` context can be referenced correctly.
|
||||
|
||||
---
|
||||
|
||||
### Mixing component kinds
|
||||
|
||||
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
|
||||
|
||||
---
|
||||
|
||||
### Special attributes
|
||||
|
||||
Mithril.js 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.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
Although Mithril.js is flexible, some code patterns are discouraged:
|
||||
|
||||
#### Avoid fat components
|
||||
|
||||
Generally speaking, a "fat" component is a component that has custom instance methods. In other words, you should avoid attaching functions to `vnode.state` or `this`. It's exceedingly rare to have logic that logically fits in a component instance method and that can't be reused by other components. It's relatively common that said logic might be needed by a different component down the road.
|
||||
|
||||
It's easier to refactor code if that logic is placed in the data layer than if it's tied to a component state.
|
||||
|
||||
Consider this fat component:
|
||||
|
||||
```javascript
|
||||
// views/Login.js
|
||||
// AVOID
|
||||
var Login = {
|
||||
username: "",
|
||||
password: "",
|
||||
setUsername: function(value) {
|
||||
this.username = value
|
||||
},
|
||||
setPassword: function(value) {
|
||||
this.password = value
|
||||
},
|
||||
canSubmit: function() {
|
||||
return this.username !== "" && this.password !== ""
|
||||
},
|
||||
login: function() {/*...*/},
|
||||
view: function() {
|
||||
return m(".login", [
|
||||
m("input[type=text]", {
|
||||
oninput: function (e) { this.setUsername(e.target.value) },
|
||||
value: this.username,
|
||||
}),
|
||||
m("input[type=password]", {
|
||||
oninput: function (e) { this.setPassword(e.target.value) },
|
||||
value: this.password,
|
||||
}),
|
||||
m("button", {disabled: !this.canSubmit(), onclick: this.login}, "Login"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Normally, in the context of a larger application, a login component like the one above exists alongside components for user registration and password recovery. Imagine that we want to be able to prepopulate the email field when navigating from the login screen to the registration or password recovery screens (or vice versa), so that the user doesn't need to re-type their email if they happened to fill the wrong page (or maybe you want to bump the user to the registration form if a username is not found).
|
||||
|
||||
Right away, we see that sharing the `username` and `password` fields from this component to another is difficult. This is because the fat component encapsulates its state, which by definition makes this state difficult to access from outside.
|
||||
|
||||
It makes more sense to refactor this component and pull the state code out of the component and into the application's data layer. This can be as simple as creating a new module:
|
||||
|
||||
```javascript
|
||||
// models/Auth.js
|
||||
// PREFER
|
||||
var Auth = {
|
||||
username: "",
|
||||
password: "",
|
||||
setUsername: function(value) {
|
||||
Auth.username = value
|
||||
},
|
||||
setPassword: function(value) {
|
||||
Auth.password = value
|
||||
},
|
||||
canSubmit: function() {
|
||||
return Auth.username !== "" && Auth.password !== ""
|
||||
},
|
||||
login: function() {/*...*/},
|
||||
}
|
||||
|
||||
module.exports = Auth
|
||||
```
|
||||
|
||||
Then, we can clean up the component:
|
||||
|
||||
```javascript
|
||||
// views/Login.js
|
||||
// PREFER
|
||||
var Auth = require("../models/Auth")
|
||||
|
||||
var Login = {
|
||||
view: function() {
|
||||
return m(".login", [
|
||||
m("input[type=text]", {
|
||||
oninput: function (e) { Auth.setUsername(e.target.value) },
|
||||
value: Auth.username
|
||||
}),
|
||||
m("input[type=password]", {
|
||||
oninput: function (e) { Auth.setPassword(e.target.value) },
|
||||
value: Auth.password
|
||||
}),
|
||||
m("button", {
|
||||
disabled: !Auth.canSubmit(),
|
||||
onclick: Auth.login
|
||||
}, "Login")
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This way, the `Auth` module is now the source of truth for auth-related state, and a `Register` component can easily access this data, and even reuse methods like `canSubmit`, if needed. In addition, if validation code is required (for example, for the email field), you only need to modify `setEmail`, and that change will do email validation for any component that modifies an email field.
|
||||
|
||||
As a bonus, notice that we no longer need to use `.bind` to keep a reference to the state for the component's event handlers.
|
||||
|
||||
#### Don't forward `vnode.attrs` itself to other vnodes
|
||||
|
||||
Sometimes, you might want to keep an interface flexible and your implementation simpler by forwarding attributes to a particular child component or element, in this case [Bootstrap's modal](https://getbootstrap.com/docs/4.1/components/modal/). It might be tempting to forward a vnode's attributes like this:
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var Modal = {
|
||||
// ...
|
||||
view: function(vnode) {
|
||||
return m(".modal[tabindex=-1][role=dialog]", vnode.attrs, [
|
||||
// forwarding `vnode.attrs` here ^
|
||||
// ...
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you do it like above, you could run into issues when using it:
|
||||
|
||||
```javascript
|
||||
var MyModal = {
|
||||
view: function() {
|
||||
return m(Modal, {
|
||||
// This toggles it twice, so it doesn't show
|
||||
onupdate: function(vnode) {
|
||||
if (toggle) $(vnode.dom).modal("toggle")
|
||||
}
|
||||
}, [
|
||||
// ...
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead, you should forward *single* attributes into vnodes:
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var Modal = {
|
||||
// ...
|
||||
view: function(vnode) {
|
||||
return m(".modal[tabindex=-1][role=dialog]", vnode.attrs.attrs, [
|
||||
// forwarding `attrs:` here ^
|
||||
// ...
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// Example
|
||||
var MyModal = {
|
||||
view: function() {
|
||||
return m(Modal, {
|
||||
attrs: {
|
||||
// This toggles it once
|
||||
onupdate: function(vnode) {
|
||||
if (toggle) $(vnode.dom).modal("toggle")
|
||||
}
|
||||
},
|
||||
// ...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Don't manipulate `children`
|
||||
|
||||
If a component is opinionated in how it applies attributes or children, you should switch to using custom attributes.
|
||||
|
||||
Often it's desirable to define multiple sets of children, for example, if a component has a configurable title and body.
|
||||
|
||||
Avoid destructuring the `children` property for this purpose.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var Header = {
|
||||
view: function(vnode) {
|
||||
return m(".section", [
|
||||
m(".header", vnode.children[0]),
|
||||
m(".tagline", vnode.children[1]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m(Header, [
|
||||
m("h1", "My title"),
|
||||
m("h2", "Lorem ipsum"),
|
||||
])
|
||||
|
||||
// awkward consumption use case
|
||||
m(Header, [
|
||||
[
|
||||
m("h1", "My title"),
|
||||
m("small", "A small note"),
|
||||
],
|
||||
m("h2", "Lorem ipsum"),
|
||||
])
|
||||
```
|
||||
|
||||
The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content:
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var BetterHeader = {
|
||||
view: function(vnode) {
|
||||
return m(".section", [
|
||||
m(".header", vnode.attrs.title),
|
||||
m(".tagline", vnode.attrs.tagline),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m(BetterHeader, {
|
||||
title: m("h1", "My title"),
|
||||
tagline: m("h2", "Lorem ipsum"),
|
||||
})
|
||||
|
||||
// clearer consumption use case
|
||||
m(BetterHeader, {
|
||||
title: [
|
||||
m("h1", "My title"),
|
||||
m("small", "A small note"),
|
||||
],
|
||||
tagline: m("h2", "Lorem ipsum"),
|
||||
})
|
||||
```
|
||||
|
||||
#### Define components statically, call them dynamically
|
||||
|
||||
##### Avoid creating component definitions inside views
|
||||
|
||||
If you create a component from within a `view` method (either directly inline or by calling a function that does so), each redraw will have a different clone of the component. When diffing component vnodes, if the component referenced by the new vnode is not strictly equal to the one referenced by the old component, the two are assumed to be different components even if they ultimately run equivalent code. This means components created dynamically via a factory will always be re-created from scratch.
|
||||
|
||||
For that reason you should avoid recreating components. Instead, consume components idiomatically.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var ComponentFactory = function(greeting) {
|
||||
// creates a new component on every call
|
||||
return {
|
||||
view: function() {
|
||||
return m("div", greeting)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.render(document.body, m(ComponentFactory("hello")))
|
||||
// calling a second time recreates div from scratch rather than doing nothing
|
||||
m.render(document.body, m(ComponentFactory("hello")))
|
||||
|
||||
// PREFER
|
||||
var Component = {
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.attrs.greeting)
|
||||
}
|
||||
}
|
||||
m.render(document.body, m(Component, {greeting: "hello"}))
|
||||
// calling a second time does not modify DOM
|
||||
m.render(document.body, m(Component, {greeting: "hello"}))
|
||||
```
|
||||
|
||||
##### Avoid creating component instances outside views
|
||||
|
||||
Conversely, for similar reasons, if a component instance is created outside of a view, future redraws will perform an equality check on the node and skip it. Therefore component instances should always be created inside views:
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var Counter = {
|
||||
count: 0,
|
||||
view: function(vnode) {
|
||||
return m("div",
|
||||
m("p", "Count: " + vnode.state.count ),
|
||||
|
||||
m("button", {
|
||||
onclick: function() {
|
||||
vnode.state.count++
|
||||
}
|
||||
}, "Increase count")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var counter = m(Counter)
|
||||
|
||||
m.mount(document.body, {
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m("h1", "My app"),
|
||||
counter
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, clicking the counter component button will increase its state count, but its view will not be triggered because the vnode representing the component shares the same reference, and therefore the render process doesn't diff them. You should always call components in the view to ensure a new vnode is created:
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var Counter = {
|
||||
count: 0,
|
||||
view: function(vnode) {
|
||||
return m("div",
|
||||
m("p", "Count: " + vnode.state.count ),
|
||||
|
||||
m("button", {
|
||||
onclick: function() {
|
||||
vnode.state.count++
|
||||
}
|
||||
}, "Increase count")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, {
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m("h1", "My app"),
|
||||
m(Counter)
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
|
@ -38,7 +38,7 @@ To send a pull request:
|
|||
|
||||
- fork the repo (button at the top right in GitHub)
|
||||
- clone the forked repo to your computer (green button in GitHub)
|
||||
- Switch to the `next` branch (run `git checkout next`)
|
||||
- Switch to the `main` branch (run `git checkout main`)
|
||||
- create a feature branch (run `git checkout -b the-feature-branch-name`)
|
||||
- make your changes
|
||||
- run the tests (run `npm test`)
|
||||
|
|
|
|||
189
docs/es6.md
|
|
@ -1,189 +0,0 @@
|
|||
<!--meta-description
|
||||
Approaches you can use to integrate ES6 into your Mithril.js-based apps, including technology and usability suggestions
|
||||
-->
|
||||
# ES6+ on legacy browsers
|
||||
|
||||
- [Setup](#setup)
|
||||
- [Using Babel with Webpack](#using-babel-with-webpack)
|
||||
|
||||
---
|
||||
|
||||
Mithril.js 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).
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Once you've got that downloaded, open a terminal and run these commands:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
Now, you can go one of a couple different routes:
|
||||
|
||||
- [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
|
||||
```
|
||||
|
||||
Now, create a `.babelrc` file and set up with `@babel/preset-env`.
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"sourceMaps": true
|
||||
}
|
||||
```
|
||||
|
||||
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 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 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 webpack webpack-cli @babel/core babel-loader @babel/preset-env --save-dev
|
||||
```
|
||||
|
||||
Now, create a `.babelrc` file and set up with `@babel/preset-env`.
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"sourceMaps": true
|
||||
}
|
||||
```
|
||||
|
||||
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: path.resolve(__dirname, 'src/index.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'app.js',
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
exclude: /\/node_modules\//,
|
||||
use: {
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
{
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And now, the command is a little easier to type and remember.
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
webpack -p
|
||||
```
|
||||
|
||||
You may want to also add this to your npm scripts, so you can build it quickly and easily.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
{
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p",
|
||||
"heroku-postbuild": "webpack -p"
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
docs/favicon.ico
|
Before Width: | Height: | Size: 15 KiB |
BIN
docs/favicon.png
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,76 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.fragment(), which allows attaching lifecycle methods and keys to a fragment vnode
|
||||
-->
|
||||
|
||||
# fragment(attrs, children)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Allows attaching lifecycle methods to a fragment [vnode](vnodes.md)
|
||||
|
||||
```javascript
|
||||
var groupVisible = true
|
||||
var log = function() {
|
||||
console.log("group is now visible")
|
||||
}
|
||||
|
||||
m("ul", [
|
||||
m("li", "child 1"),
|
||||
m("li", "child 2"),
|
||||
groupVisible ? m.fragment({oninit: log}, [
|
||||
// a fragment containing two elements
|
||||
m("li", "child 3"),
|
||||
m("li", "child 4"),
|
||||
]) : null
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
Generates a fragment [vnode](vnodes.md)
|
||||
|
||||
`vnode = m.fragment(attrs, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | --------------------------------------------------- | -------- | ---
|
||||
`attrs` | `Object` | No | HTML attributes or element properties
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats)
|
||||
**returns** | `Vnode` | | A fragment [vnode](vnodes.md#structure)
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
`m.fragment()` creates a [fragment vnode](vnodes.md) with attributes. It is meant for advanced use cases involving [keys](keys.md) or [lifecyle methods](lifecycle-methods.md).
|
||||
|
||||
A fragment vnode represents a list of DOM elements. If you want a regular element vnode that represents only one DOM element and don't require keyed logic, you should use [`m()`](hyperscript.md) instead.
|
||||
|
||||
Normally you can use simple arrays or splats instead to denote a list of nodes:
|
||||
|
||||
```javascript
|
||||
var groupVisible = true
|
||||
|
||||
m("ul",
|
||||
m("li", "child 1"),
|
||||
m("li", "child 2"),
|
||||
groupVisible ? [
|
||||
// a fragment containing two elements
|
||||
m("li", "child 3"),
|
||||
m("li", "child 4"),
|
||||
] : null
|
||||
)
|
||||
```
|
||||
|
||||
However, JavaScript arrays cannot be keyed or hold lifecycle methods. One option would be to create a wrapper element to host the key or lifecycle method, but sometimes it is not desirable to have an extra element (for example in complex table structures). In those cases, a fragment vnode can be used instead.
|
||||
|
||||
There are a few benefits that come from using `m.fragment` instead of handwriting a vnode object structure: m.fragment creates [monomorphic objects](vnodes.md#monomorphic-class), which have better performance characteristics than creating objects dynamically. In addition, using `m.fragment` makes your intentions clear to other developers, and it makes it less likely that you'll mistakenly set attributes on the vnode object itself rather than on its `attrs` map.
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
<!--meta-description
|
||||
Detailed comparison between Mithril.js and other popular frameworks
|
||||
-->
|
||||
|
||||
# Framework comparison
|
||||
|
||||
- [Why not X?](#why-not-insert-favorite-framework-here?)
|
||||
- [Why use Mithril.js?](#why-use-mithril?)
|
||||
- [React](#react)
|
||||
- [Angular](#angular)
|
||||
- [Vue](#vue)
|
||||
|
||||
If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril.js would help you solve your problems more effectively.
|
||||
|
||||
---
|
||||
|
||||
## Why not [insert favorite framework here]?
|
||||
|
||||
The reality is that most modern frameworks are fast, well-suited to build complex applications, and maintainable if you know how to use them effectively. There are examples of highly complex applications in the wild using just about every popular framework: Udemy uses Angular, AirBnB uses React, Gitlab uses Vue, Guild Wars 2 uses Mithril.js (yes, inside the game!). Clearly, these are all production-quality frameworks.
|
||||
|
||||
As a rule of thumb, if your team is already heavily invested in another framework/library/stack, it makes more sense to stick with it, unless your team agrees that there's a very strong reason to justify a costly rewrite.
|
||||
|
||||
However, if you're starting something new, do consider giving Mithril.js a try, if nothing else, to see how much value Mithril.js adopters have been getting out of under 10kb (gzipped) of code. Mithril.js is used by many well-known companies (e.g. Vimeo, Nike, Fitbit), and it powers large open-sourced platforms too (e.g. Lichess, Flarum).
|
||||
|
||||
---
|
||||
|
||||
## Why use Mithril.js?
|
||||
|
||||
In one sentence: because **Mithril.js is pragmatic**. This [10 minute guide](index.md) is a good example: that's how long it takes to learn components, XHR and routing - and that's just about the right amount of knowledge needed to build useful applications.
|
||||
|
||||
Mithril.js is all about getting meaningful work done efficiently. Doing file uploads? [The docs show you how](request.md#file-uploads). Authentication? [Documented too](route.md#authentication). Exit animations? [You got it](animation.md). No extra libraries, no magic.
|
||||
|
||||
---
|
||||
|
||||
## Comparisons
|
||||
|
||||
### React
|
||||
|
||||
React is a view library maintained by Facebook.
|
||||
|
||||
React and Mithril.js share a lot of similarities. If you already learned React, you already know almost all you need to build apps with Mithril.
|
||||
|
||||
- They both use virtual DOM, lifecycle methods and key-based reconciliation
|
||||
- They both organize views via components
|
||||
- They both use JavaScript as a flow control mechanism within views
|
||||
|
||||
The most obvious difference between React and Mithril.js is in their scope. React is a view library, so a typical React-based application relies on third-party libraries for routing, XHR and state management. Using a library oriented approach allows developers to customize their stack to precisely match their needs. The not-so-nice way of saying that is that React-based architectures can vary wildly from project to project, and that those projects are that much more likely to cross the 1MB size line.
|
||||
|
||||
Mithril.js has built-in modules for common necessities such as routing and XHR, and the [guide](simple-application.md) demonstrates idiomatic usage. This approach is preferable for teams that value consistency and ease of onboarding.
|
||||
|
||||
#### Performance
|
||||
|
||||
Both React and Mithril.js care strongly about rendering performance, but go about it in different ways. In the past React had two DOM rendering implementations (one using the DOM API, and one using `innerHTML`). Its upcoming fiber architecture introduces scheduling and prioritization of units of work. React also has a sophisticated build system that disables various checks and error messages for production deployments, and various browser-specific optimizations. In addition, there are also several performance-oriented libraries that leverage React's `shouldComponentUpdate` hook and immutable data structure libraries' fast object equality checking properties to reduce virtual DOM reconciliation times. Generally speaking, React's approach to performance is to engineer relatively complex solutions.
|
||||
|
||||
Mithril.js follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run.
|
||||
|
||||
Here's a comparison of library load times, i.e. the time it takes to parse and run the JavaScript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop:
|
||||
|
||||
React | Mithril.js
|
||||
------- | -------
|
||||
55.8 ms | 4.5 ms
|
||||
|
||||
Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques.
|
||||
|
||||
Since this is a micro-benchmark, you are encouraged to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate.
|
||||
|
||||
For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the [benchmark for React is here](https://jsfiddle.net/0ovkv64u/), and the [benchmark for Mithril.js is here](https://jsfiddle.net/o7hxooqL/). Note that we're benchmarking all of Mithril.js rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril.js' initialization speed is noticeably better than React.
|
||||
|
||||
Here's a slightly more meaningful benchmark: measuring the scripting time for creating 10,000 divs (and 10,000 text nodes). Again, here's the benchmark code for [React](https://jsfiddle.net/bfoeay4f/) and [Mithril.js](https://jsfiddle.net/fft0ht7n/). Their best results are shown below:
|
||||
|
||||
React | Mithril.js
|
||||
------- | -------
|
||||
99.7 ms | 42.8 ms
|
||||
|
||||
What these numbers show is that not only does Mithril.js initializes significantly faster, it can process upwards of 20,000 virtual DOM nodes before React is ready to use.
|
||||
|
||||
##### Update performance
|
||||
|
||||
Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running.
|
||||
|
||||
A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and JavaScript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/react/index.html) and a [Mithril.js implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Sample results are shown below:
|
||||
|
||||
React | Mithril.js
|
||||
------- | -------
|
||||
12.1 ms | 6.4 ms
|
||||
|
||||
##### Development performance
|
||||
|
||||
Another thing to keep in mind is that because React adds extra checks and helpful error messages in development mode, it is slower in development than the production version used for the benchmarks above. To illustrate, [here's the 10,000 node benchmark from above using the development version of React](https://jsfiddle.net/r1jfckrd/).
|
||||
|
||||
##### Drop-in replacements
|
||||
|
||||
There are [several](https://preactjs.com/) [projects](https://github.com/Lucifier129/react-lite) [that](https://infernojs.org/) [claim](https://github.com/alibaba/rax) API parity with React (some via compatibility layer libraries), but they are not fully compatible (e.g. PropType support is usually stubbed out, synthetic events are sometimes not supported, and some APIs have different semantics). Note that these libraries typically also include features of their own that are not part of the official React API, which may become problematic down the road if one decides to switch back to React Fiber.
|
||||
|
||||
Claims about small download size (compared to React) are accurate, but most of these libraries are slightly larger than Mithril.js' renderer module. Preact is the only exception.
|
||||
|
||||
Be wary of aggressive performance claims, as benchmarks used by some of these projects are known to be out-of-date and flawed (in the sense that they can be - and are - exploited). Boris Kaul (author of some of the benchmarks) has [written in detail about how benchmarks are gamed](https://medium.com/@localvoid/how-to-win-in-web-framework-benchmarks-8bc31af76ce7). Another thing to keep in mind is that some benchmarks aggressively use advanced optimization features and thus demonstrate *potential* performance, i.e. performance that is possible given some caveats, but realistically unlikely unless you actively spend the time to go over your entire codebase identifying optimization candidates and evaluating the regression risks brought by the optimization caveats.
|
||||
|
||||
In the spirit of demonstrating *typical* performance characteristics, the benchmarks presented in this comparison page are implemented in an apples-to-apples, naive, idiomatic way (i.e. the way you would normally write 99% of your code) and do not employ tricks or advanced optimizations to make one or other framework look artificially better. You are encouraged to contribute a PR if you feel any DbMonster implementation here could be written more idiomatically.
|
||||
|
||||
#### Complexity
|
||||
|
||||
Both React and Mithril.js have relatively small API surfaces compared to other frameworks, which help ease learning curve. However, whereas idiomatic Mithril.js can be written without loss of readability using plain ES5 and no other dependencies, idiomatic React relies heavily on complex tooling (e.g. Babel, JSX plugin, etc), and this level of complexity frequently extends to popular parts of its ecosystem, be it in the form of syntax extensions (e.g. non-standard object spread syntax in Redux), architectures (e.g. ones using immutable data libraries), or bells and whistles (e.g. hot module reloading).
|
||||
|
||||
While complex toolchains are also possible with Mithril.js and other frameworks alike, it's *strongly* recommended that you follow the [KISS](https://en.wikipedia.org/wiki/KISS_principle) and [YAGNI](https://en.wikipedia.org/wiki/You_aren't_gonna_need_it) principles when using Mithril.
|
||||
|
||||
#### Learning curve
|
||||
|
||||
Both React and Mithril.js have relatively small learning curves. React's learning curve mostly involves understanding components and their lifecycle. The learning curve for Mithril.js components is nearly identical. There are obviously more APIs to learn in Mithril.js, since Mithril.js also includes routing and XHR, but the learning curve would be fairly similar to learning React, React Router and a XHR library like superagent or axios.
|
||||
|
||||
Idiomatic React requires working knowledge of JSX and its caveats, and therefore there's also a small learning curve related to Babel.
|
||||
|
||||
#### Documentation
|
||||
|
||||
React documentation is clear and well written, and includes a good API reference, tutorials for getting started, as well as pages covering various advanced concepts. Unfortunately, since React is limited to being only a view library, its documentation does not explore how to use React idiomatically in the context of a real-life application. As a result, there are many popular state management libraries and thus architectures using React can differ drastically from company to company (or even between projects).
|
||||
|
||||
Mithril.js documentation also includes [introductory](index.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference.
|
||||
|
||||
Mithril.js documentation also demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries.
|
||||
|
||||
---
|
||||
|
||||
### Angular
|
||||
|
||||
Angular is a web application framework maintained by Google.
|
||||
|
||||
Angular and Mithril.js are fairly different, but they share a few similarities:
|
||||
|
||||
- Both support componentization
|
||||
- Both have an array of tools for various aspects of web applications (e.g. routing, XHR)
|
||||
|
||||
The most obvious difference between Angular and Mithril.js is in their complexity. This can be seen most easily in how views are implemented. Mithril.js views are plain JavaScript, and flow control is done with JavaScript built-in mechanisms such as ternary operators or `Array.prototype.map`. Angular, on the other hand, implements a directive system to extend HTML views so that it's possible to evaluate JavaScript-like expressions within HTML attributes and interpolations. Angular actually ships with a parser and a compiler written in JavaScript to achieve that. If that doesn't seem complex enough, there's actually two compilation modes (a default mode that generates JavaScript functions dynamically for performance, and [a slower mode](https://docs.angularjs.org/api/ng/directive/ngCsp) for dealing with Content Security Policy restrictions).
|
||||
|
||||
#### Performance
|
||||
|
||||
Angular has made a lot of progress in terms of performance over the years. Angular 1 used a mechanism known as dirty checking which tended to get slow due to the need to constantly diff large `$scope` structures. Angular 2 uses a template change detection mechanism that is much more performant. However, even despite Angular's improvements, Mithril.js is often faster than Angular, due to the ease of auditing that Mithril.js' small codebase size affords.
|
||||
|
||||
It's difficult to make a comparison of load times between Angular and Mithril.js for a couple of reasons. The first is that Angular 1 and 2 are in fact completely different codebases, and both versions are officially supported and maintained (and the vast majority of Angular codebases in the wild currently still use version 1). The second reason is that both Angular and Mithril.js are modular. In both cases, it's possible to remove a significant part of the framework that is not used in a given application.
|
||||
|
||||
With that being said, the smallest known Angular 2 bundle is a [29kb hello world](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/) compressed w/ the Brotli algorithm (it's 35kb using standard gzip), and with most of Angular's useful functionality removed. By comparison, a Mithril.js hello world - including the entire Mithril.js core with batteries and everything - would be about 10kb gzipped.
|
||||
|
||||
Also, remember that frameworks like Angular and Mithril.js are designed for non-trivial application, so an application that managed to use all of Angular's API surface would need to download several hundred kb of framework code, rather than merely 29kb.
|
||||
|
||||
##### Update performance
|
||||
|
||||
A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and JavaScript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/angular/index.html) and a [Mithril.js implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
|
||||
|
||||
Angular | Mithril.js
|
||||
------- | -------
|
||||
11.5 ms | 6.4 ms
|
||||
|
||||
#### Complexity
|
||||
|
||||
Angular is superior to Mithril.js in the amount of tools it offers (in the form of various directives and services), but it is also far more complex. Compare [Angular's API surface](https://angular.io/docs/ts/latest/api/) with [Mithril.js'](api.md). You can make your own judgment on which API is more self-descriptive and more relevant to your needs.
|
||||
|
||||
Angular 2 has a lot more concepts to understand: on the language level, Typescript is the recommended language, and on top of that there's also Angular-specific template syntax such as bindings, pipes, "safe navigator operator". You also need to learn about architectural concepts such as modules, components, services, directives, etc, and where it's appropriate to use what.
|
||||
|
||||
#### Learning curve
|
||||
|
||||
If we compare apples to apples, Angular 2 and Mithril.js have similar learning curves: in both, components are a central aspect of architecture, and both have reasonable routing and XHR tools.
|
||||
|
||||
With that being said, Angular has a lot more concepts to learn than Mithril. It offers Angular-specific APIs for many things that often can be trivially implemented (e.g. pluralization is essentially a switch statement, "required" validation is simply an equality check, etc). Angular templates also have several layers of abstractions to emulate what JavaScript does natively in Mithril.js - Angular's `ng-if`/`ngIf` is a *directive*, which uses a custom *parser* and *compiler* to evaluate an expression string and emulate lexical scoping... and so on. Mithril.js tends to be a lot more transparent, and therefore easier to reason about.
|
||||
|
||||
#### Documentation
|
||||
|
||||
Angular 2 documentation provides an extensive introductory tutorial, and another tutorial that implements an application. It also has various guides for advanced concepts, a cheatsheet and a style guide. Unfortunately, at the moment, the API reference leaves much to be desired. Several APIs are either undocumented or provide no context for what the API might be used for.
|
||||
|
||||
Mithril.js documentation includes [introductory](index.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference.
|
||||
|
||||
Mithril.js documentation also demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Vue
|
||||
|
||||
Vue is a view library similar to Angular.
|
||||
|
||||
Vue and Mithril.js have a lot of differences but they also share some similarities:
|
||||
|
||||
- They both use virtual DOM and lifecycle methods
|
||||
- Both organize views via components
|
||||
|
||||
Vue 2 uses a fork of Snabbdom as its virtual DOM system. In addition, Vue also provides tools for routing and state management as separate modules. Vue looks very similar to Angular and provides a similar directive system, HTML-based templates and logic flow directives. It differs from Angular in that it implements a monkeypatching reactive system that overwrites native methods in a component's data tree (whereas Angular 1 uses dirty checking and digest/apply cycles to achieve similar results). Similar to Angular 2, Vue compiles HTML templates into functions, but the compiled functions look more like Mithril.js or React views, rather than Angular's compiled rendering functions.
|
||||
|
||||
Vue is significantly smaller than Angular when comparing apples to apples, but not as small as Mithril.js (Vue core is around 23kb gzipped, whereas the equivalent rendering module in Mithril.js is around 4kb gzipped). Both have similar performance characteristics, but benchmarks usually suggest Mithril.js is slightly faster.
|
||||
|
||||
#### Performance
|
||||
|
||||
Here's a comparison of library load times, i.e. the time it takes to parse and run the JavaScript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop:
|
||||
|
||||
Vue | Mithril.js
|
||||
------- | -------
|
||||
21.8 ms | 4.5 ms
|
||||
|
||||
Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques.
|
||||
|
||||
##### Update performance
|
||||
|
||||
A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and JavaScript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [Vue implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/vue/index.html) and a [Mithril.js implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below:
|
||||
|
||||
Vue | Mithril.js
|
||||
------ | -------
|
||||
9.8 ms | 6.4 ms
|
||||
|
||||
#### Complexity
|
||||
|
||||
Vue is heavily inspired by Angular and has many things that Angular does (e.g. directives, filters, bi-directional bindings, `v-cloak`), but also has things inspired by React (e.g. components). As of Vue 2.0, it's also possible to write templates using hyperscript/JSX syntax (in addition to single-file components and the various webpack-based language transpilation plugins). Vue provides both bi-directional data binding and an optional Redux-like state management library, but unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach can cause architectural fragmentation in long-lived projects.
|
||||
|
||||
Mithril.js has far less concepts and typically organizes applications in terms of components and a data layer. All component creation styles in Mithril.js output the same vnode structure using native JavaScript features only. The direct consequence of leaning on the language is less tooling and a simpler project setup.
|
||||
|
||||
#### Documentation
|
||||
|
||||
Both Vue and Mithril.js have good documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts.
|
||||
|
||||
However, due to Vue's many-ways-to-do-one-thing approach, some things may not be adequately documented. For example, there's no documentation on hyperscript syntax or usage.
|
||||
|
||||
Mithril.js documentation typically errs on the side of being overly thorough if a topic involves things outside of the scope of Mithril. For example, when a topic involves a 3rd party library, Mithril.js documentation walks through the installation process for the 3rd party library. Mithril.js documentation also often demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries.
|
||||
|
||||
Mithril.js' tutorials also cover a lot more ground than Vue's: the [Vue tutorial](https://vuejs.org/v2/guide/#Getting-Started) finishes with a static list of foodstuff. [Mithril.js' 10 minute guide](index.md) covers the majority of its API and goes over key aspects of real-life applications, such as fetching data from a server and routing (and there's a [longer, more thorough tutorial](simple-application.md) if that's not enough).
|
||||
|
|
@ -1,529 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m(), Mithril.js' hyperscript DSL, which you can use to define the views of your app
|
||||
-->
|
||||
|
||||
# m(selector, attributes, children)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Flexibility](#flexibility)
|
||||
- [CSS selectors](#css-selectors)
|
||||
- [Attributes passed as the second argument](#attributes-passed-as-the-second-argument)
|
||||
- [DOM attributes](#dom-attributes)
|
||||
- [Style attribute](#style-attribute)
|
||||
- [Events](#events)
|
||||
- [Properties](#properties)
|
||||
- [Components](#components)
|
||||
- [Lifecycle methods](#lifecycle-methods)
|
||||
- [Keys](#keys)
|
||||
- [SVG and MathML](#svg-and-mathml)
|
||||
- [Making templates dynamic](#making-templates-dynamic)
|
||||
- [Converting HTML](#converting-html)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Represents an HTML element in a Mithril.js view
|
||||
|
||||
```javascript
|
||||
m("div.foo", {style: {color: "red"}}, "hello")
|
||||
// renders to this HTML:
|
||||
// <div class="foo" style="color: red">hello</div>
|
||||
```
|
||||
|
||||
You can also use an HTML-like syntax called [JSX](jsx.md), using Babel to convert it to equivalent hyperscript calls. This is equivalent to the above.
|
||||
|
||||
```jsx
|
||||
<div class="foo" style="color: red">hello</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`vnode = m(selector, attrs, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------------ | -------- | ---
|
||||
`selector` | `String|Object|Function` | Yes | A CSS selector or a [component](components.md)
|
||||
`attrs` | `Object` | No | HTML attributes or element properties
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats)
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md#structure)
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
Mithril.js provides a hyperscript function `m()`, which allows expressing any HTML structure using JavaScript syntax. It accepts a `selector` string (required), an `attrs` object (optional) and a `children` array (optional).
|
||||
|
||||
```javascript
|
||||
m("div", {id: "box"}, "hello")
|
||||
|
||||
// renders to this HTML:
|
||||
// <div id="box">hello</div>
|
||||
```
|
||||
|
||||
The `m()` function does not actually return a DOM element. Instead it returns a [virtual DOM node](vnodes.md), or *vnode*, which is a JavaScript object that represents the DOM element to be created.
|
||||
|
||||
```javascript
|
||||
// a vnode
|
||||
var vnode = {tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]}
|
||||
```
|
||||
|
||||
To transform a vnode into an actual DOM element, use the [`m.render()`](render.md) function:
|
||||
|
||||
```javascript
|
||||
m.render(document.body, m("br")) // puts a <br> in <body>
|
||||
```
|
||||
|
||||
Calling `m.render()` multiple times does **not** recreate the DOM tree from scratch each time. Instead, each call will only make a change to a DOM tree if it is absolutely necessary to reflect the virtual DOM tree passed into the call. This behavior is desirable because recreating the DOM from scratch is very expensive, and causes issues such as loss of input focus, among other things. By contrast, updating the DOM only where necessary is comparatively much faster and makes it easier to maintain complex UIs that handle multiple user stories.
|
||||
|
||||
---
|
||||
|
||||
### Flexibility
|
||||
|
||||
The `m()` function is both *polymorphic* and *variadic*. In other words, it's very flexible in what it expects as input parameters:
|
||||
|
||||
```javascript
|
||||
// simple tag
|
||||
m("div") // <div></div>
|
||||
|
||||
// attributes and children are optional
|
||||
m("a", {id: "b"}) // <a id="b"></a>
|
||||
m("span", "hello") // <span>hello</span>
|
||||
|
||||
// tag with child nodes
|
||||
m("ul", [ // <ul>
|
||||
m("li", "hello"), // <li>hello</li>
|
||||
m("li", "world"), // <li>world</li>
|
||||
]) // </ul>
|
||||
|
||||
// array is optional
|
||||
m("ul", // <ul>
|
||||
m("li", "hello"), // <li>hello</li>
|
||||
m("li", "world") // <li>world</li>
|
||||
) // </ul>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CSS selectors
|
||||
|
||||
The first argument of `m()` can be any CSS selector that can describe an HTML element. It accepts any valid CSS combinations of `#` (id), `.` (class) and `[]` (attribute) syntax.
|
||||
|
||||
```javascript
|
||||
m("div#hello")
|
||||
// <div id="hello"></div>
|
||||
|
||||
m("section.container")
|
||||
// <section class="container"></section>
|
||||
|
||||
m("input[type=text][placeholder=Name]")
|
||||
// <input type="text" placeholder="Name" />
|
||||
|
||||
m("a#exit.external[href='https://example.com']", "Leave")
|
||||
// <a id="exit" class="external" href="https://example.com">Leave</a>
|
||||
```
|
||||
|
||||
If you omit the tag name, Mithril.js assumes a `div` tag.
|
||||
|
||||
```javascript
|
||||
m(".box.box-bordered") // <div class="box box-bordered"></div>
|
||||
```
|
||||
|
||||
Typically, it's recommended that you use CSS selectors for static attributes (i.e. attributes whose value do not change), and pass an attributes object for dynamic attribute values.
|
||||
|
||||
```javascript
|
||||
var currentURL = "/"
|
||||
|
||||
m("a.link[href=/]", {
|
||||
class: currentURL === "/" ? "selected" : ""
|
||||
}, "Home")
|
||||
|
||||
// renders to this HTML:
|
||||
// <a href="/" class="link selected">Home</a>
|
||||
```
|
||||
|
||||
### Attributes passed as the second argument
|
||||
|
||||
You can pass attributes, properties, events and lifecycle hooks in the second, optional argument (see the next sections for details).
|
||||
|
||||
```JS
|
||||
m("button", {
|
||||
class: "my-button",
|
||||
onclick: function() {/* ... */},
|
||||
oncreate: function() {/* ... */}
|
||||
})
|
||||
```
|
||||
|
||||
If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent.
|
||||
|
||||
If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null` or `undefined`, it is ignored.
|
||||
|
||||
If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`.
|
||||
|
||||
---
|
||||
|
||||
### DOM attributes
|
||||
|
||||
Mithril.js uses both the JavaScript API and the DOM API (`setAttribute`) to resolve attributes. This means you can use both syntaxes to refer to attributes.
|
||||
|
||||
For example, in the JavaScript API, the `readonly` attribute is called `element.readOnly` (notice the uppercase). In Mithril.js, all of the following are supported:
|
||||
|
||||
```javascript
|
||||
m("input", {readonly: true}) // lowercase
|
||||
m("input", {readOnly: true}) // uppercase
|
||||
m("input[readonly]")
|
||||
m("input[readOnly]")
|
||||
```
|
||||
|
||||
This even includes custom elements. For example, you can use [A-Frame](https://aframe.io/docs/0.8.0/introduction/) within Mithril.js, no problem!
|
||||
|
||||
```javascript
|
||||
m("a-scene", [
|
||||
m("a-box", {
|
||||
position: "-1 0.5 -3",
|
||||
rotation: "0 45 0",
|
||||
color: "#4CC3D9",
|
||||
}),
|
||||
|
||||
m("a-sphere", {
|
||||
position: "0 1.25 -5",
|
||||
radius: "1.25",
|
||||
color: "#EF2D5E",
|
||||
}),
|
||||
|
||||
m("a-cylinder", {
|
||||
position: "1 0.75 -3",
|
||||
radius: "0.5",
|
||||
height: "1.5",
|
||||
color: "#FFC65D",
|
||||
}),
|
||||
|
||||
m("a-plane", {
|
||||
position: "0 0 -4",
|
||||
rotation: "-90 0 0",
|
||||
width: "4",
|
||||
height: "4",
|
||||
color: "#7BC8A4",
|
||||
}),
|
||||
|
||||
m("a-sky", {
|
||||
color: "#ECECEC",
|
||||
}),
|
||||
])
|
||||
```
|
||||
|
||||
And yes, this translates to both attributes and properties, and it works just like they would in the DOM. Using Brick's `brick-deck` (DEAD LINK, FIXME: http //brick.mozilla.io/docs/brick-deck) as an example, they have a `selected-index` attribute with a corresponding `selectedIndex` getter/setter property.
|
||||
|
||||
```javascript
|
||||
m("brick-deck[selected-index=0]", [/* ... */]) // lowercase
|
||||
m("brick-deck[selectedIndex=0]", [/* ... */]) // uppercase
|
||||
// I know these look odd, but `brick-deck`'s `selectedIndex` property is a
|
||||
// string, not a number.
|
||||
m("brick-deck", {"selected-index": "0"}, [/* ... */])
|
||||
m("brick-deck", {"selectedIndex": "0"}, [/* ... */])
|
||||
```
|
||||
|
||||
For custom elements, it doesn't auto-stringify properties, in case they are objects, numbers, or some other non-string value. So assuming you had some custom element `my-special-element` that has an `elem.whitelist` array getter/setter property, you could do this, and it'd work as you'd expect:
|
||||
|
||||
```javascript
|
||||
m("my-special-element", {
|
||||
whitelist: [
|
||||
"https://example.com",
|
||||
"https://neverssl.com",
|
||||
"https://google.com",
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
If you have classes or IDs for those elements, the shorthands still work as you would expect. To pull another A-Frame example:
|
||||
|
||||
```javascript
|
||||
// These two are equivalent
|
||||
m("a-entity#player")
|
||||
m("a-entity", {id: "player"})
|
||||
```
|
||||
|
||||
Do note that all the properties with magic semantics, like lifecycle attributes, `onevent` handlers, `key`s, `class`, and `style`, those are still treated the same way they are for normal HTML elements.
|
||||
|
||||
---
|
||||
|
||||
### Style attribute
|
||||
|
||||
Mithril.js supports both strings and objects as valid `style` values. In other words, all of the following are supported:
|
||||
|
||||
```javascript
|
||||
m("div", {style: "background:red;"})
|
||||
m("div", {style: {background: "red"}})
|
||||
m("div[style=background:red]")
|
||||
```
|
||||
|
||||
Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed.
|
||||
|
||||
You can use both hyphenated CSS property names (like `background-color`) and camel cased DOM `style` property names (like `backgroundColor`). You can also define [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables), if your browser supports them.
|
||||
|
||||
Mithril.js does not attempt to add units to number values. It simply stringifies them.
|
||||
|
||||
---
|
||||
|
||||
### Events
|
||||
|
||||
Mithril.js supports event handler binding for all DOM events, including events whose specs do not define an `on${event}` property, such as `touchstart`
|
||||
|
||||
```javascript
|
||||
function doSomething(e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
m("div", {onclick: doSomething})
|
||||
```
|
||||
|
||||
Mithril.js accepts functions and [EventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventListener) objects. So this will also work:
|
||||
|
||||
```javascript
|
||||
var clickListener = {
|
||||
handleEvent: function(e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
m("div", {onclick: clickListener})
|
||||
```
|
||||
|
||||
By default, when an event attached with hyperscript fires, this will trigger Mithril.js' auto-redraw after your event callback returns (assuming you are using `m.mount` or `m.route` instead of `m.render` directly). You can disable auto-redraw specifically for a single event by setting `e.redraw = false` on it:
|
||||
|
||||
```javascript
|
||||
m("div", {
|
||||
onclick: function(e) {
|
||||
// Prevent auto-redraw
|
||||
e.redraw = false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Properties
|
||||
|
||||
Mithril.js supports DOM functionality that is accessible via properties such as `<select>`'s `selectedIndex` and `value` properties.
|
||||
|
||||
```javascript
|
||||
m("select", {selectedIndex: 0}, [
|
||||
m("option", "Option A"),
|
||||
m("option", "Option B"),
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Components
|
||||
|
||||
[Components](components.md) allow you to encapsulate logic into a unit and use it as if it was an element. They are the base for making large, scalable applications.
|
||||
|
||||
A component is any JavaScript object that contains a `view` method. To consume a component, pass the component as the first argument to `m()` instead of passing a CSS selector string. You can pass arguments to the component by defining attributes and children, as shown in the example below.
|
||||
|
||||
```javascript
|
||||
// define a component
|
||||
var Greeter = {
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.attrs, ["Hello ", vnode.children])
|
||||
}
|
||||
}
|
||||
|
||||
// consume it
|
||||
m(Greeter, {style: "color:red;"}, "world")
|
||||
|
||||
// renders to this HTML:
|
||||
// <div style="color:red;">Hello world</div>
|
||||
```
|
||||
|
||||
To learn more about components, [see the components page](components.md).
|
||||
|
||||
---
|
||||
|
||||
### Lifecycle methods
|
||||
|
||||
Vnodes and components can have lifecycle methods (also known as *hooks*), which are called at various points during the lifetime of a DOM element. The lifecycle methods supported by Mithril.js are: `oninit`, `oncreate`, `onupdate`, `onbeforeremove`, `onremove`, and `onbeforeupdate`.
|
||||
|
||||
Lifecycle methods are defined in the same way as DOM event handlers, but receive the vnode as an argument, instead of an Event object:
|
||||
|
||||
```javascript
|
||||
function initialize(vnode) {
|
||||
console.log(vnode)
|
||||
}
|
||||
|
||||
m("div", {oninit: initialize})
|
||||
```
|
||||
|
||||
Hook | Description
|
||||
----------------------------- | ---
|
||||
`oninit(vnode)` | Runs before a vnode is rendered into a real DOM element
|
||||
`oncreate(vnode)` | Runs after a vnode is appended to the DOM
|
||||
`onupdate(vnode)` | Runs every time a redraw occurs while the DOM element is attached to the document
|
||||
`onbeforeremove(vnode)` | Runs before a DOM element is removed from the document. If a Promise is returned, Mithril.js only detaches the DOM element after the promise completes. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements.
|
||||
`onremove(vnode)` | Runs before a DOM element is removed from the document. If a `onbeforeremove` hook is defined, `onremove` is called after `done` is called. This method is triggered on the element that is detached from its parent element, and all of its children
|
||||
`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children
|
||||
|
||||
To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md).
|
||||
|
||||
---
|
||||
|
||||
### Keys
|
||||
|
||||
Vnodes in a list can have a special attribute called `key`, which can be used to manage the identity of the DOM element as the model data that generates the vnode list changes.
|
||||
|
||||
Typically, `key` should be the unique identifier field of the objects in the data array.
|
||||
|
||||
```javascript
|
||||
var users = [
|
||||
{id: 1, name: "John"},
|
||||
{id: 2, name: "Mary"},
|
||||
]
|
||||
|
||||
function userInputs(users) {
|
||||
return users.map(function(u) {
|
||||
return m("input", {key: u.id}, u.name)
|
||||
})
|
||||
}
|
||||
|
||||
m.render(document.body, userInputs(users))
|
||||
```
|
||||
|
||||
Having a key means that if the `users` array is shuffled and the view is re-rendered, the inputs will be shuffled in the exact same order, so as to maintain correct focus and DOM state.
|
||||
|
||||
To learn more about keys, [see the keys page](keys.md).
|
||||
|
||||
---
|
||||
|
||||
### SVG and MathML
|
||||
|
||||
Mithril.js fully supports SVG. Xlink is also supported, but unlike in pre-v1.0 versions of Mithril.js, must have the namespace explicitly defined:
|
||||
|
||||
```javascript
|
||||
m("svg", [
|
||||
m("image[xlink:href='image.gif']")
|
||||
])
|
||||
```
|
||||
|
||||
MathML is also fully supported.
|
||||
|
||||
---
|
||||
|
||||
### Making templates dynamic
|
||||
|
||||
Since nested vnodes are just plain JavaScript expressions, you can simply use JavaScript facilities to manipulate them
|
||||
|
||||
#### Dynamic text
|
||||
|
||||
```javascript
|
||||
var user = {name: "John"}
|
||||
|
||||
m(".name", user.name) // <div class="name">John</div>
|
||||
```
|
||||
|
||||
#### Loops
|
||||
|
||||
Use `Array` methods such as [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) to iterate over lists of data
|
||||
|
||||
```javascript
|
||||
var users = [
|
||||
{name: "John"},
|
||||
{name: "Mary"},
|
||||
]
|
||||
|
||||
m("ul", users.map(function(u) { // <ul>
|
||||
return m("li", u.name) // <li>John</li>
|
||||
// <li>Mary</li>
|
||||
})) // </ul>
|
||||
|
||||
// ES6+:
|
||||
// m("ul", users.map(u =>
|
||||
// m("li", u.name)
|
||||
// ))
|
||||
```
|
||||
|
||||
#### Conditionals
|
||||
|
||||
Use the ternary operator to conditionally set content on a view
|
||||
|
||||
```javascript
|
||||
var isError = false
|
||||
|
||||
m("div", isError ? "An error occurred" : "Saved") // <div>Saved</div>
|
||||
```
|
||||
|
||||
You cannot use JavaScript statements such as `if` or `for` within JavaScript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative.
|
||||
|
||||
---
|
||||
|
||||
### Converting HTML
|
||||
|
||||
In Mithril.js, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX.
|
||||
|
||||
When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can [use the HTML-to-Mithril-template converter](https://arthurclemens.github.io/mithril-template-converter/index.html).
|
||||
|
||||
---
|
||||
|
||||
### Avoid Anti-patterns
|
||||
|
||||
Although Mithril.js is flexible, some code patterns are discouraged:
|
||||
|
||||
#### Avoid dynamic selectors
|
||||
|
||||
Different DOM elements have different attributes, and often different behaviors. Making a selector configurable can leak the implementation details of a component out of its unit.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var BadInput = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label"),
|
||||
m(vnode.attrs.type || "input")
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead of making selectors dynamic, you are encouraged to explicitly code each valid possibility, or refactor the variable portion of the code out.
|
||||
|
||||
```javascript
|
||||
// PREFER explicit code
|
||||
var BetterInput = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label", vnode.attrs.title),
|
||||
m("input"),
|
||||
])
|
||||
}
|
||||
}
|
||||
var BetterSelect = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label", vnode.attrs.title),
|
||||
m("select"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// PREFER refactor variability out
|
||||
var BetterLabeledComponent = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label", vnode.attrs.title),
|
||||
vnode.children,
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Avoid creating vnodes outside views
|
||||
|
||||
When a redraw encounters a vnode which is strictly equal to the one in the previous render, it will be skipped and its contents will not be updated. While this may seem like an opportunity for performance optimisation, it should be avoided because it prevents dynamic changes in that node's tree - this leads to side-effects such as downstream lifecycle methods failing to trigger on redraw. In this sense, Mithril.js vnodes are immutable: new vnodes are compared to old ones; mutations to vnodes are not persisted.
|
||||
|
||||
The component documentation contains [more detail and an example of this anti-pattern](components.md#avoid-creating-component-instances-outside-views).
|
||||
398
docs/index.md
|
|
@ -1,398 +0,0 @@
|
|||
<!--meta-description
|
||||
Mithril.js is a modern, small, fast client-side Javascript framework for building Single Page Applications.
|
||||
-->
|
||||
|
||||
# Introduction
|
||||
|
||||
- [What is Mithril.js?](#what-is-mithriljs?)
|
||||
- [Getting started](#getting-started)
|
||||
- [Hello world](#hello-world)
|
||||
- [DOM elements](#dom-elements)
|
||||
- [Components](#components)
|
||||
- [Routing](#routing)
|
||||
- [XHR](#xhr)
|
||||
|
||||
---
|
||||
|
||||
### What is Mithril.js?
|
||||
|
||||
Mithril.js is a modern client-side JavaScript framework for building Single Page Applications.
|
||||
It's small (< 10kb gzip), fast and provides routing and XHR utilities out of the box.
|
||||
|
||||
<div style="display:flex;margin:0 0 30px;">
|
||||
<div style="width:50%;">
|
||||
<h5>Download size</h5>
|
||||
<small>Mithril.js (9.5kb)</small>
|
||||
<div style="animation:grow 0.08s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:4%;"></div>
|
||||
<small style="color:#aaa;">Vue + Vue-Router + Vuex + fetch (40kb)</small>
|
||||
<div style="animation:grow 0.4s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:20%"></div>
|
||||
<small style="color:#aaa;">React + React-Router + Redux + fetch (64kb)</small>
|
||||
<div style="animation:grow 0.64s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:32%"></div>
|
||||
<small style="color:#aaa;">Angular (135kb)</small>
|
||||
<div style="animation:grow 1.35s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:68%"></div>
|
||||
</div>
|
||||
<div style="width:50%;">
|
||||
<h5>Performance</h5>
|
||||
<small>Mithril.js (6.4ms)</small>
|
||||
<div style="animation:grow 0.64s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:24%;"></div>
|
||||
<small style="color:#aaa;">Vue (9.8ms)</small>
|
||||
<div style="animation:grow 0.98s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:40%"></div>
|
||||
<small style="color:#aaa;">React (12.1ms)</small>
|
||||
<div style="animation:grow 1.21s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:48%"></div>
|
||||
<small style="color:#aaa;">Angular (11.5ms)</small>
|
||||
<div style="animation:grow 1.15s;background:#1e5799;height:3px;margin:0 10px 10px 0;transform-origin:0;width:44%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess.
|
||||
|
||||
If you are an experienced developer and want to know how Mithril.js compares to other frameworks, see the [framework comparison](framework-comparison.md) page.
|
||||
|
||||
Mithril.js supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required.
|
||||
|
||||
*Looking for the v1 docs? [Click here](https://mithril.js.org/archive/v1.1.7/index.html).*
|
||||
|
||||
---
|
||||
|
||||
### Getting started
|
||||
|
||||
An easy way to try out Mithril.js is to include it from a CDN and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes.
|
||||
|
||||
Let's create an HTML file to follow along:
|
||||
|
||||
```html
|
||||
<body>
|
||||
<script src="https://unpkg.com/mithril/mithril.js"></script>
|
||||
<script>
|
||||
var root = document.body
|
||||
|
||||
// your code goes here!
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
To make things simpler you can try out Mithril.js right here. This is a live playground with Mithril.js preloaded that - by the way - is also built in Mithril.
|
||||
|
||||
```js
|
||||
var root = document.body
|
||||
|
||||
// Your code here
|
||||
|
||||
m.mount(root, {
|
||||
view: function() {
|
||||
return m("h1", "Try me out")
|
||||
}
|
||||
})
|
||||
```
|
||||
> *[Click here to open the sample on flems.io](https://flems.io/mithril@next#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvEAXwvW10QICsEqdBk2J4s+LLQCuDABQATWtSk4G+AEa15ATwoACYAB00e03oBuEGAHdEesDOrEI9WQEoDxs970AnGMRSviZYsgDkhACMYfphACq+2no4etLEYW5eZqzGrG6UIHAwsE4uaAg8ACyIAExsHCCYOHj41HACNPSMzDxsALqsQA)*
|
||||
|
||||
---
|
||||
|
||||
### Hello world
|
||||
|
||||
Let's start as small as we can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better)
|
||||
|
||||
```javascript
|
||||
var root = document.body
|
||||
|
||||
m.render(root, "Hello world")
|
||||
```
|
||||
|
||||
Now, let's change the text to something else. Add this line of code under the previous one:
|
||||
|
||||
```javascript
|
||||
m.render(root, "My first app")
|
||||
```
|
||||
|
||||
As you can see, you use the same code to both create and update HTML. Mithril.js automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch.
|
||||
|
||||
#### Live Example
|
||||
|
||||
```js
|
||||
var root = document.body
|
||||
|
||||
m.render(root, "Hello World")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DOM elements
|
||||
|
||||
Let's wrap our text in an `<h1>` tag.
|
||||
|
||||
```javascript
|
||||
m.render(root, m("h1", "My first app"))
|
||||
```
|
||||
|
||||
The `m()` function can be used to describe any HTML structure you want. So if you need to add a class to the `<h1>`:
|
||||
|
||||
```javascript
|
||||
m("h1", {class: "title"}, "My first app")
|
||||
```
|
||||
|
||||
If you want to have multiple elements:
|
||||
|
||||
```javascript
|
||||
[
|
||||
m("h1", {class: "title"}, "My first app"),
|
||||
m("button", "A button"),
|
||||
]
|
||||
```
|
||||
|
||||
And so on:
|
||||
|
||||
```javascript
|
||||
m("main", [
|
||||
m("h1", {class: "title"}, "My first app"),
|
||||
m("button", "A button"),
|
||||
])
|
||||
```
|
||||
|
||||
#### Live Example
|
||||
|
||||
```js
|
||||
var root = document.body
|
||||
|
||||
m.render(root, [
|
||||
m("main", [
|
||||
m("h1", {class: "title"}, "My first app"),
|
||||
m("button", "A button"),
|
||||
])
|
||||
])
|
||||
```
|
||||
|
||||
Note: If you prefer `<html>` syntax, [it's possible to use it via a Babel plugin](jsx.md).
|
||||
|
||||
```jsx
|
||||
// HTML syntax via Babel's JSX plugin
|
||||
<main>
|
||||
<h1 class="title">My first app</h1>
|
||||
<button>A button</button>
|
||||
</main>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Components
|
||||
|
||||
A Mithril.js component is just an object with a `view` function. Here's the code above as a component:
|
||||
|
||||
```javascript
|
||||
var Hello = {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m("h1", {class: "title"}, "My first app"),
|
||||
m("button", "A button"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To activate the component, we use `m.mount`.
|
||||
|
||||
```javascript
|
||||
m.mount(root, Hello)
|
||||
```
|
||||
|
||||
As you would expect, doing so creates this markup:
|
||||
|
||||
```html
|
||||
<main>
|
||||
<h1 class="title">My first app</h1>
|
||||
<button>A button</button>
|
||||
</main>
|
||||
```
|
||||
|
||||
The `m.mount` function is similar to `m.render`, but instead of rendering some HTML only once, it activates Mithril.js' auto-redrawing system. To understand what that means, let's add some events:
|
||||
|
||||
```javascript
|
||||
var count = 0 // added a variable
|
||||
|
||||
var Hello = {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m("h1", {class: "title"}, "My first app"),
|
||||
// changed the next line
|
||||
m("button", {onclick: function() {count++}}, count + " clicks"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(root, Hello)
|
||||
```
|
||||
|
||||
We defined an `onclick` event on the button, which increments a variable `count` (which was declared at the top). We are now also rendering the value of that variable in the button label.
|
||||
|
||||
You can now update the label of the button by clicking the button. Since we used `m.mount`, you don't need to manually call `m.render` to apply the changes in the `count` variable to the HTML; Mithril.js does it for you.
|
||||
|
||||
If you're wondering about performance, it turns out Mithril.js is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril.js actually updates.
|
||||
|
||||
#### Live Example
|
||||
|
||||
```js
|
||||
var root = document.body
|
||||
var count = 0 // added a variable
|
||||
|
||||
var Hello = {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m("h1", {
|
||||
class: "title"
|
||||
}, "My first app"),
|
||||
m("button", {
|
||||
onclick: function() {count++}
|
||||
}, count + " clicks")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(root, Hello)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Routing
|
||||
|
||||
Routing just means going from one screen to another in an application with several screens.
|
||||
|
||||
Let's add a splash page that appears before our click counter. First we create a component for it:
|
||||
|
||||
```javascript
|
||||
var Splash = {
|
||||
view: function() {
|
||||
return m("a", {href: "#!/hello"}, "Enter!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path.
|
||||
|
||||
Now that we're going to have more than one screen, we use `m.route` instead of `m.mount`.
|
||||
|
||||
```javascript
|
||||
m.route(root, "/splash", {
|
||||
"/splash": Splash,
|
||||
"/hello": Hello,
|
||||
})
|
||||
```
|
||||
|
||||
The `m.route` function still has the same auto-redrawing functionality that `m.mount` does, and it also enables URL awareness; in other words, it lets Mithril.js know what to do when it sees a `#!` in the URL.
|
||||
|
||||
The `"/splash"` right after `root` means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (`/splash` and `/hello`, in our case), then Mithril.js redirects to the default route. So if you open the page in a browser and your URL is `https://localhost`, then you get redirected to `https://localhost/#!/splash`.
|
||||
|
||||
Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to `https://localhost/#!/hello`. You can navigate back and forth to the splash page using the browser's back and next button.
|
||||
|
||||
#### Live Example
|
||||
|
||||
```js
|
||||
var root = document.body
|
||||
var count = 0
|
||||
|
||||
var Hello = {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m("h1", {
|
||||
class: "title"
|
||||
}, "My first app"),
|
||||
m("button", {
|
||||
onclick: function() {count++}
|
||||
}, count + " clicks"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
var Splash = {
|
||||
view: function() {
|
||||
return m("a", {
|
||||
href: "#!/hello"
|
||||
}, "Enter!")
|
||||
}
|
||||
}
|
||||
|
||||
m.route(root, "/splash", {
|
||||
"/splash": Splash,
|
||||
"/hello": Hello,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### XHR
|
||||
|
||||
Basically, XHR is just a way to talk to a server.
|
||||
|
||||
Let's change our click counter to make it save data on a server. For the server, we'll use [REM](https://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial.
|
||||
|
||||
First we create a function that calls `m.request`. The `url` specifies an endpoint that represents a resource, the `method` specifies the type of action we're taking (typically the `PUT` method [upserts](https://en.wiktionary.org/wiki/upsert)), `body` is the payload that we're sending to the endpoint and `withCredentials` means to enable cookies (a requirement for the REM API to work)
|
||||
|
||||
```javascript
|
||||
var count = 0
|
||||
var increment = function() {
|
||||
m.request({
|
||||
method: "PUT",
|
||||
url: "//rem-rest-api.herokuapp.com/api/tutorial/1",
|
||||
body: {count: count + 1},
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(data) {
|
||||
count = parseInt(data.count)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Calling the increment function [upserts](https://en.wiktionary.org/wiki/upsert) an object `{count: 1}` to the `/api/tutorial/1` endpoint. This endpoint returns an object with the same `count` value that was sent to it. Notice that the `count` variable is only updated after the request completes, and it's updated with the response value from the server now.
|
||||
|
||||
Let's replace the event handler in the component to call the `increment` function instead of incrementing the `count` variable directly:
|
||||
|
||||
```javascript
|
||||
var Hello = {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m("h1", {class: "title"}, "My first app"),
|
||||
m("button", {onclick: increment}, count + " clicks"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking the button should now update the count.
|
||||
|
||||
#### Live Example
|
||||
|
||||
```js
|
||||
var root = document.body
|
||||
var count = 0
|
||||
|
||||
var increment = function() {
|
||||
m.request({
|
||||
method: "PUT",
|
||||
url: "//rem-rest-api.herokuapp.com/api/tutorial/1",
|
||||
body: {count: count + 1},
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(data) {
|
||||
count = parseInt(data.count)
|
||||
})
|
||||
}
|
||||
|
||||
var Hello = {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m("h1", {
|
||||
class: "title"
|
||||
}, "My first app"),
|
||||
m("button", {
|
||||
onclick: increment
|
||||
}, count + " clicks"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(root, Hello)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR.
|
||||
|
||||
This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril.js API, [be sure to check out the simple application tutorial](simple-application.md), which walks you through building a realistic application.
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
<!--meta-description
|
||||
Instructions on how to install Mithril.js
|
||||
-->
|
||||
|
||||
# Installation
|
||||
|
||||
- [CDN and online playground](#cdn)
|
||||
- [npm](#npm)
|
||||
- [Quick start with Webpack](#quick-start-with-webpack)
|
||||
|
||||
### CDN and online playground
|
||||
|
||||
If you're new to JavaScript or just want a very simple setup to get your feet wet, you can get Mithril.js from a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network):
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/mithril/mithril.js"></script>
|
||||
```
|
||||
|
||||
If you would like to try out mithril without setting up a local environment, you can easily use an online playground at [flems.io/mithril](https://flems.io/mithril).
|
||||
|
||||
---
|
||||
|
||||
### npm
|
||||
|
||||
```bash
|
||||
$ npm install mithril --save
|
||||
```
|
||||
|
||||
TypeScript type definitions are available from DefinitelyTyped. They can be installed with:
|
||||
|
||||
```bash
|
||||
$ npm install @types/mithril --save-dev
|
||||
```
|
||||
|
||||
For example usage, to file issues or to discuss TypeScript related topics visit: https://github.com/MithrilJS/mithril.d.ts
|
||||
|
||||
Type definitions for pre-release versions of Mithril.js (on the `next` branch) align with the `next` branch of the [types development repo](https://github.com/MithrilJS/mithril.d.ts/tree/next). You can install these types with:
|
||||
|
||||
```bash
|
||||
$ npm install -D MithrilJS/mithril.d.ts#next
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Quick start with Webpack
|
||||
|
||||
1. Initialize the directory as an npm package
|
||||
```bash
|
||||
$ npm init --yes
|
||||
```
|
||||
|
||||
2. install required tools
|
||||
```bash
|
||||
$ npm install mithril --save
|
||||
$ npm install webpack webpack-cli --save-dev
|
||||
```
|
||||
|
||||
3. Add a "start" entry to the scripts section in `package.json`.
|
||||
```json
|
||||
{
|
||||
"...": "...",
|
||||
"scripts": {
|
||||
"start": "webpack ./src/index.js --output-path ./bin --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Create `src/index.js` file.
|
||||
```javascript
|
||||
import m from "mithril";
|
||||
m.render(document.body, "hello world");
|
||||
```
|
||||
|
||||
5. create `index.html`
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="bin/main.js"></script>
|
||||
</body>
|
||||
```
|
||||
|
||||
6. run bundler
|
||||
```bash
|
||||
$ npm start
|
||||
```
|
||||
|
||||
7. open `index.html` in a browser
|
||||
|
||||
Optionally, you can include Mithril.js as a global variable using Webpack's provide plugin, to avoid including `import m from "mithril"` across a large number of files:
|
||||
```js
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({m: "mithril"}),
|
||||
// ...
|
||||
]
|
||||
```
|
||||
Then, you could remove the import line from step 4 (don't forget to restart Webpack if you ran it with `--watch`), and it will work just the same.
|
||||
|
||||
#### Step by step
|
||||
|
||||
For production-level projects, the recommended way of installing Mithril.js is to use npm.
|
||||
|
||||
npm is the default package manager that is bundled with Node.js. It is widely used as the package manager for both client-side and server-side libraries in the JavaScript ecosystem. Download and install [Node](https://nodejs.org); npm is bundled with that and installed alongside it.
|
||||
|
||||
To use Mithril.js via npm, go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`.
|
||||
|
||||
```bash
|
||||
npm init --yes
|
||||
# creates a file called package.json
|
||||
```
|
||||
|
||||
Then, to install Mithril.js, run:
|
||||
|
||||
```bash
|
||||
npm install mithril --save
|
||||
```
|
||||
|
||||
This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file
|
||||
|
||||
You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules:
|
||||
|
||||
```javascript
|
||||
// index.js
|
||||
var m = require("mithril")
|
||||
|
||||
m.render(document.body, "hello world")
|
||||
```
|
||||
|
||||
Modularization is the practice of separating the code into files. Doing so makes it easier to find code, understand what code relies on what code, and test.
|
||||
|
||||
CommonJS is a de-facto standard for modularizing JavaScript code, and it's used by Node.js, as well as tools like [Browserify](http://browserify.org/) and [Webpack](https://webpack.js.org/). It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](https://rollupjs.org/) or [Babel](https://babeljs.io/).
|
||||
|
||||
Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single JavaScript file before running in a client-side application.
|
||||
|
||||
A popular way for creating a bundle is to setup an npm script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line:
|
||||
|
||||
```bash
|
||||
npm install webpack webpack-cli --save-dev
|
||||
```
|
||||
|
||||
Open the `package.json` that you created earlier, and add an entry to the `scripts` section:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack src/index.js --output bin/app.js -d --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember this is a JSON file, so object key names such as `"scripts"` and `"start"` must be inside of double quotes.
|
||||
|
||||
The `-d` flag tells webpack to use development mode, which produces source maps for a better debugging experience.
|
||||
|
||||
The `--watch` flag tells webpack to watch the file system and automatically recreate `app.js` if file changes are detected.
|
||||
|
||||
Now you can run the script via `npm start` in your command line window. This looks up the `webpack` command in the npm path, reads `index.js` and creates a file called `app.js` which includes both Mithril.js and the `hello world` code above. If you want to run the `webpack` command directly from the command line, you need to either add `node_modules/.bin` to your PATH, or install webpack globally via `npm install webpack -g`. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers.
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Now that you have created a bundle, you can then reference the `bin/app.js` file from an HTML file:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello world</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="bin/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
As you've seen above, importing a module in CommonJS is done via the `require` function. You can reference npm modules by their library names (e.g. `require("mithril")` or `require("jquery")`), and you can reference your own modules via relative paths minus the file extension (e.g. if you have a file called `mycomponent.js` in the same folder as the file you're importing to, you can import it by calling `require("./mycomponent")`).
|
||||
|
||||
To export a module, assign what you want to export to the special `module.exports` object:
|
||||
|
||||
```javascript
|
||||
// mycomponent.js
|
||||
module.exports = {
|
||||
view: function() {return "hello from a module"}
|
||||
}
|
||||
```
|
||||
|
||||
In the `index.js`, you would then write this code to import that module:
|
||||
|
||||
```javascript
|
||||
// index.js
|
||||
var m = require("mithril")
|
||||
|
||||
var MyComponent = require("./mycomponent")
|
||||
|
||||
m.mount(document.body, MyComponent)
|
||||
```
|
||||
|
||||
Note that in this example, we're using `m.mount`, which wires up the component to Mithril.js' autoredraw system. In most applications, you will want to use `m.mount` (or `m.route` if your application has multiple screens) instead of `m.render` to take advantage of the autoredraw system, rather than re-rendering manually every time a change occurs.
|
||||
|
||||
#### Production build
|
||||
|
||||
If you open bin/app.js, you'll notice that the Webpack bundle is not minified, so this file is not ideal for a live application. To generate a minified file, open `package.json` and add a new npm script:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack src/index.js --output bin/app.js -d --watch",
|
||||
"build": "webpack src/index.js --output bin/app.js -p",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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/):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p",
|
||||
"heroku-postbuild": "webpack -p"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Alternate ways to use Mithril.js
|
||||
|
||||
#### Live reload development environment
|
||||
|
||||
Live reload is a feature where code changes automatically trigger the page to reload. [Budo](https://github.com/mattdesl/budo) is one tool that enables live reloading.
|
||||
|
||||
```bash
|
||||
# 1) install
|
||||
npm install mithril --save
|
||||
npm install budo -g
|
||||
|
||||
# 2) add this line into the scripts section in package.json
|
||||
# "scripts": {
|
||||
# "start": "budo --live --open index.js"
|
||||
# }
|
||||
|
||||
# 3) create an `index.js` file
|
||||
|
||||
# 4) run budo
|
||||
npm start
|
||||
```
|
||||
|
||||
The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. Any changes in the source files will instantly get recompiled and the browser will refresh reflecting the changes.
|
||||
|
||||
#### Vanilla
|
||||
|
||||
If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello world</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://unpkg.com/mithril/mithril.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// index.js
|
||||
|
||||
// if a CommonJS environment is not detected, Mithril.js will be created in the global scope
|
||||
m.render(document.body, "hello world")
|
||||
```
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
<!--meta-description
|
||||
How you can integrate a third party library into a Mithril.js application, using lifecycle methods.
|
||||
-->
|
||||
|
||||
# 3rd Party Integration
|
||||
|
||||
Integration with third party libraries or vanilla JavaScript code can be achieved via [lifecycle methods](lifecycle-methods.md).
|
||||
|
||||
## noUiSlider Example
|
||||
|
||||
```javascript
|
||||
/** NoUiSlider wrapper component */
|
||||
function Slider() {
|
||||
var slider
|
||||
|
||||
return {
|
||||
oncreate: function(vnode) {
|
||||
// Initialize 3rd party lib here
|
||||
slider = noUiSlider.create(vnode.dom, {
|
||||
start: 0,
|
||||
range: {min: 0, max: 100}
|
||||
})
|
||||
slider.on('update', function(values) {
|
||||
vnode.attrs.onChange(values[0])
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
onremove: function() {
|
||||
// Cleanup 3rd party lib on removal
|
||||
slider.destroy()
|
||||
},
|
||||
view: function() {
|
||||
return m('div')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Demo app component */
|
||||
function Demo() {
|
||||
var showSlider = false
|
||||
var value = 0
|
||||
|
||||
return {
|
||||
view: function() {
|
||||
return m('.app',
|
||||
m('p',
|
||||
m('button',
|
||||
{
|
||||
type: 'button',
|
||||
onclick: function() {
|
||||
showSlider = !showSlider
|
||||
}
|
||||
},
|
||||
showSlider ? "Destroy Slider" : "Create Slider"
|
||||
)
|
||||
),
|
||||
showSlider && m(Slider, {
|
||||
onChange: function(v) {
|
||||
value = v
|
||||
}
|
||||
}),
|
||||
m('p', value)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Demo)
|
||||
```
|
||||
|
||||
[Live Demo](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvEAXwvW10QICsEqdBk2J4A9ACoJAAgBytAKoQAylAgATGACdpAdy0YADoe3S6WQ-RHSJYgDpowAVzTViEetNUbtACgCU0sAO0tIAbhg6cGqaWg4h0lowxE5aaEEJofTUSRiMiNLOru70vmFotJqBwemhdWJi0gCSaBDuGGoAXjDSAMxa6tKGkcQAntJqAEbShNowmXXRPjoAvNIVSt6x+DkweTBlFZr46rRYFBm1dYvEIwUADBQL1wZoAOYwBcBYEGgPF1gMAAPAoARnu91Yz2krH80KW21KAHInIZ1PskRcim4PGgyh0nPBqtDQuVKjB8HliFo4Ph6ABhQgYd4HCJQQlwZD3AC6cKu12kWHwSXUBl0AWhsIW7AW9CSWFoYU+hRcONKxP5oQa0npsGZqL6AyGI3GU2knnlio68Ji2hO8GptFGEv5Mv5YQgMF0BWxJTxGoFiWSqXSWF8SPUEDCSL51yhtXj8YckhkABEYArpEZDGYzpY0NZbA5fbjpOmFQFLqFYMRpHBCLRdFtTGswB04PNajXwgSemt7vFakkUmkq3UPV6faq-ZWaoHhyHBeHKcZMSSl0jDGvNQKw0jJk5iMR6NvA4G52fA2MTAV94fj2hT5eBdk1NQANZT4q42fry-1xtm1WaQAEIAKbW04h3S942fOo3Tg0JwKA6QAH5pDsEB0zgR1xiAzDpAKTD6VyRgvEgzC-2kWMz38J5oLrBsIOWaQADJWKXICLgvS8GSZFkvzVPEwgDRC2UJaQ1jCKjYLPWF6MvPctwucSYBo651JhBJE0HIUFRcYhfFOagnBwBh8EmSpRguctaD5NgOBATAcDwHY4AEGh6EYZgeDYbkqDUNB3wQFBOBcngKicCAEW0SgQFScgeBIYhDDgRAGhcQx3zeHYzjESLosggABUEACZ8FBfB7jESMcK0CAD0YfLaCimKtHwfg4uvbgQDgHIIEMUR2DCnqCratyPISvBktS9KxEy7LcqwZrWuKsqKqqmroupBrDxgFbCuWCautGEw8Bw0ZYAcka8B+YhCHq8gqCmpKj1mjK0CynLzDEO6HugEqNoANl+tp-qgDqPO687+sGvzWCAA)
|
||||
|
||||
## Bootstrap FullCalendar Example
|
||||
|
||||
```javascript
|
||||
/** FullCalendar wrapper component */
|
||||
var FullCalendar = {
|
||||
oncreate: function (vnode) {
|
||||
console.log('FullCalendar::oncreate')
|
||||
$(vnode.dom).fullCalendar({
|
||||
// put your initial options and callbacks here
|
||||
})
|
||||
},
|
||||
onremove: function (vnode) {
|
||||
// Run any destroy / cleanup methods here.
|
||||
$(vnode.dom).fullCalendar('destroy')
|
||||
},
|
||||
view: function (vnode) {
|
||||
return m('div')
|
||||
}
|
||||
}
|
||||
|
||||
/** Demo app component */
|
||||
function Demo() {
|
||||
var fullCalendarEl
|
||||
|
||||
function next() {
|
||||
$(fullCalendarEl).fullCalendar('next')
|
||||
}
|
||||
|
||||
function prev() {
|
||||
$(fullCalendarEl).fullCalendar('prev')
|
||||
}
|
||||
|
||||
return {
|
||||
view: function (vnode) {
|
||||
return [
|
||||
m('h1', 'Calendar'),
|
||||
m(FullCalendar, {
|
||||
oncreate: function(vnode) {
|
||||
fullCalendarEl = vnode.dom
|
||||
}
|
||||
}),
|
||||
m('button', {
|
||||
onclick: prev
|
||||
}, 'Mithril.js Button -'),
|
||||
m('button', {
|
||||
onclick: next
|
||||
}, 'Mithril.js Button +')
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Demo)
|
||||
```
|
||||
|
||||
[Live Demo](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHigjQGsACAJxigBeADog4xAJ6w4hGDGKjuhfmBEgSxAA5xEAel3UAJmgBWcfNSi0ArobBQM-C7Sy6MJjAA9d7AEZxdMGsoKGoMWDRDR10AZnwAdnwABkDg0PCmKN58LA4LODhRAD4QAF8KdGxcRAIzShp6RmYa3QAqVu4AMTSAYQzIx24Ad14MTU0YXm46LE16JmJuVt1hNAA3Qe6Qvois7kFuYFXhYnpqfgxGRG4gtGpiCHpuAAo1tFpDGABKQ+OFYjoaDgtFg+CsAHNngByLZQHYDXiIRBnC6MKFfP4nAAkr3en3whhcX3wQW2-SyzyOaBONOI+m4mmsiwkNimHAgD3C3Fomge9Dg3AwkWm4Sgvgw1E4Atk-ExxFKGOp8oof3o-CwtDWMGut3ujzQLzeH2+vyVJ3pACVrAahRJuJ9xLxaHbdNNYELrJpuDhiIQPtLJjB8HKcUb8YSsMTSXDyY5oQ7iE6JOi-uU-msIDAhjrrXqnrjjT8qbT+MRrLwDVh4xA1imlaVVg3qWg2h0ACIwDWC8bTFxzNALJYrNC6vkGjsa55F1bcbgbKbRnaZRwAUSgxwNN1zY+4A88xCnptns5xi9jvDXUd65+he+IddnTZnW7uO80-DWh6px+4p+vu1XKArzJADeGhd8YFrRVHw3WdS3LA1v2PDMsxzV99UNPETSQn94IrbhkGfH9ZyrKFCAARihChuChJcEXRFVN2I71nlhOismonDmO5O5UW1F88zQAtPmnJjuNnM9QLXfY5ywgkXCI7im3EhVGPE0jfCZU40Coo9xJ4ywIEla4ILWRSf3KGiAFkOWUaBuAAIS0p4AFoGPM48NOcnTOI8n8znYYzdxgfc-O4SyoRs31eHspziG07gAGo6w8gBdRTlPCxsNywHIbAYZ5CWoawcAYfBfA+CRqInWhFTKCoQEwHA8HyBAqEBJpiDwMpUqodguAQFBKmampcmi6B6nLcgag0bQ9F0a1NE4cFnFcMa7KgAABcj8B2gBWXR1piqB8DqKhJAmPA4HOCBeXq4bqhADVSq6qgprwWadH0RbltWw6XAWTaACZdoATnwIH-pe062pAC7HuumK7vKB68BMABHaxJgkSbeGm9R4rm760CWlaZl0DGsd4CRNriEHyN0QwIHECnMexmH6nhq6buRhqmse6MwlA3H8c++afrJlxUhCIXl14WmEmSRnmbpQXzw586JEumpEdurrSlS0ogA)
|
||||
442
docs/jsx.md
|
|
@ -1,442 +0,0 @@
|
|||
<!--meta-description
|
||||
Explanation, examples, and build notes on how to use JSX in your Mithril.js-based apps
|
||||
-->
|
||||
|
||||
# JSX
|
||||
|
||||
- [Description](#description)
|
||||
- [Setup](#setup)
|
||||
- [Production build](#production-build)
|
||||
- [Using Babel with Webpack](#using-babel-with-webpack)
|
||||
- [Differences with React](#differences-with-react)
|
||||
- [JSX vs hyperscript](#jsx-vs-hyperscript)
|
||||
- [Tips and Tricks](#tips-and-tricks)
|
||||
|
||||
---
|
||||
|
||||
### 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 you or your team's preferences.
|
||||
|
||||
```jsx
|
||||
function MyComponent() {
|
||||
return {
|
||||
view: () => m("main", [m("h1", "Hello world")]),
|
||||
};
|
||||
}
|
||||
|
||||
// can be written as:
|
||||
|
||||
function MyComponent() {
|
||||
return {
|
||||
view: () => (
|
||||
<main>
|
||||
<h1>Hello world</h1>
|
||||
</main>
|
||||
),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
When using JSX, it's possible to interpolate JavaScript expressions within JSX tags by using curly braces:
|
||||
|
||||
```jsx
|
||||
var greeting = "Hello";
|
||||
var url = "https://google.com";
|
||||
var link = <a href={url}>{greeting}!</a>;
|
||||
// yields <a href="https://google.com">Hello!</a>
|
||||
```
|
||||
|
||||
Components can be used by using a convention of uppercasing the first letter of the component name or by accessing it as a property:
|
||||
|
||||
```jsx
|
||||
m.render(document.body, <MyComponent />)
|
||||
// equivalent to m.render(document.body, m(MyComponent))
|
||||
<m.route.Link href="/home">Go home</m.route.Link>
|
||||
// equivalent to m(m.route.Link, {href: "/home"}, "Go home")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Setup
|
||||
|
||||
The simplest way to use JSX is via a [Babel](https://babeljs.io/) plugin.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
```
|
||||
|
||||
If you want to use Webpack and Babel together, [skip to the section below](#using-babel-with-webpack).
|
||||
|
||||
To install Babel as a standalone tool, use this command:
|
||||
|
||||
```bash
|
||||
npm install @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
|
||||
```
|
||||
|
||||
Create a `.babelrc` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
{
|
||||
"pragma": "m",
|
||||
"pragmaFrag": "'['"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To run Babel, setup an npm script. Open `package.json` and add this entry under `"scripts"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"babel": "babel src --out-dir bin --source-maps"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can now run Babel using this command:
|
||||
|
||||
```bash
|
||||
npm run babel
|
||||
```
|
||||
|
||||
#### Using Babel with Webpack
|
||||
|
||||
If you haven't already installed Webpack as a bundler, use this command:
|
||||
|
||||
```bash
|
||||
npm install webpack webpack-cli --save-dev
|
||||
```
|
||||
|
||||
You can integrate Babel to Webpack by following these steps.
|
||||
|
||||
```bash
|
||||
npm install @babel/core babel-loader @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
|
||||
```
|
||||
|
||||
Create a `.babelrc` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
{
|
||||
"pragma": "m",
|
||||
"pragmaFrag": "'['"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Next, create a file called `webpack.config.js`
|
||||
|
||||
```jsx
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/index.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "./bin"),
|
||||
filename: "app.js",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /\/node_modules\//,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx"],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
For those familiar with Webpack already, please note that adding the Babel options to the `babel-loader` section of your `webpack.config.js` will throw an error, so you need to include them in the separate `.babelrc` file.
|
||||
|
||||
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`.
|
||||
|
||||
To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack --mode development --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can now then run the bundler by running this from the command line:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Production build
|
||||
|
||||
To generate a minified file, open `package.json` and add a new npm script called `build`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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/):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"start": "webpack -d --watch",
|
||||
"build": "webpack -p",
|
||||
"heroku-postbuild": "webpack -p"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Making `m` accessible globally
|
||||
|
||||
In order to access `m` globally from all your project first import `webpack` in your `webpack.config.js` like this:
|
||||
|
||||
```js
|
||||
const webpack = require('webpack')
|
||||
```
|
||||
|
||||
Then create a new plugin in the `plugins` property of the Webpack configuration object:
|
||||
|
||||
```js
|
||||
{
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
m: "mithril",
|
||||
}),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
See [the Webpack docs](https://webpack.js.org/plugins/provide-plugin/) for more information on `ProvidePlugin`.
|
||||
|
||||
---
|
||||
|
||||
### Differences with React
|
||||
|
||||
JSX in Mithril has some subtle but important differences compared to React's JSX.
|
||||
|
||||
#### Attribute and style property case conventions
|
||||
|
||||
React requires you use the camel-cased DOM property names instead of HTML attribute names for all attributes other than `data-*` and `aria-*` attributes. For example using `className` instead of `class` and `htmlFor` instead of `for`. In Mithril, it's more idiomatic to use the lowercase HTML attribute names instead. Mithril always falls back to setting attributes if a property doesn't exist which aligns more intuitively with HTML. Note that in most cases, the DOM property and HTML attribute names are either the same or very similar. For example `value`/`checked` for inputs and the `tabindex` global attribute vs the `elem.tabIndex` property on HTML elements. Very rarely do they differ beyond case: the `elem.className` property for the `class` attribute or the `elem.htmlFor` property for the `for` attribute are among the few exceptions.
|
||||
|
||||
Similarly, React always uses the camel-cased style property names exposed in the DOM via properties of `elem.style` (like `cssHeight` and `backgroundColor`). Mithril supports both that and the kebab-cased CSS property names (like `height` and `background-color`) and idiomatically prefers the latter. Only `cssHeight`, `cssFloat`, and vendor-prefixed properties differ in more than case.
|
||||
|
||||
#### DOM events
|
||||
|
||||
React upper-cases the first character of all event handlers: `onClick` listens for `click` events and `onSubmit` for `submit` events. Some are further altered as they're multiple words concatenated together. For instance, `onMouseMove` listens for `mousemove` events. Mithril does not do this case mapping but instead just prepends `on` to the native event, so you'd add listeners for `onclick` and `onmousemove` to listen to those two events respectively. This corresponds much more closely to HTML's naming scheme and is much more intuitive if you come from an HTML or vanilla DOM background.
|
||||
|
||||
React supports scheduling event listeners during the capture phase (in the first pass, out to in, as opposed to the default bubble phase going in to out in the second pass) by appending `Capture` to that event. Mithril currently lacks such functionality, but it could gain this in the future. If this is necessary you can manually add and remove your own listeners in [lifecycle hooks](lifecycle-methods.md).
|
||||
|
||||
---
|
||||
|
||||
### JSX vs hyperscript
|
||||
|
||||
JSX and hyperscript are two different syntaxes you can use for specifying vnodes, and they have different tradeoffs:
|
||||
|
||||
- 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.
|
||||
|
||||
- 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.
|
||||
|
||||
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 [@dead-claudia](https://github.com/dead-claudia) with some alterations for clarity and readability:
|
||||
|
||||
```javascript
|
||||
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 m(".feed", [
|
||||
type,
|
||||
m("a", { href }, m("img.feed-icon[src=./feed-icon-16.gif]")),
|
||||
]);
|
||||
}
|
||||
|
||||
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 }),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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>
|
||||
),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tips and tricks
|
||||
|
||||
#### Converting HTML to JSX
|
||||
|
||||
In Mithril.js, well-formed HTML is generally valid JSX. Little more than just pasting raw HTML is required for things to just work. About the only things you'd normally have to do are change unquoted property values like `attr=value` to `attr="value"` and change void elements like `<input>` to `<input />`, this being due to JSX being based on XML and not HTML.
|
||||
|
||||
When using hyperscript, you often need to translate HTML to hyperscript syntax to use it. To help speed up this process along, you can use a [community-created HTML-to-Mithril-template converter](https://arthurclemens.github.io/mithril-template-converter/index.html) to do much of it for you.
|
||||
553
docs/keys.md
|
|
@ -1,81 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Mithril.js</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/themes/prism.min.css" rel="stylesheet" />
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="[metaDescription]">
|
||||
</head>
|
||||
<body onload="window.requestAnimationFrame(function(){document.getElementById('archive-docs').selectedIndex = 0})" /* handle back navigation */>
|
||||
<header>
|
||||
<section>
|
||||
<a class="hamburger" href="javascript:;">≡</a>
|
||||
<h1><img src="logo.svg"> Mithril [archive-docs]</h1>
|
||||
<nav>
|
||||
<a href="index.html">Guide</a>
|
||||
<a href="api.html">API</a>
|
||||
<a href="https://mithril.zulipchat.com/">Chat</a>
|
||||
<a href="https://github.com/MithrilJS/mithril.js">GitHub</a>
|
||||
</nav>
|
||||
</section>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
[body]
|
||||
<hr />
|
||||
<small>License: MIT. © Leo Horie.</small>
|
||||
</section>
|
||||
</main>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/components/prism-jsx.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/components/prism-diff.min.js"></script>
|
||||
<script src="https://unpkg.com/mithril@[version]/mithril.js" async></script>
|
||||
<script src="https://flems.io/flems.html" id="flems" defer></script>
|
||||
<script>
|
||||
document.querySelector(".hamburger").onclick = function() {
|
||||
document.body.className = document.body.className === "navigating" ? "" : "navigating"
|
||||
document.querySelector("h1 + ul").onclick = function() {
|
||||
document.body.className = ''
|
||||
}
|
||||
}
|
||||
document.getElementById("flems").onload = function() {
|
||||
var systemFonts = [
|
||||
"body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; }",
|
||||
"body { height: 100%; overflow:hidden; display:flex; justify-content: center; align-items:center; }"
|
||||
].join("")
|
||||
;[].forEach.call(document.querySelectorAll("pre code.language-js"), function(el) {
|
||||
el = el.parentNode
|
||||
|
||||
var div = document.createElement("div")
|
||||
window.Flems(div, {
|
||||
middle : 60,
|
||||
editable : true,
|
||||
toolbar : false,
|
||||
shareButton : true,
|
||||
console : false,
|
||||
autoHeight : true,
|
||||
files: [{
|
||||
name: ".js",
|
||||
content: el.textContent
|
||||
}, {
|
||||
name: ".css",
|
||||
content: systemFonts
|
||||
}],
|
||||
links: [{
|
||||
name: "mithril",
|
||||
type: "script",
|
||||
url: "https://unpkg.com/mithril@[version]/mithril.js"
|
||||
}]
|
||||
}, "https://flems.io/flems.html")
|
||||
|
||||
el.parentNode.insertBefore(div, el)
|
||||
el.parentNode.removeChild(el)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<!--meta-description
|
||||
Links to Mithril.js learning content
|
||||
-->
|
||||
|
||||
# Learning Resources
|
||||
|
||||
Links to Mithril.js learning content:
|
||||
|
||||
- [Mithril 0-60](https://vimeo.com/showcase/5584199)
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on Mithril.js' lifecycle methods, also known as lifecycle "hooks"
|
||||
-->
|
||||
|
||||
# Lifecycle methods
|
||||
|
||||
- [Usage](#usage)
|
||||
- [The DOM element lifecycle](#the-dom-element-lifecycle)
|
||||
- [oninit](#oninit)
|
||||
- [oncreate](#oncreate)
|
||||
- [onupdate](#onupdate)
|
||||
- [onbeforeremove](#onbeforeremove)
|
||||
- [onremove](#onremove)
|
||||
- [onbeforeupdate](#onbeforeupdate)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
### Usage
|
||||
|
||||
[Components](components.md) and [virtual DOM nodes](vnodes.md) can have lifecycle methods, also known as *hooks*, which are called at various points during the lifetime of a DOM element.
|
||||
|
||||
```javascript
|
||||
// Sample hook in component
|
||||
var ComponentWithHook = {
|
||||
oninit: function(vnode) {
|
||||
console.log("initialize component")
|
||||
},
|
||||
view: function() {
|
||||
return "hello"
|
||||
}
|
||||
}
|
||||
|
||||
// Sample hook in vnode
|
||||
function initializeVnode() {
|
||||
console.log("initialize vnode")
|
||||
}
|
||||
|
||||
m(ComponentWithHook, {oninit: initializeVnode})
|
||||
```
|
||||
|
||||
All lifecyle methods receive the vnode as their first arguments, and have their `this` keyword bound to `vnode.state`.
|
||||
|
||||
Lifecycle methods are only called as a side effect of a [`m.render()`](render.md) call. They are not called if the DOM is modified outside of Mithril.
|
||||
|
||||
---
|
||||
|
||||
### The DOM element lifecycle
|
||||
|
||||
A DOM element is typically created and appended to the document. It may then have attributes or child nodes updated when a UI event is triggered and data is changed; and the element may alternatively be removed from the document.
|
||||
|
||||
After an element is removed, it may be temporarily retained in a memory pool. The pooled element may be reused in a subsequent update (in a process called *DOM recycling*). Recycling an element avoids incurring the performance cost of recreating a copy of an element that existed recently.
|
||||
|
||||
---
|
||||
|
||||
### oninit
|
||||
|
||||
The `oninit(vnode)` hook is called before a vnode is touched by the virtual DOM engine. `oninit` is guaranteed to run before its DOM element is attached to the document, and it is guaranteed to run on parent vnodes before their children, but it does not offer any guarantees regarding the existence of ancestor or descendant DOM elements. You should never access the `vnode.dom` from the `oninit` method.
|
||||
|
||||
This hook does not get called when an element is updated, but it does get called if an element is recycled.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `oninit` callback points to `vnode.state`.
|
||||
|
||||
The `oninit` hook is useful for initializing component state based on arguments passed via `vnode.attrs` or `vnode.children`.
|
||||
|
||||
```javascript
|
||||
function ComponentWithState() {
|
||||
var initialData
|
||||
return {
|
||||
oninit: function(vnode) {
|
||||
initialData = vnode.attrs.data
|
||||
},
|
||||
view: function(vnode) {
|
||||
return [
|
||||
// displays data from initialization time:
|
||||
m("div", "Initial: " + initialData),
|
||||
// displays current data:
|
||||
m("div", "Current: " + vnode.attrs.data)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentWithState, {data: "Hello"})
|
||||
```
|
||||
|
||||
You should not modify model data synchronously from this method. Since `oninit` makes no guarantees regarding the status of other elements, model changes created from this method may not be reflected in all parts of the UI until the next render cycle.
|
||||
|
||||
---
|
||||
|
||||
### oncreate
|
||||
|
||||
The `oncreate(vnode)` hook is called after a DOM element is created and attached to the document. `oncreate` is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as `vnode.dom.offsetHeight` and `vnode.dom.getBoundingClientRect()` from this method.
|
||||
|
||||
This hook does not get called when an element is updated.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `oncreate` callback points to `vnode.state`. DOM elements whose vnodes have an `oncreate` hook do not get recycled.
|
||||
|
||||
The `oncreate` hook is useful for reading layout values that may trigger a repaint, starting animations and for initializing third party libraries that require a reference to the DOM element.
|
||||
|
||||
```javascript
|
||||
var HeightReporter = {
|
||||
oncreate: function(vnode) {
|
||||
console.log("Initialized with height of: ", vnode.dom.offsetHeight)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
|
||||
m(HeightReporter, {data: "Hello"})
|
||||
```
|
||||
|
||||
You should not modify model data synchronously from this method. Since `oncreate` is run at the end of the render cycle, model changes created from this method will not be reflected in the UI until the next render cycle.
|
||||
|
||||
---
|
||||
|
||||
### onupdate
|
||||
|
||||
The `onupdate(vnode)` hook is called after a DOM element is updated, while attached to the document. `onupdate` is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as `vnode.dom.offsetHeight` and `vnode.dom.getBoundingClientRect()` from this method.
|
||||
|
||||
This hook is only called if the element existed in the previous render cycle. It is not called when an element is created or when it is recycled.
|
||||
|
||||
DOM elements whose vnodes have an `onupdate` hook do not get recycled.
|
||||
|
||||
The `onupdate` hook is useful for reading layout values that may trigger a repaint, and for dynamically updating UI-affecting state in third party libraries after model data has been changed.
|
||||
|
||||
```javascript
|
||||
function RedrawReporter() {
|
||||
var count = 0
|
||||
return {
|
||||
onupdate: function() {
|
||||
console.log("Redraws so far: ", ++count)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
}
|
||||
|
||||
m(RedrawReporter, {data: "Hello"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### onbeforeremove
|
||||
|
||||
The `onbeforeremove(vnode)` hook is called before a DOM element is detached from the document. If a Promise is returned, Mithril.js only detaches the DOM element after the promise completes.
|
||||
|
||||
This hook is only called on the DOM element that loses its `parentNode`, but it does not get called in its child elements.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `onbeforeremove` callback points to `vnode.state`. DOM elements whose vnodes have an `onbeforeremove` hook do not get recycled.
|
||||
|
||||
```javascript
|
||||
var Fader = {
|
||||
onbeforeremove: function(vnode) {
|
||||
vnode.dom.classList.add("fade-out")
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "Bye")
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### onremove
|
||||
|
||||
The `onremove(vnode)` hook is called before a DOM element is removed from the document. If a `onbeforeremove` hook is also defined, the `onremove` hook runs after the promise returned from `onbeforeremove` is completed.
|
||||
|
||||
This hook is called on any element that is removed from the document, regardless of whether it was directly detached from its parent or whether it is a child of another element that was detached.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `onremove` callback points to `vnode.state`. DOM elements whose vnodes have an `onremove` hook do not get recycled.
|
||||
|
||||
The `onremove` hook is useful for running clean up tasks.
|
||||
|
||||
```javascript
|
||||
function Timer() {
|
||||
var timeout = setTimeout(function() {
|
||||
console.log("timed out")
|
||||
}, 1000)
|
||||
|
||||
return {
|
||||
onremove: function() {
|
||||
clearTimeout(timeout)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### onbeforeupdate
|
||||
|
||||
The `onbeforeupdate(vnode, old)` hook is called before a vnode is diffed in a update. If this function is defined and returns false, Mithril.js prevents a diff from happening to the vnode, and consequently to the vnode's children.
|
||||
|
||||
This hook by itself does not prevent a virtual DOM subtree from being generated unless the subtree is encapsulated within a component.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `onbeforeupdate` callback points to `vnode.state`.
|
||||
|
||||
This hook is useful to reduce lag in updates in cases where there is a overly large DOM tree.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
Although Mithril.js is flexible, some code patterns are discouraged:
|
||||
|
||||
#### Avoid premature optimizations
|
||||
|
||||
You should only use `onbeforeupdate` to skip diffing as a last resort. Avoid using it unless you have a noticeable performance issue.
|
||||
|
||||
Typically performance problems that can be fixed via `onbeforeupdate` boil down to one large array of items. In this context, typically "large" means any array that contains a large number of nodes, be it in a wide spread (the infamous 5000 row table), or in a deep, dense tree.
|
||||
|
||||
If you do have a performance issue, first consider whether the UI presents a good user experience and change it if it doesn't. For example, it's highly unlikely that a user would ever sift through 5000 rows of raw table data, and highly likely that it would be easier for a user to use a search feature that returns only the top few most relevant items.
|
||||
|
||||
If a design-based solution is not feasible, and you must optimize a UI with a large number of DOM element, apply `onbeforeupdate` on the parent node of the largest array and re-evaluate performance. In the vast majority of cases, a single check should be sufficient. In the rare case that it is not, rinse and repeat, but you should be increasingly wary of each new `onbeforeupdate` declaration. Multiple `onbeforeupdate`s are a code smell that indicates prioritization problems in the design workflow.
|
||||
|
||||
Avoid applying the optimization to other areas of your application "just-in-case". Remember that, generally speaking, more code incurs a higher maintenance cost than less code, and `onbeforeupdate` related bugs can be especially difficult to troubleshoot if you rely on object identity for its conditional checks.
|
||||
|
||||
Again, **the `onbeforeupdate` hook should only be used as a last resort.**
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 38.044 38.044" version="1.1" viewBox="0 0 38.044 38.044" xmlns="http://www.w3.org/2000/svg"><path d="m31.716 24.543c-0.017 7.03-5.742 12.745-12.776 12.745-7.045 0-12.777-5.732-12.777-12.777 0-0.022 4e-3 -0.043 4e-3 -0.065-3.69-2.243-6.167-6.29-6.167-10.914 0-7.046 5.731-12.777 12.777-12.777 2.268 0 4.395 0.601 6.244 1.642 1.849-1.041 3.977-1.642 6.245-1.642 7.046 0 12.777 5.732 12.777 12.777 0 4.691-2.548 8.789-6.327 11.011zm-12.695-19.461c-2.312 1.713-3.906 4.341-4.22 7.352 1.3-0.448 2.689-0.702 4.139-0.702 1.514 0 2.96 0.278 4.307 0.764-0.298-3.037-1.898-5.689-4.226-7.414zm-10.504 18.063c1.303 0.579 2.743 0.909 4.26 0.909 1.475 0 2.879-0.307 4.154-0.858-2.114-1.826-3.629-4.325-4.195-7.167-2.263 1.662-3.838 4.2-4.219 7.116zm10.423-9.157c-1.457 0-2.846 0.298-4.109 0.837 0.361 2.928 1.929 5.482 4.19 7.157 2.243-1.662 3.802-4.187 4.18-7.085-1.304-0.581-2.744-0.909-4.261-0.909zm2.171 9.209c1.275 0.55 2.679 0.858 4.154 0.858 1.457 0 2.846-0.298 4.11-0.837-0.356-2.885-1.883-5.404-4.089-7.082-0.582 2.799-2.087 5.257-4.175 7.061zm-2.171 11.836c5.432 0 9.915-4.137 10.466-9.425-1.3 0.447-2.689 0.702-4.14 0.702-2.268 0-4.396-0.601-6.245-1.642-1.848 1.041-3.975 1.642-6.244 1.642-1.514 0-2.96-0.278-4.307-0.763 0.523 5.317 5.018 9.486 10.47 9.486zm-6.163-32.024c-5.803 0-10.523 4.72-10.523 10.523 0 3.418 1.645 6.451 4.177 8.375 0.744-3.581 2.999-6.607 6.059-8.408 0.011-3.847 1.735-7.293 4.442-9.631-1.276-0.552-2.679-0.859-4.155-0.859zm12.489 0c-1.475 0-2.879 0.307-4.154 0.858 2.715 2.345 4.444 5.804 4.444 9.664 0 0.022-4e-3 0.044-4e-3 0.065 3.007 1.829 5.209 4.852 5.918 8.416 2.613-1.917 4.319-4.999 4.319-8.48-1e-3 -5.802-4.721-10.523-10.523-10.523z" fill="#010002"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,935 +0,0 @@
|
|||
# Migrating from v0.2.x
|
||||
|
||||
v1.x and v2.x are largely API-compatible with v0.2.x, but there are some breaking changes. Migrating to v2.x is nearly identical, so the notes below apply mostly to both.
|
||||
|
||||
If you are migrating, consider using the [mithril-codemods](https://www.npmjs.com/package/mithril-codemods) tool to help automate the most straightforward migrations.
|
||||
|
||||
- [`m.prop` removed](#mprop-removed)
|
||||
- [`m.component` removed](#mcomponent-removed)
|
||||
- [`m.withAttr` removed](#mwithattr-removed)
|
||||
- [`m.version` removed](#mversion-removed)
|
||||
- [`config` function](#config-function)
|
||||
- [Changes in redraw behaviour](#changes-in-redraw-behaviour)
|
||||
- [No more redraw locks](#no-more-redraw-locks)
|
||||
- [Cancelling redraw from event handlers](#cancelling-redraw-from-event-handlers)
|
||||
- [Synchronous redraw changed](#synchronous-redraw)
|
||||
- [`m.startComputation`/`m.endComputation` removed](#mstartcomputationmendcomputation-removed)
|
||||
- [Component `controller` function](#component-controller-function)
|
||||
- [Component arguments](#component-arguments)
|
||||
- [Component vnode children](#component-children)
|
||||
- [DOM vnode children](#dom-vnode-children)
|
||||
- [Keys](#keys)
|
||||
- [`view()` parameters](#view-parameters)
|
||||
- [Passing components to `m()`](#passing-components-to-m)
|
||||
- [Passing vnodes to `m.mount()` and `m.route()`](#passing-vnodes-to-mmount-and-mroute)
|
||||
- [`m.route.mode`](#mroutemode)
|
||||
- [`m.route()` and anchor tags](#mroute-and-anchor-tags)
|
||||
- [Path templates](#path-templates)
|
||||
- [Reading/writing the current route](#readingwriting-the-current-route)
|
||||
- [Accessing route params](#accessing-route-params)
|
||||
- [Building/Parsing query strings](#buildingparsing-query-strings)
|
||||
- [Preventing unmounting](#preventing-unmounting)
|
||||
- [Run code on component removal](#run-code-on-component-removal)
|
||||
- [`m.request`](#mrequest)
|
||||
- [Default `responseType` for `m.request`](#default-responsetype-for-mrequest)
|
||||
- [`m.deferred` removed](#mdeferred-removed)
|
||||
- [`m.sync` removed](#msync-removed)
|
||||
- [`xlink` namespace required](#xlink-namespace-required)
|
||||
- [Nested arrays in views](#nested-arrays-in-views)
|
||||
- [`vnode` equality checks](#vnode-equality-checks)
|
||||
|
||||
---
|
||||
|
||||
## `m.prop` removed
|
||||
|
||||
In v2.x, `m.prop()` was converted into now a more powerful stream micro-library, but it's no longer part of core. You can read about how to use the optional Streams module in [the documentation](stream.md).
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var m = require("mithril")
|
||||
|
||||
var num = m.prop(1)
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var m = require("mithril")
|
||||
var prop = require("mithril/stream")
|
||||
|
||||
var num = prop(1)
|
||||
var doubled = num.map(function(n) { return n * 2 })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.component` removed
|
||||
|
||||
In v0.2.x components could be created using either `m(Component)` or `m.component(Component)`. v2.x only support `m(Component)`.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
// These are equivalent
|
||||
m.component(Component)
|
||||
m(Component)
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m(Component)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.withAttr` removed
|
||||
|
||||
In v0.2.x event listeners could use `oninput: m.withAttr("value", func)` and similar. In v2.x, just read them directly from the event's target. It synergized well with `m.prop`, but since that was removed in favor of an out of core solution and v1.x didn't see similar broad, idiomatic usage of streams, `m.withAttr` lost most of its usefulness.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var value = m.prop("")
|
||||
|
||||
// In your view
|
||||
m("input[type=text]", {
|
||||
value: value(),
|
||||
oninput: m.withAttr("value", value),
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var value = ""
|
||||
|
||||
// In your view
|
||||
m("input[type=text]", {
|
||||
value: value,
|
||||
oninput: function (ev) { value = ev.target.value },
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.version` removed
|
||||
|
||||
It served little use in general, and you can always add it back yourself. You should prefer feature detection for knowing what features are available, and the v2.x API is designed to better enable this.
|
||||
|
||||
---
|
||||
|
||||
## `config` function
|
||||
|
||||
In v0.2.x Mithril.js provided a single lifecycle method, `config`. v2.x provide much more fine-grained control over the lifecycle of a vnode.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m("div", {
|
||||
config: function(element, isInitialized) {
|
||||
// runs on each redraw
|
||||
// isInitialized is a boolean representing if the node has been added to the DOM
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
More documentation on these new methods is available in [lifecycle-methods.md](lifecycle-methods.md).
|
||||
|
||||
```javascript
|
||||
m("div", {
|
||||
// Called before the DOM node is created
|
||||
oninit: function(vnode) { /*...*/ },
|
||||
// Called after the DOM node is created
|
||||
oncreate: function(vnode) { /*...*/ },
|
||||
// Called before the node is updated, return false to cancel
|
||||
onbeforeupdate: function(vnode, old) { /*...*/ },
|
||||
// Called after the node is updated
|
||||
onupdate: function(vnode) { /*...*/ },
|
||||
// Called before the node is removed, return a Promise that resolves when
|
||||
// ready for the node to be removed from the DOM
|
||||
onbeforeremove: function(vnode) { /*...*/ },
|
||||
// Called before the node is removed, but after onbeforeremove calls done()
|
||||
onremove: function(vnode) { /*...*/ }
|
||||
})
|
||||
```
|
||||
|
||||
If available the DOM-Element of the vnode can be accessed at `vnode.dom`.
|
||||
|
||||
---
|
||||
|
||||
## Changes in redraw behaviour
|
||||
|
||||
Mithril.js' rendering engine still operates on the basis of semi-automated global redraws, but some APIs and behaviours differ:
|
||||
|
||||
### No more redraw locks
|
||||
|
||||
In v0.2.x, Mithril.js allowed 'redraw locks' which temporarily prevented blocked draw logic: by default, `m.request` would lock the draw loop on execution and unlock when all pending requests had resolved - the same behaviour could be invoked manually using `m.startComputation()` and `m.endComputation()`. The latter APIs and the associated behaviour has been removed in v2.x without replacement. Redraw locking can lead to buggy UIs: the concerns of one part of the application should not be allowed to prevent other parts of the view from updating to reflect change.
|
||||
|
||||
### Cancelling redraw from event handlers
|
||||
|
||||
`m.mount()` and `m.route()` still automatically redraw after a DOM event handler runs. Cancelling these redraws from within your event handlers is now done by setting the `redraw` property on the passed-in event object to `false`.
|
||||
|
||||
#### v0.2.x
|
||||
|
||||
```javascript
|
||||
m("div", {
|
||||
onclick: function(e) {
|
||||
m.redraw.strategy("none")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### v2.x
|
||||
|
||||
```javascript
|
||||
m("div", {
|
||||
onclick: function(e) {
|
||||
e.redraw = false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Synchronous redraw changed
|
||||
|
||||
In v0.2.x it was possible to force Mithril.js to redraw immediately by passing a truthy value to `m.redraw()`. In v2.x, this functionality was split into two different methods for clarity.
|
||||
|
||||
#### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.redraw(true) // redraws immediately & synchronously
|
||||
```
|
||||
|
||||
#### v2.x
|
||||
|
||||
```javascript
|
||||
m.redraw() // schedules a redraw on the next requestAnimationFrame tick
|
||||
m.redraw.sync() // invokes a redraw immediately and waits for it to complete
|
||||
```
|
||||
|
||||
### `m.startComputation`/`m.endComputation` removed
|
||||
|
||||
They are considered anti-patterns and have a number of problematic edge cases, so they were removed without replacement in v2.x.
|
||||
|
||||
---
|
||||
|
||||
## Component `controller` function
|
||||
|
||||
In v2.x, there is no more `controller` property in components - use `oninit` instead.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.mount(document.body, {
|
||||
controller: function() {
|
||||
var ctrl = this
|
||||
|
||||
ctrl.fooga = 1
|
||||
},
|
||||
|
||||
view: function(ctrl) {
|
||||
return m("p", ctrl.fooga)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m.mount(document.body, {
|
||||
oninit: function(vnode) {
|
||||
vnode.state.fooga = 1
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return m("p", vnode.state.fooga)
|
||||
}
|
||||
})
|
||||
|
||||
// OR
|
||||
|
||||
m.mount(document.body, {
|
||||
// this is bound to vnode.state by default
|
||||
oninit: function(vnode) {
|
||||
this.fooga = 1
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return m("p", this.fooga)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component arguments
|
||||
|
||||
Arguments to a component in v2.x must be an object, simple values like `String`/`Number`/`Boolean` will be treated as text children. Arguments are accessed within the component by reading them from the `vnode.attrs` object.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
controller: function(options) {
|
||||
// options.fooga === 1
|
||||
},
|
||||
|
||||
view: function(ctrl, options) {
|
||||
// options.fooga === 1
|
||||
}
|
||||
}
|
||||
|
||||
m("div", m.component(Component, { fooga: 1 }))
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
oninit: function(vnode) {
|
||||
// vnode.attrs.fooga === 1
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
// vnode.attrs.fooga === 1
|
||||
}
|
||||
}
|
||||
|
||||
m("div", m(Component, { fooga: 1 }))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component vnode children
|
||||
|
||||
In v0.2.x, component vnode children were not normalized, just passed as extra arguments, and they were not flattened, either. (Internally, it was just returning a partially applied component that was diffed based on the component being partially applied.) In v2.x, component vnode children are passed via `vnode.children` as a resolved array of children, but like v0.2.x, the individual children themselves are not normalized, nor is the children array flattened.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
controller: function(value, renderProp) {
|
||||
// value === "value"
|
||||
// typeof renderProp === "function"
|
||||
},
|
||||
|
||||
view: function(ctrl, value, renderProp) {
|
||||
// value === "value"
|
||||
// typeof renderProp === "function"
|
||||
}
|
||||
}
|
||||
|
||||
m("div", m.component(Component, "value", function(key) { return "child" }))
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
oninit: function(vnode) {
|
||||
// vnode.children[0] === "value"
|
||||
// typeof vnode.children[1] === "function"
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
// vnode.children[0] === "value"
|
||||
// typeof vnode.children[1] === "function"
|
||||
},
|
||||
}
|
||||
|
||||
m("div", m(Component, "value", function(key) { return "child" }))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DOM vnode children
|
||||
|
||||
In v0.2.x, the children of DOM nodes were represented literally with no normalization aside from using the children directly if only a single array child is present. It returned a structure more like this, with the strings represented literally.
|
||||
|
||||
```javascript
|
||||
m("div", "value", ["nested"])
|
||||
|
||||
// Becomes:
|
||||
{
|
||||
tag: "div",
|
||||
attrs: {},
|
||||
children: [
|
||||
"value",
|
||||
["nested"],
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In v2.x, children of DOM vnodes are normalized to objects of a single consistent structure.
|
||||
|
||||
```javascript
|
||||
m("div", "value", ["nested"])
|
||||
|
||||
// Becomes roughly:
|
||||
{
|
||||
tag: "div",
|
||||
attrs: null,
|
||||
children: [
|
||||
{tag: "#", children: "value"},
|
||||
{tag: "[", children: [
|
||||
{tag: "#", children: "nested"},
|
||||
]},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If only a single text child is present on a DOM vnode, it instead sets `text` to that value.
|
||||
|
||||
```javascript
|
||||
m("div", "value")
|
||||
|
||||
// Becomes roughly:
|
||||
{
|
||||
tag: "div",
|
||||
attrs: null,
|
||||
text: "",
|
||||
children: undefined,
|
||||
}
|
||||
```
|
||||
|
||||
See [the vnode docs](vnodes.md) for more details on the v2.x vnode structure and how things are normalized.
|
||||
|
||||
*Most of the v2.x vnode properties here are omitted for brevity.*
|
||||
|
||||
---
|
||||
|
||||
## Keys
|
||||
|
||||
In v0.2.x, you could mix keyed and unkeyed vnodes freely.
|
||||
|
||||
In v2.x, children lists of both fragments and elements must be either all keyed or all unkeyed. Holes are considered unkeyed for the purposes of this check, too - it no longer ignores them.
|
||||
|
||||
If you need to work around it, use the idiom of a fragment containing a single vnode, like `[m("div", {key: whatever})]`.
|
||||
|
||||
---
|
||||
|
||||
## `view()` parameters
|
||||
|
||||
In v0.2.x view functions are passed a reference to the `controller` instance and (optionally) any options passed to the component. In v2.x they are passed **only** the `vnode`, exactly like the `controller` function.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.mount(document.body, {
|
||||
controller: function() {},
|
||||
|
||||
view: function(ctrl, options) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m.mount(document.body, {
|
||||
oninit: function(vnode) {
|
||||
// ...
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
// Use vnode.state instead of ctrl
|
||||
// Use vnode.attrs instead of options
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Passing components to `m()`
|
||||
|
||||
In v0.2.x you could pass components as the second argument of `m()` w/o any wrapping required. To help with consistency in v2.x they must always be wrapped with a `m()` invocation.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m("div", Component)
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m("div", m(Component))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Passing vnodes to `m.mount()` and `m.route()`
|
||||
|
||||
In v0.2.x, `m.mount(element, component)` tolerated [vnodes](vnodes.md) as second arguments instead of [components](components.md) (even though it wasn't documented). Likewise, `m.route(element, defaultRoute, routes)` accepted vnodes as values in the `routes` object.
|
||||
|
||||
In v2.x, components are required instead in both cases.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.mount(element, m('i', 'hello'))
|
||||
m.mount(element, m(Component, attrs))
|
||||
|
||||
m.route(element, '/', {
|
||||
'/': m('b', 'bye')
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m.mount(element, {view: function () {return m('i', 'hello')}})
|
||||
m.mount(element, {view: function () {return m(Component, attrs)}})
|
||||
|
||||
m.route(element, '/', {
|
||||
'/': {view: function () {return m('b', 'bye')}}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.route.mode`
|
||||
|
||||
In v0.2.x the routing mode could be set by assigning a string of `"pathname"`, `"hash"`, or `"search"` to `m.route.mode`. In `v.1.x` it is replaced by `m.route.prefix = prefix` where `prefix` can any prefix. If it starts with `#`, it works in "hash" mode, `?` for "search" mode, and any other character (or the empty string) for "pathname" mode. It also supports combinations of the above like `m.route.prefix = "/path/#!"` or `?#`.
|
||||
|
||||
The default was changed to also use a `#!` (hashbang) prefix instead of just `#`. So if you were using the default behavior and want to retain your existing URLs, specify `m.route.prefix = "#"` before initializing the routes.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.route.mode = "hash"
|
||||
m.route.mode = "pathname"
|
||||
m.route.mode = "search"
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
// Direct equivalents
|
||||
m.route.prefix = "#"
|
||||
m.route.prefix = ""
|
||||
m.route.prefix = "?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.route()` and anchor tags
|
||||
|
||||
Handling routable links now uses a special built-in component instead of an attribute. If you were using this on `<button>`s and the like, you can specify that tag name using a `selector: "button"` attribute.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
// When clicked this link will load the "/path" route instead of navigating
|
||||
m("a", {
|
||||
href: "/path",
|
||||
config: m.route
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
// When clicked this link will load the "/path" route instead of navigating
|
||||
m(m.route.Link, {
|
||||
href: "/path",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Path templates
|
||||
|
||||
In v1.x, there were three separate path template syntaxes that, although they were similar, had 2 separately designed syntaxes and 3 different implementations. It was defined in a fairly ad-hoc way, and parameters weren't generally escaped. Now, everything is either encoded if it's `:key`, raw if it's `:key...`. If things are unexpectedly encoded, use `:path...`. It's that simple.
|
||||
|
||||
Concretely, here's how it affects each method:
|
||||
|
||||
### `m.request` URLs
|
||||
|
||||
Path components in v2.x are escaped automatically when interpolated and they read their values from `params`. In v0.2.x, `m.request({url: "/user/:name/photos/:id", data: {name: "a/b", id: "c/d"}})` would send its request with the URL set to `/user/a%2Fb/photos/c/d`. In v2.x, the corresponding `m.request({url: "/user/:name/photos/:id", params: {name: "a/b", id: "c/d"}})` would send its request to `/user/a%2Fb/photos/c%2Fd`. If you deliberately *want* to interpolate a key unescaped, use `:key...` instead.
|
||||
|
||||
Interpolations in inline query strings, like in `/api/search?q=:query`, are not performed in v2.x. Pass those via `params` with appropriate key names instead, without specifying it in the query string.
|
||||
|
||||
Do note that this applies to `m.jsonp` as well. When migrating from `m.request` + `dataType: "jsonp"` to `m.jsonp`, you also need to be aware of this.
|
||||
|
||||
### `m.route(route, params, shouldReplaceHistoryEntry)` paths
|
||||
|
||||
These permit interpolations now, and they work identically to that of `m.request`.
|
||||
|
||||
### `m.route` route patterns
|
||||
|
||||
Path keys of the form `:key...` return their URL decoded in v1.x, but return the raw URL in v2.x.
|
||||
|
||||
Previously, stuff like `:key.md` were erroneously accepted, with the resulting parameter's value set to `keymd: "..."`. This is no longer the case - the `.md` is part of the pattern now, not the name.
|
||||
|
||||
---
|
||||
|
||||
## Reading/writing the current route
|
||||
|
||||
In v0.2.x all interaction w/ the current route happened via `m.route()`. In v2.x this has been broken out into two functions.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
// Getting the current route
|
||||
m.route()
|
||||
|
||||
// Setting a new route
|
||||
m.route("/other/route")
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
// Getting the current route
|
||||
m.route.get()
|
||||
|
||||
// Setting a new route
|
||||
m.route.set("/other/route")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessing route params
|
||||
|
||||
In v0.2.x reading route params was entirely handled through `m.route.param()`. This API is still available in v2.x, and additionally any route params are passed as properties in the `attrs` object on the vnode.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/booga", {
|
||||
"/:attr": {
|
||||
controller: function() {
|
||||
m.route.param("attr") // "booga"
|
||||
},
|
||||
view: function() {
|
||||
m.route.param("attr") // "booga"
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/booga", {
|
||||
"/:attr": {
|
||||
oninit: function(vnode) {
|
||||
vnode.attrs.attr // "booga"
|
||||
m.route.param("attr") // "booga"
|
||||
},
|
||||
view: function(vnode) {
|
||||
vnode.attrs.attr // "booga"
|
||||
m.route.param("attr") // "booga"
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building/Parsing query strings
|
||||
|
||||
v0.2.x used methods hanging off of `m.route`, `m.route.buildQueryString()` and `m.route.parseQueryString()`. In v2.x these have been broken out and moved to the root `m`.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var qs = m.route.buildQueryString({ a: 1 });
|
||||
|
||||
var obj = m.route.parseQueryString("a=1");
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var qs = m.buildQueryString({ a: 1 });
|
||||
|
||||
var obj = m.parseQueryString("a=1");
|
||||
```
|
||||
|
||||
Also, in v2.x, `{key: undefined}` is serialized as `key=undefined` by `m.buildQueryString` and methods that use it like `m.request`. In v0.2.x, the key was omitted and this carried over to `m.request`. If you were previously relying on this, change your code to omit the keys from the object entirely. It may be worth using a simple utility to strip all keys from an object whose values are `undefined` if you can't easily do that and you need to retain v0.2.x behavior.
|
||||
|
||||
```javascript
|
||||
// Call whenever you need to omit `undefined` parameters from an object.
|
||||
function omitUndefineds(object) {
|
||||
var result = {}
|
||||
|
||||
for (var key in object) {
|
||||
if ({}.hasOwnProperty.call(object, key)) {
|
||||
var value = object[key]
|
||||
if (Array.isArray(value)) {
|
||||
result[key] = value.map(omitUndefineds)
|
||||
} else if (value != null && typeof value === "object") {
|
||||
result[key] = omitUndefineds(value)
|
||||
} else if (value !== undefined) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Preventing unmounting
|
||||
|
||||
It is no longer possible to prevent unmounting via `onunload`'s `e.preventDefault()`. Instead you should explicitly call `m.route.set` when the expected conditions are met.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
controller: function() {
|
||||
this.onunload = function(e) {
|
||||
if (condition) e.preventDefault()
|
||||
}
|
||||
},
|
||||
view: function() {
|
||||
return m("a[href=/]", {config: m.route})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
view: function() {
|
||||
return m("a", {
|
||||
onclick: function() { if (!condition) m.route.set("/") },
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Run code on component removal
|
||||
|
||||
Components no longer call `this.onunload` when they are being removed. They now use the standardized lifecycle hook `onremove`.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
controller: function() {
|
||||
this.onunload = function(e) {
|
||||
// ...
|
||||
}
|
||||
},
|
||||
view: function() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
onremove: function() {
|
||||
// ...
|
||||
}
|
||||
view: function() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.request`
|
||||
|
||||
Promises returned by [m.request](request.md) are no longer `m.prop` getter-setters. In addition, `initialValue`, `unwrapSuccess` and `unwrapError` are no longer supported options.
|
||||
|
||||
In addition, requests no longer have `m.startComputation`/`m.endComputation` semantics. Instead, redraws are always triggered when a request promise chain completes (unless `background: true` is set).
|
||||
|
||||
The `data` parameter has now been split into `params`, query parameters interpolated into the URL and appended to the request, and `body`, the body to send in the underlying XHR.
|
||||
|
||||
In v0.2.x, you would use a `dataType: "jsonp"` to initiate a JSONP request. In v2.x, you now use [`m.jsonp`](jsonp.md), which carries mostly the same API as `m.request` without the XHR-related parts.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var data = m.request({
|
||||
method: "GET",
|
||||
url: "https://api.github.com/",
|
||||
initialValue: [],
|
||||
})
|
||||
|
||||
setTimeout(function() {
|
||||
console.log(data())
|
||||
}, 1000)
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "https://api.github.com/",
|
||||
data: someJson,
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var data = []
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "https://api.github.com/",
|
||||
})
|
||||
.then(function (responseBody) {
|
||||
data = responseBody
|
||||
})
|
||||
|
||||
setTimeout(function() {
|
||||
console.log(data) // note: not a getter-setter
|
||||
}, 1000)
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "https://api.github.com/",
|
||||
body: someJson,
|
||||
})
|
||||
|
||||
// OR
|
||||
|
||||
var data = []
|
||||
m.request("https://api.github.com/")
|
||||
.then(function (responseBody) {
|
||||
data = responseBody
|
||||
})
|
||||
|
||||
setTimeout(function() {
|
||||
console.log(data) // note: not a getter-setter
|
||||
}, 1000)
|
||||
|
||||
m.request("https://api.github.com/", {
|
||||
method: "POST",
|
||||
body: someJson,
|
||||
})
|
||||
```
|
||||
|
||||
Additionally, if the `extract` option is passed to `m.request` the return value of the provided function will be used directly to resolve the request promise, and the `deserialize` callback is ignored.
|
||||
|
||||
---
|
||||
|
||||
## `m.request` headers
|
||||
|
||||
In v0.2.x, Mithril.js didn't set any headers on requests by default. Now, it sets up to 2 headers:
|
||||
|
||||
- `Content-Type: application/json; charset=utf-8` for requests with JSON bodies that are `!= null`
|
||||
- `Accept: application/json, text/*` for requests expecting JSON responses
|
||||
|
||||
The first of the two headers, `Content-Type`, will trigger a CORS prefetch as it [is not a CORS-safelisted request header](https://fetch.spec.whatwg.org/#cors-safelisted-request-header) due to the specified content type, and that could introduce new errors depending on how CORS is configured on your server. If you run into issues with this, you may need to override that header in question by passing `headers: {"Content-Type": "text/plain"}`. (The `Accept` header doesn't trigger anything, so you don't need to override that.)
|
||||
|
||||
*The only content types that the Fetch spec lets avoid CORS prefetch checks are `application/x-www-form-urlencoded`, `multipart/form-data`, and `text/plain`. It doesn't allow anything else, and it intentionally disallows JSON.*
|
||||
|
||||
---
|
||||
|
||||
## `m.deferred` removed
|
||||
|
||||
v0.2.x used its own custom asynchronous contract object, exposed as `m.deferred`, which was used as the basis for `m.request`. v2.x uses Promises instead, and implements a [polyfill](promise.md) in non-supporting environments. In situations where you would have used `m.deferred`, you should use Promises instead.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
var greetAsync = function() {
|
||||
var deferred = m.deferred()
|
||||
setTimeout(function() {
|
||||
deferred.resolve("hello")
|
||||
}, 1000)
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
greetAsync()
|
||||
.then(function(value) {return value + " world"})
|
||||
.then(function(value) {console.log(value)}) //logs "hello world" after 1 second
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var greetAsync = function() {
|
||||
return new Promise(function(resolve){
|
||||
setTimeout(function() {
|
||||
resolve("hello")
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
greetAsync()
|
||||
.then(function(value) {return value + " world"})
|
||||
.then(function(value) {console.log(value)}) //logs "hello world" after 1 second
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.sync` removed
|
||||
|
||||
Since v2.x uses standards-compliant Promises, `m.sync` is redundant. Use `Promise.all` instead.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m.sync([
|
||||
m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
|
||||
m.request({ method: 'GET', url: 'https://api.github.com/users/dead-claudia' }),
|
||||
])
|
||||
.then(function (users) {
|
||||
console.log("Contributors:", users[0].name, "and", users[1].name)
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
Promise.all([
|
||||
m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
|
||||
m.request({ method: 'GET', url: 'https://api.github.com/users/dead-claudia' }),
|
||||
])
|
||||
.then(function (users) {
|
||||
console.log("Contributors:", users[0].name, "and", users[1].name)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `xlink` namespace required
|
||||
|
||||
In v0.2.x, the `xlink` namespace was the only supported attribute namespace, and it was supported via special casing behavior. Now namespace parsing is fully supported, and namespaced attributes should explicitly declare their namespace.
|
||||
|
||||
### v0.2.x
|
||||
|
||||
```javascript
|
||||
m("svg",
|
||||
// the `href` attribute is namespaced automatically
|
||||
m("image[href='image.gif']")
|
||||
)
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m("svg",
|
||||
// User-specified namespace on the `href` attribute
|
||||
m("image[xlink:href='image.gif']")
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nested arrays in views
|
||||
|
||||
Arrays now represent [fragments](fragment.md), which are structurally significant in v2.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v2.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays.
|
||||
|
||||
---
|
||||
|
||||
## `vnode` equality checks
|
||||
|
||||
If a vnode is strictly equal to the vnode occupying its place in the last draw, v2.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views).
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
# Migrating from v1.x
|
||||
|
||||
v2.x is almost entirely API-compatible with v1.x, but there are some breaking changes.
|
||||
|
||||
- [Assigning to `vnode.state`](#assigning-to-vnodestate)
|
||||
- [Changes to route anchors](#changes-to-route-anchors)
|
||||
- [Changes to `m.request` errors](#changes-to-mrequest-errors)
|
||||
- [`m.withAttr` removed](#mwithattr-removed)
|
||||
- [`m.route.prefix`](#mrouteprefix)
|
||||
- [`m.request`/`m.jsonp` params and body](#mrequest-params-and-body)
|
||||
- [Path templates](#path-templates)
|
||||
- [Lifecycle call order](#lifecycle-call-order)
|
||||
- [`m.redraw` synchronicity](#mredraw)
|
||||
- [Selector attribute precedence](#selector-attribute-precedence)
|
||||
- [Children normalization](#children-normalization)
|
||||
- [Default `responseType` for `m.request`](#default-responsetype-for-mrequest)
|
||||
- [`m.request` headers](#mrequest-headers)
|
||||
- [Query parameters in hash strings in routes](#query-parameters-in-hash-strings-in-routes)
|
||||
- [Keys](#keys)
|
||||
- [`m.version` removed](#mversion-removed)
|
||||
|
||||
---
|
||||
|
||||
## Assigning to `vnode.state`
|
||||
|
||||
In v1.x, you could manipulate `vnode.state` and assign anything you wanted. In v2.x, an error will be thrown if it changes. Migration may vary, but most cases, it's as simple as changing references `vnode.state` to `vnode.state.foo`, picking an appropriate name for `foo` (like maybe `count` if it's a counter's current value).
|
||||
|
||||
### v1.x
|
||||
|
||||
```javascript
|
||||
var Counter = {
|
||||
oninit: function(vnode) { vnode.state = 0 },
|
||||
view: function(vnode) {
|
||||
return m(".counter", [
|
||||
m("button", {onclick: function() { vnode.state-- }}, "-"),
|
||||
vnode.state,
|
||||
m("button", {onclick: function() { vnode.state++ }}, "+")
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var Counter = {
|
||||
oninit: function(vnode) { vnode.state.count = 0 },
|
||||
view: function(vnode) {
|
||||
return m(".counter", [
|
||||
m("button", {onclick: function() { vnode.state.count-- }}, "-"),
|
||||
vnode.state.count,
|
||||
m("button", {onclick: function() { vnode.state.count++ }}, "+")
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*When v1.0 first released, class and closure components didn't exist, so it just pulled what it needed from `vnode.tag`. This implementation detail is what allowed you to do it, and some began to rely on it. It was also implied as possible in some places within the docs. Now, things are different, and this makes it a little easier to manage from an implementation standpoint as there's only one reference to state, not two.*
|
||||
|
||||
---
|
||||
|
||||
## Changes to route anchors
|
||||
|
||||
In v1.x, you previously used `oncreate: m.route.link` and, if the link could change, `onupdate: m.route.link` as well, each as lifecycle hooks on the vnode that could be routed with. In v2.x, you now use an [`m.route.Link` component](route.md#mroutelink). The selector can be specified via a `selector:` attribute in case you were using anything other than `m("a", ...)`, options can be specified via `options:`, you can disable it via `disabled:`, and other attributes can be specified inline including `href:` (required). The `selector:` itself can contain be any selector valid as the first argument for `m`, and the attributes `[href=...]` and `[disabled]` can be specified in the selector as well as the normal options.
|
||||
|
||||
### v1.x
|
||||
|
||||
```javascript
|
||||
m("a", {
|
||||
href: "/path",
|
||||
oncreate: m.route.link,
|
||||
})
|
||||
|
||||
m("button", {
|
||||
href: "/path",
|
||||
oncreate: m.route.link,
|
||||
})
|
||||
|
||||
m("button.btn[href=/path]", {
|
||||
oncreate: m.route.link,
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m(m.route.Link, {
|
||||
href: "/path",
|
||||
})
|
||||
|
||||
m(m.route.Link, {
|
||||
selector: "button",
|
||||
href: "/path",
|
||||
})
|
||||
|
||||
m(m.route.Link, {
|
||||
selector: "button.btn[href=/path]",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Changes to `m.request` errors
|
||||
|
||||
In v1.x, `m.request` parsed errors from JSON calls and assigned the resulting parsed object's properties to the response. So, if you received a response with status 403 and a body of `{"code": "backoff", "timeout": 1000}`, the error would have two extra properties: `err.code = "backoff"` and `err.timeout = 1000`.
|
||||
|
||||
In v2.x, the response is assigned to a `response` property on the result instead, and a `code` property contains the resulting status code. So if you received a response with status 403 and a body of `{"code": "backoff", "timeout": 1000}`, the error would have assigned to it two properties: `err.response = {code: "backoff", timeout: 1000}` and `err.code = 403`.
|
||||
|
||||
---
|
||||
|
||||
## `m.withAttr` removed
|
||||
|
||||
In v1.x, event listeners could use `oninput: m.withAttr("value", func)` and similar. In v2.x, just read them directly from the event's target. It synergized well with streams, but since the idiom of `m.withAttr("value", stream)` was not *nearly* as common as `m.withAttr("value", prop)`, `m.withAttr` lost most of its usefulness and so it was removed.
|
||||
|
||||
### v1.x
|
||||
|
||||
```javascript
|
||||
var value = ""
|
||||
|
||||
// In your view
|
||||
m("input[type=text]", {
|
||||
value: value(),
|
||||
oninput: m.withAttr("value", function(v) { value = v }),
|
||||
})
|
||||
|
||||
// OR
|
||||
|
||||
var value = m.stream("")
|
||||
|
||||
// In your view
|
||||
m("input[type=text]", {
|
||||
value: value(),
|
||||
oninput: m.withAttr("value", value),
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var value = ""
|
||||
|
||||
// In your view
|
||||
m("input[type=text]", {
|
||||
value: value,
|
||||
oninput: function (ev) { value = ev.target.value },
|
||||
})
|
||||
|
||||
// OR
|
||||
|
||||
var value = m.stream("")
|
||||
|
||||
// In your view
|
||||
m("input[type=text]", {
|
||||
value: value(),
|
||||
oninput: function (ev) { value(ev.target.value) },
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.route.prefix`
|
||||
|
||||
In v1.x, `m.route.prefix` was a function called via `m.route.prefix(prefix)`. It's now a property you set to via `m.route.prefix = prefix`
|
||||
|
||||
### v1.x
|
||||
|
||||
```javascript
|
||||
m.route.prefix("/root")
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m.route.prefix = "/root"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.request`/`m.jsonp` params and body
|
||||
|
||||
The `data` and `useBody` were refactored into `params`, query parameters interpolated into the URL and appended to the request, and `body`, the body to send in the underlying XHR. This gives you much better control over the actual request sent and allows you to both interpolate into query parameters with `POST` requests and create `GET` requests with bodies.
|
||||
|
||||
`m.jsonp`, having no meaningful "body", just uses `params`, so renaming `data` to `params` is sufficient for that method.
|
||||
|
||||
### v1.x
|
||||
|
||||
```javascript
|
||||
m.request("https://example.com/api/user/:id", {
|
||||
method: "GET",
|
||||
data: {id: user.id}
|
||||
})
|
||||
|
||||
m.request("https://example.com/api/user/create", {
|
||||
method: "POST",
|
||||
data: userData
|
||||
})
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
m.request("https://example.com/api/user/:id", {
|
||||
method: "GET",
|
||||
params: {id: user.id}
|
||||
})
|
||||
|
||||
m.request("https://example.com/api/user/create", {
|
||||
method: "POST",
|
||||
body: userData
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Path templates
|
||||
|
||||
In v1.x, there were three separate path template syntaxes that, although they were similar, had 2 separately designed syntaxes and 3 different implementations. It was defined in a fairly ad-hoc way, and parameters weren't generally escaped. Now, everything is either encoded if it's `:key`, raw if it's `:key...`. If things are unexpectedly encoded, use `:path...`. It's that simple.
|
||||
|
||||
Concretely, here's how it affects each method:
|
||||
|
||||
### `m.request` and `m.jsonp` URLs, `m.route.set` paths
|
||||
|
||||
Path components in v2.x are escaped automatically when interpolated. Suppose you invoke `m.route.set("/user/:name/photos/:id", {name: user.name, id: user.id})`. Previously, if `user` was `{name: "a/b", id: "c/d"}`, this would set the route to `/user/a%2Fb/photos/c/d`, but it will now set it to `/user/a%2Fb/photos/c%2Fd`. If you deliberately *want* to interpolate a key unescaped, use `:key...` instead.
|
||||
|
||||
Keys in v2.x cannot contain any instances of `.` or `-`. In v1.x, they could contain anything other than `/`.
|
||||
|
||||
Interpolations in inline query strings, like in `/api/search?q=:query`, are not performed in v2.x. Pass those via `params` with appropriate key names instead, without specifying it in the query string.
|
||||
|
||||
### `m.route` route patterns
|
||||
|
||||
Path keys of the form `:key...` return their URL decoded in v1.x, but return the raw URL in v2.x.
|
||||
|
||||
Previously, stuff like `:key.md` were erroneously accepted, with the resulting parameter's value set to `keymd: "..."`. This is no longer the case - the `.md` is part of the pattern now, not the name.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle call order
|
||||
|
||||
In v1.x, attribute lifecycle hooks on component vnodes were called *before* the component's own lifecycle hooks in all cases. In v2.x, this is the case only for `onbeforeupdate`. So you may need to adjust your code accordingly.
|
||||
|
||||
### v1.x
|
||||
|
||||
```javascript
|
||||
var Comp = {
|
||||
oncreate: function() {
|
||||
console.log("Component oncreate")
|
||||
},
|
||||
view: function() {
|
||||
return m("div")
|
||||
},
|
||||
}
|
||||
|
||||
m.mount(document.body, {
|
||||
view: function() {
|
||||
return m(Comp, {
|
||||
oncreate: function() {
|
||||
console.log("Attrs oncreate")
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Logs:
|
||||
// Attrs oncreate
|
||||
// Component oncreate
|
||||
```
|
||||
|
||||
### v2.x
|
||||
|
||||
```javascript
|
||||
var Comp = {
|
||||
oncreate: function() {
|
||||
console.log("Component oncreate")
|
||||
},
|
||||
view: function() {
|
||||
return m("div")
|
||||
},
|
||||
}
|
||||
|
||||
m.mount(document.body, {
|
||||
view: function() {
|
||||
return m(Comp, {
|
||||
oncreate: function() {
|
||||
console.log("Attrs oncreate")
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Logs:
|
||||
// Component oncreate
|
||||
// Attrs oncreate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `m.redraw` synchronicity
|
||||
|
||||
`m.redraw()` in v2.x is always async. You can specifically request a synchronous redraw via `m.redraw.sync()` provided no redraw is currently occurring.
|
||||
|
||||
---
|
||||
|
||||
## Selector attribute precedence
|
||||
|
||||
In v1.x, selector attributes took precedence over attributes specified in the attributes object. For instance, `m("[a=b]", {a: "c"}).attrs` returned `{a: "b"}`.
|
||||
|
||||
In v2.x, attributes specified in the attributes object take precedence over selector attributes. For instance, `m("[a=b]", {a: "c"}).attrs` returns `{a: "c"}`.
|
||||
|
||||
Note that this is technically reverting to v0.2.x behavior.
|
||||
|
||||
---
|
||||
|
||||
## Children normalization
|
||||
|
||||
In v1.x, component vnode children were normalized like other vnodes. In v2.x, this is no longer the case and you will need to plan accordingly. This does not affect the normalization done on render.
|
||||
|
||||
---
|
||||
|
||||
## `m.request` headers
|
||||
|
||||
In v1.x, Mithril.js set these two headers on all non-`GET` requests, but only when `useBody` was set to `true` (the default) and the other conditions listed hold:
|
||||
|
||||
- `Content-Type: application/json; charset=utf-8` for requests with JSON bodies
|
||||
- `Accept: application/json, text/*` for requests expecting JSON responses
|
||||
|
||||
In v2.x, Mithril.js sets the first for all requests with JSON bodies that are `!= null` and omits it by default otherwise, and this is done independent of which method is chosen, including on `GET` requests.
|
||||
|
||||
The first of the two headers, `Content-Type`, will trigger a CORS prefetch as it [is not a CORS-safelisted request header](https://fetch.spec.whatwg.org/#cors-safelisted-request-header) due to the specified content type, and that could introduce new errors depending on how CORS is configured on your server. If you run into issues with this, you may need to override that header in question by passing `headers: {"Content-Type": "text/plain"}`. (The `Accept` header doesn't trigger anything, so you don't need to override that.)
|
||||
|
||||
*The only content types that the Fetch spec lets avoid CORS prefetch checks are `application/x-www-form-urlencoded`, `multipart/form-data`, and `text/plain`. It doesn't allow anything else, and it intentionally disallows JSON.*
|
||||
|
||||
---
|
||||
|
||||
## Query parameters in hash strings in routes
|
||||
|
||||
In v1.x, you could specify query parameters for routes in both the query string and hash string, so `m.route.set("/route?foo=1&bar=2")`, `m.route.set("/route?foo=1#bar=2")`, and `m.route.set("/route#foo=1&bar=2")` were all equivalent and the attributes extracted from them would have been `{foo: "1", bar: "2"}`.
|
||||
|
||||
In v2.x, the contents of hash strings are ignored but preserved. So the attributes extracted from each would be this:
|
||||
|
||||
- `m.route.set("/route?foo=1&bar=2")` → `{foo: "1", bar: "2"}`
|
||||
- `m.route.set("/route?foo=1#bar=2")` → `{foo: "1"}`
|
||||
- `m.route.set("/route#foo=1&bar=2")` → `{}`
|
||||
|
||||
The reason for doing this is because URLs like `https://example.com/#!/route#key` are technically invalid per the [URL spec](https://url.spec.whatwg.org/#url-code-points) and were even invalid per the [RFC that preceded it](https://tools.ietf.org/html/rfc3986#appendix-A), and it's only a quirk of the HTML spec that they're allowed. (The HTML spec should've required IDs and location fragments to be valid URL fragments from the start instead if it wanted to follow spec.)
|
||||
|
||||
Or in short, stop using invalid URLs!
|
||||
|
||||
---
|
||||
|
||||
## Keys
|
||||
|
||||
In v1.x, you could mix keyed and unkeyed vnodes freely. If the first node is keyed, a keyed diff is performed, assuming every element has a key and just ignoring holes as it goes. Otherwise, an iterative diff is performed, and if a node has a key, it would be checked that it didn't change at the same time tags and similar are checked.
|
||||
|
||||
In v2.x, children lists of both fragments and elements must be either all keyed or all unkeyed. Holes are considered unkeyed for the purposes of this check, too - it no longer ignores them.
|
||||
|
||||
If you need to work around it, use the idiom of a fragment containing a single vnode, like `[m("div", {key: whatever})]`.
|
||||
|
||||
---
|
||||
|
||||
## `m.version` removed
|
||||
|
||||
It served little use in general, and you can always add it back yourself. You should prefer feature detection for knowing what features are available, and the v2.x API is designed to better enable this.
|
||||
116
docs/mount.md
|
|
@ -1,116 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.mount(), which binds a Mithril.js component to a given DOM node
|
||||
-->
|
||||
|
||||
# mount(root, component)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Headless mounts](#headless-mounts)
|
||||
- [Performance considerations](#performance-considerations)
|
||||
- [Differences from m.render](#differences-from-mrender)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Activates a component, enabling it to autoredraw on user events
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
count: 0,
|
||||
inc: function() {state.count++}
|
||||
}
|
||||
|
||||
var Counter = {
|
||||
view: function() {
|
||||
return m("div", {onclick: state.inc}, state.count)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Counter)
|
||||
```
|
||||
|
||||
To pass arguments when mounting a component use:
|
||||
```javascript
|
||||
m.mount(element, {view: function () {return m(Component, attrs)}})
|
||||
```
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`m.mount(element, Component)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`element` | `Element` | Yes | A DOM element that will be the parent node to the subtree
|
||||
`Component` | `Component|null` | Yes | The [component](components.md) to be rendered. `null` unmounts the tree and cleans up internal state.
|
||||
**returns** | | | Returns nothing
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
`m.mount(element, Component)`, when called renders the component into the element and subscribe the `(element, Component)` pair to the redraw subsystem. That tree will be re-rendered when [manual](redraw.md) or [automatic](autoredraw.md) redraws are triggered.
|
||||
|
||||
On redraw, the new vDOM tree is compared (or "diffed") with the old one, and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all.
|
||||
|
||||
#### Replace a component
|
||||
|
||||
Running `mount(element, OtherComponent)` where `element` is a current mount point replaces the component previously mounted with `OtherComponent`.
|
||||
|
||||
#### Unmount
|
||||
|
||||
Using `m.mount(element, null)` on an element with a previously mounted component unmounts it and cleans up Mithril.js internal state. This can be useful to prevent memory leaks when removing the `root` node manually from the DOM.
|
||||
|
||||
#### Headless mounts
|
||||
|
||||
In certain more advanced situations, you may want to subscribe and listen for [redraws](autoredraw.md) without rendering anything to the screen. This can be done using a headless mount, created by simply invoking `m.mount` with an element that's not added to the live DOM tree and putting all your useful logic in the component you're mounting with. You still need a `view` in your component, just it doesn't have to return anything useful and it can just return a junk value like `null` or `undefined`.
|
||||
|
||||
```javascript
|
||||
var elem = document.createElement("div")
|
||||
|
||||
// Subscribe
|
||||
m.mount(elem, {
|
||||
oncreate: function() {
|
||||
// once added
|
||||
},
|
||||
onupdate: function() {
|
||||
// on each redraw
|
||||
},
|
||||
onremove: function() {
|
||||
// clean up whatever you need
|
||||
},
|
||||
|
||||
// Necessary boilerplate
|
||||
view: function () {},
|
||||
})
|
||||
|
||||
// Unsubscribe
|
||||
m.mount(elem, null)
|
||||
```
|
||||
|
||||
There's no need to worry about other mount roots. Multiple roots are supported and they won't step on each other. You can even do the above in a component when integrating with another framework, and it won't be a problem.
|
||||
|
||||
---
|
||||
|
||||
### Performance considerations
|
||||
|
||||
It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing JavaScript data structures is surprisingly cheap compared to reading and modifying the DOM.
|
||||
|
||||
Touching the DOM can be extremely expensive for a couple of reasons. Alternating reads and writes can adversely affect performance by causing several browser repaints to occur in quick succession, whereas comparing virtual dom trees allows writes to be batched into a single repaint. Also, the performance characteristics of various DOM operations vary between implementations and can be difficult to learn and optimize for all browsers. For example, in some implementations, reading `childNodes.length` has a complexity of O(n); in some, reading `parentNode` causes a repaint, etc.
|
||||
|
||||
In contrast, traversing a JavaScript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern JavaScript engines to apply aggressive optimizations such as hidden classes for even better performance.
|
||||
|
||||
---
|
||||
|
||||
### Differences from m.render
|
||||
|
||||
A component rendered via `m.mount` [automatically redraws](autoredraw.md) in response to view events, `m.redraw()` calls or `m.request()` calls. Vnodes rendered via `m.render()` do not.
|
||||
|
||||
`m.mount()` is suitable for application developers integrating Mithril.js widgets into existing codebases where routing is handled by another library or framework, while still enjoying Mithril.js' auto-redrawing facilities.
|
||||
|
||||
`m.render()` is suitable for library authors who wish to manually control rendering (e.g. when hooking to a third party router, or using third party data-layer libraries like Redux).
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
- Getting Started
|
||||
- [Introduction](index.md)
|
||||
- [Installation](installation.md)
|
||||
- [Tutorial](simple-application.md)
|
||||
- [Learning Resources](learning-mithril.md)
|
||||
- [Getting Help](support.md)
|
||||
- Resources
|
||||
- [JSX](jsx.md)
|
||||
- [ES6+ on legacy browsers](es6.md)
|
||||
- [Animation](animation.md)
|
||||
- [Testing](testing.md)
|
||||
- [Examples](examples.md)
|
||||
- [3rd Party Integration](integrating-libs.md)
|
||||
- [Path Handling](paths.md)
|
||||
- Key concepts
|
||||
- [Vnodes](vnodes.md)
|
||||
- [Components](components.md)
|
||||
- [Lifecycle methods](lifecycle-methods.md)
|
||||
- [Keys](keys.md)
|
||||
- [Autoredraw system](autoredraw.md)
|
||||
- Social
|
||||
- [Mithril.js Jobs](https://github.com/MithrilJS/mithril.js/wiki/JOBS)
|
||||
- [How to contribute](contributing.md)
|
||||
- [Credits](credits.md)
|
||||
- [Code of Conduct](code-of-conduct.md)
|
||||
- Misc
|
||||
- [Framework comparison](framework-comparison.md)
|
||||
- [Change log/Migration](changelog.md)
|
||||
- [v1 Documentation](https://mithril.js.org/archive/v1.1.7/)
|
||||
- [v0.2 Documentation](https://mithril.js.org/archive/v0.2.5/)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
- Core
|
||||
- [m](hyperscript.md)
|
||||
- [m.render](render.md)
|
||||
- [m.mount](mount.md)
|
||||
- [m.route](route.md)
|
||||
- [m.request](request.md)
|
||||
- [m.parseQueryString](parseQueryString.md)
|
||||
- [m.buildQueryString](buildQueryString.md)
|
||||
- [m.buildPathname](buildPathname.md)
|
||||
- [m.parsePathname](parsePathname.md)
|
||||
- [m.trust](trust.md)
|
||||
- [m.fragment](fragment.md)
|
||||
- [m.redraw](redraw.md)
|
||||
- Optional
|
||||
- [Stream](stream.md)
|
||||
- Tooling
|
||||
- [Ospec](https://github.com/MithrilJS/ospec)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.parsePathname(), which parses URLs to path and query object
|
||||
-->
|
||||
# parsePathname(string)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Turns a string of the form `/path/user?a=1&b=2` to an object
|
||||
|
||||
```javascript
|
||||
var object = m.parsePathname("/path/user?a=1&b=2")
|
||||
// {path: "/path/user", params: {a: "1", b: "2"}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`object = m.parsePathname(string)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------- | -------- | ---
|
||||
`url` | `String` | Yes | A URL
|
||||
**returns** | `Object` | | A `{path, params}` pair where `path` is the [normalized path](paths.md#path-normalization) and `params` is the [parsed parameters](paths.md#parameter-normalization).
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.parsePathname` method creates an object from a path with a possible query string. It is useful for parsing a local path name into its parts, and it's what [`m.route`](route.md) uses internally to normalize paths to later match them. It uses [`m.parseQueryString`](parseQueryString.md) to parse the query parameters into an object.
|
||||
|
||||
```javascript
|
||||
var data = m.parsePathname("/path/user?a=hello&b=world")
|
||||
|
||||
// data.path is "/path/user"
|
||||
// data.params is {a: "hello", b: "world"}
|
||||
```
|
||||
|
||||
### General-purpose URL parsing
|
||||
|
||||
The method is called `parsePathname` because it applies to pathnames. If you want a general-purpose URL parser, you should use [the global `URL` class](https://developer.mozilla.org/en-US/docs/Web/API/URL) instead.
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.parseQueryString(), which converts a string like "a=1&b=2" into an object like {a: "1", b: "2"}
|
||||
-->
|
||||
|
||||
# parseQueryString(string)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Turns a string of the form `?a=1&b=2` to an object
|
||||
|
||||
```javascript
|
||||
var object = m.parseQueryString("a=1&b=2")
|
||||
// {a: "1", b: "2"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`object = m.parseQueryString(string)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------------ | -------- | ---
|
||||
`string` | `String` | Yes | A querystring
|
||||
**returns** | `Object` | | A key-value map
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.parseQueryString` method creates an object from a querystring. It is useful for handling data from URL
|
||||
|
||||
```javascript
|
||||
var data = m.parseQueryString("a=hello&b=world")
|
||||
|
||||
// data is {a: "hello", b: "world"}
|
||||
```
|
||||
|
||||
#### Boolean type casting
|
||||
|
||||
This method attempts to cast boolean values if possible. This helps prevents bugs related to loose truthiness and unintended type casts.
|
||||
|
||||
```javascript
|
||||
var data = m.parseQueryString("a=true&b=false")
|
||||
|
||||
// data is {a: true, b: false}
|
||||
```
|
||||
|
||||
#### Leading question mark tolerance
|
||||
|
||||
For convenience, the `m.parseQueryString` method ignores a leading question mark, if present:
|
||||
|
||||
```javascript
|
||||
var data = m.parseQueryString("?a=hello&b=world")
|
||||
|
||||
// data is {a: "hello", b: "world"}
|
||||
```
|
||||
|
||||
#### Deep data structures
|
||||
|
||||
Querystrings that contain bracket notation are correctly parsed into deep data structures
|
||||
|
||||
```javascript
|
||||
m.parseQueryString("a[0]=hello&a[1]=world")
|
||||
|
||||
// data is {a: ["hello", "world"]}
|
||||
```
|
||||
148
docs/paths.md
|
|
@ -1,148 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on how to work with paths in Mithril.js
|
||||
-->
|
||||
# Path Handling
|
||||
|
||||
- [Path types](#path-types)
|
||||
- [Path parameters](#path-parameters)
|
||||
- [Parameter normalization](#parameter-normalization)
|
||||
- [Path normalization](#path-normalization)
|
||||
- [Path escaping](#path-escaping)
|
||||
|
||||
-----
|
||||
|
||||
[`m.route`](route.md) and [`m.request`](request.md) each have a concept called a path. This is used to generate the URL you route to or fetch from.
|
||||
|
||||
### Path types
|
||||
|
||||
There are two general types of paths: raw paths and parameterized paths.
|
||||
|
||||
- Raw paths are simply strings used directly as URLs. Nothing is substituted or even split. It's just normalized with all the parameters appended to the end.
|
||||
- Parameterized paths let you insert values into paths, escaped by default for convenience and safety against URL injection.
|
||||
|
||||
For [`m.request`](request.md) these can be pretty much any URL, but for [routes](route.md), these can only be absolute URL path names without schemes or domains.
|
||||
|
||||
### Path parameters
|
||||
|
||||
Path parameters are themselves pretty simple. They come in two forms:
|
||||
|
||||
- `:foo` - This injects a simple `params.foo` into the URL, escaping its value first.
|
||||
- `:foo...` - This injects a raw `params.foo` path into the URL without escaping anything.
|
||||
|
||||
You're probably wondering what that `params` object is supposed to be. It's pretty simple: it's the `params` in either [`m.route.set(path, params)`](route.md#mrouteset), [`m.request({url, params})`](request.md#signature).
|
||||
|
||||
When receiving routes via [`m.route(root, defaultRoute, routes)`](route.md#signature), you can use these parameters to *extract* values from routes. They work basically the same way as generating the paths, just in the opposite direction.
|
||||
|
||||
```javascript
|
||||
// Edit a single item
|
||||
m.route(document.body, "/edit/1", {
|
||||
"/edit/:id": {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Editing user " + m.route.param("id"))
|
||||
]
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Edit an item identified by path
|
||||
m.route(document.body, "/edit/pictures/image.jpg", {
|
||||
"/edit/:file...": {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Editing file " + m.route.param("file"))
|
||||
]
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In the first example, assuming you're navigating to the default route in each, `m.route.param("id")` would be read as `"1"` and `m.route.param("file")` would be read as `pictures/image.jpg`.
|
||||
|
||||
Path parameters may be delimited by either a `/`, `-`, or `.`. This lets you have dynamic path segments, and they're considerably more flexible than just a path name. For example, you could match against routes like `"/edit/:name.:ext"` for editing based on file extension or `"/:lang-:region/view"` for a localized route.
|
||||
|
||||
Path parameters are greedy: given a declared route `"/edit/:name.:ext"`, if you navigate to `/edit/file.test.png`, the parameters extracted will be `{name: "file.test", ext: "png"}`, not `{name: "file", ext: "test.png"}`. Similarly, given `"/route/:path.../view/:child..."`, if you go to `/route/foo/view/bar/view/baz`, the parameters extracted will be `{path: "foo/view/bar", child: "baz"}`.
|
||||
|
||||
### Parameter normalization
|
||||
|
||||
Path parameters that are interpolated into path names are omitted from the query string, for convenience and to keep the path name reasonably readable. For example, this sends a server request of `GET /api/user/1/connections?sort=name-asc`, omitting the duplicate `id=1` in the URL string.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
url: "https://example.com/api/user/:userID/connections",
|
||||
params: {
|
||||
userID: 1,
|
||||
sort: "name-asc"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
You can also specify parameters explicitly in the query string itself, such as in this, which is equivalent to the above:
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
url: "https://example.com/api/user/:userID/connections?sort=name-asc",
|
||||
params: {
|
||||
userID: 1
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
And of course, you can mix and match. This fires a request to `GET /api/user/1/connections?sort=name-asc&first=10`.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
url: "https://example.com/api/user/:userID/connections?sort=name-asc",
|
||||
params: {
|
||||
userID: 1,
|
||||
first: 10
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This even extends to route matching: you can match against a route *with* explicit query strings. It retains the matched parameter for convenience, so you can still access them via vnode parameters or via [`m.route.param`](route.md#mrouteparam). Note that although this *is* possible, it's not generally recommended, since you should prefer paths for pages. It could sometimes useful if you need to generate a somewhat different view just for a particular file type, but it still logically is a query-like parameter, not a whole separate page.
|
||||
|
||||
```javascript
|
||||
// Note: this is generally *not* recommended - you should prefer paths for route
|
||||
// declarations, not query strings.
|
||||
m.route(document.body, "/edit/1", {
|
||||
"/edit?type=image": {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Editing photo")
|
||||
]
|
||||
}
|
||||
},
|
||||
"/edit": {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Editing " + m.route.param("type"))
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Query parameters are implicitly consumed - you don't need to name them to accept them. You can match based on an existing value, like in `"/edit?type=image"`, but you don't need to use `"/edit?type=:type"` to accept the value. In fact, Mithril.js would treat that as you trying to literally match against `m.route.param("type") === ":type"`, so you probably don't want to do that. In short, use `m.route.param("key")` or route component attributes to read query parameters.
|
||||
|
||||
### Path normalization
|
||||
|
||||
Parsed paths are always returned with all the duplicate parameters and extra slashes dropped, and they always start with a slash. These little differences often get in the way, and it makes routing and path handling a lot more complicated than it should be. Mithril.js internally normalizes paths for routing, but it does not expose the current, normalized route directly. (You could compute it via [`m.parsePathname(m.route.get()).path`](parsePathname.md).)
|
||||
|
||||
When parameters are deduplicated during matching, parameters in the query string are preferred over parameters in the path name, and parameters towards the end of the URL are preferred over parameters closer to the start of the URL.
|
||||
|
||||
### Path escaping
|
||||
|
||||
There are some characters that, if you want to use them literally, you need to escape. Conveniently, `encodeURIComponent` encodes these (and more), and when you substitute parameters and add query parameters, they're encoded as necessary using this. Here's the ones Mithril.js interprets:
|
||||
|
||||
- `:` = `%3A`
|
||||
- `/` = `%2F` (required only in paths)
|
||||
- `%` = `%25`
|
||||
- `?` = `%3F` (required only in paths)
|
||||
- `#` = `%23`
|
||||
|
||||
Of course, there's others you have to escape per the URL spec, like spaces. But as already noted, `encodeURIComponent` does that for you, and Mithril.js uses that implicitly when you substitute parameters. So you only really need to care if you're specifying parameters explicitly like in `m.request("https://example.com/api/user/User%20Name/:field", {params: {field: ...}})`.
|
||||
|
|
@ -1,4 +1,115 @@
|
|||
|
||||
# Release v2.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
#### [Bump the normal group across 1 directory with 2 updates (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2976)
|
||||
|
||||
Bumps the normal group with 2 updates in the / directory: [chokidar](https://github.com/paulmillr/chokidar) and [eslint](https://github.com/eslint/eslint).
|
||||
#### [Cleaning up code by making vnode.attrs always non-null (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2977)
|
||||
|
||||
Commit f9e5163 made vnode.attrs always non-null, so there is no need for code to make vnode.attrs null or assume vnode.attrs is null.
|
||||
|
||||
# Release v2.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
#### [Bump gh-pages from 2.1.1 to 5.0.0 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2890)
|
||||
|
||||
Bumps [gh-pages](https://github.com/tschaub/gh-pages) from 2.1.1 to 5.0.0. Release notes. Sourced from gh-pages's releases. v5.0.0.
|
||||
#### [Bump @babel/parser from 7.7.5 to 7.25.6 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2959)
|
||||
|
||||
Bumps [@babel/parser](https://github.com/babel/babel/tree/HEAD/packages/babel-parser) from 7.7.5 to 7.25.6. Release notes. Sourced from @babel/parser's releases.
|
||||
#### [Bump minimatch from 3.0.4 to 3.1.2 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2816)
|
||||
|
||||
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. Commits. 699c459 3.1.2. 2f2b5ff fix: trim pattern. 25d7c0d 3.1.1.
|
||||
#### [Bump yaml and lint-staged (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2830)
|
||||
|
||||
Bumps [yaml](https://github.com/eemeli/yaml) to 2.2.2 and updates ancestor dependency [lint-staged](https://github.com/okonet/lint-staged).
|
||||
#### [Bump gh-pages from 5.0.0 to 6.1.1 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2958)
|
||||
|
||||
Bumps [gh-pages](https://github.com/tschaub/gh-pages) from 5.0.0 to 6.1.1. Release notes. Sourced from gh-pages's releases. v6.1.1. Fixes.
|
||||
#### [Bump glob from 7.1.4 to 11.0.0 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2957)
|
||||
|
||||
Bumps [glob](https://github.com/isaacs/node-glob) from 7.1.4 to 11.0.0. Changelog. Sourced from glob's changelog. changeglob. 11.0. Drop support for node before v20.
|
||||
#### [Bump rimraf from 3.0.2 to 6.0.1 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2960)
|
||||
|
||||
Bumps [rimraf](https://github.com/isaacs/rimraf) from 3.0.2 to 6.0.1. Changelog. Sourced from rimraf's changelog. 6.0. Drop support for nodes before v20.
|
||||
#### [Bump lint-staged from 13.2.1 to 15.2.10 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2961)
|
||||
|
||||
[//]: # (dependabot-start). ⚠️ **Dependabot is rebasing this PR** ⚠️. Rebasing might not happen immediately, so don't worry if this takes some time.
|
||||
#### [Revise issue templates (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2955)
|
||||
|
||||
|
||||
#### [Update ospec and a few other dependencies (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2907)
|
||||
|
||||
1. Update ospec to the version I just published. 2.
|
||||
#### [Fix some outstanding bugs in the docs linter. (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2906)
|
||||
|
||||
Missed an edge case in the task queue, and I also wanted to fully dedupe network requests. Locally it passes.
|
||||
#### [Rewrite docs linter, ease JSFiddle request debugging (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2904)
|
||||
|
||||
I'll defer to the commit descriptions. They're self-descriptive. The first diff is quite large.
|
||||
#### [Update vnodes.md (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2903)
|
||||
|
||||
Fix a broken link. Did some further digging (it's been a while since I've played with the scripts) and found that the JSFiddle errors are just warnings.
|
||||
#### [Migrate to Node 20, clean up workflows (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2902)
|
||||
|
||||
**Note: ignore the commits. It's a mess. Just read the combined diff - the PR itself is the standalone unit. I plan to squash this as I merge anyways.**.
|
||||
#### [Remove dependance on global window and document (@KoryNunn)](https://github.com/MithrilJS/mithril.js/pull/2897)
|
||||
|
||||
Use window and document from render target instead of using globals. This makes unit and intergration testing much easier.
|
||||
#### [Bump braces from 3.0.2 to 3.0.3 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2896)
|
||||
|
||||
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. Commits. 74b2db2 3.0.3. 88f1429 update eslint. lint, fix unit tests.
|
||||
#### [Tweak docs with warning to fix #2508 (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2895)
|
||||
|
||||
[z] Documentation change. [z] My change requires a change to the documentation. [z] I have updated the documentation accordingly.
|
||||
#### [Bump qs from 6.5.2 to 6.5.3 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2807)
|
||||
|
||||
Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3. Changelog. Sourced from qs's changelog. 6.5.3. [Fix] parse: ignore __proto__ keys (#428).
|
||||
#### [Temporarily host REM on fly to fix the docs (@JAForbes)](https://github.com/MithrilJS/mithril.js/pull/2893)
|
||||
|
||||
Fixes REM examples in the docs. The documentation currently has a dead link as REM is no longer hosted on heroku.
|
||||
#### [Move from individual code owners to just pinging all collaborators (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2844)
|
||||
|
||||
Most collaborators have commit access, and it'd make it a little easier (and more likely) for pull requests to get reviewed.
|
||||
#### [docs: absolute url in version selector to avoid 404 errors (2 of 2) (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2839)
|
||||
|
||||
Fixes #2832 (2 of 2 pull requests). See my comment at https://github.com/MithrilJS/mithril.js/pull/2835#issuecomment-1535657892.
|
||||
#### [fix markdown editor example, bump marked.js version up (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2848)
|
||||
|
||||
Fixes the strange behavior of markdown editor example. Using newest version of marked.js, fixed strange behavior of markdown editor example. See #2845.
|
||||
#### [Bump word-wrap from 1.2.3 to 1.2.4 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2856)
|
||||
|
||||
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. Release notes. Sourced from word-wrap's releases. 1.2.4. What's Changed.
|
||||
#### [Add missing `m.censor` to API navigation (@dead-claudia)](https://github.com/MithrilJS/mithril.js/pull/2843)
|
||||
|
||||
Not sure how I forgot about this when I added the method.
|
||||
#### [docs: fix regex for parsing page title (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2837)
|
||||
|
||||
Fixes https://github.com/MithrilJS/mithril.js/issues/2833. I tested the generated documentation on my dev machine successfully.
|
||||
#### [docs: fix broken anchor link on github/npm (@tbreuss)](https://github.com/MithrilJS/mithril.js/pull/2838)
|
||||
|
||||
Fixed a not working anchor link on github and npm by removing the question mark.
|
||||
#### [hyperscript: handles shared empty attrs, fixes #2821 (@kfule)](https://github.com/MithrilJS/mithril.js/pull/2822)
|
||||
|
||||
Whenever there are selector-derived attrs, the attrs object will be regenerated and not shared.
|
||||
#### [Fix typos in `stream()` docs (@mtsknn)](https://github.com/MithrilJS/mithril.js/pull/2825)
|
||||
|
||||
Noticed these typos while reading through the page.
|
||||
#### [Bump async from 2.6.3 to 2.6.4 (@dependabot[bot])](https://github.com/MithrilJS/mithril.js/pull/2815)
|
||||
|
||||
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. Changelog. Sourced from async's changelog. v2.6.4.
|
||||
|
||||
# Release v2.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
#### [Use markdown for the README badges (@pygy)](https://github.com/MithrilJS/mithril.js/pull/2773)
|
||||
|
||||
Use markdown for the README badges.
|
||||
|
||||
# Release v2.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.redraw(), which schedules an update of all components mounted via m.mount()
|
||||
-->
|
||||
|
||||
# redraw()
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [Static members](#static-members)
|
||||
-[m.redraw.sync()](#mredrawsync)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Updates the DOM after a change in the application data layer.
|
||||
|
||||
You DON'T need to call it if data is modified within the execution context of an event handler defined in a Mithril.js view, or after request completion when using `m.request`. The [autoredraw](autoredraw.md) system, which is built on top of `m.redraw()` will take care of it.
|
||||
|
||||
You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries.
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`m.redraw()`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
**returns** | | | Returns nothing
|
||||
|
||||
#### Static members
|
||||
|
||||
##### m.redraw.sync
|
||||
|
||||
`m.redraw.sync()`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
**returns** | | | Returns nothing
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
When callbacks outside of Mithril.js run, you need to notify Mithril.js' rendering engine that a redraw is needed. External callbacks could be `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, web socket library callbacks, event handlers in jQuery plugins, third party XHR request callbacks, etc.
|
||||
|
||||
To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you used `m.mount` or `m.route`. If you rendered via `m.render`, you should use `m.render` to redraw.
|
||||
|
||||
`m.redraw()` always triggers an asynchronous redraws, whereas `m.redraw.sync()` triggers a synchronous one. `m.redraw()` is tied to `window.requestAnimationFrame()`. It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate.
|
||||
|
||||
`m.redraw.sync()` is mostly intended to make videos play work in iOS. That only works in response to user-triggered events. It comes with several caveat:
|
||||
|
||||
- You should not call `m.redraw.sync()` from a [lifecycle method](lifecycle-methods.md) or the `view()` method of a component. Doing so will result in undefined behavior (it throws an error when possible).
|
||||
- `m.redraw.sync()` called from an event handler can cause the DOM to be modified while an event is bubbling. Depending on the structure of the old and new DOM trees, the event can finish the bubbling phase in the new tree and trigger unwanted handlers.
|
||||
- It is not throttled. One call to `m.redraw.sync()` causes immediately one `m.render()` call per root registered with [`m.mount()`](mount.md) or [`m.route()`](route.md).
|
||||
|
||||
`m.redraw()` doesn't have any of those issues, you can call it from wherever you like.
|
||||
|
|
@ -16,7 +16,7 @@ pr-release handles the following:
|
|||
|
||||
## For contributors
|
||||
|
||||
Contributors should create their feature branch targetting the default branch `next`. When this branch is merged `pr-release` will either generate or update a release PR from `next` to `main`.
|
||||
Contributors should create their feature branch targetting the default branch `main`. When this branch is merged `pr-release` will either generate or update a release PR from `main` to `release`.
|
||||
|
||||
The description and title will be managed by [pr-release], including the semver version.
|
||||
|
||||
|
|
@ -26,12 +26,7 @@ If you do not have permissions, the maintainer will set the label on your behalf
|
|||
|
||||
## Changelog
|
||||
|
||||
There are two changelogs in the Mithril.js project
|
||||
|
||||
- `docs/changelog.md` a hand written curated reflection of changes to the codebase
|
||||
- `docs/release.md` an automatically prepended log of changes, managed by pr-release
|
||||
|
||||
In future we may collapse these into a single file, the separation is due to the fact the `changelog.md` predates the `release.md` file.
|
||||
Currently, `docs/recent-changes.md` holds an automatically prepended log of changes, managed by pr-release. Ideally, I want to get rid of this and just have pr-release somehow push to https://github.com/MithrilJS/docs automatically, but that may take some work.
|
||||
|
||||
## For maintainers
|
||||
|
||||
|
|
@ -39,28 +34,4 @@ Whenever a new feature branch is opened, a reviewing maintainer should add the c
|
|||
|
||||
If a `major` or `minor` feature branch is merged but no labels were set, you can still go back and edit the semver labels. On label change the release pr will automatically be regenerated and will recalculate the semver version.
|
||||
|
||||
## Updating mithril.js.org
|
||||
|
||||
Fixes to documentation can land whenever, updates to the site are built and published via `scripts/update-docs.js`.
|
||||
|
||||
```bash
|
||||
# These steps assume that MithrilJS/mithril.js is a git remote named "mithriljs"
|
||||
|
||||
# Ensure your next branch is up to date
|
||||
$ git checkout next
|
||||
$ git pull mithriljs next
|
||||
|
||||
# Splat the docs folder from next onto master
|
||||
$ git checkout master
|
||||
$ git checkout next -- ./docs
|
||||
|
||||
# Manually ensure that no new feature docs were added
|
||||
|
||||
$ node scripts/update-docs
|
||||
```
|
||||
|
||||
After the docs build completes, the updated docs should appear on https://mithril.js.org in a few minutes.
|
||||
|
||||
**Note:** When updating the stable version with a release candidate out, ***make sure to update the index + navigation to point to the new stable version!!!***
|
||||
|
||||
[pr-release]: https://pr-release.org/
|
||||
|
|
|
|||
534
docs/request.md
|
|
@ -1,534 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.request(), a utility for making XHR/AJAX requests
|
||||
-->
|
||||
|
||||
# request(options)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Typical usage](#typical-usage)
|
||||
- [Error handling](#error-handling)
|
||||
- [Loading icons and error messages](#loading-icons-and-error-messages)
|
||||
- [Dynamic URLs](#dynamic-urls)
|
||||
- [Aborting requests](#aborting-requests)
|
||||
- [File uploads](#file-uploads)
|
||||
- [Monitoring progress](#monitoring-progress)
|
||||
- [Casting response to a type](#casting-response-to-a-type)
|
||||
- [Non-JSON responses](#non-json-responses)
|
||||
- [Retrieving response details](#retrieving-response-details)
|
||||
- [Why JSON instead of HTML](#why-json-instead-of-html)
|
||||
- [Why XHR instead of fetch](#why-xhr-instead-of-fetch)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Makes XHR (aka AJAX) requests, and returns a promise
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "PUT",
|
||||
url: "/api/v1/users/:id",
|
||||
params: {id: 1},
|
||||
body: {name: "test"}
|
||||
})
|
||||
.then(function(result) {
|
||||
console.log(result)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`promise = m.request(options)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------------------- | --------------------------------- | -------- | ---
|
||||
`options` | `Object` | Yes | The request options to pass.
|
||||
`options.method` | `String` | No | The HTTP method to use. This value should be one of the following: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` or `OPTIONS`. Defaults to `GET`.
|
||||
`options.url` | `String` | Yes | The [path name](paths.md) to send the request to, optionally interpolated with values from `options.params`.
|
||||
`options.params` | `Object` | No | The data to be interpolated into the URL and/or serialized into the query string.
|
||||
`options.body` | `Object` | No | The data to be serialized into the body (for other types of requests).
|
||||
`options.async` | `Boolean` | No | Whether the request should be asynchronous. Defaults to `true`.
|
||||
`options.user` | `String` | No | A username for HTTP authorization. Defaults to `undefined`.
|
||||
`options.password` | `String` | No | A password for HTTP authorization. Defaults to `undefined`. This option is provided for `XMLHttpRequest` compatibility, but you should avoid using it because it sends the password in plain text over the network.
|
||||
`options.withCredentials` | `Boolean` | No | Whether to send cookies to 3rd party domains. Defaults to `false`
|
||||
`options.timeout` | `Number` | No | The amount of milliseconds a request can take before automatically being [terminated](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout). Defaults to `undefined`.
|
||||
`options.responseType` | `String` | No | The expected [type](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) of the response. Defaults to `""` if `extract` is defined, `"json"` if missing. If `responseType: "json"`, it internally performs `JSON.parse(responseText)`.
|
||||
`options.config` | `xhr = Function(xhr)` | No | Exposes the underlying XMLHttpRequest object for low-level configuration and optional replacement (by returning a new XHR).
|
||||
`options.headers` | `Object` | No | Headers to append to the request before sending it (applied right before `options.config`).
|
||||
`options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function).
|
||||
`options.serialize` | `string = Function(any)` | No | A serialization method to be applied to `body`. Defaults to `JSON.stringify`, or if `options.body` is an instance of [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData) or [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams), defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function) (i.e. `function(value) {return value}`).
|
||||
`options.deserialize` | `any = Function(any)` | No | A deserialization method to be applied to the `xhr.response` or normalized `xhr.responseText`. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). If `extract` is defined, `deserialize` will be skipped.
|
||||
`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `options.deserialize(parsedResponse)`, throwing an exception when the server response status code indicates an error or when the response is syntactically invalid. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will be left as-is when the promise resolves.
|
||||
`options.background` | `Boolean` | No | If `false`, redraws mounted components upon completion of the request. If `true`, it does not. Defaults to `false`.
|
||||
**returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods. If the response status code indicates an error, the promise rejects, but this can be prevented by setting the `extract` option.
|
||||
|
||||
`promise = m.request(url, options)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | --------- | -------- | ---
|
||||
`url` | `String` | Yes | The [path name](paths.md) to send the request to. `options.url` overrides this when present.
|
||||
`options` | `Object` | No | The request options to pass.
|
||||
**returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods
|
||||
|
||||
This second form is mostly equivalent to `m.request(Object.assign({url: url}, options))`, just it does not depend on the ES6 global `Object.assign` internally.
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.request` utility is a thin wrapper around [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), and allows making HTTP requests to remote servers in order to save and/or retrieve data from a database.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/users",
|
||||
})
|
||||
.then(function(users) {
|
||||
console.log(users)
|
||||
})
|
||||
```
|
||||
|
||||
A call to `m.request` returns a promise and triggers a redraw upon completion of its promise chain.
|
||||
|
||||
By default, `m.request` assumes the response is in JSON format and parses it into a JavaScript object (or array).
|
||||
|
||||
If the HTTP response status code indicates an error, the returned Promise will be rejected. Supplying an extract callback will prevent the promise rejection.
|
||||
|
||||
---
|
||||
|
||||
### Typical usage
|
||||
|
||||
Here's an illustrative example of a component that uses `m.request` to retrieve some data from a server.
|
||||
|
||||
```javascript
|
||||
var Data = {
|
||||
todos: {
|
||||
list: [],
|
||||
fetch: function() {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/todos",
|
||||
})
|
||||
.then(function(items) {
|
||||
Data.todos.list = items
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Todos = {
|
||||
oninit: Data.todos.fetch,
|
||||
view: function(vnode) {
|
||||
return Data.todos.list.map(function(item) {
|
||||
return m("div", item.title)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": Todos
|
||||
})
|
||||
```
|
||||
|
||||
Let's assume making a request to the server URL `/api/items` returns an array of objects in JSON format.
|
||||
|
||||
When `m.route` is called at the bottom, the `Todos` component is initialized. `oninit` is called, which calls `m.request`. This retrieves an array of objects from the server asynchronously. "Asynchronously" means that JavaScript continues running other code while it waits for the response from server. In this case, it means `fetch` returns, and the component is rendered using the original empty array as `Data.todos.list`. Once the request to the server completes, the array of objects `items` is assigned to `Data.todos.list` and the component is rendered again, yielding a list of `<div>`s containing the titles of each `todo`.
|
||||
|
||||
---
|
||||
|
||||
### Error handling
|
||||
|
||||
When a non-`file:` request returns with any status other than 2xx or 304, it rejects with an error. This error is a normal [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance, but with a few special properties.
|
||||
|
||||
- `error.message` is set to the raw response text.
|
||||
- `error.code` is set to the status code itself.
|
||||
- `error.response` is set to the parsed response, using `options.extract` and `options.deserialize` as is done with normal responses.
|
||||
|
||||
This is useful in many cases where errors are themselves things you can account for. If you want to detect if a session expired - you can do `if (error.code === 401) return promptForAuth().then(retry)`. If you hit an API's throttling mechanism and it returned an error with a `"timeout": 1000`, you could do a `setTimeout(retry, error.response.timeout)`.
|
||||
|
||||
---
|
||||
|
||||
### Loading icons and error messages
|
||||
|
||||
Here's an expanded version of the example above that implements a loading indicator and an error message:
|
||||
|
||||
```javascript
|
||||
var Data = {
|
||||
todos: {
|
||||
list: null,
|
||||
error: "",
|
||||
fetch: function() {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/todos",
|
||||
})
|
||||
.then(function(items) {
|
||||
Data.todos.list = items
|
||||
})
|
||||
.catch(function(e) {
|
||||
Data.todos.error = e.message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Todos = {
|
||||
oninit: Data.todos.fetch,
|
||||
view: function(vnode) {
|
||||
return Data.todos.error ? [
|
||||
m(".error", Data.todos.error)
|
||||
] : Data.todos.list ? [
|
||||
Data.todos.list.map(function(item) {
|
||||
return m("div", item.title)
|
||||
})
|
||||
] : m(".loading-icon")
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": Todos
|
||||
})
|
||||
```
|
||||
|
||||
There are a few differences between this example and the one before. Here, `Data.todos.list` is `null` at the beginning. Also, there's an extra field `error` for holding an error message, and the view of the `Todos` component was modified to displays an error message if one exists, or display a loading icon if `Data.todos.list` is not an array.
|
||||
|
||||
---
|
||||
|
||||
### Dynamic URLs
|
||||
|
||||
Request URLs may contain interpolations:
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/users/:id",
|
||||
params: {id: 123},
|
||||
}).then(function(user) {
|
||||
console.log(user.id) // logs 123
|
||||
})
|
||||
```
|
||||
|
||||
In the code above, `:id` is populated with the data from the `params` object, and the request becomes `GET /api/v1/users/123`.
|
||||
|
||||
Interpolations are ignored if no matching data exists in the `params` property.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/users/foo:bar",
|
||||
params: {id: 123},
|
||||
})
|
||||
```
|
||||
|
||||
In the code above, the request becomes `GET /api/v1/users/foo:bar?id=123`
|
||||
|
||||
---
|
||||
|
||||
### Aborting requests
|
||||
|
||||
Sometimes, it is desirable to abort a request. For example, in an autocompleter/typeahead widget, you want to ensure that only the last request completes, because typically autocompleters fire several requests as the user types and HTTP requests may complete out of order due to the unpredictable nature of networks. If another request finishes after the last fired request, the widget would display less relevant (or potentially wrong) data than if the last fired request finished last.
|
||||
|
||||
`m.request()` exposes its underlying `XMLHttpRequest` object via the `options.config` parameter, which allows you to save a reference to that object and call its `abort` method when required:
|
||||
|
||||
```javascript
|
||||
var searchXHR = null
|
||||
function search() {
|
||||
abortPreviousSearch()
|
||||
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/users",
|
||||
params: {search: query},
|
||||
config: function(xhr) {searchXHR = xhr}
|
||||
})
|
||||
}
|
||||
function abortPreviousSearch() {
|
||||
if (searchXHR !== null) searchXHR.abort()
|
||||
searchXHR = null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### File uploads
|
||||
|
||||
To upload files, first you need to get a reference to a [`File`](https://developer.mozilla.org/en/docs/Web/API/File) object. The easiest way to do that is from a `<input type="file">`.
|
||||
|
||||
```javascript
|
||||
m.render(document.body, [
|
||||
m("input[type=file]", {onchange: upload})
|
||||
])
|
||||
|
||||
function upload(e) {
|
||||
var file = e.target.files[0]
|
||||
}
|
||||
```
|
||||
|
||||
The snippet above renders a file input. If a user picks a file, the `onchange` event is triggered, which calls the `upload` function. `e.target.files` is a list of `File` objects.
|
||||
|
||||
Next, you need to create a [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData) object to create a [multipart request](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html), which is a specially formatted HTTP request that is able to send file data in the request body.
|
||||
|
||||
```javascript
|
||||
function upload(e) {
|
||||
var file = e.target.files[0]
|
||||
|
||||
var body = new FormData()
|
||||
body.append("myfile", file)
|
||||
}
|
||||
```
|
||||
|
||||
Next, you need to call `m.request` and set `options.method` to an HTTP method that uses body (e.g. `POST`, `PUT`, `PATCH`) and use the `FormData` object as `options.body`.
|
||||
|
||||
```javascript
|
||||
function upload(e) {
|
||||
var file = e.target.files[0]
|
||||
|
||||
var body = new FormData()
|
||||
body.append("myfile", file)
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/api/v1/upload",
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Assuming the server is configured to accept multipart requests, the file information will be associated with the `myfile` key.
|
||||
|
||||
#### Multiple file uploads
|
||||
|
||||
It's possible to upload multiple files in one request. Doing so will make the batch upload atomic, i.e. no files will be processed if there's an error during the upload, so it's not possible to have only part of the files saved. If you want to save as many files as possible in the event of a network failure, you should consider uploading each file in a separate request instead.
|
||||
|
||||
To upload multiple files, simply append them all to the `FormData` object. When using a file input, you can get a list of files by adding the `multiple` attribute to the input:
|
||||
|
||||
```javascript
|
||||
m.render(document.body, [
|
||||
m("input[type=file][multiple]", {onchange: upload})
|
||||
])
|
||||
|
||||
function upload(e) {
|
||||
var files = e.target.files
|
||||
|
||||
var body = new FormData()
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
body.append("file" + i, files[i])
|
||||
}
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/api/v1/upload",
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Monitoring progress
|
||||
|
||||
Sometimes, if a request is inherently slow (e.g. a large file upload), it's desirable to display a progress indicator to the user to signal that the application is still working.
|
||||
|
||||
`m.request()` exposes its underlying `XMLHttpRequest` object via the `options.config` parameter, which allows you to attach event listeners to the XMLHttpRequest object:
|
||||
|
||||
```javascript
|
||||
var progress = 0
|
||||
|
||||
m.mount(document.body, {
|
||||
view: function() {
|
||||
return [
|
||||
m("input[type=file]", {onchange: upload}),
|
||||
progress + "% completed"
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
function upload(e) {
|
||||
var file = e.target.files[0]
|
||||
|
||||
var body = new FormData()
|
||||
body.append("myfile", file)
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/api/v1/upload",
|
||||
body: body,
|
||||
config: function(xhr) {
|
||||
xhr.upload.addEventListener("progress", function(e) {
|
||||
progress = e.loaded / e.total
|
||||
|
||||
m.redraw() // tell Mithril.js that data changed and a re-render is needed
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, a file input is rendered. If the user picks a file, an upload is initiated, and in the `config` callback, a `progress` event handler is registered. This event handler is fired whenever there's a progress update in the XMLHttpRequest. Because the XMLHttpRequest's progress event is not directly handled by Mithril.js' virtual DOM engine, `m.redraw()` must be called to signal to Mithril.js that data has changed and a redraw is required.
|
||||
|
||||
---
|
||||
|
||||
### Casting response to a type
|
||||
|
||||
Depending on the overall application architecture, it may be desirable to transform the response data of a request to a specific class or type (for example, to uniformly parse date fields or to have helper methods).
|
||||
|
||||
You can pass a constructor as the `options.type` parameter and Mithril.js will instantiate it for each object in the HTTP response.
|
||||
|
||||
```javascript
|
||||
function User(data) {
|
||||
this.name = data.firstName + " " + data.lastName
|
||||
}
|
||||
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/users",
|
||||
type: User
|
||||
})
|
||||
.then(function(users) {
|
||||
console.log(users[0].name) // logs a name
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, assuming `/api/v1/users` returns an array of objects, the `User` constructor will be instantiated (i.e. called as `new User(data)`) for each object in the array. If the response returned a single object, that object would be used as the `body` argument.
|
||||
|
||||
---
|
||||
|
||||
### Non-JSON responses
|
||||
|
||||
Sometimes a server endpoint does not return a JSON response: for example, you may be requesting an HTML file, an SVG file, or a CSV file. By default Mithril.js attempts to parse a response as if it was JSON. To override that behavior, define a custom `options.deserialize` function:
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/files/icon.svg",
|
||||
deserialize: function(value) {return value}
|
||||
})
|
||||
.then(function(svg) {
|
||||
m.render(document.body, m.trust(svg))
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, the request retrieves an SVG file, does nothing to parse it (because `deserialize` merely returns the value as-is), and then renders the SVG string as trusted HTML.
|
||||
|
||||
Of course, a `deserialize` function may be more elaborate:
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/files/data.csv",
|
||||
deserialize: parseCSV
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
function parseCSV(data) {
|
||||
// naive implementation for the sake of keeping example simple
|
||||
return data.split("\n").map(function(row) {
|
||||
return row.split(",")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Ignoring the fact that the parseCSV function above doesn't handle a lot of cases that a proper CSV parser would, the code above logs an array of arrays.
|
||||
|
||||
Custom headers may also be helpful in this regard. For example, if you're requesting an SVG, you probably want to set the content type accordingly. To override the default JSON request type, set `options.headers` to an object of key-value pairs corresponding to request header names and values.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/files/image.svg",
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml; charset=utf-8",
|
||||
"Accept": "image/svg, text/*"
|
||||
},
|
||||
deserialize: function(value) {return value}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Retrieving response details
|
||||
|
||||
By default Mithril.js attempts to parse `xhr.responseText` as JSON and returns the parsed object. It may be useful to inspect a server response in more detail and process it manually. This can be accomplished by passing a custom `options.extract` function:
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/v1/users",
|
||||
extract: function(xhr) {return {status: xhr.status, body: xhr.responseText}}
|
||||
})
|
||||
.then(function(response) {
|
||||
console.log(response.status, response.body)
|
||||
})
|
||||
```
|
||||
|
||||
The parameter to `options.extract` is the XMLHttpRequest object once its operation is completed, but before it has been passed to the returned promise chain, so the promise may still end up in an rejected state if processing throws an exception.
|
||||
|
||||
---
|
||||
|
||||
### Why JSON instead of HTML
|
||||
|
||||
Many server-side frameworks provide a view engine that interpolates database data into a template before serving HTML (on page load or via AJAX) and then employ jQuery to handle user interactions.
|
||||
|
||||
By contrast, Mithril.js is framework designed for thick client applications, which typically download templates and data separately and combine them in the browser via JavaScript. Doing the templating heavy-lifting in the browser can bring benefits like reducing operational costs by freeing server resources. Separating templates from data also allow template code to be cached more effectively and enables better code reusability across different types of clients (e.g. desktop, mobile). Another benefit is that Mithril.js enables a [retained mode](https://en.wikipedia.org/wiki/Retained_mode) UI development paradigm, which greatly simplifies development and maintenance of complex user interactions.
|
||||
|
||||
By default, `m.request` expects response data to be in JSON format. In a typical Mithril.js application, that JSON data is then usually consumed by a view.
|
||||
|
||||
You should avoid trying to render server-generated dynamic HTML with Mithril. If you have an existing application that does use a server-side templating system, and you wish to re-architecture it, first decide whether the effort is feasible at all to begin with. Migrating from a thick server architecture to a thick client architecture is typically a somewhat large effort, and involves refactoring logic out of templates into logical data services (and the testing that goes with it).
|
||||
|
||||
Data services may be organized in many different ways depending on the nature of the application. [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) architectures are popular with API providers, and [service oriented architectures](https://en.wikipedia.org/wiki/Service-oriented_architecture) are often required where there are lots of highly transactional workflows.
|
||||
|
||||
---
|
||||
|
||||
### Why XHR instead of fetch
|
||||
|
||||
[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) is a newer Web API for fetching resources from servers, similar to `XMLHttpRequest`.
|
||||
|
||||
Mithril.js' `m.request` uses `XMLHttpRequest` instead of `fetch()` for a number of reasons:
|
||||
|
||||
- `fetch` is not fully standardized yet, and may be subject to specification changes.
|
||||
- `XMLHttpRequest` calls can be aborted before they resolve (e.g. to avoid race conditions in for instant search UIs).
|
||||
- `XMLHttpRequest` provides hooks for progress listeners for long running requests (e.g. file uploads).
|
||||
- `XMLHttpRequest` is supported by all browsers, whereas `fetch()` is not supported by Internet Explorer and older Android (prior to 5.0 Lollipop).
|
||||
|
||||
Currently, due to lack of browser support, `fetch()` typically requires a [polyfill](https://github.com/github/fetch), which is over 11kb uncompressed - nearly three times larger than Mithril.js' XHR module.
|
||||
|
||||
Despite being much smaller, Mithril.js' XHR module supports many important and not-so-trivial-to-implement features like [URL interpolation](#dynamic-urls) and querystring serialization in addition to its ability to integrate seamlessly to Mithril.js' autoredrawing subsystem. The `fetch` polyfill does not support any of those, and requires extra libraries and boilerplates to achieve the same level of functionality.
|
||||
|
||||
In addition, Mithril.js' XHR module is optimized for JSON-based endpoints and makes that most common case appropriately terse - i.e. `m.request(url)` - whereas `fetch` requires an additional explicit step to parse the response data as JSON: `fetch(url).then(function(response) {return response.json()})`
|
||||
|
||||
The `fetch()` API does have a few technical advantages over `XMLHttpRequest` in a few uncommon cases:
|
||||
|
||||
- it provides a streaming API (in the "video streaming" sense, not in the reactive programming sense), which enables better latency and memory consumption for very large responses (at the cost of code complexity).
|
||||
- it integrates to the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which provides an extra layer of control over how and when network requests happen. This API also allows access to push notifications and background synchronization features.
|
||||
|
||||
In typical scenarios, streaming won't provide noticeable performance benefits because it's generally not advisable to download megabytes of data to begin with. Also, the memory gains from repeatedly reusing small buffers may be offset or nullified if they result in excessive browser repaints. For those reasons, choosing `fetch()` streaming instead of `m.request` is only recommended for extremely resource intensive applications.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
#### Promises are not the response data
|
||||
|
||||
The `m.request` method returns a `Promise`, not the response data itself. It cannot return that data directly because an HTTP request may take a long time to complete (due to network latency), and if JavaScript waited for it, it would freeze the application until the data was available.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var users = m.request("/api/v1/users")
|
||||
console.log("list of users:", users)
|
||||
// `users` is NOT a list of users, it's a promise
|
||||
|
||||
// PREFER
|
||||
m.request("/api/v1/users").then(function(users) {
|
||||
console.log("list of users:", users)
|
||||
})
|
||||
```
|
||||
887
docs/route.md
|
|
@ -1,887 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.route(), Mithril.js' client-side router
|
||||
-->
|
||||
|
||||
# route(root, defaultRoute, routes)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [Static members](#static-members)
|
||||
- [m.route.set](#mrouteset)
|
||||
- [m.route.get](#mrouteget)
|
||||
- [m.route.prefix](#mrouteprefix)
|
||||
- [m.route.Link](#mroutelink)
|
||||
- [m.route.param](#mrouteparam)
|
||||
- [m.route.SKIP](#mrouteskip)
|
||||
- [RouteResolver](#routeresolver)
|
||||
- [routeResolver.onmatch](#routeresolveronmatch)
|
||||
- [routeResolver.render](#routeresolverrender)
|
||||
- [How it works](#how-it-works)
|
||||
- [Typical usage](#typical-usage)
|
||||
- [Navigating to different routes](#navigating-to-different-routes)
|
||||
- [Routing parameters](#routing-parameters)
|
||||
- [Key parameter](#key-parameter)
|
||||
- [Variadic routes](#variadic-routes)
|
||||
- [History state](#history-state)
|
||||
- [Changing router prefix](#changing-router-prefix)
|
||||
- [Advanced component resolution](#advanced-component-resolution)
|
||||
- [Wrapping a layout component](#wrapping-a-layout-component)
|
||||
- [Redirection](#redirection)
|
||||
- [Preloading data](#preloading-data)
|
||||
- [Code splitting](#code-splitting)
|
||||
- [Typed routes](#typed-routes)
|
||||
- [Hidden routes](#hidden-routes)
|
||||
- [Route cancellation / blocking](#route-cancellation--blocking)
|
||||
- [Third-party integration](#third-party-integration)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Navigate between "pages" within an application
|
||||
|
||||
```javascript
|
||||
var Home = {
|
||||
view: function() {
|
||||
return "Welcome"
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/home", {
|
||||
"/home": Home, // defines `https://localhost/#!/home`
|
||||
})
|
||||
```
|
||||
|
||||
You can only have one `m.route` call per application.
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`m.route(root, defaultRoute, routes)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
---------------------- | ---------------------------------------- | -------- | ---
|
||||
`root` | `Element` | Yes | A DOM element that will be the parent node to the subtree
|
||||
`defaultRoute` | `String` | Yes | The route to redirect to if the current URL does not match a route. Note, this is not the initial route. Initial route will be your address bar's url.
|
||||
`routes` | `Object<String,Component|RouteResolver>` | Yes | An object whose keys are route strings and values are either components or a [RouteResolver](#routeresolver)
|
||||
**returns** | | | Returns `undefined`
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
|
||||
#### Static members
|
||||
|
||||
##### m.route.set
|
||||
|
||||
Redirects to a matching route, or to the default route if no matching routes can be found. Triggers an asynchronous redraw off all mount points.
|
||||
|
||||
`m.route.set(path, params, options)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------------- | --------- | -------- | ---
|
||||
`path` | `String` | Yes | The [path name](paths.md) to route to, without a prefix. The path may include parameters, interpolated with values from `params`.
|
||||
`params` | `Object` | No | Routing parameters. If `path` has routing parameter slots, the properties of this object are interpolated into the path string
|
||||
`options.replace` | `Boolean` | No | Whether to create a new history entry or to replace the current one. Defaults to false
|
||||
`options.state` | `Object` | No | The `state` object to pass to the underlying `history.pushState` / `history.replaceState` call. This state object becomes available in the `history.state` property, and is merged into the [routing parameters](#routing-parameters) object. Note that this option only works when using the pushState API, but is ignored if the router falls back to hashchange mode (i.e. if the pushState API is not available)
|
||||
`options.title` | `String` | No | The `title` string to pass to the underlying `history.pushState` / `history.replaceState` call.
|
||||
**returns** | | | Returns `undefined`
|
||||
|
||||
Remember that when using `.set` with `params` you also need to define the route:
|
||||
```javascript
|
||||
var Article = {
|
||||
view: function(vnode) {
|
||||
return "This is article " + vnode.attrs.articleid
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, {
|
||||
'/article/:articleid': Article
|
||||
})
|
||||
m.route.set('/article/:articleid', {articleid: 1})
|
||||
```
|
||||
|
||||
##### m.route.get
|
||||
|
||||
Returns the last fully resolved routing path, without the prefix. It may differ from the path displayed in the location bar while an asynchronous route is [pending resolution](#code-splitting).
|
||||
|
||||
`path = m.route.get()`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------------- | --------- | -------- | ---
|
||||
**returns** | `String` | | Returns the last fully resolved path
|
||||
|
||||
##### m.route.prefix
|
||||
|
||||
Defines a router prefix. The router prefix is a fragment of the URL that dictates the underlying [strategy](#routing-strategies) used by the router.
|
||||
|
||||
`m.route.prefix = prefix`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------------- | --------- | -------- | ---
|
||||
`prefix` | `String` | Yes | The prefix that controls the underlying [routing strategy](#routing-strategies) used by Mithril.
|
||||
|
||||
This is a simple property, so you can both read it and write to it.
|
||||
|
||||
##### m.route.Link
|
||||
|
||||
This component creates a dynamic routed link. Its essential function is to produce `a` links with local `href`s transformed to take account of the [route prefix](#mrouteprefix).
|
||||
|
||||
```javascript
|
||||
m(m.route.Link, {href: "/foo"}, "foo")
|
||||
|
||||
// Unless m.route.prefix has changed from the default strategy, render to:
|
||||
// <a href="#!/foo">foo</a>
|
||||
```
|
||||
|
||||
Links accept a selection of special attributes:
|
||||
* `selector` is what would be passed as the first argument to [`m`](hyperscript.md): any selector is valid, including non-`a` elements.
|
||||
* `params` & `options` are the arguments with the same names as defined in [`m.route.set`](#mrouteset).
|
||||
* `disabled`, if true, disables routing behaviour and any bound `onclick` handler, and attaches a `data-disabled="true"` attribute for accessibility hints; if the element is an `a`, the `href` is removed.
|
||||
|
||||
*Routing behaviour cannot be prevented using the event handling API: use `disabled` instead.*
|
||||
|
||||
```javascript
|
||||
m(m.route.Link, {
|
||||
href: "/foo",
|
||||
selector: "button.large",
|
||||
disabled: true,
|
||||
params: {key: "value"},
|
||||
options: {replace: true},
|
||||
}, "link name")
|
||||
|
||||
// Renders to:
|
||||
// <button disabled aria-disabled="true" class="large">link name</button>
|
||||
```
|
||||
|
||||
`vnode = m(m.route.Link, attributes, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
--------------------- | ------------------------------------ | -------- | ---
|
||||
`attributes.href` | `Object` | Yes | The target route to navigate to.
|
||||
`attributes.disabled` | `Boolean` | No | Disables the element accessibly.
|
||||
`attributes.selector` | `String|Object|Function` | No | A selector for [`m`](hyperscript.md), defaults to `"a"`.
|
||||
`attributes.options` | `Object` | No | Sets the `options` passed to [`m.route.set`](#mrouteset).
|
||||
`attributes.params` | `Object` | No | Sets the `params` passed to [`m.route.set`](#mrouteset).
|
||||
`attributes` | `Object` | No | Any other attributes to be forwarded to `m`.
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md) for this link.
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md).
|
||||
|
||||
##### m.route.param
|
||||
|
||||
Retrieves a route parameter from the last fully resolved route. A route parameter is a key-value pair. Route parameters may come from a few different places:
|
||||
|
||||
- route interpolations (e.g. if a route is `/users/:id`, and it resolves to `/users/1`, the route parameter has a key `id` and value `"1"`)
|
||||
- router querystrings (e.g. if the path is `/users?page=1`, the route parameter has a key `page` and value `"1"`)
|
||||
- `history.state` (e.g. if history.state is `{foo: "bar"}`, the route parameter has key `foo` and value `"bar"`)
|
||||
|
||||
`value = m.route.param(key)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------------- | --------------- | -------- | ---
|
||||
`key` | `String` | No | A route parameter name (e.g. `id` in route `/users/:id`, or `page` in path `/users/1?page=3`, or a key in `history.state`)
|
||||
**returns** | `String|Object` | | Returns a value for the specified key. If a key is not specified, it returns an object that contains all the interpolation keys
|
||||
|
||||
Note that in the `onmatch` function of a RouteResolver, the new route hasn't yet been fully resolved, and `m.route.param()` will return the parameters of the previous route, if any. `onmatch` receives the parameters of the new route as an argument.
|
||||
|
||||
##### m.route.SKIP
|
||||
|
||||
A special value that can be returned from a [route resolver's `onmatch`](#routeresolveronmatch) to skip to the next route.
|
||||
|
||||
#### RouteResolver
|
||||
|
||||
A RouteResolver is a non-component object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present.
|
||||
|
||||
If an object can be detected as a component (by the presence of a `view` method or by being a `function`/`class`), it will be treated as such even if it has `onmatch` or `render` methods. Since a RouteResolver is not a component, it does not have lifecycle methods.
|
||||
|
||||
As a rule of thumb, RouteResolvers should be in the same file as the `m.route` call, whereas component definitions should be in their own modules.
|
||||
|
||||
`routeResolver = {onmatch, render}`
|
||||
|
||||
When using components, you could think of them as special sugar for this route resolver, assuming your component is `Home`:
|
||||
|
||||
```javascript
|
||||
var routeResolver = {
|
||||
onmatch: function() { return Home },
|
||||
render: function(vnode) { return [vnode] },
|
||||
}
|
||||
```
|
||||
|
||||
##### routeResolver.onmatch
|
||||
|
||||
The `onmatch` hook is called when the router needs to find a component to render. It is called once per router path changes, but not on subsequent redraws while on the same path. It can be used to run logic before a component initializes (for example authentication logic, data preloading, redirection analytics tracking, etc)
|
||||
|
||||
This method also allows you to asynchronously define what component will be rendered, making it suitable for code splitting and asynchronous module loading. To render a component asynchronously return a promise that resolves to a component.
|
||||
|
||||
For more information on `onmatch`, see the [advanced component resolution](#advanced-component-resolution) section
|
||||
|
||||
`routeResolver.onmatch(args, requestedPath, route)`
|
||||
|
||||
Argument | Type | Description
|
||||
--------------- | ---------------------------------------- | ---
|
||||
`args` | `Object` | The [routing parameters](#routing-parameters)
|
||||
`requestedPath` | `String` | The router path requested by the last routing action, including interpolated routing parameter values, but without the prefix. When `onmatch` is called, the resolution for this path is not complete and `m.route.get()` still returns the previous path.
|
||||
`route` | `String` | The router path requested by the last routing action, excluding interpolated routing parameter values
|
||||
**returns** | `Component|Promise<Component>|undefined` | Returns a component or a promise that resolves to a component
|
||||
|
||||
If `onmatch` returns a component or a promise that resolves to a component, this component is used as the `vnode.tag` for the first argument in the RouteResolver's `render` method. Otherwise, `vnode.tag` is set to `"div"`. Similarly, if the `onmatch` method is omitted, `vnode.tag` is also `"div"`.
|
||||
|
||||
If `onmatch` returns a promise that gets rejected, the router redirects back to `defaultRoute`. You may override this behavior by calling `.catch` on the promise chain before returning it.
|
||||
|
||||
##### routeResolver.render
|
||||
|
||||
The `render` method is called on every redraw for a matching route. It is similar to the `view` method in components and it exists to simplify [component composition](#wrapping-a-layout-component). It also lets you escape from Mithril.js' normal behavior of replacing the entire subtree.
|
||||
|
||||
`vnode = routeResolver.render(vnode)`
|
||||
|
||||
Argument | Type | Description
|
||||
------------------- | -------------------- | -----------
|
||||
`vnode` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If onmatch does not return a component or a promise that resolves to a component, the vnode's `tag` field defaults to `"div"`
|
||||
`vnode.attrs` | `Object` | A map of URL parameter values
|
||||
**returns** | `Array<Vnode>|Vnode` | The [vnodes](vnodes.md) to be rendered
|
||||
|
||||
The `vnode` parameter is just `m(Component, m.route.param())` where `Component` is the resolved component for the route (after `routeResolver.onmatch`) and `m.route.param()` is as documented [here](#mrouteparam). If you omit this method, the default return value is `[vnode]`, wrapped in a fragment so you can use [key parameters](#key-parameter). Combined with a `:key` parameter, it becomes a [single-element keyed fragment](keys.md#reinitializing-views-with-single-child-keyed-fragments), since it ends up rendering to something like `[m(Component, {key: m.route.param("key"), ...})]`.
|
||||
|
||||
---
|
||||
|
||||
#### How it works
|
||||
|
||||
Routing is a system that allows creating Single Page Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh.
|
||||
|
||||
It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism.
|
||||
|
||||
Routing without page refreshes is made partially possible by the [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History_API#The_pushState%28%29_method) API. Using this API, it's possible to programmatically change the URL displayed by the browser after a page has loaded, but it's the application developer's responsibility to ensure that navigating to any given URL from a cold state (e.g. a new tab) will render the appropriate markup.
|
||||
|
||||
#### Routing strategies
|
||||
|
||||
The routing strategy dictates how a library might actually implement routing. There are three general strategies that can be used to implement a SPA routing system, and each has different caveats:
|
||||
|
||||
- `m.route.prefix = '#!'` (default) – Using the [fragment identifier](https://en.wikipedia.org/wiki/Fragment_identifier) (aka the hash) portion of the URL. A URL using this strategy typically looks like `https://localhost/#!/page1`
|
||||
- `m.route.prefix = '?'` – Using the querystring. A URL using this strategy typically looks like `https://localhost/?/page1`
|
||||
- `m.route.prefix = ''` – Using the pathname. A URL using this strategy typically looks like `https://localhost/page1`
|
||||
|
||||
Using the hash strategy is guaranteed to work in browsers that don't support `history.pushState`, because it can fall back to using `onhashchange`. Use this strategy if you want to keep the hashes purely local.
|
||||
|
||||
The querystring strategy allows server-side detection, but it doesn't appear as a normal path. Use this strategy if you want to support and potentially detect anchored links server-side and you are not able to make the changes necessary to support the pathname strategy (like if you're using Apache and can't modify your .htaccess).
|
||||
|
||||
The pathname strategy produces the cleanest looking URLs, but requires setting up the server to serve the single page application code from every URL that the application can route to. Use this strategy if you want cleaner-looking URLs.
|
||||
|
||||
Single page applications that use the hash strategy often use the convention of having an exclamation mark after the hash to indicate that they're using the hash as a routing mechanism and not for the purposes of linking to anchors. The `#!` string is known as a *hashbang*.
|
||||
|
||||
The default strategy uses the hashbang.
|
||||
|
||||
---
|
||||
|
||||
### Typical usage
|
||||
|
||||
Normally, you need to create a few [components](components.md) to map routes to:
|
||||
|
||||
```javascript
|
||||
var Home = {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Home")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var Page1 = {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Page 1")
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, there are two components: `Home` and `Page1`. Each contains a menu and some text. The menu is itself being defined as a component to avoid repetition:
|
||||
|
||||
```javascript
|
||||
var Menu = {
|
||||
view: function() {
|
||||
return m("nav", [
|
||||
m(m.route.Link, {href: "/"}, "Home"),
|
||||
m(m.route.Link, {href: "/page1"}, "Page 1"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now we can define routes and map our components to them:
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/": Home,
|
||||
"/page1": Page1,
|
||||
})
|
||||
```
|
||||
|
||||
Here we specify two routes: `/` and `/page1`, which render their respective components when the user navigates to each URL.
|
||||
|
||||
---
|
||||
|
||||
### Navigating to different routes
|
||||
|
||||
In the example above, the `Menu` component has two `m.route.Link`s. That creates an element, by default an `<a>`, and sets it up to where if the user clicks on it, it navigates to another route on its own. It doesn't navigate remotely, just locally.
|
||||
|
||||
You can also navigate programmatically, via `m.route.set(route)`. For example, `m.route.set("/page1")`.
|
||||
|
||||
When navigating between routes, the router prefix is handled for you. In other words, leave out the hashbang `#!` (or whatever prefix you set `m.route.prefix` to) when linking Mithril.js routes, including in both `m.route.set` and in `m.route.Link`.
|
||||
|
||||
Do note that when navigating between components, the entire subtree is replaced. Use [a route resolver with a `render` method](#routeresolverrender) if you want to just patch the subtree.
|
||||
|
||||
---
|
||||
|
||||
### Routing parameters
|
||||
|
||||
Sometimes we want to have a variable id or similar data appear in a route, but we don't want to explicitly specify a separate route for every possible id. In order to achieve that, Mithril.js supports [parameterized routes](paths.md#path-parameters):
|
||||
|
||||
```javascript
|
||||
var Edit = {
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Editing " + vnode.attrs.id)
|
||||
]
|
||||
}
|
||||
}
|
||||
m.route(document.body, "/edit/1", {
|
||||
"/edit/:id": Edit,
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, we defined a route `/edit/:id`. This creates a dynamic route that matches any URL that starts with `/edit/` and is followed by some data (e.g. `/edit/1`, `edit/234`, etc). The `id` value is then mapped as an attribute of the component's [vnode](vnodes.md) (`vnode.attrs.id`)
|
||||
|
||||
It's possible to have multiple arguments in a route, for example `/edit/:projectID/:userID` would yield the properties `projectID` and `userID` on the component's vnode attributes object.
|
||||
|
||||
#### Key parameter
|
||||
|
||||
When a user navigates from a parameterized route to the same route with a different parameter (e.g. going from `/page/1` to `/page/2` given a route `/page/:id`, the component would not be recreated from scratch since both routes resolve to the same component, and thus result in a virtual dom in-place diff. This has the side-effect of triggering the `onupdate` hook, rather than `oninit`/`oncreate`. However, it's relatively common for a developer to want to synchronize the recreation of the component to the route change event.
|
||||
|
||||
To achieve that, it's possible to combine route parameterization with [keys](#reinitializing-views-with-single-child-keyed-fragments) for a very convenient pattern:
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/edit/1", {
|
||||
"/edit/:key": Edit,
|
||||
})
|
||||
```
|
||||
|
||||
This means that the [vnode](vnodes.md) that is created for the root component of the route has a route parameter object `key`. Route parameters become `attrs` in the vnode. Thus, when jumping from one page to another, the `key` changes and causes the component to be recreated from scratch (since the key tells the virtual dom engine that old and new components are different entities).
|
||||
|
||||
You can take that idea further to create components that recreate themselves when reloaded:
|
||||
|
||||
`m.route.set(m.route.get(), {key: Date.now()})`
|
||||
|
||||
Or even use the [`history state`](#history-state) feature to achieve reloadable components without polluting the URL:
|
||||
|
||||
`m.route.set(m.route.get(), null, {state: {key: Date.now()}})`
|
||||
|
||||
Note that the key parameter works only for component routes. If you're using a route resolver, you'll need to use a [single-child keyed fragment](keys.md#reinitializing-views-with-single-child-keyed-fragments), passing `key: m.route.param("key")`, to accomplish the same.
|
||||
|
||||
#### Variadic routes
|
||||
|
||||
It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes:
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/edit/pictures/image.jpg", {
|
||||
"/edit/:file...": Edit,
|
||||
})
|
||||
```
|
||||
|
||||
#### Handling 404s
|
||||
|
||||
For isomorphic / universal JavaScript app, an url param and a variadic route combined is very useful to display custom 404 error page.
|
||||
|
||||
In a case of 404 Not Found error, the server send back the custom page to client. When Mithril.js is loaded, it will redirect client to the default route because it can't know that route.
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/": homeComponent,
|
||||
// [...]
|
||||
"/:404...": errorPageComponent
|
||||
});
|
||||
```
|
||||
|
||||
#### History state
|
||||
|
||||
It's possible to take full advantage of the underlying `history.pushState` API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form.
|
||||
|
||||
For example, you could create a form like this:
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
term: "",
|
||||
search: function() {
|
||||
// save the state for this route
|
||||
// this is equivalent to `history.replaceState({term: state.term}, null, location.href)`
|
||||
m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}})
|
||||
|
||||
// navigate away
|
||||
location.href = "https://google.com/?q=" + state.term
|
||||
}
|
||||
}
|
||||
|
||||
var Form = {
|
||||
oninit: function(vnode) {
|
||||
state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button
|
||||
},
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("input[placeholder='Search']", {
|
||||
oninput: function (e) { state.term = e.target.value },
|
||||
value: state.term
|
||||
}),
|
||||
m("button", {onclick: state.search}, "Search")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": Form,
|
||||
})
|
||||
```
|
||||
|
||||
This way, if the user searches and presses the back button to return to the application, the input will still be populated with the search term. This technique can improve the user experience of large forms and other apps where non-persisted state is laborious for a user to produce.
|
||||
|
||||
---
|
||||
|
||||
### Changing router prefix
|
||||
|
||||
The router prefix is a fragment of the URL that dictates the underlying [strategy](#routing-strategies) used by the router.
|
||||
|
||||
```javascript
|
||||
// set to pathname strategy
|
||||
m.route.prefix = ""
|
||||
|
||||
// set to querystring strategy
|
||||
m.route.prefix = "?"
|
||||
|
||||
// set to hash without bang
|
||||
m.route.prefix = "#"
|
||||
|
||||
// set to pathname strategy on a non-root URL
|
||||
// e.g. if the app lives under `https://localhost/my-app` and something else
|
||||
// lives under `https://localhost`
|
||||
m.route.prefix = "/my-app"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Advanced component resolution
|
||||
|
||||
Instead of mapping a component to a route, you can specify a RouteResolver object. A RouteResolver object contains a `onmatch()` and/or a `render()` method. Both methods are optional but at least one of them must be present.
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
onmatch: function(args, requestedPath, route) {
|
||||
return Home
|
||||
},
|
||||
render: function(vnode) {
|
||||
return vnode // equivalent to m(Home)
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
RouteResolvers are useful for implementing a variety of advanced routing use cases.
|
||||
|
||||
---
|
||||
|
||||
#### Wrapping a layout component
|
||||
|
||||
It's often desirable to wrap all or most of the routed components in a reusable shell (often called a "layout"). In order to do that, you first need to create a component that contains the common markup that will wrap around the various different components:
|
||||
|
||||
```javascript
|
||||
var Layout = {
|
||||
view: function(vnode) {
|
||||
return m(".layout", vnode.children)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the layout merely consists of a `<div class="layout">` that contains the children passed to the component, but in a real life scenario it could be as complex as needed.
|
||||
|
||||
One way to wrap the layout is to define an anonymous component in the routes map:
|
||||
|
||||
```javascript
|
||||
// example 1
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
view: function() {
|
||||
return m(Layout, m(Home))
|
||||
},
|
||||
},
|
||||
"/form": {
|
||||
view: function() {
|
||||
return m(Layout, m(Form))
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
However, note that because the top level component is an anonymous component, jumping from the `/` route to the `/form` route (or vice-versa) will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had [lifecycle methods](lifecycle-methods.md) defined, the `oninit` and `oncreate` hooks would fire on every route change. Depending on the application, this may or may not be desirable.
|
||||
|
||||
If you would prefer to have the Layout component be diffed and maintained intact rather than recreated from scratch, you should instead use a RouteResolver as the root object:
|
||||
|
||||
```javascript
|
||||
// example 2
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
render: function() {
|
||||
return m(Layout, m(Home))
|
||||
},
|
||||
},
|
||||
"/form": {
|
||||
render: function() {
|
||||
return m(Layout, m(Form))
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Note that in this case, if the Layout component has `oninit` and `oncreate` lifecycle methods, they would only fire on the first route change (assuming all routes use the same layout).
|
||||
|
||||
To clarify the difference between the two examples, example 1 is equivalent to this code:
|
||||
|
||||
```javascript
|
||||
// functionally equivalent to example 1
|
||||
var Anon1 = {
|
||||
view: function() {
|
||||
return m(Layout, m(Home))
|
||||
},
|
||||
}
|
||||
var Anon2 = {
|
||||
view: function() {
|
||||
return m(Layout, m(Form))
|
||||
},
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
render: function() {
|
||||
return m(Anon1)
|
||||
}
|
||||
},
|
||||
"/form": {
|
||||
render: function() {
|
||||
return m(Anon2)
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Since `Anon1` and `Anon2` are different components, their subtrees (including `Layout`) are recreated from scratch. This is also what happens when components are used directly without a RouteResolver.
|
||||
|
||||
In example 2, since `Layout` is the top-level component in both routes, the DOM for the `Layout` component is diffed (i.e. left intact if it has no changes), and only the change from `Home` to `Form` triggers a recreation of that subsection of the DOM.
|
||||
|
||||
---
|
||||
|
||||
#### Redirection
|
||||
|
||||
The RouteResolver's `onmatch` hook can be used to run logic before the top level component in a route is initialized. You can use either Mithril's `m.route.set()` or native HTML's `history` API. When redirecting with the `history` API, the `onmatch` hook must return a never-resolving Promise to prevent resolution of the matched route. `m.route.set()` cancels resolution of the matched route internally, so this isn't necessary with it.
|
||||
|
||||
##### Example: authentication
|
||||
|
||||
The example below shows how to implement a login wall that prevents users from seeing the `/secret` page unless they login.
|
||||
|
||||
```javascript
|
||||
var isLoggedIn = false
|
||||
|
||||
var Login = {
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("button[type=button]", {
|
||||
onclick: function() {
|
||||
isLoggedIn = true
|
||||
m.route.set("/secret")
|
||||
}
|
||||
}, "Login")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/secret", {
|
||||
"/secret": {
|
||||
onmatch: function() {
|
||||
if (!isLoggedIn) m.route.set("/login")
|
||||
else return Home
|
||||
}
|
||||
},
|
||||
"/login": Login
|
||||
})
|
||||
```
|
||||
|
||||
When the application loads, `onmatch` is called and since `isLoggedIn` is false, the application redirects to `/login`. Once the user pressed the login button, `isLoggedIn` would be set to true, and the application would redirect to `/secret`. The `onmatch` hook would run once again, and since `isLoggedIn` is true this time, the application would render the `Home` component.
|
||||
|
||||
For the sake of simplicity, in the example above, the user's logged in status is kept in a global variable, and that flag is merely toggled when the user clicks the login button. In a real life application, a user would obviously have to supply proper login credentials, and clicking the login button would trigger a request to a server to authenticate the user:
|
||||
|
||||
```javascript
|
||||
var Auth = {
|
||||
username: "",
|
||||
password: "",
|
||||
|
||||
setUsername: function(value) {
|
||||
Auth.username = value
|
||||
},
|
||||
setPassword: function(value) {
|
||||
Auth.password = value
|
||||
},
|
||||
login: function() {
|
||||
m.request({
|
||||
url: "/api/v1/auth",
|
||||
params: {username: Auth.username, password: Auth.password}
|
||||
}).then(function(data) {
|
||||
localStorage.setItem("auth-token", data.token)
|
||||
m.route.set("/secret")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var Login = {
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("input[type=text]", {
|
||||
oninput: function (e) { Auth.setUsername(e.target.value) },
|
||||
value: Auth.username
|
||||
}),
|
||||
m("input[type=password]", {
|
||||
oninput: function (e) { Auth.setPassword(e.target.value) },
|
||||
value: Auth.password
|
||||
}),
|
||||
m("button[type=button]", {onclick: Auth.login}, "Login")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/secret", {
|
||||
"/secret": {
|
||||
onmatch: function() {
|
||||
if (!localStorage.getItem("auth-token")) m.route.set("/login")
|
||||
else return Home
|
||||
}
|
||||
},
|
||||
"/login": Login
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Preloading data
|
||||
|
||||
Typically, a component can load data upon initialization. Loading data this way renders the component twice. The first render pass occurs upon routing, and the second fires after the request completes. Take care to note that `loadUsers()` returns a Promise, but any Promise returned by `oninit` is currently ignored. The second render pass comes from the [`background` option for `m.request`](request.md).
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
users: [],
|
||||
loadUsers: function() {
|
||||
return m.request("/api/v1/users").then(function(users) {
|
||||
state.users = users
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/user/list", {
|
||||
"/user/list": {
|
||||
oninit: state.loadUsers,
|
||||
view: function() {
|
||||
return state.users.length > 0 ? state.users.map(function(user) {
|
||||
return m("div", user.id)
|
||||
}) : "loading"
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, on the first render, the UI displays `"loading"` since `state.users` is an empty array before the request completes. Then, once data is available, the UI redraws and a list of user ids is shown.
|
||||
|
||||
RouteResolvers can be used as a mechanism to preload data before rendering a component in order to avoid UI flickering and thus bypassing the need for a loading indicator:
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
users: [],
|
||||
loadUsers: function() {
|
||||
return m.request("/api/v1/users").then(function(users) {
|
||||
state.users = users
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/user/list", {
|
||||
"/user/list": {
|
||||
onmatch: state.loadUsers,
|
||||
render: function() {
|
||||
return state.users.map(function(user) {
|
||||
return m("div", user.id)
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Above, `render` only runs after the request completes, making the ternary operator redundant.
|
||||
|
||||
---
|
||||
|
||||
#### Code splitting
|
||||
|
||||
In a large application, it may be desirable to download the code for each route on demand, rather than upfront. Dividing the codebase this way is known as code splitting or lazy loading. In Mithril.js, this can be accomplished by returning a promise from the `onmatch` hook:
|
||||
|
||||
At its most basic form, one could do the following:
|
||||
|
||||
```javascript
|
||||
// Home.js
|
||||
module.export = {
|
||||
view: function() {
|
||||
return [
|
||||
m(Menu),
|
||||
m("h1", "Home")
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// index.js
|
||||
function load(file) {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: file,
|
||||
extract: function(xhr) {
|
||||
return new Function("var module = {};" + xhr.responseText + ";return module.exports;")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
onmatch: function() {
|
||||
return load("Home.js")
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
However, realistically, in order for that to work on a production scale, it would be necessary to bundle all of the dependencies for the `Home.js` module into the file that is ultimately served by the server.
|
||||
|
||||
Fortunately, there are a number of tools that facilitate the task of bundling modules for lazy loading. Here's an example using [native dynamic `import(...)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import), supported by many bundlers:
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
onmatch: function() {
|
||||
return import('./Home.js')
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Typed routes
|
||||
|
||||
In certain advanced routing cases, you may want to constrain a value further than just the path itself, only matching something like a numeric ID. You can do that pretty easily by returning `m.route.SKIP` from a route.
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/view/:id": {
|
||||
onmatch: function(args) {
|
||||
if (!/^\d+$/.test(args.id)) return m.route.SKIP
|
||||
return ItemView
|
||||
},
|
||||
},
|
||||
"/view/:name": UserView,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Hidden routes
|
||||
|
||||
In rare circumstances, you may want to hide certain routes for some users, but not all. For instance, a user might be prohibited from viewing a particular user, and instead of showing a permission error, you'd rather pretend it doesn't exist and redirect to a 404 view instead. In this case, you can use `m.route.SKIP` to just pretend the route doesn't exist.
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/user/:id": {
|
||||
onmatch: function(args) {
|
||||
return Model.checkViewable(args.id).then(function(viewable) {
|
||||
return viewable ? UserView : m.route.SKIP
|
||||
})
|
||||
},
|
||||
},
|
||||
"/:404...": PageNotFound,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Route cancellation / blocking
|
||||
|
||||
RouteResolver `onmatch` can prevent route resolution by returning a promise that never resolves. This can be used to detect attempted redundant route resolutions and cancel them:
|
||||
|
||||
```javascript
|
||||
m.route(document.body, "/", {
|
||||
"/": {
|
||||
onmatch: function(args, requestedPath) {
|
||||
if (m.route.get() === requestedPath)
|
||||
return new Promise(function() {})
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Third-party integration
|
||||
|
||||
In certain situations, you may find yourself needing to interoperate with another framework like React. Here's how you do it:
|
||||
|
||||
- Define all your routes using `m.route` as normal, but make sure you only use it *once*. Multiple route points are not supported.
|
||||
- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. `m.route` uses `m.mount` internally to hook everything up, so it's not magic.
|
||||
|
||||
Here's an example with React:
|
||||
|
||||
```jsx
|
||||
class Child extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.root = React.createRef()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
m.route(this.root, "/", {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUnmount() {
|
||||
m.mount(this.root, null)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={this.root} />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And here's the rough equivalent with Vue:
|
||||
|
||||
```html
|
||||
<div ref="root"></div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
Vue.component("my-child", {
|
||||
template: `<div ref="root"></div>`,
|
||||
mounted: function() {
|
||||
m.route(this.$refs.root, "/", {
|
||||
// ...
|
||||
})
|
||||
},
|
||||
destroyed: function() {
|
||||
m.mount(this.$refs.root, null)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<!--meta-description
|
||||
Describes, how to read the signatures in Mithril.js' documentation.
|
||||
-->
|
||||
|
||||
# How to read signatures
|
||||
|
||||
Signature sections typically look like this:
|
||||
|
||||
`vnode = m(selector, attributes, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------ | -------- | ---
|
||||
`selector` | `String|Object` | Yes | A CSS selector or a component
|
||||
`attributes` | `Object` | No | HTML attributes or element properties
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md). Can be written as [splat arguments](signatures.md#splats)
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md)
|
||||
|
||||
The signature line above the table indicates the general syntax of the method, showing the name of the method, the order of its arguments and a suggested variable name for its return value.
|
||||
|
||||
The **Argument** column in the table indicates which part of the signature is explained by the respective table row. The `returns` row displays information about the return value of the method.
|
||||
|
||||
The **Type** column indicates the expected type for the argument.
|
||||
|
||||
A pipe (`|`) indicates that an argument is valid if it has any of the listed types. For example, `String|Object` indicates that `selector` can be a string OR an object.
|
||||
|
||||
Angled brackets (`< >`) after an `Array` indicate the expected type for array items. For exampe, `Array<String>` indicates that the argument must be an array and that all items in that array must be strings. Angled brackets after an `Object` indicate a map. For example, `Object<String,Component>` indicates that the argument must be an object, whose keys are strings and values are [components](components.md)
|
||||
|
||||
Sometimes non-native types may appear to indicate that a specific object signature is required. For example, `Vnode` is an object that has a [virtual DOM node](vnodes.md) structure.
|
||||
|
||||
The **Required** column indicates whether an argument is required or optional. If an argument is optional, you may set it to `null` or `undefined`, or omit it altogether, such that the next argument appears in its place.
|
||||
|
||||
---
|
||||
|
||||
### Optional arguments
|
||||
|
||||
Function arguments surrounded by square brackets `[ ]` are optional. In the example below, `url` is an optional argument:
|
||||
|
||||
`m.request([url,] options)`
|
||||
|
||||
---
|
||||
|
||||
### Splats
|
||||
|
||||
A splat argument means that if the argument is an array, you can omit the square brackets and have a variable number of arguments in the method instead.
|
||||
|
||||
In the example at the top, this means that `m("div", {id: "foo"}, ["a", "b", "c"])` can also be written as `m("div", {id: "foo"}, "a", "b", "c")`.
|
||||
|
||||
Splats are useful in some compile-to-JS languages such as CoffeeScript, and also allow helpful shorthands for some common use cases.
|
||||
|
||||
---
|
||||
|
||||
### Function signatures
|
||||
|
||||
Functions are denoted with an arrow (`->`). The left side of the arrow indicates the types of the input arguments and the right side indicates the type for the return value.
|
||||
|
||||
For example, `parseFloat` has the signature `String -> Number`, i.e. it takes a string as input and returns a number as output.
|
||||
|
||||
Functions with multiple arguments are denoted with parenthesis: `(String, Array) -> Number`
|
||||
|
||||
---
|
||||
|
||||
### Component signatures
|
||||
|
||||
Components are denoted via calls to `m`, but with the initial selector argument set to a constant named in the relevant prose:
|
||||
|
||||
`vnode = m(m.route.Link, attributes, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
--------------------- | ------------------------------------ | -------- | ---
|
||||
`attributes.href` | `Object` | Yes | The target route to navigate to.
|
||||
`attributes.selector` | `String|Object|Function` | No | This sets the tag name to use. Must be a valid selector for [`m`](hyperscript.md) if given, defaults to `"a"`.
|
||||
`attributes.options` | `Object` | No | This sets the options passed to [`m.route.set`](#mrouteset).
|
||||
`attributes` | `Object` | No | Other attributes to apply to the returned vnode may be passed.
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md) for this link.
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md).
|
||||
|
||||
Children here, if specified, are assumed to be able to be written as [splat arguments](#splats), unless otherwise specified in prose.
|
||||
|
||||
An element with no sensible children and/or attributes may choose to elide the relevant parameter entirely:
|
||||
|
||||
`vnode = m(Component, attributes)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------------- | -------- | -------- | ---
|
||||
`attributes.href` | `Object` | Yes | The
|
||||
`attributes` | `Object` | No | Other attributes to apply to the returned vnode
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md)
|
||||
|
|
@ -1,736 +0,0 @@
|
|||
<!--meta-description
|
||||
A complete walkthrough tutorial for building your first simple application in Mithril.js, from beginning to end
|
||||
-->
|
||||
|
||||
# Simple application
|
||||
|
||||
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 example of the end result can be seen [here](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcICqkBOBZA9ktAHQBWYIANCAGYCWMFqA2qAHYCGAthGpjgSJQyFarHxtEkvgDcO2AARYIigLwLgAHTYLdCqHTDxUC5gF1K2vfvwckAGUPGFNAK5sEdCQAoAlBqtrPWwIeFdsHS5iEIBHVwgjby0dINSeeAALQhNNEABxAFEAFVzLFNTrcKgckAz4eAAHMFQAehaQrgBaEKNOjga6YgyVfABrV36G4nEuFv66FtccMFLAir0AdzpMgGEQokk6DihmhXhseLL1gF9fNaDiTIg2bzcPeC8XntcoeH9k9bWZTYYgGIwKdTfX7EJAceAce7WW73a5le6wcIhSQmYCo+5QWxIExvTw+OhIf6I4KhcKRaIQOIJeBJKlpUJZIkKXKFEpUVlBKo1OqNZptDrdJl9AZDEbjSbTfCzeaLZYtXIKADUCnJV0Bui2u32zw+x1O50u-OR5QeTxeJI+PihfwC1vWwOmmONEIUTstd2teLY9zAHBkEGJ7lJL0prupYQiCiisXiiQBesT7OyXJAAAUMLzdXrBdnhU1Wu0IF0evApYNhtgxhMGlMZnMBiqVGA1SBNUocB7sFj4MQdfzrAAjQgATxM7oxg+NhcBBoye2QxqOJxM5ogS6R-qR2mu2io4GgcAdbCYIAAjKgbwBmECo9jcXjoYGOIwiU-iSTG2R5D7FQv3gb0010CQ6DYbZZ37Ak7FApcZDoCANgjd5Pj8F11hCeNIm8XJiCWFROjBeBSmAkFyOILh+leSNL28EjsBjdMfRpBMuG8JN8FcRBiEcNhRkoDQMhCGgahaZBtm7XsWJHJBRNgKAODAU4AHIWLIpxOm2SsNNRBQFPobAjAAOTfXt1XVLUFNUiy3wPCpbmc3RjzYDzn0oV8eD4YEADF8GwKJyF-CQpHgQDFEC4KuHA+4oJg5x7SwmQ2EICB-ndBCkG8dLMuIOFzjARTfEDawULQjCo2wiCgjw2lE0I2g4so+r2IkMBXHHLhYJcRisKynD2IqCBiAaEIw0kAARCAaA4H5mTc0agX7EMwz8McXO29zROYXbmtyVTxxIE7oEo3IAroMywM4Hhcl8PcKm43JoIafiRzYD74GYeApwaCBVEQAAPeAzGYBpVNgCAsigIhsFUa7boUe6IDMdrDusJKfpqpjhuAOdPUkYhTMcnhvXG+FsAAc1CYg5CgeIKtWhRGfiOCVAHIdSZu8mIEO25nrSFrzuEMXLpAew1Lut9HuFoJXpAd7PpV36oY4GG4YR1RpfBNGMaoEbVpx-i8aG7L+3nHmHPgSyKfUKn5Dp4d2YgFnVrdzmQWt41QRl+2BdjVIhcOpXx34+AJGICP6gkP6AaB7reu2Q3RNyABlUMBZAJ7+TMFaPK8l8QDRvhpanPjhzCsQIoA9A5EUCuq4S60qvQgbMJ8AqiDYipGq4lq6Og-3K-4yiDuDxWWs4GRaOeVwJ6xvRuN4-jxqEkTjdZ8T5pMDSWnIjTl727NgRWXPDoLhXrCVyAo0onvxtgDIGCQLEVusAuUSPE8S7L9A8oa4gD-JFPga8BKTXmnQEG3oNIAGJj5sAgRAbwSB8AYh4CTScSApzpxAIfJwmNrS5EIUYXIOJ+RYgRubHwfc9QDwIs3fioluKficL4T+egPJIiXKQmS8AWioHJBQ7e1hqEqFoS8J+9DASMOasw+ArDvCxRCqJJ+RV6hmU4ZaFEZQrTeV8u+EA0x1LhX-NIdAOC8EKC+j9USMdI4SBGjQCKJgMohWOAoG8AA2BosCABqKhYScAANyBDorTaCJgABMAAGfx4TPLaG0KPFuaZIk02id4hJsDFpRySboOiINOhbCQJkEwN44nVMSb-IMbB55sFcCNTJ2S4kKHaQ+XJSSPKpO0uREa5FOhGCnDAdxEgICFMTM7NpHScm1JSA0OwSBoI0xMHEnpKSGn9N0vpeKaZxya1GDTBs7hOTwIWpcjgUzJzYBod4-xCgwD4AMEgBQ8CkCfKmeIAk2ATDwIfICqZKywAaxnAoccBJYCjCma0tg6y5k3gWboJZnzVkmAAByPJvAAVmRWcCAYNOhEHENgOEnxxlsEmXUvpOAdK9D2agLIYZFBplBjWElwVyUSBMGclQBgqWbPqf7U6UARogrBSYSFGCYURJmfCuZ7S8UgyFaktWI1bn3KRbA55rz3mfKQDc4KCNuh2DoEsEwD58WThKWAOgAAvdFELjWkRtcCwwkqIVQtlSkOFCL2lVPxailZbA1nzNgbi-FZSKk5LiQAUlVQ02OUcdAHKOScvibBzkQBzUau5kiHk6peeSfVXzAiatImSlZFqFBWpVYEH5wV-mAofO60FqlwXQQFRATo0roWwvlf68NUzg1OsDRG5VUz2XErgFyy8lLqXJOFcmnlzKVAavTacrN-yIAYt3buoVz4LAgAFaMJgrBS5vnAdscSDBTxVD4KWUUixvrHIVLMPqmRsAMAAAIxOIHE4gAAWFon7b3CD6g04B-1AZ8DALAb9DQorXDMNcIAA)*
|
||||
|
||||
First let's create an entry point for the application. Create a file `index.html`:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>My Application</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="bin/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The `<!doctype html>` line indicates this is an HTML 5 document. The first `charset` meta tag indicates the encoding of the document and the `viewport` meta tag dictates how mobile browsers should scale the page. The `title` tag contains the text to be displayed on the browser tab for this application, and the `script` tag indicates what is the path to the JavaScript file that controls the application.
|
||||
|
||||
We could create the entire application in a single JavaScript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into *modules*, and assemble these modules into a *bundle* `bin/app.js`.
|
||||
|
||||
There are many ways to setup a bundler tool, but most are distributed via npm. In fact, most modern JavaScript libraries and tools are distributed that way, including Mithril. To download npm, [install Node.js](https://nodejs.org/en/); npm is installed automatically with it. Once you have Node.js and npm installed, open the command line and run this command:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
```
|
||||
|
||||
If npm is installed correctly, a file `package.json` will be created. This file will contain a skeleton project meta-description file. Feel free to edit the project and author information in this file.
|
||||
|
||||
---
|
||||
|
||||
To install Mithril.js, follow the instructions in the [installation](installation.md) page. Once you have a project skeleton with Mithril.js installed, we are ready to create the application.
|
||||
|
||||
Let's start by creating a module to store our state. Let's create a file called `src/models/User.js`
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var User = {
|
||||
list: []
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
```
|
||||
|
||||
Now let's add code to load some data from a server. To communicate with a server, we can use Mithril.js' XHR utility, `m.request`. First, we include Mithril.js in the module:
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var m = require("mithril")
|
||||
|
||||
var User = {
|
||||
list: []
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
```
|
||||
|
||||
Next we create a function that will trigger an XHR call. Let's call it `loadList`
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var m = require("mithril")
|
||||
|
||||
var User = {
|
||||
list: [],
|
||||
loadList: function() {
|
||||
// TODO: make XHR call
|
||||
}
|
||||
}
|
||||
|
||||
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 (DEAD LINK, FIXME: https //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")
|
||||
|
||||
var User = {
|
||||
list: [],
|
||||
loadList: function() {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users",
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(result) {
|
||||
User.list = result.data
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
```
|
||||
|
||||
The `method` option is an [HTTP method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods). To retrieve data from the server without causing side-effects on the server, we need to use the `GET` method. The `url` is the address for the API endpoint. The `withCredentials: true` line indicates that we're using cookies (which is a requirement for the REM API).
|
||||
|
||||
The `m.request` call returns a Promise that resolves to the data from the endpoint. By default, Mithril.js assumes a HTTP response body are in JSON format and automatically parses it into a JavaScript object or array. The `.then` callback runs when the XHR request completes. In this case, the callback assigns the `result.data` array to `User.list`.
|
||||
|
||||
Notice we also have a `return` statement in `loadList`. This is a general good practice when working with Promises, which allows us to register more callbacks to run after the completion of the XHR request.
|
||||
|
||||
This simple model exposes two members: `User.list` (an array of user objects), and `User.loadList` (a method that populates `User.list` with server data).
|
||||
|
||||
---
|
||||
|
||||
Now, let's create a view module so that we can display data from our User model module.
|
||||
|
||||
Create a file called `src/views/UserList.js`. First, let's include Mithril.js and our model, since we'll need to use both:
|
||||
|
||||
```javascript
|
||||
// src/views/UserList.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
```
|
||||
|
||||
Next, let's create a Mithril.js component. A component is simply an object that has a `view` method:
|
||||
|
||||
```javascript
|
||||
// src/views/UserList.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
// TODO add code here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By default, Mithril.js views are described using [hyperscript](hyperscript.md). Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and since its syntax is just JavaScript, it's possible to leverage a lot of JavaScript tooling ecosystem. For example:
|
||||
|
||||
- You can use [Babel](es6.md) to transpile ES6+ to ES5 for IE and to transpile [JSX](jsx.md) (an inline HTML-like syntax extension) to appropriate hyperscript calls.
|
||||
- You can use [ESLint](https://eslint.org/) for easy linting with no special plugins.
|
||||
- You can use [Terser](https://github.com/terser-js/terser) or [UglifyJS](https://github.com/mishoo/UglifyJS2) (ES5 only) to minify your code easily.
|
||||
- You can use [Istanbul](https://github.com/istanbuljs/nyc) for code coverage.
|
||||
- You can use [TypeScript](https://www.typescriptlang.org/) for easy code analysis. (There are [community-supported type definitions available](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mithril), so you don't need to roll your own.)
|
||||
|
||||
Let's start off with hyperscript and create a list of items. Hyperscript is the idiomatic way to use Mithril.js, but [JSX](jsx.md) works pretty similarly.
|
||||
|
||||
```javascript
|
||||
// src/views/UserList.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
return m(".user-list")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `".user-list"` string is a CSS selector, and as you would expect, `.user-list` represents a class. When a tag is not specified, `div` is the default. So this view is equivalent to `<div class="user-list"></div>`.
|
||||
|
||||
Now, let's reference the list of users from the model we created earlier (`User.list`) to dynamically loop through data:
|
||||
|
||||
```javascript
|
||||
// src/views/UserList.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
return m(".user-list", User.list.map(function(user) {
|
||||
return m(".user-list-item", user.firstName + " " + user.lastName)
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since `User.list` is a JavaScript array, and since hyperscript views are just JavaScript, we can loop through the array using the `.map` method. This creates an array of vnodes that represents a list of `div`s, each containing the name of a user.
|
||||
|
||||
The problem, of course, is that we never called the `User.loadList` function. Therefore, `User.list` is still an empty array, and thus this view would render a blank page. Since we want `User.loadList` to be called when we render this component, we can take advantage of component [lifecycle methods](lifecycle-methods.md):
|
||||
|
||||
```javascript
|
||||
// src/views/UserList.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
oninit: User.loadList,
|
||||
view: function() {
|
||||
return m(".user-list", User.list.map(function(user) {
|
||||
return m(".user-list-item", user.firstName + " " + user.lastName)
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we added an `oninit` method to the component, which references `User.loadList`. This means that when the component initializes, User.loadList will be called, triggering an XHR request. When the server returns a response, `User.list` gets populated.
|
||||
|
||||
Also notice we **didn't** do `oninit: User.loadList()` (with parentheses at the end). The difference is that `oninit: User.loadList()` calls the function once and immediately, but `oninit: User.loadList` only calls that function when the component renders. This is an important difference and a common pitfall for developers new to JavaScript: calling the function immediately means that the XHR request will fire as soon as the source code is evaluated, even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected.
|
||||
|
||||
---
|
||||
|
||||
Let's render the view from the entry point file `src/index.js` we created earlier:
|
||||
|
||||
```javascript
|
||||
// src/index.js
|
||||
var m = require("mithril")
|
||||
|
||||
var UserList = require("./views/UserList")
|
||||
|
||||
m.mount(document.body, UserList)
|
||||
```
|
||||
|
||||
The `m.mount` call renders the specified component (`UserList`) into a DOM element (`document.body`), erasing any DOM that was there previously. Opening the HTML file in a browser should now display a list of person names.
|
||||
|
||||
---
|
||||
|
||||
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:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>My Application</title>
|
||||
<link href="styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="bin/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
Reloading the browser window now should display some styled elements.
|
||||
|
||||
---
|
||||
|
||||
Let's add routing to our application.
|
||||
|
||||
Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril.js is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via JavaScript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML).
|
||||
|
||||
We can add routing by changing the `m.mount` call to a `m.route` call:
|
||||
|
||||
```javascript
|
||||
// src/index.js
|
||||
var m = require("mithril")
|
||||
|
||||
var UserList = require("./views/UserList")
|
||||
|
||||
m.route(document.body, "/list", {
|
||||
"/list": UserList
|
||||
})
|
||||
```
|
||||
|
||||
The `m.route` call specifies that the application will be rendered into `document.body`. The `"/list"` argument is the default route. That means the user will be redirected to that route if they land in a route that does not exist. The `{"/list": UserList}` object declares a map of existing routes, and what components each route resolves to.
|
||||
|
||||
Refreshing the page in the browser should now append `#!/list` to the URL to indicate that routing is working. Since that route render UserList, we should still see the list of people on screen as before.
|
||||
|
||||
The `#!` snippet is known as a hashbang, and it's a commonly used string for implementing client-side routing. It's possible to configure this string it via [`m.route.prefix`](route.md#mrouteprefix). Some configurations require supporting server-side changes, so we'll just continue using the hashbang for the rest of this tutorial.
|
||||
|
||||
---
|
||||
|
||||
Let's add another route to our application for editing users. First let's create a module called `views/UserForm.js`
|
||||
|
||||
```javascript
|
||||
// src/views/UserForm.js
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
// TODO implement view
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then we can `require` this new module from `src/index.js`
|
||||
|
||||
```javascript
|
||||
// src/index.js
|
||||
var m = require("mithril")
|
||||
|
||||
var UserList = require("./views/UserList")
|
||||
var UserForm = require("./views/UserForm")
|
||||
|
||||
m.route(document.body, "/list", {
|
||||
"/list": UserList
|
||||
})
|
||||
```
|
||||
|
||||
And finally, we can create a route that references it:
|
||||
|
||||
```javascript
|
||||
// src/index.js
|
||||
var m = require("mithril")
|
||||
|
||||
var UserList = require("./views/UserList")
|
||||
var UserForm = require("./views/UserForm")
|
||||
|
||||
m.route(document.body, "/list", {
|
||||
"/list": UserList,
|
||||
"/edit/:id": UserForm,
|
||||
})
|
||||
```
|
||||
|
||||
Notice that the new route has a `:id` in it. This is a route parameter; you can think of it as a wild card; the route `/edit/1` would resolve to `UserForm` with an `id` of `"1"`. `/edit/2` would also resolve to `UserForm`, but with an `id` of `"2"`. And so on.
|
||||
|
||||
Let's implement the `UserForm` component so that it can respond to those route parameters:
|
||||
|
||||
```javascript
|
||||
// src/views/UserForm.js
|
||||
var m = require("mithril")
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("label.label", "First name"),
|
||||
m("input.input[type=text][placeholder=First name]"),
|
||||
m("label.label", "Last name"),
|
||||
m("input.input[placeholder=Last name]"),
|
||||
m("button.button[type=submit]", "Save"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And let's add some more styles to `styles.css`:
|
||||
|
||||
```css
|
||||
/* styles.css */
|
||||
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;
|
||||
}
|
||||
|
||||
.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:
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var m = require("mithril")
|
||||
|
||||
var User = {
|
||||
list: [],
|
||||
loadList: function() {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users",
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(result) {
|
||||
User.list = result.data
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
```
|
||||
|
||||
Let's add code to allow us to load a single user
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var m = require("mithril")
|
||||
|
||||
var User = {
|
||||
list: [],
|
||||
loadList: function() {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users",
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(result) {
|
||||
User.list = result.data
|
||||
})
|
||||
},
|
||||
|
||||
current: {},
|
||||
load: function(id) {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users/" + id,
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(result) {
|
||||
User.current = result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
```
|
||||
|
||||
Notice we added a `User.current` property, and a `User.load(id)` method which populates that property. We can now populate the `UserForm` view using this new method:
|
||||
|
||||
```javascript
|
||||
// src/views/UserForm.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
oninit: function(vnode) {User.load(vnode.attrs.id)},
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("label.label", "First name"),
|
||||
m("input.input[type=text][placeholder=First name]", {value: User.current.firstName}),
|
||||
m("label.label", "Last name"),
|
||||
m("input.input[placeholder=Last name]", {value: User.current.lastName}),
|
||||
m("button.button[type=submit]", "Save"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Similar to the `UserList` component, `oninit` calls `User.load()`. Remember we had a route parameter called `:id` on the `"/edit/:id": UserForm` route? The route parameter becomes an attribute of the `UserForm` component's vnode, so routing to `/edit/1` would make `vnode.attrs.id` have a value of `"1"`.
|
||||
|
||||
Now, let's modify the `UserList` view so that we can navigate from there to a `UserForm`:
|
||||
|
||||
```javascript
|
||||
// src/views/UserList.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
oninit: User.loadList,
|
||||
view: function() {
|
||||
return m(".user-list", User.list.map(function(user) {
|
||||
return m(m.route.Link, {
|
||||
class: "user-list-item",
|
||||
href: "/edit/" + user.id,
|
||||
}, user.firstName + " " + user.lastName)
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we swapped out the `.user-list-item` vnode with an `m.route.Link` with that class and the same children. We added an `href` that references the route we want. What this means is that clicking the link would change the part of URL that comes after the hashbang `#!` (thus changing the route without unloading the current HTML page). Behind the scenes, it uses an `<a>` to implement the link, and it all just works.
|
||||
|
||||
If you refresh the page in the browser, you should now be able to click on a person and be taken to a form. You should also be able to press the back button in the browser to go back from the form to the list of people.
|
||||
|
||||
---
|
||||
|
||||
The form itself still doesn't save when you press "Save". Let's make this form work:
|
||||
|
||||
```javascript
|
||||
// src/views/UserForm.js
|
||||
var m = require("mithril")
|
||||
var User = require("../models/User")
|
||||
|
||||
module.exports = {
|
||||
oninit: function(vnode) {User.load(vnode.attrs.id)},
|
||||
view: function() {
|
||||
return m("form", {
|
||||
onsubmit: function(e) {
|
||||
e.preventDefault()
|
||||
User.save()
|
||||
}
|
||||
}, [
|
||||
m("label.label", "First name"),
|
||||
m("input.input[type=text][placeholder=First name]", {
|
||||
oninput: function (e) {User.current.firstName = e.target.value},
|
||||
value: User.current.firstName
|
||||
}),
|
||||
m("label.label", "Last name"),
|
||||
m("input.input[placeholder=Last name]", {
|
||||
oninput: function (e) {User.current.lastName = e.target.value},
|
||||
value: User.current.lastName
|
||||
}),
|
||||
m("button.button[type=submit]", "Save"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We added `oninput` events to both inputs, that set the `User.current.firstName` and `User.current.lastName` properties when a user types.
|
||||
|
||||
In addition, we declared that a `User.save` method should be called when the "Save" button is pressed. Let's implement that method:
|
||||
|
||||
```javascript
|
||||
// src/models/User.js
|
||||
var m = require("mithril")
|
||||
|
||||
var User = {
|
||||
list: [],
|
||||
loadList: function() {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users",
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(result) {
|
||||
User.list = result.data
|
||||
})
|
||||
},
|
||||
|
||||
current: {},
|
||||
load: function(id) {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users/" + id,
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(function(result) {
|
||||
User.current = result
|
||||
})
|
||||
},
|
||||
|
||||
save: function() {
|
||||
return m.request({
|
||||
method: "PUT",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users/" + User.current.id,
|
||||
body: User.current,
|
||||
withCredentials: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
```
|
||||
|
||||
In the `save` method at the bottom, we used the `PUT` HTTP method to indicate that we are upserting data to the server.
|
||||
|
||||
Now try editing the name of a user in the application. Once you save a change, you should be able to see the change reflected in the list of users.
|
||||
|
||||
---
|
||||
|
||||
Currently, we're only able to navigate back to the user list via the browser back button. Ideally, we would like to have a menu - or more generically, a layout where we can put global UI elements
|
||||
|
||||
Let's create a file `src/views/Layout.js`:
|
||||
|
||||
```javascript
|
||||
// src/views/Layout.js
|
||||
var m = require("mithril")
|
||||
|
||||
module.exports = {
|
||||
view: function(vnode) {
|
||||
return m("main.layout", [
|
||||
m("nav.menu", [
|
||||
m(m.route.Link, {href: "/list"}, "Users")
|
||||
]),
|
||||
m("section", vnode.children)
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This component is fairly straightforward, it has a `<nav>` with a link to the list of users. Similar to what we did to the `/edit` links, this link uses `m.route.Link` to create a routable link.
|
||||
|
||||
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.
|
||||
|
||||
And let's update the styles once more:
|
||||
|
||||
```css
|
||||
/* styles.css */
|
||||
body, .input, .button {
|
||||
font: normal 16px Verdana;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.layout {
|
||||
margin: 10px auto;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
|
||||
.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:
|
||||
|
||||
```javascript
|
||||
// src/index.js
|
||||
var m = require("mithril")
|
||||
|
||||
var UserList = require("./views/UserList")
|
||||
var UserForm = require("./views/UserForm")
|
||||
var Layout = require("./views/Layout")
|
||||
|
||||
m.route(document.body, "/list", {
|
||||
"/list": {
|
||||
render: function() {
|
||||
return m(Layout, m(UserList))
|
||||
}
|
||||
},
|
||||
"/edit/:id": {
|
||||
render: function(vnode) {
|
||||
return m(Layout, m(UserForm, vnode.attrs))
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
We replaced each component with a [RouteResolver](route.md#routeresolver) (basically, an object with a `render` method). The `render` methods can be written in the same way as regular component views would be, by nesting `m()` calls.
|
||||
|
||||
The interesting thing to pay attention to is how components can be used instead of a selector string in a `m()` call. Here, in the `/list` route, we have `m(Layout, m(UserList))`. This means there's a root vnode that represents an instance of `Layout`, which has a `UserList` vnode as its only child.
|
||||
|
||||
In the `/edit/:id` route, there's also a `vnode` argument that carries the route parameters into the `UserForm` component. So if the URL is `/edit/1`, then `vnode.attrs` in this case is `{id: 1}`, and this `m(UserForm, vnode.attrs)` is equivalent to `m(UserForm, {id: 1})`. The equivalent JSX code would be `<UserForm id={vnode.attrs.id} />`.
|
||||
|
||||
Refresh the page in the browser and now you'll see the global navigation on every page in the app.
|
||||
|
||||
---
|
||||
|
||||
This concludes the tutorial.
|
||||
|
||||
In this tutorial, we went through the process of creating a very simple application where we can list users from a server and edit them individually. As an extra exercise, try to implement user creation and deletion on your own.
|
||||
|
||||
If you want to see more examples of Mithril.js code, check the [examples](examples.md) page. If you have questions, feel free to drop by the [Mithril.js chat room](https://mithril.zulipchat.com/).
|
||||
593
docs/stream.md
|
|
@ -1,593 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.stream(), a reactive data structure provided optionally within Mithril.js
|
||||
-->
|
||||
|
||||
# stream()
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [Static members](#static-members)
|
||||
- [Stream.combine](#streamcombine)
|
||||
- [Stream.merge](#streammerge)
|
||||
- [Stream.scan](#streamscan)
|
||||
- [Stream.scanMerge](#streamscanmerge)
|
||||
- [Stream.lift](#streamlift)
|
||||
- [Stream.SKIP](#streamskip)
|
||||
- [Stream["fantasy-land/of"]](#streamfantasy-landof)
|
||||
- [Instance members](#instance-members)
|
||||
- [stream.map](#streammap)
|
||||
- [stream.end](#streamend)
|
||||
- [stream["fantasy-land/of"]](#streamfantasy-landof-1)
|
||||
- [stream["fantasy-land/map"]](#streamfantasy-landmap)
|
||||
- [stream["fantasy-land/ap"]](#streamfantasy-landap)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Streams as variables](#streams-as-variables)
|
||||
- [Bidirectional bindings](#bidirectional-bindings)
|
||||
- [Computed properties](#computed-properties)
|
||||
- [Chaining streams](#chaining-streams)
|
||||
- [Combining streams](#combining-streams)
|
||||
- [Stream states](#stream-states)
|
||||
- [Serializing streams](#serializing-streams)
|
||||
- [Streams do not trigger rendering](#streams-do-not-trigger-rendering)
|
||||
- [What is Fantasy Land](#what-is-fantasy-land)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
A Stream is a reactive data structure, similar to cells in spreadsheet applications.
|
||||
|
||||
For example, in a spreadsheet, if `A1 = B1 + C1`, then changing the value of `B1` or `C1` automatically changes the value of `A1`.
|
||||
|
||||
Similarly, you can make a stream depend on other streams so that changing the value of one automatically updates the other. This is useful when you have very expensive computations and want to only run them when necessary, as opposed to, say, on every redraw.
|
||||
|
||||
Streams are NOT bundled with Mithril.js' core distribution. To include the Streams module, use:
|
||||
|
||||
```javascript
|
||||
var Stream = require("mithril/stream")
|
||||
```
|
||||
|
||||
You can also download the module directly if your environment does not support a bundling toolchain:
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
|
||||
```
|
||||
|
||||
When loaded directly with a `<script>` tag (rather than required), the stream library will be exposed as `window.m.stream`. If `window.m` is already defined (e.g. because you also use the main Mithril.js script), it will attach itself to the existing object. Otherwise it creates a new `window.m`. If you want to use streams in conjunction with Mithril.js as raw script tags, you should include Mithril.js in your page before `mithril/stream`, because `mithril` will otherwise overwrite the `window.m` object defined by `mithril/stream`. This is not a concern when the libraries are consumed as CommonJS modules (using `require(...)`).
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
Creates a stream
|
||||
|
||||
`stream = Stream(value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the stream is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
#### Static members
|
||||
|
||||
##### Stream.combine
|
||||
|
||||
Creates a computed stream that reactively updates if any of its upstreams are updated. See [combining streams](#combining-streams)
|
||||
|
||||
`stream = Stream.combine(combiner, streams)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | --------------------------- | -------- | ---
|
||||
`combiner` | `(Stream..., Array) -> any` | Yes | See [combiner](#combiner) argument
|
||||
`streams` | `Array<Stream>` | Yes | A list of streams to be combined
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
###### combiner
|
||||
|
||||
Specifies how the value of a computed stream is generated. See [combining streams](#combining-streams)
|
||||
|
||||
`any = combiner(streams..., changed)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams...` | splat of `Streams` | No | Splat of zero or more streams that correspond to the streams passed as the second argument to [`stream.combine`](#stream-combine)
|
||||
`changed` | `Array<Stream>` | Yes | List of streams that were affected by an update
|
||||
**returns** | `any` | | Returns a computed value
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### Stream.merge
|
||||
|
||||
Creates a stream whose value is the array of values from an array of streams
|
||||
|
||||
`stream = Stream.merge(streams)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams` | `Array<Stream>` | Yes | A list of streams
|
||||
**returns** | `Stream` | | Returns a stream whose value is an array of input stream values
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### Stream.scan
|
||||
|
||||
Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value.
|
||||
|
||||
Note that you can prevent dependent streams from being updated by returning the special value `stream.SKIP` inside the accumulator function.
|
||||
|
||||
`stream = Stream.scan(fn, accumulator, stream)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------- | -------------------------------- | -------- | ---
|
||||
`fn` | `(accumulator, value) -> result \| SKIP` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value of the same type
|
||||
`accumulator` | `any` | Yes | The starting value for the accumulator
|
||||
`stream` | `Stream` | Yes | Stream containing the values
|
||||
**returns** | `Stream` | | Returns a new stream containing the result
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### Stream.scanMerge
|
||||
|
||||
Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream.
|
||||
|
||||
`stream = Stream.scanMerge(pairs, accumulator)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------- | ------------------------------------------------ | -------- | ---
|
||||
`pairs` | `Array<[Stream, (accumulator, value) -> value]>` | Yes | An array of tuples of stream and scan functions
|
||||
`accumulator` | `any` | Yes | The starting value for the accumulator
|
||||
**returns** | `Stream` | | Returns a new stream containing the result
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### Stream.lift
|
||||
|
||||
Creates a computed stream that reactively updates if any of its upstreams are updated. See [combining streams](#combining-streams). Unlike `combine`, the input streams are a variable number of arguments (instead of an array) and the callback receives the stream values instead of streams. There is no `changed` parameter. This is generally a more user-friendly function for applications than `combine`.
|
||||
|
||||
`stream = Stream.lift(lifter, stream1, stream2, ...)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | --------------------------- | -------- | ---
|
||||
`lifter` | `(any...) -> any` | Yes | See [lifter](#lifter) argument
|
||||
`streams...` | list of `Streams` | Yes | Streams to be lifted
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
###### lifter
|
||||
|
||||
Specifies how the value of a computed stream is generated. See [combining streams](#combining-streams)
|
||||
|
||||
`any = lifter(streams...)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams...` | splat of `Streams` | No | Splat of zero or more streams that correspond to the streams passed to [`stream.lift`](#stream-lift)
|
||||
**returns** | `any` | | Returns a computed value
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### Stream.SKIP
|
||||
|
||||
A special value that can be returned to stream callbacks to skip execution of downstreams
|
||||
|
||||
---
|
||||
|
||||
##### Stream["fantasy-land/of"]
|
||||
|
||||
This method is functionally identical to `stream`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = Stream["fantasy-land/of"](value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the stream is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
#### Instance members
|
||||
|
||||
##### stream.map
|
||||
|
||||
Creates a dependent stream whose value is set to the result of the callback function. This method is an alias of [stream["fantasy-land/map"]](#streamfantasy-landmap).
|
||||
|
||||
`dependentStream = stream().map(callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream.end
|
||||
|
||||
A co-dependent stream that unregisters dependent streams when set to true. See [ended state](#ended-state).
|
||||
|
||||
`endStream = stream().end`
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/of"]
|
||||
|
||||
This method is functionally identical to `stream`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = stream()["fantasy-land/of"](value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the stream is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/map"]
|
||||
|
||||
Creates a dependent stream whose value is set to the result of the callback function. See [chaining streams](#chaining-streams)
|
||||
|
||||
This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`dependentStream = stream()["fantasy-land/map"](callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/ap"]
|
||||
|
||||
The name of this method stands for `apply`. If a stream `a` has a function as its value, another stream `b` can use it as the argument to `b.ap(a)`. Calling `ap` will call the function with the value of stream `b` as its argument, and it will return another stream whose value is the result of the function call. This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = stream()["fantasy-land/ap"](apply)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`apply` | `Stream` | Yes | A stream whose value is a function
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
### Basic usage
|
||||
|
||||
Streams are not part of the core Mithril.js distribution. To include them in a project, require its module:
|
||||
|
||||
```javascript
|
||||
var stream = require("mithril/stream")
|
||||
```
|
||||
|
||||
|
||||
#### Streams as variables
|
||||
|
||||
`stream()` returns a stream. At its most basic level, a stream works similar to a variable or a getter-setter property: it can hold state, which can be modified.
|
||||
|
||||
```javascript
|
||||
var username = stream("John")
|
||||
console.log(username()) // logs "John"
|
||||
|
||||
username("John Doe")
|
||||
console.log(username()) // logs "John Doe"
|
||||
```
|
||||
|
||||
The main difference is that a stream is a function, and therefore can be composed into higher order functions.
|
||||
|
||||
```javascript
|
||||
var users = stream()
|
||||
|
||||
// request users from a server using the fetch API
|
||||
fetch("/api/users")
|
||||
.then(function(response) {return response.json()})
|
||||
.then(users)
|
||||
```
|
||||
|
||||
In the example above, the `users` stream is populated with the response data when the request resolves.
|
||||
|
||||
#### Bidirectional bindings
|
||||
|
||||
Streams can also be populated from event callbacks and similar.
|
||||
|
||||
```javascript
|
||||
// a stream
|
||||
var user = stream("")
|
||||
|
||||
// a bi-directional binding to the stream
|
||||
m("input", {
|
||||
oninput: function (e) { user(e.target.value) },
|
||||
value: user()
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, when the user types in the input, the `user` stream is updated to the value of the input field.
|
||||
|
||||
#### Computed properties
|
||||
|
||||
Streams are useful for implementing computed properties:
|
||||
|
||||
```javascript
|
||||
var title = stream("")
|
||||
var slug = title.map(function(value) {
|
||||
return value.toLowerCase().replace(/\W/g, "-")
|
||||
})
|
||||
|
||||
title("Hello world")
|
||||
console.log(slug()) // logs "hello-world"
|
||||
```
|
||||
|
||||
In the example above, the value of `slug` is computed when `title` is updated, not when `slug` is read.
|
||||
|
||||
It's of course also possible to compute properties based on multiple streams:
|
||||
|
||||
```javascript
|
||||
var firstName = stream("John")
|
||||
var lastName = stream("Doe")
|
||||
var fullName = stream.merge([firstName, lastName]).map(function(values) {
|
||||
return values.join(" ")
|
||||
})
|
||||
|
||||
console.log(fullName()) // logs "John Doe"
|
||||
|
||||
firstName("Mary")
|
||||
|
||||
console.log(fullName()) // logs "Mary Doe"
|
||||
```
|
||||
|
||||
Computed properties in Mithril.js are updated atomically: streams that depend on multiple streams will never be called more than once per value update, no matter how complex the computed property's dependency graph is.
|
||||
|
||||
---
|
||||
|
||||
### Chaining streams
|
||||
|
||||
Streams can be chained using the `map` method. A chained stream is also known as a *dependent stream*.
|
||||
|
||||
```javascript
|
||||
// parent stream
|
||||
var value = stream(1)
|
||||
|
||||
// dependent stream
|
||||
var doubled = value.map(function(value) {
|
||||
return value * 2
|
||||
})
|
||||
|
||||
console.log(doubled()) // logs 2
|
||||
```
|
||||
|
||||
Dependent streams are *reactive*: their values are updated any time the value of their parent stream is updated. This happens regardless of whether the dependent stream was created before or after the value of the parent stream was set.
|
||||
|
||||
You can prevent dependent streams from being updated by returning the special value `stream.SKIP`
|
||||
|
||||
```javascript
|
||||
var skipped = stream(1).map(function(value) {
|
||||
return stream.SKIP
|
||||
})
|
||||
|
||||
skipped.map(function() {
|
||||
// never runs
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Combining streams
|
||||
|
||||
Streams can depend on more than one parent stream. These kinds of streams can be created via `stream.merge()`
|
||||
|
||||
```javascript
|
||||
var a = stream("hello")
|
||||
var b = stream("world")
|
||||
|
||||
var greeting = stream.merge([a, b]).map(function(values) {
|
||||
return values.join(" ")
|
||||
})
|
||||
|
||||
console.log(greeting()) // logs "hello world"
|
||||
```
|
||||
|
||||
Or you can use the helper function `stream.lift()`
|
||||
|
||||
```javascript
|
||||
var a = stream("hello")
|
||||
var b = stream("world")
|
||||
|
||||
var greeting = stream.lift(function(_a, _b) {
|
||||
return _a + " " + _b
|
||||
}, a, b)
|
||||
|
||||
console.log(greeting()) // logs "hello world"
|
||||
```
|
||||
|
||||
There's also a lower level method called `stream.combine()` that exposes the stream themselves in the reactive computations for more advanced use cases
|
||||
|
||||
```javascript
|
||||
var a = stream(5)
|
||||
var b = stream(7)
|
||||
|
||||
var added = stream.combine(function(a, b) {
|
||||
return a() + b()
|
||||
}, [a, b])
|
||||
|
||||
console.log(added()) // logs 12
|
||||
```
|
||||
|
||||
A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also brings the performance benefits of not recomputing downstreams unnecessarily.
|
||||
|
||||
You can prevent dependent streams from being updated by returning the special value `stream.SKIP`
|
||||
|
||||
```javascript
|
||||
var skipped = stream.combine(function(stream) {
|
||||
return stream.SKIP
|
||||
}, [stream(1)])
|
||||
|
||||
skipped.map(function() {
|
||||
// never runs
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Stream states
|
||||
|
||||
At any given time, a stream can be in one of three states: *pending*, *active*, and *ended*.
|
||||
|
||||
#### Pending state
|
||||
|
||||
Pending streams can be created by calling `stream()` with no parameters.
|
||||
|
||||
```javascript
|
||||
var pending = stream()
|
||||
```
|
||||
|
||||
If a stream is dependent on more than one stream, any of its parent streams is in a pending state, the dependent streams is also in a pending state, and does not update its value.
|
||||
|
||||
```javascript
|
||||
var a = stream(5)
|
||||
var b = stream() // pending stream
|
||||
|
||||
var added = stream.combine(function(a, b) {
|
||||
return a() + b()
|
||||
}, [a, b])
|
||||
|
||||
console.log(added()) // logs undefined
|
||||
```
|
||||
|
||||
In the example above, `added` is a pending stream, because its parent `b` is also pending.
|
||||
|
||||
This also applies to dependent streams created via `stream.map`:
|
||||
|
||||
```javascript
|
||||
var value = stream()
|
||||
var doubled = value.map(function(value) {return value * 2})
|
||||
|
||||
console.log(doubled()) // logs undefined because `doubled` is pending
|
||||
```
|
||||
|
||||
#### Active state
|
||||
|
||||
When a stream receives a value, it becomes active (unless the stream is ended).
|
||||
|
||||
```javascript
|
||||
var stream1 = stream("hello") // stream1 is active
|
||||
|
||||
var stream2 = stream() // stream2 starts off pending
|
||||
stream2("world") // then becomes active
|
||||
```
|
||||
|
||||
A dependent stream with multiple parents becomes active if all of its parents are active.
|
||||
|
||||
```javascript
|
||||
var a = stream("hello")
|
||||
var b = stream()
|
||||
|
||||
var greeting = stream.merge([a, b]).map(function(values) {
|
||||
return values.join(" ")
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, the `a` stream is active, but `b` is pending. setting `b("world")` would cause `b` to become active, and therefore `greeting` would also become active, and be updated to have the value `"hello world"`
|
||||
|
||||
#### Ended state
|
||||
|
||||
A stream can stop affecting its dependent streams by calling `stream.end(true)`. This effectively removes the connection between a stream and its dependent streams.
|
||||
|
||||
```javascript
|
||||
var value = stream()
|
||||
var doubled = value.map(function(value) {return value * 2})
|
||||
|
||||
value.end(true) // set to ended state
|
||||
|
||||
value(5)
|
||||
|
||||
console.log(doubled())
|
||||
// logs undefined because `doubled` no longer depends on `value`
|
||||
```
|
||||
|
||||
Ended streams still have state container semantics, i.e. you can still use them as getter-setters, even after they are ended.
|
||||
|
||||
```javascript
|
||||
var value = stream(1)
|
||||
value.end(true) // set to ended state
|
||||
|
||||
console.log(value(1)) // logs 1
|
||||
|
||||
value(2)
|
||||
console.log(value()) // logs 2
|
||||
```
|
||||
|
||||
Ending a stream can be useful in cases where a stream has a limited lifetime (for example, reacting to `mousemove` events only while a DOM element is being dragged, but not after it's been dropped).
|
||||
|
||||
---
|
||||
|
||||
### Serializing streams
|
||||
|
||||
Streams implement a `.toJSON()` method. When a stream is passed as the argument to `JSON.stringify()`, the value of the stream is serialized.
|
||||
|
||||
```javascript
|
||||
var value = stream(123)
|
||||
var serialized = JSON.stringify(value)
|
||||
console.log(serialized) // logs 123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Streams do not trigger rendering
|
||||
|
||||
Unlike libraries like Knockout, Mithril.js streams do not trigger re-rendering of templates. Redrawing happens in response to event handlers defined in Mithril.js component views, route changes, or after [`m.request`](request.md) calls resolve.
|
||||
|
||||
If redrawing is desired in response to other asynchronous events (e.g. `setTimeout`/`setInterval`, websocket subscription, 3rd party library event handler, etc), you should manually call [`m.redraw()`](redraw.md)
|
||||
|
||||
---
|
||||
|
||||
### What is Fantasy Land
|
||||
|
||||
[Fantasy Land](https://github.com/fantasyland/fantasy-land) specifies interoperability of common algebraic structures. In plain english, that means that libraries that conform to Fantasy Land specs can be used to write generic functional style code that works regardless of how these libraries implement the constructs.
|
||||
|
||||
For example, say we want to create a generic function called `plusOne`. The naive implementation would look like this:
|
||||
|
||||
```javascript
|
||||
function plusOne(a) {
|
||||
return a + 1
|
||||
}
|
||||
```
|
||||
|
||||
The problem with this implementation is that it can only be used with a number. However it's possible that whatever logic produces a value for `a` could also produce an error state (wrapped in a Maybe or an Either from a library like [Sanctuary](https://github.com/sanctuary-js/sanctuary) or [Ramda-Fantasy](https://github.com/ramda/ramda-fantasy)), or it could be a Mithril.js stream, or a [flyd](https://github.com/paldepind/flyd) stream, etc. Ideally, we wouldn't want to write a similar version of the same function for every possible type that `a` could have and we wouldn't want to be writing wrapping/unwrapping/error handling code repeatedly.
|
||||
|
||||
This is where Fantasy Land can help. Let's rewrite that function in terms of a Fantasy Land algebra:
|
||||
|
||||
```javascript
|
||||
var fl = require("fantasy-land")
|
||||
|
||||
function plusOne(a) {
|
||||
return a[fl.map](function(value) {return value + 1})
|
||||
}
|
||||
```
|
||||
|
||||
Now this method works with any Fantasy Land compliant [Functor](https://github.com/fantasyland/fantasy-land#functor), such as [`R.Maybe`](https://github.com/ramda/ramda-fantasy/blob/master/docs/Maybe.md), [`S.Either`](https://github.com/sanctuary-js/sanctuary#either-type), `stream`, etc.
|
||||
|
||||
This example may seem convoluted, but it's a trade-off in complexity: the naive `plusOne` implementation makes sense if you have a simple system and only ever increment numbers, but the Fantasy Land implementation becomes more powerful if you have a large system with many wrapper abstractions and reused algorithms.
|
||||
|
||||
When deciding whether you should adopt Fantasy Land, you should consider your team's familiarity with functional programming, and be realistic regarding the level of discipline that your team can commit to maintaining code quality (vs the pressure of writing new features and meeting deadlines). Functional style programming heavily depends on compiling, curating and mastering a large set of small, precisely defined functions, and therefore it's not suitable for teams who do not have solid documentation practices, and/or lack experience in functional oriented languages.
|
||||
105
docs/style.css
|
|
@ -1,105 +0,0 @@
|
|||
body {background:white;-webkit-text-size-adjust: 100%;}
|
||||
body,table,h5 {font-weight:normal;font-size:16px;font-family:'Open Sans',sans-serif;}
|
||||
body>header,body>main {margin: auto;max-width:1000px;}
|
||||
header section {position:absolute;width:250px;}
|
||||
nav a {border-left:1px solid #ddd;padding:0 10px;}
|
||||
nav a:first-child {border:0;padding-left:0;}
|
||||
main {margin-bottom:100px;}
|
||||
main section {margin-left:270px;}
|
||||
p {margin:0 0 15px;}
|
||||
pre,code {background:#eee;font-family:monospace;font-size:14px;}
|
||||
pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;margin:20px 0;}
|
||||
code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:5px 3px;white-space:pre;}
|
||||
pre code {border:0;margin:0;padding:0;}
|
||||
table {border-collapse:collapse;margin:0 0 30px;width:100%;}
|
||||
tbody tr:nth-child(odd) {background:#fafafa;}
|
||||
thead tr,tbody tr:nth-child(even) {background:#f3f3f3;}
|
||||
tr {border-bottom:1px solid #eee;}
|
||||
th {text-align:left;}
|
||||
th,td {padding:3px 10px;vertical-align:top;}
|
||||
a {color:#1e5799;text-decoration:none;}
|
||||
a:hover {text-decoration:underline;}
|
||||
hr {border:0;border-bottom:1px solid #ddd;margin:30px 0;}
|
||||
|
||||
/* Headings */
|
||||
h1,h2,h3,h4,h5 {position:relative}
|
||||
h1 {font-size:24px;margin:0 0 15px;}
|
||||
h2 {font-size:22px;margin:45px 0 15px;}
|
||||
h3 {font-size:20px;margin:45px 0 15px;}
|
||||
h4 {font-size:18px;margin:30px 0 15px;}
|
||||
h5 {font-weight:bold;margin:15px 0 15px;}
|
||||
h1 img {vertical-align:middle;width:20px;}
|
||||
h1 small {font-size:16px;}
|
||||
h2 a,h3 a,h4 a,h5 a,
|
||||
h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,
|
||||
h2 a:active,h3 a:active,h4 a:active,h5 a:active,
|
||||
h2 a:visited,h3 a:visited,h4 a:visited,h5 a:visited {color:#000;text-decoration:none;}
|
||||
h2::before,h3::before,h4::before,h5::before {content:"#";position:absolute;left:-20px;visibility:hidden;}
|
||||
h2:hover::before,h3:hover::before,h4:hover::before,h5:hover::before {visibility:visible;}
|
||||
#signature + p code {padding:3px 10px;}
|
||||
h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;z-index:1;}
|
||||
h1 + ul + hr {display:none;}
|
||||
h1 + ul li {list-style:none;margin:0;padding:0;}
|
||||
h1 + ul li:last-child {border-bottom:0;}
|
||||
h1 + ul ul {margin:0 0 2px;padding:0 0 0 15px;}
|
||||
h1 + ul ul li {border:0;}
|
||||
h1 + ul strong + ul {border-left:3px solid #1e5799;}
|
||||
|
||||
.hamburger {display:none;}
|
||||
|
||||
@keyframes grow {
|
||||
from {transform:scaleX(0)}
|
||||
to {transform:scaleX(100%)}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.hamburger {display:block;font-size:30px;padding:0 10px;position:fixed;right:0;top:0;z-index:2;}
|
||||
.hamburger:hover {text-decoration:none;}
|
||||
main section {margin:0;}
|
||||
header section {margin:0 0 20px;position:static;width:auto;}
|
||||
h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;z-index:1}
|
||||
h1 + ul + hr {display:block;}
|
||||
.navigating h1 + ul {display:block;}
|
||||
.navigating {overflow:hidden;}
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
table,table tbody,table tr,table th,table td {display:block;}
|
||||
table thead {display:none;}
|
||||
table td:before {display:inline-block;font-style:italic;font-weight:bold;padding:0 10px 0 0;width:100px;}
|
||||
table tr:not(:last-child) td:nth-child(1):before {content:"Argument:";}
|
||||
table tr:last-child td:nth-child(3) {display:none;}
|
||||
table td:nth-child(2):before {content:"Type:";}
|
||||
table td:nth-child(3):before {content:"Required:";}
|
||||
table td:nth-child(4):before {content:"Description:";}
|
||||
#structure ~ table td:nth-child(1):before {content:"Property:";}
|
||||
#structure ~ table td:nth-child(2):before {content:"Type:";}
|
||||
#structure ~ table td:nth-child(3):before {content:"Description:";}
|
||||
#vnode-types ~ table td:nth-child(1):before {content:"Vnode type:";}
|
||||
#vnode-types ~ table td:nth-child(2):before {content:"Example:";}
|
||||
#vnode-types ~ table td:nth-child(3):before {content:"Description:";}
|
||||
#lifecycle-methods ~ table td:nth-child(1):before {content:"Hook:";}
|
||||
#lifecycle-methods ~ table td:nth-child(2):before {content:"Description:";}
|
||||
#react ~ table td:nth-child(1):before {content:"React:";}
|
||||
#angular ~ table td:nth-child(1):before {content:"Angular:";}
|
||||
#vue ~ table td:nth-child(1):before {content:"Vue:";}
|
||||
#comparisons ~ table td:nth-child(2):before {content:"Mithril:";}
|
||||
}
|
||||
@media print {
|
||||
nav,h1 + ul {display:none;}
|
||||
main section {margin:0;}
|
||||
}
|
||||
|
||||
/* prism theming */
|
||||
.token.comment,.token.prolog,.token.doctype,.token.cdata {color:#888;}
|
||||
.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol {color:#905;}
|
||||
.token.selector,.token.attr-name,.token.string,.token.builtin {color:#690;}
|
||||
.token.atrule,.token.attr-value,.token.punctuation,.token.keyword {color:#1e5799;}
|
||||
.token.regex,.token.important {color:#e90;}
|
||||
|
||||
/* flems theming */
|
||||
.flems main { margin: 0; max-width: auto; }
|
||||
.flems { margin: 20px 0; max-height: 400px; }
|
||||
.flems .runtime { border: 1px solid #ddd; }
|
||||
@media (max-width: 500px) {
|
||||
.flems { min-height: calc(100vw * 1.3); }
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!--meta-description
|
||||
Where to find help with problems related to Mithril.js
|
||||
-->
|
||||
|
||||
# Getting Help
|
||||
|
||||
Mithril.js has an active & welcoming community on [Zulip](https://mithril.zulipchat.com/), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag.
|
||||
178
docs/testing.md
|
|
@ -1,178 +0,0 @@
|
|||
<!--meta-description
|
||||
Approaches you can use to testing your Mithril.js-based apps, including technology and usability suggestions
|
||||
-->
|
||||
|
||||
# Testing
|
||||
|
||||
- [Setup](#setup)
|
||||
- [Best practices](#best-practices)
|
||||
- [Unit testing](#unit-testing)
|
||||
|
||||
---
|
||||
|
||||
### Setup
|
||||
|
||||
Testing Mithril.js applications is relatively easy. The easiest way to get started is with [ospec](https://github.com/MithrilJS/ospec), [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": "ospec"` 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 --require ./test-setup.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Create a setup file, `test-setup.js`, that looks like this:
|
||||
|
||||
```javascript
|
||||
var o = require("ospec")
|
||||
var jsdom = require("jsdom")
|
||||
var dom = new jsdom.JSDOM("", {
|
||||
// So we can get `requestAnimationFrame`
|
||||
pretendToBeVisual: true,
|
||||
})
|
||||
|
||||
// Fill in the globals Mithril.js 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.js 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")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
––––––
|
||||
All 1 assertions passed in 0ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Best practices
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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 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:
|
||||
|
||||
```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.js components, too. Suppose you have this component:
|
||||
|
||||
```javascript
|
||||
// MyComponent.js
|
||||
var m = require("mithril")
|
||||
|
||||
function MyComponent() {
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
vnode.attrs.type === "goodbye"
|
||||
? "Goodbye, world!"
|
||||
: "Hello, world!"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MyComponent
|
||||
```
|
||||
|
||||
You could easily create a few unit tests for that.
|
||||
|
||||
```javascript
|
||||
var mq = require("mithril-query")
|
||||
var MyComponent = require("./MyComponent")
|
||||
|
||||
o.spec("MyComponent", function() {
|
||||
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!")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
188
docs/trust.md
|
|
@ -1,188 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on m.trust(), a utility for rendering raw HTML and SVG within Mithril.js, along with tips on when to (and not to) use it
|
||||
-->
|
||||
|
||||
# trust(html)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Security considerations](#security-considerations)
|
||||
- [Scripts that do not run](#scripts-that-do-not-run)
|
||||
- [Avoid trusting HTML](#avoid-trusting-html)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Turns an HTML or SVG string into unescaped HTML or SVG. **Do not use `m.trust` on unsanitized user input.**
|
||||
|
||||
Always try to use an [alternative method](#avoid-trusting-html) first, before considering using `m.trust`.
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`vnode = m.trust(html)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`html` | `String` | Yes | A string containing HTML or SVG text
|
||||
**returns** | `Vnode` | | A trusted HTML [vnode](vnodes.md) that represents the input string
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
By default, Mithril.js escapes all values in order to prevent a class of security problems called [XSS injections](https://en.wikipedia.org/wiki/Cross-site_scripting).
|
||||
|
||||
```javascript
|
||||
var userContent = "<script>alert('evil')</script>"
|
||||
var view = m("div", userContent)
|
||||
|
||||
m.render(document.body, view)
|
||||
|
||||
// equivalent HTML
|
||||
// <div><script>alert('evil')</script></div>
|
||||
```
|
||||
|
||||
However, sometimes it is desirable to render rich text and formatting markup. To fill that need, `m.trust` creates trusted HTML [vnodes](vnodes.md) which are rendered as HTML.
|
||||
|
||||
```javascript
|
||||
var view = m("div", [
|
||||
m.trust("<h1>Here's some <em>HTML</em></h1>")
|
||||
])
|
||||
|
||||
m.render(document.body, view)
|
||||
|
||||
// equivalent HTML
|
||||
// <div><h1>Here's some <em>HTML</em></h1></div>
|
||||
```
|
||||
|
||||
Trusted HTML vnodes are objects, not strings; therefore they cannot be concatenated with regular strings.
|
||||
|
||||
---
|
||||
|
||||
### Security considerations
|
||||
|
||||
You **must sanitize the input** of `m.trust` to ensure there's no user-generated malicious code in the HTML string. If you don't sanitize an HTML string and mark it as a trusted string, any asynchronous JavaScript call points within the HTML string will be triggered and run with the authorization level of the user viewing the page.
|
||||
|
||||
There are many ways in which an HTML string may contain executable code. The most common ways to inject security attacks are to add an `onload` or `onerror` attributes in `<img>` or `<iframe>` tags, and to use unbalanced quotes such as `" onerror="alert(1)` to inject executable contexts in unsanitized string interpolations.
|
||||
|
||||
```javascript
|
||||
var data = {}
|
||||
|
||||
// Sample vulnerable HTML string
|
||||
var description = "<img alt='" + data.title + "'> <span>" + data.description + "</span>"
|
||||
|
||||
// An attack using JavaScript-related attributes
|
||||
data.description = "<img onload='alert(1)'>"
|
||||
|
||||
// An attack using unbalanced tags
|
||||
data.description = "</span><img onload='alert(1)'><span"
|
||||
|
||||
// An attack using unbalanced quotes
|
||||
data.title = "' onerror='alert(1)"
|
||||
|
||||
// An attack using a different attribute
|
||||
data.title = "' onmouseover='alert(1)"
|
||||
|
||||
// An attack that does not use JavaScript
|
||||
data.description = "<a href='https://evil.com/login-page-that-steals-passwords.html'>Click here to read more</a>"
|
||||
```
|
||||
|
||||
There are countless non-obvious ways of creating malicious code, so it is highly recommended that you use a [whitelist](https://en.wikipedia.org/wiki/Whitelist) of permitted HTML tags, attributes and attribute values, as opposed to a [blacklist](https://en.wikipedia.org/wiki/Blacklisting) to sanitize the user input. It's also highly recommended that you use a proper HTML parser, instead of regular expressions for sanitization, because regular expressions are extremely difficult to test for all edge cases.
|
||||
|
||||
---
|
||||
|
||||
### Scripts that do not run
|
||||
|
||||
Even though there are many obscure ways to make an HTML string run JavaScript, `<script>` tags are one thing that does not run when it appears in an HTML string.
|
||||
|
||||
For historical reasons, browsers ignore `<script>` tags that are inserted into the DOM via innerHTML. They do this because once the element is ready (and thus, has an accessible innerHTML property), the rendering engines cannot backtrack to the parsing-stage if the script calls something like `document.write("</body>")`.
|
||||
|
||||
This browser behavior may seem surprising to a developer coming from jQuery, because jQuery implements code specifically to find script tags and run them in this scenario. Mithril.js follows the browser behavior. If jQuery behavior is desired, you should consider either moving the code out of the HTML string and into an `oncreate` [lifecycle method](lifecycle-methods.md), or use jQuery (or re-implement its script parsing code).
|
||||
|
||||
---
|
||||
|
||||
### Avoid trusting HTML
|
||||
|
||||
As a general rule of thumb, you should avoid using `m.trust` unless you are explicitly rendering rich text and there's no other way to get the results that you want.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
m("div", m.trust("hello world"))
|
||||
|
||||
// PREFER
|
||||
m("div", "hello world")
|
||||
```
|
||||
|
||||
#### Avoid blind copying and pasting
|
||||
|
||||
One common way to misuse `m.trust` is when working with third party services whose tutorials include HTML code to be copied and pasted. In most cases, HTML should be written using vnodes (typically via the [`m()`](hyperscript.md) utility)
|
||||
|
||||
Here's the example snippet for the [Facebook Like button](https://developers.facebook.com/docs/plugins/like-button):
|
||||
|
||||
```html
|
||||
<!-- Load Facebook SDK for JavaScript -->
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<!-- Your like button code -->
|
||||
<div class="fb-like"
|
||||
data-href="https://www.your-domain.com/your-page.html"
|
||||
data-layout="standard"
|
||||
data-action="like"
|
||||
data-show-faces="true">
|
||||
</div>
|
||||
```
|
||||
|
||||
And here's how to refactor into a Mithril.js component in a way that avoids `m.trust`:
|
||||
|
||||
```javascript
|
||||
var FacebookLikeButton = {
|
||||
oncreate: function() {
|
||||
(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));
|
||||
},
|
||||
view: function() {
|
||||
return [
|
||||
m("#fb-root"),
|
||||
m("#fb-like[data-href=https://www.your-domain.com/your-page.html][data-layout=standard][data-action=like][data-show-faces=true]")
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The Mithril.js component above simply copies the script tag's code into the `oncreate` hook and declares the remaining HTML tags using Mithril.js' `m()` syntax.
|
||||
|
||||
#### Avoid HTML entities
|
||||
|
||||
A common way to misuse `m.trust` is to use it for HTML entities. A better approach is to use the corresponding unicode characters:
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
m("h1", "Coca-Cola", m.trust("™"))
|
||||
|
||||
// PREFER
|
||||
m("h1", "Coca-Cola™")
|
||||
```
|
||||
|
||||
Unicode characters for accented characters can be typed using a keyboard layout for an applicable language, and one may also choose to memorize keyboard shortcuts to produce commonly used symbols (e.g. `Alt+0153` in Windows, or `Option+2` on Mac for the ™ symbol). Another simple method to produce them is to simply copy and paste the desired character from a [unicode character table](https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references). Yet another related method is to type an escaped unicode codepoint (e.g. `"\u2122"` for the ™ symbol).
|
||||
|
||||
All characters that are representable as HTML entities have unicode counterparts, including non-visible characters such as ` ` and `­`.
|
||||
|
||||
To avoid encoding issues, you should set the file encoding to UTF-8 on the JavaScript file, as well as add the `<meta charset="utf-8">` meta tag in the host HTML file.
|
||||
177
docs/vnodes.md
|
|
@ -1,177 +0,0 @@
|
|||
<!--meta-description
|
||||
Documentation on Mithril.js' virtual DOM nodes (vnodes) and how they work
|
||||
-->
|
||||
|
||||
# Virtual DOM nodes
|
||||
|
||||
- [What is virtual DOM](#what-is-virtual-dom)
|
||||
- [Basics](#basics)
|
||||
- [Structure](#structure)
|
||||
- [Vnode types](#vnode-types)
|
||||
- [Monomorphic class](#monomorphic-class)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
### What is virtual DOM
|
||||
|
||||
A virtual DOM tree is a JavaScript data structure that describes a DOM tree. It consists of nested virtual DOM nodes, also known as *vnodes*.
|
||||
|
||||
The first time a virtual DOM tree is rendered, it is used as a blueprint to create a DOM tree that matches its structure.
|
||||
|
||||
Typically, virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril.js *diffs* a vnode tree against its previous version and only modifies DOM elements in spots where there are changes.
|
||||
|
||||
It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern JavaScript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.
|
||||
|
||||
For that reason, Mithril.js uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril.js *also* generates carefully crafted vnode data structures that are compiled by JavaScript engines for near-native data structure access performance. In addition, Mithril.js aggressively optimizes the function that creates vnodes as well.
|
||||
|
||||
The reason Mithril.js goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide a declarative [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics%29) API, a style of rendering that makes it drastically easier to manage UI complexity.
|
||||
|
||||
To illustrate why immediate mode is so important, consider the DOM API and HTML. The DOM API is an imperative [retained mode](https://en.wikipedia.org/wiki/Retained_mode) API and requires 1. writing out exact instructions to assemble a DOM tree procedurally, and 2. writing out other instructions to update that tree. The imperative nature of the DOM API means you have many opportunities to micro-optimize your code, but it also means that you have more chances of introducing bugs and more chances to make code harder to understand.
|
||||
|
||||
In contrast, HTML is closer to an immediate mode rendering system. With HTML, you can write a DOM tree in a far more natural and readable way, without worrying about forgetting to append a child to a parent, running into stack overflows when rendering extremely deep trees, etc.
|
||||
|
||||
Virtual DOM goes one step further than HTML by allowing you to write *dynamic* DOM trees without having to manually write multiple sets of DOM API calls to efficiently synchronize the UI to arbitrary data changes.
|
||||
|
||||
---
|
||||
|
||||
### Basics
|
||||
|
||||
Virtual DOM nodes, or *vnodes*, are JavaScript objects that represent DOM elements (or parts of the DOM). Mithril.js' virtual DOM engine consumes a tree of vnodes to produce a DOM tree.
|
||||
|
||||
Vnodes are created via the [`m()`](hyperscript.md) hyperscript utility:
|
||||
|
||||
```javascript
|
||||
m("div", {id: "test"}, "hello")
|
||||
```
|
||||
|
||||
Hyperscript can also consume [components](components.md):
|
||||
|
||||
```javascript
|
||||
// define a component
|
||||
var ExampleComponent = {
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.attrs, ["Hello ", vnode.children])
|
||||
}
|
||||
}
|
||||
|
||||
// consume it
|
||||
m(ExampleComponent, {style: "color:red;"}, "world")
|
||||
|
||||
// equivalent HTML:
|
||||
// <div style="color:red;">Hello world</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
Virtual DOM nodes, or *vnodes*, are JavaScript objects that represent an element (or parts of the DOM) and have the following properties:
|
||||
|
||||
Property | Type | Description
|
||||
---------- | -------------------------------- | ---
|
||||
`tag` | `String|Object` | The `nodeName` of a DOM element. It may also be the string `[` if a vnode is a fragment, `#` if it's a text vnode, or `<` if it's a trusted HTML vnode. Additionally, it may be a component.
|
||||
`key` | `String?` | The value used to map a DOM element to its respective item in a array of data.
|
||||
`attrs` | `Object?` | A hashmap of [DOM attributes](hyperscript.md#dom-attributes), [events](hyperscript.md#events), [properties](hyperscript.md#properties) and [lifecycle methods](hyperscript.md#lifecycle-methods).
|
||||
`children` | `(Array|String|Number|Boolean)?` | In most vnode types, the `children` property is an array of vnodes. For text and trusted HTML vnodes, The `children` property is either a string, a number or a boolean.
|
||||
`text` | `(String|Number|Boolean)?` | This is used instead of `children` if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the `text` property even if they have a text node as their only child.
|
||||
`dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragments and trusted HTML vnodes, `dom` points to the first element in the range.
|
||||
`domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property).
|
||||
`state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the `state` inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure.
|
||||
`events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril.js, do not use or modify it.
|
||||
`instance` | `Object?` | For components, a storage location for the value returned by the `view`. This property is only used internally by Mithril.js, do not use or modify it.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Vnode types
|
||||
|
||||
The `tag` property of a vnode determines its type. There are five vnode types:
|
||||
|
||||
Vnode type | Example | Description
|
||||
------------ | ------------------------------ | ---
|
||||
Element | `{tag: "div"}` | Represents a DOM element.
|
||||
Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode.
|
||||
Text | `{tag: "#", children: ""}` | Represents a DOM text node.
|
||||
Trusted HTML | `{tag: "<", children: "<br>"}` | Represents a list of DOM elements from an HTML string.
|
||||
Component | `{tag: ExampleComponent}` | If `tag` is a JavaScript object with a `view` method, the vnode represents the DOM generated by rendering the component.
|
||||
|
||||
Everything in a virtual DOM tree is a vnode, including text. The `m()` utility automatically normalizes its `children` argument and turns strings into text vnodes and nested arrays into fragment vnodes.
|
||||
|
||||
Only element tag names and components can be the first argument of the `m()` function. In other words, `[`, `#` and `<` are not valid `selector` arguments for `m()`. Trusted HTML vnodes can be created via [`m.trust()`](trust.md)
|
||||
|
||||
---
|
||||
|
||||
### Monomorphic class
|
||||
|
||||
The `mithril/render/vnode` module is used by Mithril.js to generate all vnodes. This ensures modern JavaScript engines can optimize virtual dom diffing by always compiling vnodes to the same hidden class.
|
||||
|
||||
When creating libraries that emit vnodes, you should use this module instead of writing naked JavaScript objects in order to ensure a high level of rendering performance.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
#### Avoid memoizing mutable vnodes
|
||||
|
||||
Vnodes are supposed to represent the state of the DOM at a certain point in time. Mithril.js' rendering engine assumes a reused vnode is unchanged, so modifying a vnode that was used in a previous render will result in undefined behavior.
|
||||
|
||||
It is possible to reuse vnodes to prevent a diff, but it's preferable to use the `onbeforeupdate` hook to make your intent clear to other developers (or your future self).
|
||||
|
||||
#### Avoid passing model data directly to components via attributes
|
||||
|
||||
The `key` property may appear in your data model in a way that conflicts with Mithril.js' key logic, and your model might itself be a mutable instance with a method that shares a name with a lifecycle hook like `onupdate` or `onremove`. For example, a model might use a `key` property to represent a customizable color key. When this changes, it can lead to components receiving wrong data, changing positions unexpectedly, or other unexpected, unwanted behavior. Instead, pass it as an attribute so Mithril.js doesn't misinterpret it (and so you still can potentially mutate it or call prototype methods on it later on):
|
||||
|
||||
```javascript
|
||||
// Data model
|
||||
var users = [
|
||||
{id: 1, name: "John", key: 'red'},
|
||||
{id: 2, name: "Mary", key: 'blue'},
|
||||
]
|
||||
|
||||
// Later on...
|
||||
users[0].key = 'yellow'
|
||||
|
||||
// AVOID
|
||||
users.map(function(user){
|
||||
// The component for John will be destroyed and recreated
|
||||
return m(UserComponent, user)
|
||||
})
|
||||
|
||||
// PREFER
|
||||
users.map(function(user){
|
||||
// Key is specifically extracted: data model is given its own property
|
||||
return m(UserComponent, {key: user.id, model: user})
|
||||
})
|
||||
```
|
||||
|
||||
#### Avoid statements in view methods
|
||||
|
||||
JavaScript statements in view methods often require changing the naturally nested structure of an HTML tree, making the code more verbose and less readable.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var BadListComponent = {
|
||||
view: function(vnode) {
|
||||
var list = []
|
||||
for (var i = 0; i < vnode.attrs.items.length; i++) {
|
||||
list.push(m("li", vnode.attrs.items[i]))
|
||||
}
|
||||
|
||||
return m("ul", list)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead, prefer using JavaScript expressions such as the ternary operator for conditional rendering and Array methods for list-like structures.
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var BetterListComponent = {
|
||||
view: function(vnode) {
|
||||
return m("ul", vnode.attrs.items.map(function(item) {
|
||||
return m("li", item)
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
Before Width: | Height: | Size: 59 KiB |
|
|
@ -1,73 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mosaic</title>
|
||||
<style>
|
||||
#root {margin:auto;max-width:600px;width:100%;}
|
||||
.slice {animation:enter 1s;animation-fill-mode:forwards;background-image:url(flowers.jpg);height:60px;float:left;opacity:0;width:60px;}
|
||||
.slice:nth-child(10n) {clear:right;}
|
||||
.exit {animation:exit 1s;}
|
||||
|
||||
@keyframes enter {
|
||||
from {opacity:0;transform:rotate(-90deg) scale(0);}
|
||||
to {opacity:1;transform:rotate(0) scale(1);}
|
||||
}
|
||||
@keyframes exit {
|
||||
from {opacity:1;transform:rotate(0) scale(1);}
|
||||
to {opacity:0;transform:rotate(90deg) scale(0);}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="../../mithril.js"></script>
|
||||
<script src="../../stream/stream.js"></script>
|
||||
<script>
|
||||
var root = document.getElementById("root")
|
||||
|
||||
const range = (start, end) => {
|
||||
let full = []
|
||||
for (let i = start; i < end; i++) full.push(i)
|
||||
return full
|
||||
}
|
||||
|
||||
const exit = vnode => {
|
||||
vnode.dom.classList.add("exit")
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
const backgroundPosition = step => i =>
|
||||
// X
|
||||
(i % step * (step+1)) + "% "
|
||||
// Y
|
||||
+ (Math.floor(i / step) * (step+1)) + "%"
|
||||
|
||||
// actions -> state -> vnode
|
||||
const view = exit => cells =>
|
||||
m(".container", cells.map(i =>
|
||||
m(".slice", {
|
||||
style: {backgroundPosition: backgroundPosition(10)(i)},
|
||||
onbeforeremove: exit
|
||||
})
|
||||
))
|
||||
|
||||
// delayed alternate array[0] and array[1] and pipe into actions stream
|
||||
const alternate = delay => array => actions => {
|
||||
let i = 1
|
||||
setInterval(() => actions(array[i = 1 - i]), delay)
|
||||
return actions
|
||||
}
|
||||
|
||||
// prepare stream alternating between empty and full
|
||||
alternate(2000)([[], range(0, 10 * 10)])(m.stream())
|
||||
// pipe state stream into view
|
||||
.map(view(exit))
|
||||
// pipe view stream into render
|
||||
.map(vnode => m.render(root, vnode))
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 59 KiB |
|
|
@ -1,61 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mosaic</title>
|
||||
<style>
|
||||
#root {margin:auto;max-width:600px;width:100%;}
|
||||
.slice {animation:enter 1s;animation-fill-mode:forwards;background-image:url(flowers.jpg);height:60px;float:left;opacity:0;width:60px;}
|
||||
.slice:nth-child(10n) {clear:right;}
|
||||
.exit {animation:exit 1s;}
|
||||
|
||||
@keyframes enter {
|
||||
from {opacity:0;transform:rotate(-90deg) scale(0);}
|
||||
to {opacity:1;transform:rotate(0) scale(1);}
|
||||
}
|
||||
@keyframes exit {
|
||||
from {opacity:1;transform:rotate(0) scale(1);}
|
||||
to {opacity:0;transform:rotate(90deg) scale(0);}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="../../mithril.js"></script>
|
||||
<script>
|
||||
var root = document.getElementById("root")
|
||||
|
||||
var empty = []
|
||||
var full = []
|
||||
for (var i = 0; i < 100; i++) full.push(i)
|
||||
|
||||
var cells
|
||||
|
||||
function view() {
|
||||
return m(".container", cells.map(function(i) {
|
||||
return m(".slice", {
|
||||
style: {backgroundPosition: (i % 10 * 11) + "% " + (Math.floor(i / 10) * 11) + "%"},
|
||||
onbeforeremove: exit
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function exit(vnode) {
|
||||
vnode.dom.classList.add("exit")
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
function run() {
|
||||
cells = cells === full ? empty : full
|
||||
|
||||
m.render(root, [view()])
|
||||
|
||||
setTimeout(run, 2000)
|
||||
}
|
||||
|
||||
run()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
var ENV = ENV || (function() {
|
||||
|
||||
var first = true;
|
||||
var counter = 0;
|
||||
var data;
|
||||
var _base;
|
||||
(_base = String.prototype).lpad || (_base.lpad = function(padding, toLength) {
|
||||
return padding.repeat((toLength - this.length) / padding.length).concat(this);
|
||||
});
|
||||
|
||||
function formatElapsed(value) {
|
||||
str = parseFloat(value).toFixed(2);
|
||||
if (value > 60) {
|
||||
minutes = Math.floor(value / 60);
|
||||
comps = (value % 60).toFixed(2).split('.');
|
||||
seconds = comps[0].lpad('0', 2);
|
||||
ms = comps[1];
|
||||
str = minutes + ":" + seconds + "." + ms;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function getElapsedClassName(elapsed) {
|
||||
var className = 'Query elapsed';
|
||||
if (elapsed >= 10.0) {
|
||||
className += ' warn_long';
|
||||
}
|
||||
else if (elapsed >= 1.0) {
|
||||
className += ' warn';
|
||||
}
|
||||
else {
|
||||
className += ' short';
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
function countClassName(queries) {
|
||||
var countClassName = "label";
|
||||
if (queries >= 20) {
|
||||
countClassName += " label-important";
|
||||
}
|
||||
else if (queries >= 10) {
|
||||
countClassName += " label-warning";
|
||||
}
|
||||
else {
|
||||
countClassName += " label-success";
|
||||
}
|
||||
return countClassName;
|
||||
}
|
||||
|
||||
function updateQuery(object) {
|
||||
if (!object) {
|
||||
object = {};
|
||||
}
|
||||
var elapsed = Math.random() * 15;
|
||||
object.elapsed = elapsed;
|
||||
object.formatElapsed = formatElapsed(elapsed);
|
||||
object.elapsedClassName = getElapsedClassName(elapsed);
|
||||
object.query = "SELECT blah FROM something";
|
||||
object.waiting = Math.random() < 0.5;
|
||||
if (Math.random() < 0.2) {
|
||||
object.query = "<IDLE> in transaction";
|
||||
}
|
||||
if (Math.random() < 0.1) {
|
||||
object.query = "vacuum";
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
function cleanQuery(value) {
|
||||
if (value) {
|
||||
value.formatElapsed = "";
|
||||
value.elapsedClassName = "";
|
||||
value.query = "";
|
||||
value.elapsed = null;
|
||||
value.waiting = null;
|
||||
} else {
|
||||
return {
|
||||
query: "***",
|
||||
formatElapsed: "",
|
||||
elapsedClassName: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function generateRow(object, keepIdentity, counter) {
|
||||
var nbQueries = Math.floor((Math.random() * 10) + 1);
|
||||
if (!object) {
|
||||
object = {};
|
||||
}
|
||||
object.lastMutationId = counter;
|
||||
object.nbQueries = nbQueries;
|
||||
if (!object.lastSample) {
|
||||
object.lastSample = {};
|
||||
}
|
||||
if (!object.lastSample.topFiveQueries) {
|
||||
object.lastSample.topFiveQueries = [];
|
||||
}
|
||||
if (keepIdentity) {
|
||||
// for Angular optimization
|
||||
if (!object.lastSample.queries) {
|
||||
object.lastSample.queries = [];
|
||||
for (var l = 0; l < 12; l++) {
|
||||
object.lastSample.queries[l] = cleanQuery();
|
||||
}
|
||||
}
|
||||
for (var j in object.lastSample.queries) {
|
||||
var value = object.lastSample.queries[j];
|
||||
if (j <= nbQueries) {
|
||||
updateQuery(value);
|
||||
} else {
|
||||
cleanQuery(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
object.lastSample.queries = [];
|
||||
for (var j = 0; j < 12; j++) {
|
||||
if (j < nbQueries) {
|
||||
var value = updateQuery(cleanQuery());
|
||||
object.lastSample.queries.push(value);
|
||||
} else {
|
||||
object.lastSample.queries.push(cleanQuery());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < 5; i++) {
|
||||
var source = object.lastSample.queries[i];
|
||||
object.lastSample.topFiveQueries[i] = source;
|
||||
}
|
||||
object.lastSample.nbQueries = nbQueries;
|
||||
object.lastSample.countClassName = countClassName(nbQueries);
|
||||
return object;
|
||||
}
|
||||
|
||||
function getData(keepIdentity) {
|
||||
var oldData = data;
|
||||
if (!keepIdentity) { // reset for each tick when !keepIdentity
|
||||
data = [];
|
||||
for (var i = 1; i <= ENV.rows; i++) {
|
||||
data.push({ dbname: 'cluster' + i, query: "", formatElapsed: "", elapsedClassName: "" });
|
||||
data.push({ dbname: 'cluster' + i + ' replica', query: "", formatElapsed: "", elapsedClassName: "" });
|
||||
}
|
||||
}
|
||||
if (!data) { // first init when keepIdentity
|
||||
data = [];
|
||||
for (var i = 1; i <= ENV.rows; i++) {
|
||||
data.push({ dbname: 'cluster' + i });
|
||||
data.push({ dbname: 'cluster' + i + ' replica' });
|
||||
}
|
||||
oldData = data;
|
||||
}
|
||||
for (var i in data) {
|
||||
var row = data[i];
|
||||
if (!keepIdentity && oldData && oldData[i]) {
|
||||
row.lastSample = oldData[i].lastSample;
|
||||
}
|
||||
if (!row.lastSample || Math.random() < ENV.mutations()) {
|
||||
counter = counter + 1;
|
||||
if (!keepIdentity) {
|
||||
row.lastSample = null;
|
||||
}
|
||||
generateRow(row, keepIdentity, counter);
|
||||
} else {
|
||||
data[i] = oldData[i];
|
||||
}
|
||||
}
|
||||
first = false;
|
||||
return {
|
||||
toArray: function() {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var mutationsValue = 0.5;
|
||||
|
||||
function mutations(value) {
|
||||
if (value) {
|
||||
mutationsValue = value;
|
||||
return mutationsValue;
|
||||
} else {
|
||||
return mutationsValue;
|
||||
}
|
||||
}
|
||||
|
||||
var body = document.querySelector('body');
|
||||
var theFirstChild = body.firstChild;
|
||||
|
||||
var sliderContainer = document.createElement( 'div' );
|
||||
sliderContainer.style.cssText = "display: flex";
|
||||
var slider = document.createElement('input');
|
||||
var text = document.createElement('label');
|
||||
text.innerHTML = 'mutations : ' + (mutationsValue * 100).toFixed(0) + '%';
|
||||
text.id = "ratioval";
|
||||
slider.setAttribute("type", "range");
|
||||
slider.style.cssText = 'margin-bottom: 10px; margin-top: 5px';
|
||||
slider.addEventListener('change', function(e) {
|
||||
ENV.mutations(e.target.value / 100);
|
||||
document.querySelector('#ratioval').innerHTML = 'mutations : ' + (ENV.mutations() * 100).toFixed(0) + '%';
|
||||
});
|
||||
sliderContainer.appendChild( text );
|
||||
sliderContainer.appendChild( slider );
|
||||
body.insertBefore( sliderContainer, theFirstChild );
|
||||
|
||||
return {
|
||||
generateData: getData,
|
||||
rows: 50,
|
||||
timeout: 0,
|
||||
mutations: mutations
|
||||
};
|
||||
})();
|
||||
56
examples/dbmonster/angular/app.js
vendored
|
|
@ -1,56 +0,0 @@
|
|||
var renderStage = 0
|
||||
perfMonitor.startFPSMonitor()
|
||||
perfMonitor.startMemMonitor()
|
||||
perfMonitor.initProfiler("render")
|
||||
|
||||
var AppComponent = ng.core.Component({selector: "my-app"})
|
||||
.View({
|
||||
directives: [ng.common.CORE_DIRECTIVES],
|
||||
template: "<div>" +
|
||||
"<table class='table table-striped latest-data'>" +
|
||||
"<tbody>" +
|
||||
"<tr *ngFor='let db of databases'>" +
|
||||
"<td class='dbname'>{{db.dbname}}</td>" +
|
||||
"<td class='query-count'>" +
|
||||
"<span [class]='db.lastSample.countClassName'>{{db.lastSample.nbQueries}}</span>" +
|
||||
"</td>" +
|
||||
"<td *ngFor='let q of db.lastSample.topFiveQueries' [class]='\"Query \" + q.elapsedClassName'>" +
|
||||
"{{q.formatElapsed}}" +
|
||||
"<div class='popover left'>" +
|
||||
"<div class='popover-content'>{{q.query}}</div>" +
|
||||
"<div class='arrow'></div>" +
|
||||
"</div>" +
|
||||
"</td>" +
|
||||
"</tr>" +
|
||||
"</tbody>" +
|
||||
"</table>" +
|
||||
"</div>"
|
||||
})
|
||||
.Class({
|
||||
constructor: function() {
|
||||
this.databases = []
|
||||
this.update()
|
||||
},
|
||||
update: function() {
|
||||
requestAnimationFrame(function() {self.update()})
|
||||
|
||||
var self = this
|
||||
self.databases = ENV.generateData().toArray()
|
||||
|
||||
if (renderStage === 0) {
|
||||
renderStage = 1
|
||||
perfMonitor.startProfile("render")
|
||||
}
|
||||
},
|
||||
ngAfterViewChecked: function() {
|
||||
if (renderStage === 1) {
|
||||
perfMonitor.endProfile("render")
|
||||
renderStage = 0
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
ng.core.enableProdMode()
|
||||
ng.platform.browser.bootstrap(AppComponent)
|
||||
})
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="../lib/bootstrap.min.css" rel="stylesheet" type="text/css" />
|
||||
<link href="../styles.css" rel="stylesheet" type="text/css" />
|
||||
<meta charset="utf-8">
|
||||
<title>dbmon (Angular2)</title>
|
||||
</head>
|
||||
<body>
|
||||
<my-app></my-app>
|
||||
<script src="https://code.angularjs.org/2.0.0-beta.17/angular2-polyfills.js"></script>
|
||||
<script src="https://code.angularjs.org/2.0.0-beta.17/Rx.umd.js"></script>
|
||||
<script src="https://code.angularjs.org/2.0.0-beta.17/angular2-all.umd.js"></script>
|
||||
<script src="../ENV.js"></script>
|
||||
<script src="https://localvoid.github.io/perf-monitor/0.1/perf-monitor.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
/**
|
||||
* @author mrdoob / http://mrdoob.com/
|
||||
* @author jetienne / http://jetienne.com/
|
||||
* @author paulirish / http://paulirish.com/
|
||||
*/
|
||||
var MemoryStats = function (){
|
||||
|
||||
var msMin = 100;
|
||||
var msMax = 0;
|
||||
|
||||
var container = document.createElement( 'div' );
|
||||
container.id = 'stats';
|
||||
container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer';
|
||||
|
||||
var msDiv = document.createElement( 'div' );
|
||||
msDiv.id = 'ms';
|
||||
msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;';
|
||||
container.appendChild( msDiv );
|
||||
|
||||
var msText = document.createElement( 'div' );
|
||||
msText.id = 'msText';
|
||||
msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px';
|
||||
msText.innerHTML= 'Memory';
|
||||
msDiv.appendChild( msText );
|
||||
|
||||
var msGraph = document.createElement( 'div' );
|
||||
msGraph.id = 'msGraph';
|
||||
msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0';
|
||||
msDiv.appendChild( msGraph );
|
||||
|
||||
while ( msGraph.children.length < 74 ) {
|
||||
|
||||
var bar = document.createElement( 'span' );
|
||||
bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131';
|
||||
msGraph.appendChild( bar );
|
||||
|
||||
}
|
||||
|
||||
var updateGraph = function ( dom, height, color ) {
|
||||
|
||||
var child = dom.appendChild( dom.firstChild );
|
||||
child.style.height = height + 'px';
|
||||
if( color ) child.style.backgroundColor = color;
|
||||
|
||||
}
|
||||
|
||||
var perf = window.performance || {};
|
||||
// polyfill usedJSHeapSize
|
||||
if (!perf && !perf.memory){
|
||||
perf.memory = { usedJSHeapSize : 0 };
|
||||
}
|
||||
if (perf && !perf.memory){
|
||||
perf.memory = { usedJSHeapSize : 0 };
|
||||
}
|
||||
|
||||
// support of the API?
|
||||
if( perf.memory.totalJSHeapSize === 0 ){
|
||||
console.warn('totalJSHeapSize === 0... performance.memory is only available in Chrome .')
|
||||
}
|
||||
|
||||
// TODO, add a sanity check to see if values are bucketed.
|
||||
// If so, reminde user to adopt the --enable-precise-memory-info flag.
|
||||
// open -a "/Applications/Google Chrome.app" --args --enable-precise-memory-info
|
||||
|
||||
var lastTime = Date.now();
|
||||
var lastUsedHeap= perf.memory.usedJSHeapSize;
|
||||
return {
|
||||
domElement: container,
|
||||
|
||||
update: function () {
|
||||
|
||||
// refresh only 30time per second
|
||||
if( Date.now() - lastTime < 1000/30 ) return;
|
||||
lastTime = Date.now()
|
||||
|
||||
var delta = perf.memory.usedJSHeapSize - lastUsedHeap;
|
||||
lastUsedHeap = perf.memory.usedJSHeapSize;
|
||||
var color = delta < 0 ? '#830' : '#131';
|
||||
|
||||
var ms = perf.memory.usedJSHeapSize;
|
||||
msMin = Math.min( msMin, ms );
|
||||
msMax = Math.max( msMax, ms );
|
||||
msText.textContent = "Mem: " + bytesToSize(ms, 2);
|
||||
|
||||
var normValue = ms / (30*1024*1024);
|
||||
var height = Math.min( 30, 30 - normValue * 30 );
|
||||
updateGraph( msGraph, height, color);
|
||||
|
||||
function bytesToSize( bytes, nFractDigit ){
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes == 0) return 'n/a';
|
||||
nFractDigit = nFractDigit !== undefined ? nFractDigit : 0;
|
||||
var precision = Math.pow(10, nFractDigit);
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes*precision / Math.pow(1024, i))/precision + ' ' + sizes[i];
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
perfMonitor.startFPSMonitor()
|
||||
perfMonitor.startMemMonitor()
|
||||
perfMonitor.initProfiler("render")
|
||||
|
||||
var data = []
|
||||
|
||||
m.mount(document.getElementById("app"), {
|
||||
view: function() {
|
||||
return m("div", [
|
||||
m("table", {className: "table table-striped latest-data"}, [
|
||||
m("tbody",
|
||||
data.map(function(db) {
|
||||
return m("tr", {key: db.dbname}, [
|
||||
m("td", {className: "dbname"}, db.dbname),
|
||||
m("td", {className: "query-count"}, [
|
||||
m("span", {className: db.lastSample.countClassName}, db.lastSample.nbQueries)
|
||||
]),
|
||||
db.lastSample.topFiveQueries.map(function(query) {
|
||||
return m("td", {className: query.elapsedClassName}, [
|
||||
query.formatElapsed,
|
||||
m("div", {className: "popover left"}, [
|
||||
m("div", {className: "popover-content"}, query.query),
|
||||
m("div", {className: "arrow"})
|
||||
])
|
||||
])
|
||||
})
|
||||
])
|
||||
})
|
||||
)
|
||||
])
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
function update() {
|
||||
requestAnimationFrame(update)
|
||||
|
||||
data = ENV.generateData().toArray()
|
||||
|
||||
perfMonitor.startProfile("render")
|
||||
m.redraw()
|
||||
perfMonitor.endProfile("render")
|
||||
}
|
||||
|
||||
update()
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="description" content="DBMON Mithril" />
|
||||
<meta charset="utf-8">
|
||||
<link href="../styles.css" rel="stylesheet" type="text/css" />
|
||||
<title>dbmon (Mithril)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="../../../mithril.js"></script>
|
||||
<script src="../ENV.js"></script>
|
||||
<script src="https://localvoid.github.io/perf-monitor/0.1/perf-monitor.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
var Monitoring = Monitoring || (function() {
|
||||
|
||||
var stats = new MemoryStats();
|
||||
stats.domElement.style.position = 'fixed';
|
||||
stats.domElement.style.right = '0px';
|
||||
stats.domElement.style.bottom = '0px';
|
||||
document.body.appendChild( stats.domElement );
|
||||
requestAnimationFrame(function rAFloop(){
|
||||
stats.update();
|
||||
requestAnimationFrame(rAFloop);
|
||||
});
|
||||
|
||||
var RenderRate = function () {
|
||||
var container = document.createElement( 'div' );
|
||||
container.id = 'stats';
|
||||
container.style.cssText = 'width:150px;opacity:0.9;cursor:pointer;position:fixed;right:80px;bottom:0px;';
|
||||
|
||||
var msDiv = document.createElement( 'div' );
|
||||
msDiv.id = 'ms';
|
||||
msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;';
|
||||
container.appendChild( msDiv );
|
||||
|
||||
var msText = document.createElement( 'div' );
|
||||
msText.id = 'msText';
|
||||
msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px';
|
||||
msText.innerHTML= 'Repaint rate: 0/sec';
|
||||
msDiv.appendChild( msText );
|
||||
|
||||
var bucketSize = 20;
|
||||
var bucket = [];
|
||||
var lastTime = Date.now();
|
||||
return {
|
||||
domElement: container,
|
||||
ping: function () {
|
||||
var start = lastTime;
|
||||
var stop = Date.now();
|
||||
var rate = 1000 / (stop - start);
|
||||
bucket.push(rate);
|
||||
if (bucket.length > bucketSize) {
|
||||
bucket.shift();
|
||||
}
|
||||
var sum = 0;
|
||||
for (var i = 0; i < bucket.length; i++) {
|
||||
sum = sum + bucket[i];
|
||||
}
|
||||
msText.textContent = "Repaint rate: " + (sum / bucket.length).toFixed(2) + "/sec";
|
||||
lastTime = stop;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var renderRate = new RenderRate();
|
||||
document.body.appendChild( renderRate.domElement );
|
||||
|
||||
return {
|
||||
memoryStats: stats,
|
||||
renderRate: renderRate
|
||||
};
|
||||
|
||||
})();
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
var h = React.createElement
|
||||
|
||||
perfMonitor.startFPSMonitor()
|
||||
perfMonitor.startMemMonitor()
|
||||
perfMonitor.initProfiler("render")
|
||||
|
||||
var data = []
|
||||
|
||||
var DBMon = React.createClass({
|
||||
render: function() {
|
||||
return h("div", null,
|
||||
h("table", {className: "table table-striped latest-data"},
|
||||
h("tbody", null,
|
||||
data.map(function(db) {
|
||||
return h("tr", {key: db.dbname},
|
||||
h("td", {className: "dbname"}, db.dbname),
|
||||
h("td", {className: "query-count"},
|
||||
h("span", {className: db.lastSample.countClassName}, db.lastSample.nbQueries)
|
||||
),
|
||||
db.lastSample.topFiveQueries.map(function(query, i) {
|
||||
return h("td", {key: i, className: query.elapsedClassName},
|
||||
query.formatElapsed,
|
||||
h("div", {className: "popover left"},
|
||||
h("div", {className: "popover-content"}, query.query),
|
||||
h("div", {className: "arrow"})
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
var root = document.getElementById("app")
|
||||
function update() {
|
||||
requestAnimationFrame(update)
|
||||
|
||||
data = ENV.generateData().toArray()
|
||||
|
||||
perfMonitor.startProfile("render")
|
||||
ReactDOM.render(h(DBMon, null), root)
|
||||
perfMonitor.endProfile("render")
|
||||
}
|
||||
|
||||
update()
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="description" content="DBMON Mithril" />
|
||||
<meta charset="utf-8">
|
||||
<link href="../styles.css" rel="stylesheet" type="text/css" />
|
||||
<title>dbmon (React)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://unpkg.com/react@15.3.2/dist/react.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@15.3.2/dist/react-dom.min.js"></script>
|
||||
<script src="../ENV.js"></script>
|
||||
<script src="https://localvoid.github.io/perf-monitor/0.1/perf-monitor.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||