From 6042b001f0f6c5ac1b4e5e13c8fdc5404c6d1f53 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Wed, 7 Nov 2018 12:18:55 -0500 Subject: [PATCH] Add `m.prop` (#2268) Fixes #2095 --- docs/api.md | 20 ++++++ docs/change-log.md | 1 + docs/nav-methods.md | 1 + docs/prop.md | 152 ++++++++++++++++++++++++++++++++++++++++ index.js | 1 + util/prop.js | 9 +++ util/tests/index.html | 4 +- util/tests/test-prop.js | 16 +++++ 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 docs/prop.md create mode 100644 util/prop.js create mode 100644 util/tests/test-prop.js diff --git a/docs/api.md b/docs/api.md index 872ab605..d25c8dec 100644 --- a/docs/api.md +++ b/docs/api.md @@ -143,6 +143,26 @@ 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 d547e955..3580b8f1 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -43,6 +43,7 @@ - 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)) #### Bug fixes diff --git a/docs/nav-methods.md b/docs/nav-methods.md index 06ae1f42..9b0d108f 100644 --- a/docs/nav-methods.md +++ b/docs/nav-methods.md @@ -8,6 +8,7 @@ - [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 new file mode 100644 index 00000000..f79afd40 --- /dev/null +++ b/docs/prop.md @@ -0,0 +1,152 @@ +# 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 +var Component = { + oninit: function(vnode) { + vnode.state.current = m.prop("") + }, + view: function(vnode) { + return m("input", { + oninput: m.withAttr("value", vnode.state.current.set), + value: vnode.state.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/index.js b/index.js index 123ba4ca..469a832b 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ 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/util/prop.js b/util/prop.js new file mode 100644 index 00000000..41b2b354 --- /dev/null +++ b/util/prop.js @@ -0,0 +1,9 @@ +"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 index a0295a7b..44c8c0b8 100644 --- a/util/tests/index.html +++ b/util/tests/index.html @@ -8,8 +8,10 @@ + - + + diff --git a/util/tests/test-prop.js b/util/tests/test-prop.js new file mode 100644 index 00000000..38950071 --- /dev/null +++ b/util/tests/test-prop.js @@ -0,0 +1,16 @@ +"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) + }) +})