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:
Isiah Meadows 2018-11-30 20:41:24 -05:00 committed by GitHub
parent 86c16820f7
commit 26b8d994ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 28 additions and 445 deletions

View file

@ -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

View file

@ -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)).

View file

@ -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", {

View file

@ -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)

View file

@ -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,
})
}
```

View file

@ -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")
])
}

View file

@ -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"),

View file

@ -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()
})
```

View file

@ -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
View file

@ -19,7 +19,6 @@ var namedExports = [
"fragment",
"mount",
"route",
"withAttr",
"render",
"redraw",
"request",

View file

@ -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))),

View file

@ -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

View file

@ -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")

View file

@ -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 }
}
}

View file

@ -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>

View file

@ -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)
})
})

View file

@ -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)
})
})

View file

@ -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))
}
}