Building a save as you type editor with Ember Concurrency

Conor Linehan
Oct 4, 2016 · 6 min read

Getting Started

git clone https://github.com/ConorLinehan/autosave-editor-post.git
cd autosave-editor-post
npm install && bower install && ember s
visit http://localhost:4200/
controller/editor.jsimport Ember from 'ember';
import { task, timeout } from 'ember-concurrency';
// 1
const DEBOUNCE_TIME = (Ember.testing) ? 20 : 500;
const FIVE_SECONDS = (Ember.testing) ? 40 : 5000;
export default Ember.Controller.extend({
});
<nav class="navigation">
<section class="container">
<h4 class="float-left">My Editor</h4>
</section>
</nav>
<div class="container">
<div class="row">
<div class="column">
<label>Body</label>
// 1
{{medium-editor}}
</div>
</div>
</div>
acceptance/editor-test.jsimport { test } from 'qunit';
import moduleForAcceptance from 'autosave-editor-post/tests/helpers/module-for-acceptance';
import Ember from 'ember';
import { timeout } from 'ember-concurrency';
// 1
const triggerInput = (editor, text, $element) =>{
editor.trigger('editableInput', {
target: {
innerHTML: text
}
}, $element);
};
moduleForAcceptance('Acceptance | editor', {
beforeEach() {
server.create('post', {text: ''}); // 2
}
});
test('it debounces a save', function(assert) {
// [Enter Code Here]
});
test('it forces a save', function(assert) {
// [Enter Code Here]
});

Our First Task

import Ember from 'ember';
import { task, timeout } from 'ember-concurrency';
const DEBOUNCE_TIME = (Ember.testing) ? 20 : 500;
const FIVE_SECONDS = (Ember.testing) ? 40 : 5000;
export default Ember.Controller.extend({
//1
saveModelTask: task(function *() {
yield this.get('model').save();
}).keepLatest(),
// 2
updatedModelTask: task(function *() {
yield timeout(DEBOUNCE_TIME);
this.get('saveModelTask').perform();
}).restartable()
});
{{medium-editor
updateText=
(pipe-action
(action (mut model.text))
(perform updatedModelTask))}}

Indication

{{#if saveModelTask.isRunning}}
<h4 class="float-right">Saving..</h4>
{{/if}}

Error Handling

import Mirage from 'ember-cli-mirage';export default function() {
this.timing = 800;
this.get('/posts/:id');
this.patch('/posts/:id');
// Uncomment to fake error case
this.patch('/posts/:id', () =>{
return new Mirage.Response(500);
});
}
{{#if saveModelTask.isRunning}}
<h4 class="float-right">Saving..</h4>
{{else if saveModelTask.last.error}}
<a {{action (perform saveModelTask)}} class="float-right">
<h4>Failed Retry?</h4>
</a>
{{/if}}

Force Save

forceSaveTask: task(function *() {
yield timeout(FIVE_SECONDS);
this.get('saveModelTask').perform();
}).drop(),
updatedModelTask: task(function *() {
this.get('forceSaveTask').perform();
yield timeout(DEBOUNCE_TIME);
this.get('forceSaveTask').cancelAll();
this.get('saveModelTask').perform();
}).restartable()
Took more goes than I’d like to admit o_0

Testing

// 1
visit('/');
andThen(() =>{
// 2
let $editor = Ember.$('.editor')[0];
let editor = MediumEditor.getEditorFromElement($editor);
triggerInput(editor, 'old Text', $editor);
// 3
timeout(5)
.then(() =>{
triggerInput(editor, 'new text', $editor);
timeout(25)
.then(() =>{
// 4
assert.equal(server.db.posts[0].text, 'new text');
let numberOfSaves = server.pretender.handledRequests
.filter(r => r.method === 'PATCH').length;
assert.equal(numberOfSaves, 1);
});
});
});
// Same as above
visit('/');
andThen(() =>{
let $editor = Ember.$('.editor')[0];
let editor = MediumEditor.getEditorFromElement($editor);
// 1
triggerInput(editor, '1', $editor);
timeout(15).then(() =>{
triggerInput(editor, '2', $editor);
timeout(15).then(() =>{
triggerInput(editor, '3', $editor);
timeout(15).then(() =>{
// 2
assert.equal(server.db.posts[0].text, '3');
let numberOfSaves = server.pretender.handledRequests
.filter(r => r.method === 'PATCH').length;
assert.equal(numberOfSaves, 1);
});
});
});
});

Summary

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade