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