Remove m.prop + m.withAttr (#2317)
* Remove `m.prop` + `m.withAttr` - For many uses, `m.withAttr` is *more* verbose than just directly using an event handler - If you're using it with a bound callback, you're literally wasting a single character in the human readable version (and you're *saving* them in the minified output). - It sometimes obscures your intent, if overused. - Functions are easier to compress than `m.withAttr`, resulting in slightly smaller bundles. - `m.withAttr` is overused anyways. - `m.prop` is basically useless without `m.withAttr`, and the API doesn't have the same benefits it had with 0.2.x. * Update changelog
This commit is contained in:
parent
86c16820f7
commit
26b8d994ce
18 changed files with 28 additions and 445 deletions
42
docs/api.md
42
docs/api.md
|
|
@ -121,48 +121,6 @@ var querystring = m.buildQueryString({a: "1", b: "2"})
|
|||
|
||||
---
|
||||
|
||||
#### m.withAttr(attrName, callback) - [docs](withAttr.md)
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
value: "",
|
||||
setValue: function(v) {state.value = v}
|
||||
}
|
||||
|
||||
var Component = {
|
||||
view: function() {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", state.setValue),
|
||||
value: state.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Component)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.prop(initial) - [docs](prop.md)
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
oninit: function(vnode) {
|
||||
vnode.state.current = m.prop("")
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("input", {
|
||||
oninput: function(ev) { vnode.state.current.set(ev.target.value) },
|
||||
value: vnode.state.current.get(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Component)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### m.trust(htmlString) - [docs](trust.md)
|
||||
|
||||
```javascript
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
|
||||
- cast className using toString ([#2309](https://github.com/MithrilJS/mithril.js/pull/2309))
|
||||
- render: call attrs' hooks first, with express exception of `onbeforeupdate` to allow attrs to block components from even diffing ([#2297](https://github.com/MithrilJS/mithril.js/pull/2297))
|
||||
- API: `m.withAttr` removed. ([#2317](https://github.com/MithrilJS/mithril.js/pull/2317))
|
||||
|
||||
#### News
|
||||
|
||||
|
|
@ -47,7 +48,6 @@
|
|||
- 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.
|
||||
- API: Introduction of `m.prop()` ([#2268](https://github.com/MithrilJS/mithril.js/pull/2268))
|
||||
- docs: Emphasize Closure Components for stateful components, use them for all stateful component examples.
|
||||
- stream: Add `stream.lift` as a user-friendly alternative to `merge -> map` or `combine` [#1944](https://github.com/MithrilJS/mithril.js/issues/1944)
|
||||
- API: ES module bundles are now available for `mithril` and `mithril/stream` ([#2194](https://github.com/MithrilJS/mithril.js/pull/2194) [@porsager](https://github.com/porsager)).
|
||||
|
|
|
|||
|
|
@ -365,8 +365,14 @@ var Login = {
|
|||
login: function() {/*...*/},
|
||||
view: function() {
|
||||
return m(".login", [
|
||||
m("input[type=text]", {oninput: m.withAttr("value", this.setUsername.bind(this)), value: this.username}),
|
||||
m("input[type=password]", {oninput: m.withAttr("value", this.setPassword.bind(this)), value: this.password}),
|
||||
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"),
|
||||
])
|
||||
}
|
||||
|
|
@ -411,11 +417,11 @@ var Login = {
|
|||
view: function() {
|
||||
return m(".login", [
|
||||
m("input[type=text]", {
|
||||
oninput: m.withAttr("value", Auth.setUsername),
|
||||
oninput: function (e) { Auth.setUsername(e.target.value) },
|
||||
value: Auth.username
|
||||
}),
|
||||
m("input[type=password]", {
|
||||
oninput: m.withAttr("value", Auth.setPassword),
|
||||
oninput: function (e) { Auth.setPassword(e.target.value) },
|
||||
value: Auth.password
|
||||
}),
|
||||
m("button", {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
- [m.jsonp](jsonp.md)
|
||||
- [m.parseQueryString](parseQueryString.md)
|
||||
- [m.buildQueryString](buildQueryString.md)
|
||||
- [m.withAttr](withAttr.md)
|
||||
- [m.prop](prop.md)
|
||||
- [m.trust](trust.md)
|
||||
- [m.fragment](fragment.md)
|
||||
- [m.redraw](redraw.md)
|
||||
|
|
|
|||
152
docs/prop.md
152
docs/prop.md
|
|
@ -1,152 +0,0 @@
|
|||
# prop(attrName, callback)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Sending through requests](#sending-through-requests)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Returns a simple getter/setter object.
|
||||
|
||||
```javascript
|
||||
var name = m.prop("John")
|
||||
|
||||
var oldName = name.get() // First, it's set to "John"
|
||||
name.set("Mary") // Set the value to "Mary"
|
||||
var newName = name.get() // Now it's "Mary", not "John"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`prop = m.prop(initial?)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | ------ | -------- | ---
|
||||
`initial` | `any` | No | The prop's initial value
|
||||
**returns** | `Prop` | | A prop
|
||||
|
||||
`value = prop.get()`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | ----- | -------- | ---
|
||||
**returns** | `any` | | The prop's current value
|
||||
|
||||
`newValue = prop.set(newValue)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | ----- | -------- | ---
|
||||
`newValue` | `any` | Yes | The value to set the prop to
|
||||
**returns** | `any` | | The value you just set the prop to, for convenience
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.prop` method creates a prop, a getter/setter object wrapping a single mutable reference. You can get the current value with `prop.get()` and set it with `prop.set(value)`. Unlike [streams](stream.md), you can't observe them, so you can't do as much with them.
|
||||
|
||||
In conjunction with [`m.withAttr`](withAttr.md), you can emulate two-way binding pretty easily.
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
var current = m.prop("")
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", current.set),
|
||||
value: current.get(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
They're also useful for making simpler models.
|
||||
|
||||
```javascript
|
||||
// With props
|
||||
var Auth = {
|
||||
username: m.prop(""),
|
||||
password: m.prop(""),
|
||||
canSubmit: function() {
|
||||
return Auth.username.get() !== "" && Auth.password.get() !== ""
|
||||
},
|
||||
login: function() {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
|
||||
// Without props
|
||||
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() {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sending through requests
|
||||
|
||||
For convenience, props define `.toJSON` as an alias for `.get`. This is so you can send them through `m.request` without serializing them manually.
|
||||
|
||||
We could also take this model and simplify it:
|
||||
|
||||
```javascript
|
||||
// How it's loaded
|
||||
User.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 = {
|
||||
id: result.id,
|
||||
firstName: m.prop(result.firstName),
|
||||
lastName: m.prop(result.lastName),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Original
|
||||
User.save = function(user) {
|
||||
return m.request({
|
||||
method: "PUT",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users/" + user.id,
|
||||
data: {
|
||||
id: user.id,
|
||||
firstName: user.firstName.get(),
|
||||
lastName: user.lastName.get(),
|
||||
},
|
||||
withCredentials: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Simplified
|
||||
User.save = function(user) {
|
||||
return m.request({
|
||||
method: "PUT",
|
||||
url: "https://rem-rest-api.herokuapp.com/api/users/" + user.id,
|
||||
data: user,
|
||||
withCredentials: true,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
|
@ -383,7 +383,10 @@ var Form = {
|
|||
},
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}),
|
||||
m("input[placeholder='Search']", {
|
||||
oninput: function (e) { state.term = e.target.value },
|
||||
value: state.term
|
||||
}),
|
||||
m("button", {onclick: state.search}, "Search")
|
||||
])
|
||||
}
|
||||
|
|
@ -589,8 +592,14 @@ var Auth = {
|
|||
var Login = {
|
||||
view: function() {
|
||||
return m("form", [
|
||||
m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
|
||||
m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
|
||||
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")
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -471,12 +471,12 @@ module.exports = {
|
|||
}, [
|
||||
m("label.label", "First name"),
|
||||
m("input.input[type=text][placeholder=First name]", {
|
||||
oninput: m.withAttr("value", function(value) {User.current.firstName = value}),
|
||||
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: m.withAttr("value", function(value) {User.current.lastName = value}),
|
||||
oninput: function (e) {User.current.lastName = e.target.value},
|
||||
value: User.current.lastName
|
||||
}),
|
||||
m("button.button[type=submit]", "Save"),
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ In the example above, the `users` stream is populated with the response data whe
|
|||
|
||||
#### Bidirectional bindings
|
||||
|
||||
Streams can also be populated from other higher order functions, such as [`m.withAttr`](withAttr.md)
|
||||
Streams can also be populated from event callbacks and similar.
|
||||
|
||||
```javascript
|
||||
// a stream
|
||||
|
|
@ -314,7 +314,7 @@ var user = stream("")
|
|||
|
||||
// a bi-directional binding to the stream
|
||||
m("input", {
|
||||
oninput: m.withAttr("value", user),
|
||||
oninput: function (e) { user(e.target.value) },
|
||||
value: user()
|
||||
})
|
||||
```
|
||||
|
|
|
|||
136
docs/withAttr.md
136
docs/withAttr.md
|
|
@ -1,136 +0,0 @@
|
|||
# withAttr(attrName, callback)
|
||||
|
||||
- [Description](#description)
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Predictable event target](#predictable-event-target)
|
||||
- [Attributes and properties](#attributes-and-properties)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
Returns an event handler that runs `callback` with the value of the specified DOM attribute
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
value: "",
|
||||
setValue: function(v) {state.value = v}
|
||||
}
|
||||
|
||||
var Component = {
|
||||
view: function() {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", state.setValue),
|
||||
value: state.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, Component)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`m.withAttr(attrName, callback, thisArg?)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`attrName` | `String` | Yes | The name of the attribute or property whose value will be used
|
||||
`callback` | `any -> undefined` | Yes | The callback
|
||||
`thisArg` | `any` | No | An object to bind to the `this` keyword in the callback function
|
||||
**returns** | `Event -> undefined` | | An event handler function
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
The `m.withAttr` method creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument.
|
||||
|
||||
This helper function is provided to help decouple the browser's event model from application code.
|
||||
|
||||
```javascript
|
||||
// standalone usage
|
||||
document.body.onclick = m.withAttr("title", function(value) {
|
||||
console.log(value) // logs the title of the <body> element when clicked
|
||||
})
|
||||
```
|
||||
|
||||
Typically, `m.withAttr()` can be used in Mithril component views to avoid polluting the data layer with DOM event model concerns:
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
email: "",
|
||||
setEmail: function(email) {
|
||||
state.email = email.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", state.setEmail),
|
||||
value: state.email
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Predictable event target
|
||||
|
||||
The `m.withAttr()` helper reads the value of the element to which the event handler is bound, which is not necessarily the same as the element where the event originated.
|
||||
|
||||
```javascript
|
||||
var state = {
|
||||
url: "",
|
||||
setURL: function(url) {state.url = url}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("a[href='/foo']", {onclick: m.withAttr("href", state.setURL)}, [
|
||||
m("span", state.url)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent)
|
||||
```
|
||||
|
||||
In the example above, if the user clicks on the text within the link, `e.target` will point to the `<span>`, not the `<a>`.
|
||||
|
||||
While this behavior works as per its specs, it's not very intuitive or useful most of the time. Therefore, `m.withAttr` uses the value of `e.currentTarget` which does point to the `<a>`, as one would normally expect.
|
||||
|
||||
---
|
||||
|
||||
### Attributes and properties
|
||||
|
||||
The first argument of `m.withAttr()` can be either an attribute or a property.
|
||||
|
||||
```javascript
|
||||
// reads from `select.selectedIndex` property
|
||||
var state = {
|
||||
index: 0,
|
||||
setIndex: function(index) {state.index = index}
|
||||
}
|
||||
m("select", {onclick: m.withAttr("selectedIndex", state.setIndex)})
|
||||
```
|
||||
|
||||
If a value can be both an attribute *and* a property, the property value is used.
|
||||
|
||||
```javascript
|
||||
// value is a boolean, because the `input.checked` property is boolean
|
||||
var state = {
|
||||
selected: false,
|
||||
setSelected: function(selected) {state.selected = selected}
|
||||
}
|
||||
m("input[type=checkbox]", {onclick: m.withAttr("checked", state.setSelected)})
|
||||
```
|
||||
1
esm.js
1
esm.js
|
|
@ -19,7 +19,6 @@ var namedExports = [
|
|||
"fragment",
|
||||
"mount",
|
||||
"route",
|
||||
"withAttr",
|
||||
"render",
|
||||
"redraw",
|
||||
"request",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ var Editor = {
|
|||
view: function() {
|
||||
return [
|
||||
m("textarea.input", {
|
||||
oninput: m.withAttr("value", state.update),
|
||||
oninput: function (e) { state.update(e.traget.value) },
|
||||
value: state.text
|
||||
}),
|
||||
m(".preview", m.trust(marked(state.text))),
|
||||
|
|
|
|||
2
index.js
2
index.js
|
|
@ -13,8 +13,6 @@ requestService.setCompletionCallback(redrawService.redraw)
|
|||
|
||||
m.mount = require("./mount")
|
||||
m.route = require("./route")
|
||||
m.withAttr = require("./util/withAttr")
|
||||
m.prop = require("./util/prop")
|
||||
m.render = require("./render").render
|
||||
m.redraw = redrawService.redraw
|
||||
m.request = requestService.request
|
||||
|
|
|
|||
|
|
@ -45,16 +45,6 @@ o.spec("api", function() {
|
|||
o(vnode.children[0].tag).equals("div")
|
||||
})
|
||||
})
|
||||
o.spec("m.withAttr", function() {
|
||||
o("works", function() {
|
||||
var spy = o.spy()
|
||||
var handler = m.withAttr("value", spy)
|
||||
|
||||
handler({currentTarget: {value: 10}})
|
||||
|
||||
o(spy.args[0]).equals(10)
|
||||
})
|
||||
})
|
||||
o.spec("m.parseQueryString", function() {
|
||||
o("works", function() {
|
||||
var query = m.parseQueryString("?a=1&b=2")
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
module.exports = function (store) {
|
||||
return {
|
||||
get: function() { return store },
|
||||
toJSON: function() { return store },
|
||||
set: function(value) { return store = value }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<script src="../../module/module.js"></script>
|
||||
<script src="../../ospec/ospec.js"></script>
|
||||
|
||||
<script src="../../util/withAttr.js"></script>
|
||||
<script src="../../util/prop.js"></script>
|
||||
<script src="test-withAttr.js"></script>
|
||||
<script src="test-prop.js"></script>
|
||||
|
||||
<script>require("../../ospec/ospec").run()</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
var o = require("../../ospec/ospec")
|
||||
var prop = require("../../util/prop")
|
||||
|
||||
o.spec("prop", function() {
|
||||
o("works", function() {
|
||||
var p = prop(1)
|
||||
|
||||
o(p.get()).equals(1)
|
||||
o(p.toJSON()).equals(1)
|
||||
o(p.set(2)).equals(2)
|
||||
o(p.get()).equals(2)
|
||||
o(p.toJSON()).equals(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
var o = require("../../ospec/ospec")
|
||||
var withAttr = require("../../util/withAttr")
|
||||
|
||||
o.spec("withAttr", function() {
|
||||
o("works", function() {
|
||||
var spy = o.spy()
|
||||
var context = {
|
||||
handler: withAttr("value", spy)
|
||||
}
|
||||
context.handler({currentTarget: {value: 1}})
|
||||
|
||||
o(spy.args).deepEquals([1])
|
||||
o(spy.this).equals(context)
|
||||
})
|
||||
o("works with attribute", function() {
|
||||
var target = {
|
||||
getAttribute: function() {return "readonly"}
|
||||
}
|
||||
var spy = o.spy()
|
||||
var context = {
|
||||
handler: withAttr("readonly", spy)
|
||||
}
|
||||
context.handler({currentTarget: target})
|
||||
|
||||
o(spy.args).deepEquals(["readonly"])
|
||||
o(spy.this).equals(context)
|
||||
})
|
||||
o("context arg works", function() {
|
||||
var spy = o.spy()
|
||||
var context = {}
|
||||
var handler = withAttr("value", spy, context)
|
||||
handler({currentTarget: {value: 1}})
|
||||
|
||||
o(spy.this).equals(context)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
module.exports = function(attrName, callback, context) {
|
||||
return function(e) {
|
||||
callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue