Skip to content

Commit 3ef3bc3

Browse files
authored
feat(atom): Implement Atom save hook (#472)
Save hook is on the atom's `env` and accepts arguments: `(value, payload)`. Calling the save hook rerenders the atom. Example: ``` let atom = { name: 'my-atom', type: 'dom', render({env, value, payload}) { let el = document.createElement('button'); let clicks = payload.clicks || 0; el.appendChild(document.createTextNode('Clicks: ' + clicks)); el.onclick = () => { payload.clicks = payload.clicks || 0; payload.clicks++; env.save(value, payload); }; return el; } }; ``` Also: improve postAbstract buildFromText to accept data for an atom, i.e. "abc@(...jsondata...)", e.g.: ``` buildFromText('abc@("name": "my-atom", "value": "bob", "payload": {"foo": "bar"})def'); // -> "abc" + atom with name "my-atom", value "bob", payload {foo: 'bar'} + "def" ``` Fixes #399
1 parent a59ae74 commit 3ef3bc3

File tree

5 files changed

+113
-7
lines changed

5 files changed

+113
-7
lines changed

ATOMS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ must be of the correct type (a DOM Node for the dom renderer, a string of html o
2929

3030
* `name` [string] - the name of the atom
3131
* `onTeardown` [function] - The atom can pass a callback function: `onTeardown(callbackFn)`. The callback will be called when the rendered content is torn down.
32+
* `save` [function] - Call this function with the arguments `(newValue, newPayload)` to update the atom's value and payload and rerender it.
3233

3334
## Atom Examples
3435

@@ -56,3 +57,23 @@ let atom = {
5657
}
5758
};
5859
```
60+
61+
Example dom atom that uses the `save` hook:
62+
```js
63+
let atom = {
64+
name: 'click-counter',
65+
type: 'dom',
66+
render({env, value, payload}) {
67+
let clicks = payload.clicks || 0;
68+
let button = document.createElement('button');
69+
button.appendChild(document.createTextNode('Clicks: ' + clicks));
70+
71+
button.onclick = () => {
72+
payload.clicks = clicks + 1;
73+
env.save(value, payload); // updates payload.clicks, rerenders button
74+
};
75+
76+
return button;
77+
}
78+
};
79+
```

src/js/models/atom-node.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ export default class AtomNode {
2525
get env() {
2626
return {
2727
name: this.atom.name,
28-
onTeardown: (callback) => this._teardownCallback = callback
28+
onTeardown: (callback) => this._teardownCallback = callback,
29+
save: (value, payload={}) => {
30+
this.model.value = value;
31+
this.model.payload = payload;
32+
33+
this.editor._postDidChange();
34+
this.teardown();
35+
this.render();
36+
}
2937
};
3038
}
3139

src/js/models/post-node-builder.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ class PostNodeBuilder {
134134

135135
/**
136136
* @param {String} name
137-
* @param {String} [text='']
137+
* @param {String} [value='']
138138
* @param {Object} [payload={}]
139139
* @param {Markup[]} [markups=[]]
140140
* @return {Atom}
141141
*/
142-
createAtom(name, text='', payload={}, markups=[]) {
143-
const atom = new Atom(name, text, payload, markups);
142+
createAtom(name, value='', payload={}, markups=[]) {
143+
const atom = new Atom(name, value, payload, markups);
144144
atom.builder = this;
145145
return atom;
146146
}

tests/helpers/post-abstract.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,46 @@ function parsePositionOffsets(text) {
6666
}
6767

6868
const DEFAULT_ATOM_NAME = 'some-atom';
69+
const DEFAULT_ATOM_VALUE = '@atom';
6970

7071
function parseTextIntoMarkers(text, builder) {
7172
text = text.replace(cursorRegex,'');
7273
let markers = [];
7374

7475
if (text.indexOf('@') !== -1) {
7576
let atomIndex = text.indexOf('@');
76-
let atom = builder.atom(DEFAULT_ATOM_NAME);
77-
let pieces = [text.slice(0, atomIndex), atom, text.slice(atomIndex+1)];
77+
let afterAtomIndex = atomIndex + 1;
78+
let atomName = DEFAULT_ATOM_NAME,
79+
atomValue = DEFAULT_ATOM_VALUE,
80+
atomPayload = {};
81+
82+
// If "@" is followed by "( ... json ... )", parse the json data
83+
if (text[atomIndex+1] === "(") {
84+
let jsonStartIndex = atomIndex+1;
85+
let jsonEndIndex = text.indexOf(")",jsonStartIndex);
86+
afterAtomIndex = jsonEndIndex + 1;
87+
if (jsonEndIndex === -1) {
88+
throw new Error('Atom JSON data had unmatched "(": ' + text);
89+
}
90+
let jsonString = text.slice(jsonStartIndex+1, jsonEndIndex);
91+
jsonString = "{" + jsonString + "}";
92+
try {
93+
let json = JSON.parse(jsonString);
94+
if (json.name) { atomName = json.name; }
95+
if (json.value) { atomValue = json.value; }
96+
if (json.payload) { atomPayload = json.payload; }
97+
} catch(e) {
98+
throw new Error('Failed to parse atom JSON data string: ' + jsonString + ', ' + e);
99+
}
100+
}
101+
102+
// create the atom
103+
let atom = builder.atom(atomName, atomValue, atomPayload);
104+
105+
// recursively parse the remaining text pieces
106+
let pieces = [text.slice(0, atomIndex), atom, text.slice(afterAtomIndex)];
107+
108+
// join the markers together
78109
pieces.forEach(piece => {
79110
if (piece === atom) {
80111
markers.push(piece);
@@ -151,7 +182,9 @@ function parseSingleText(text, builder) {
151182
* Use "|" to indicate the cursor position or "<" and ">" to indicate a range.
152183
* Use "[card-name]" to indicate a card
153184
* Use asterisks to indicate bold text: "abc *bold* def"
154-
* Use "@" to indicate an atom
185+
* Use "@" to indicate an atom, default values for name,value,payload are DEFAULT_ATOM_NAME,DEFAULT_ATOM_VALUE,{}
186+
* Use "@(name, value, payload)" to specify name,value and/or payload for an atom. The string from `(` to `)` is parsed as
187+
* JSON, e.g.: '@("name": "my-atom", "value": "abc", "payload": {"foo": "bar"})' -> atom named "my-atom" with value 'abc', payload {foo: 'bar'}
155188
* Use "* " at the start of the string to indicate a list item ("ul")
156189
*
157190
* Examples:

tests/unit/editor/atom-lifecycle-test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,47 @@ test('mutating the content of an atom does not trigger an update', (assert) => {
316316
done();
317317
});
318318
});
319+
320+
test('atom env has "save" method, rerenders atom', (assert) => {
321+
let atomArgs = {};
322+
let render = 0;
323+
let teardown = 0;
324+
let postDidChange = 0;
325+
let save;
326+
327+
const atom = {
328+
name: DEFAULT_ATOM_NAME,
329+
type: 'dom',
330+
render({env, value, payload}) {
331+
render++;
332+
atomArgs.value = value;
333+
atomArgs.payload = payload;
334+
save = env.save;
335+
336+
env.onTeardown(() => teardown++);
337+
338+
return makeEl('the-atom', value);
339+
}
340+
};
341+
342+
editor = Helpers.editor.buildFromText('abc|@("value": "initial-value", "payload": {"foo": "bar"})def', {autofocus: true, atoms:[atom], element: editorElement});
343+
editor.postDidChange(() => postDidChange++);
344+
345+
assert.equal(render, 1, 'precond - renders atom');
346+
assert.equal(teardown, 0, 'precond - did not teardown');
347+
assert.ok(!!save, 'precond - save hook');
348+
assert.deepEqual(atomArgs, {value:'initial-value', payload:{foo: "bar"}}, 'args initially empty');
349+
assert.hasElement(`#the-atom`, 'precond - displays atom');
350+
351+
let value = 'new-value';
352+
let payload = {foo: 'baz'};
353+
postDidChange = 0;
354+
355+
save(value, payload);
356+
357+
assert.equal(render, 2, 'rerenders atom');
358+
assert.equal(teardown, 1, 'tears down atom');
359+
assert.deepEqual(atomArgs, {value, payload}, 'updates atom values');
360+
assert.ok(postDidChange, 'post changed when saving atom');
361+
assert.hasElement(`#the-atom:contains(${value})`);
362+
});

0 commit comments

Comments
 (0)