From 26b8d994cef6941df88dd3b22a579f36c3bfcf61 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 30 Nov 2018 20:41:24 -0500 Subject: [PATCH] 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 --- docs/api.md | 42 ---------- docs/change-log.md | 2 +- docs/components.md | 14 +++- docs/nav-methods.md | 2 - docs/prop.md | 152 ------------------------------------ docs/route.md | 15 +++- docs/simple-application.md | 4 +- docs/stream.md | 4 +- docs/withAttr.md | 136 -------------------------------- esm.js | 1 - examples/editor/index.html | 2 +- index.js | 2 - tests/test-api.js | 10 --- util/prop.js | 9 --- util/tests/index.html | 17 ---- util/tests/test-prop.js | 16 ---- util/tests/test-withAttr.js | 38 --------- util/withAttr.js | 7 -- 18 files changed, 28 insertions(+), 445 deletions(-) delete mode 100644 docs/prop.md delete mode 100644 docs/withAttr.md delete mode 100644 util/prop.js delete mode 100644 util/tests/index.html delete mode 100644 util/tests/test-prop.js delete mode 100644 util/tests/test-withAttr.js delete mode 100644 util/withAttr.js diff --git a/docs/api.md b/docs/api.md index d25c8dec..53b54d9f 100644 --- a/docs/api.md +++ b/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 diff --git a/docs/change-log.md b/docs/change-log.md index 9f61c7cf..e6b73d92 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -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)). diff --git a/docs/components.md b/docs/components.md index 1c1cadf7..3dccc5c1 100644 --- a/docs/components.md +++ b/docs/components.md @@ -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", { diff --git a/docs/nav-methods.md b/docs/nav-methods.md index 9b0d108f..ed0d7af7 100644 --- a/docs/nav-methods.md +++ b/docs/nav-methods.md @@ -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) diff --git a/docs/prop.md b/docs/prop.md deleted file mode 100644 index 30b8b2d7..00000000 --- a/docs/prop.md +++ /dev/null @@ -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, - }) -} -``` diff --git a/docs/route.md b/docs/route.md index 07357a8d..a93199c5 100644 --- a/docs/route.md +++ b/docs/route.md @@ -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") ]) } diff --git a/docs/simple-application.md b/docs/simple-application.md index baa46d12..3482caa9 100644 --- a/docs/simple-application.md +++ b/docs/simple-application.md @@ -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"), diff --git a/docs/stream.md b/docs/stream.md index d49b1fd2..0e318f30 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -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() }) ``` diff --git a/docs/withAttr.md b/docs/withAttr.md deleted file mode 100644 index 53dfd9c9..00000000 --- a/docs/withAttr.md +++ /dev/null @@ -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 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 ``, not the ``. - -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 ``, 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)}) -``` diff --git a/esm.js b/esm.js index 07dd3f95..97dbabd2 100644 --- a/esm.js +++ b/esm.js @@ -19,7 +19,6 @@ var namedExports = [ "fragment", "mount", "route", - "withAttr", "render", "redraw", "request", diff --git a/examples/editor/index.html b/examples/editor/index.html index 42eceb2e..a246a667 100644 --- a/examples/editor/index.html +++ b/examples/editor/index.html @@ -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))), diff --git a/index.js b/index.js index 3d86749d..9e422940 100644 --- a/index.js +++ b/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 diff --git a/tests/test-api.js b/tests/test-api.js index d298b4e1..1304542f 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -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") diff --git a/util/prop.js b/util/prop.js deleted file mode 100644 index 41b2b354..00000000 --- a/util/prop.js +++ /dev/null @@ -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 } - } -} diff --git a/util/tests/index.html b/util/tests/index.html deleted file mode 100644 index 44c8c0b8..00000000 --- a/util/tests/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/util/tests/test-prop.js b/util/tests/test-prop.js deleted file mode 100644 index 38950071..00000000 --- a/util/tests/test-prop.js +++ /dev/null @@ -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) - }) -}) diff --git a/util/tests/test-withAttr.js b/util/tests/test-withAttr.js deleted file mode 100644 index e9d21d5d..00000000 --- a/util/tests/test-withAttr.js +++ /dev/null @@ -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) - }) -}) diff --git a/util/withAttr.js b/util/withAttr.js deleted file mode 100644 index 5df4332a..00000000 --- a/util/withAttr.js +++ /dev/null @@ -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)) - } -}