Skip to content

Commit

Permalink
feat(atom): Implement Atom save hook (#472)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bantic committed Aug 25, 2016
1 parent a59ae74 commit 3ef3bc3
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 7 deletions.
21 changes: 21 additions & 0 deletions ATOMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ must be of the correct type (a DOM Node for the dom renderer, a string of html o

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

## Atom Examples

Expand Down Expand Up @@ -56,3 +57,23 @@ let atom = {
}
};
```

Example dom atom that uses the `save` hook:
```js
let atom = {
name: 'click-counter',
type: 'dom',
render({env, value, payload}) {
let clicks = payload.clicks || 0;
let button = document.createElement('button');
button.appendChild(document.createTextNode('Clicks: ' + clicks));

button.onclick = () => {
payload.clicks = clicks + 1;
env.save(value, payload); // updates payload.clicks, rerenders button
};

return button;
}
};
```
10 changes: 9 additions & 1 deletion src/js/models/atom-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ export default class AtomNode {
get env() {
return {
name: this.atom.name,
onTeardown: (callback) => this._teardownCallback = callback
onTeardown: (callback) => this._teardownCallback = callback,
save: (value, payload={}) => {
this.model.value = value;
this.model.payload = payload;

this.editor._postDidChange();
this.teardown();
this.render();
}
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/js/models/post-node-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,13 @@ class PostNodeBuilder {

/**
* @param {String} name
* @param {String} [text='']
* @param {String} [value='']
* @param {Object} [payload={}]
* @param {Markup[]} [markups=[]]
* @return {Atom}
*/
createAtom(name, text='', payload={}, markups=[]) {
const atom = new Atom(name, text, payload, markups);
createAtom(name, value='', payload={}, markups=[]) {
const atom = new Atom(name, value, payload, markups);
atom.builder = this;
return atom;
}
Expand Down
39 changes: 36 additions & 3 deletions tests/helpers/post-abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,46 @@ function parsePositionOffsets(text) {
}

const DEFAULT_ATOM_NAME = 'some-atom';
const DEFAULT_ATOM_VALUE = '@atom';

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

if (text.indexOf('@') !== -1) {
let atomIndex = text.indexOf('@');
let atom = builder.atom(DEFAULT_ATOM_NAME);
let pieces = [text.slice(0, atomIndex), atom, text.slice(atomIndex+1)];
let afterAtomIndex = atomIndex + 1;
let atomName = DEFAULT_ATOM_NAME,
atomValue = DEFAULT_ATOM_VALUE,
atomPayload = {};

// If "@" is followed by "( ... json ... )", parse the json data
if (text[atomIndex+1] === "(") {
let jsonStartIndex = atomIndex+1;
let jsonEndIndex = text.indexOf(")",jsonStartIndex);
afterAtomIndex = jsonEndIndex + 1;
if (jsonEndIndex === -1) {
throw new Error('Atom JSON data had unmatched "(": ' + text);
}
let jsonString = text.slice(jsonStartIndex+1, jsonEndIndex);
jsonString = "{" + jsonString + "}";
try {
let json = JSON.parse(jsonString);
if (json.name) { atomName = json.name; }
if (json.value) { atomValue = json.value; }
if (json.payload) { atomPayload = json.payload; }
} catch(e) {
throw new Error('Failed to parse atom JSON data string: ' + jsonString + ', ' + e);
}
}

// create the atom
let atom = builder.atom(atomName, atomValue, atomPayload);

// recursively parse the remaining text pieces
let pieces = [text.slice(0, atomIndex), atom, text.slice(afterAtomIndex)];

// join the markers together
pieces.forEach(piece => {
if (piece === atom) {
markers.push(piece);
Expand Down Expand Up @@ -151,7 +182,9 @@ function parseSingleText(text, builder) {
* Use "|" to indicate the cursor position or "<" and ">" to indicate a range.
* Use "[card-name]" to indicate a card
* Use asterisks to indicate bold text: "abc *bold* def"
* Use "@" to indicate an atom
* Use "@" to indicate an atom, default values for name,value,payload are DEFAULT_ATOM_NAME,DEFAULT_ATOM_VALUE,{}
* Use "@(name, value, payload)" to specify name,value and/or payload for an atom. The string from `(` to `)` is parsed as
* JSON, e.g.: '@("name": "my-atom", "value": "abc", "payload": {"foo": "bar"})' -> atom named "my-atom" with value 'abc', payload {foo: 'bar'}
* Use "* " at the start of the string to indicate a list item ("ul")
*
* Examples:
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/editor/atom-lifecycle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,47 @@ test('mutating the content of an atom does not trigger an update', (assert) => {
done();
});
});

test('atom env has "save" method, rerenders atom', (assert) => {
let atomArgs = {};
let render = 0;
let teardown = 0;
let postDidChange = 0;
let save;

const atom = {
name: DEFAULT_ATOM_NAME,
type: 'dom',
render({env, value, payload}) {
render++;
atomArgs.value = value;
atomArgs.payload = payload;
save = env.save;

env.onTeardown(() => teardown++);

return makeEl('the-atom', value);
}
};

editor = Helpers.editor.buildFromText('abc|@("value": "initial-value", "payload": {"foo": "bar"})def', {autofocus: true, atoms:[atom], element: editorElement});
editor.postDidChange(() => postDidChange++);

assert.equal(render, 1, 'precond - renders atom');
assert.equal(teardown, 0, 'precond - did not teardown');
assert.ok(!!save, 'precond - save hook');
assert.deepEqual(atomArgs, {value:'initial-value', payload:{foo: "bar"}}, 'args initially empty');
assert.hasElement(`#the-atom`, 'precond - displays atom');

let value = 'new-value';
let payload = {foo: 'baz'};
postDidChange = 0;

save(value, payload);

assert.equal(render, 2, 'rerenders atom');
assert.equal(teardown, 1, 'tears down atom');
assert.deepEqual(atomArgs, {value, payload}, 'updates atom values');
assert.ok(postDidChange, 'post changed when saving atom');
assert.hasElement(`#the-atom:contains(${value})`);
});

0 comments on commit 3ef3bc3

Please sign in to comment.