Isnor Creative
Isnor Creative Blog
Ruby, Ruby on Rails, Ember, Elm, Phoenix, Elixir, React, Vue

Feb 14, 2019

Developing an Ember application with local storage

I recently released a tea timer progressive web application built with the Ember Octane blueprint circa Feb 2019 (prerelease). The application stores a list of teas in the user’s local storage, and allows people to add, remove, edit or restore from a preset list of teas.

I started out building out the local storage using vanilla JS. It was remarkably easy to do, and worked surprisingly well. At a certain point though, I decided I wanted to add a few validations as it made no sense to have teas without a name or a brewing time. At this point I wanted the convenience of a familiar validation library such as ember-changeset-validations and I decided to switch my own implementation of the local storage management to Ember Data and the excellent ember-local-storage addon which had served me well in the past.

Here’s a walkthrough of both ways of doing this…


Version 1: No ember data, local storage implemented with vanilla JS

Setup

I started with a preset list of teas that was an array of POJOs.

let Teas = [
  {id: 1, name: "Green", ...etc   },
  ...etc
];
export default Teas;

Application route

The application route here checks to see if data exists in local storage, if it doesn’t it stringifies the JSON seed data and saves it to local storage, if it does, it parses the the locally stored string to JSON. I use the application route’s model throughout the application.

import Route from '@ember/routing/route';
import Teas from 'tea-time/src/teas';

export default class ApplicationRoute extends Route {
  model() {
    let stored =  window.localStorage.getItem('teas');
      if (stored !== null) {
      return JSON.parse(stored);
    } else {
      window.localStorage.setItem('teas', JSON.stringify(Teas));
      return Teas;
    }
  }
}

Record detail route

This route loads a single item. Here I am finding the item from my already loaded application route model. I am adding parseInt on the ID param.

import Route from '@ember/routing/route';

export default class TeaRoute extends Route {
  model(params) {
    let teas = this.modelFor('application');
    return teas.findBy('id', parseInt(params.id));
  }
}  

Update an item route

I’m not using a controller here. The form’s save action is hitting the route, which hooks into the model for the application route. It removes the item we are modifying, adds it back in modified form, and then stringifies the model and saves it back to local storage.

The delete action is doing a similar thing to save – removing the current item, not then not adding anything, just persisting the shortened list to local storage.

import Route from '@ember/routing/route';
import { action } from '@ember-decorators/object';

export default class EditTeaRoute extends Route {

  model(params) {
    let teas = this.modelFor('application');
    return teas.findBy('id', parseInt(params.id));
  }

  @action
  save(model) {
    let teas = this.modelFor('application')
    teas.rejectBy('id', model.id);
    teas.pushObject(model);
    window.localStorage.setItem('teas', JSON.stringify(teas));
    this.transitionTo('index');
  }

  @action 
  delete(tea) {
    let teas = this.modelFor('application')
    teas.rejectBy('id', model.id);
    window.localStorage.setItem('teas', JSON.stringify(teas));
    this.transitionTo('settings')
  }

}

Add an item route

Our model here is just returning an empty object. The save action is pushing that object onto the application route’s model, and then saving the stringified model back to local storage.

import Route from '@ember/routing/route';
import { action } from '@ember-decorators/object';

export default class AddTeaRoute extends Route {

  model() {
    return {}
  }

  @action
  save(model) {
    let teas = this.modelFor('application')
    teas.pushObject(model);
    window.localStorage.setItem('teas', JSON.stringify(teas));
    this.transitionTo('index');
  }

}

Restore to defaults route

This route allows the user to wipe any changes they have made, and restore to “Factory Defaults”. It is similar to the original application route in storing the original presets to local storage, but then it also refreshes the application route so that the changes are instantly visible.

import Route from '@ember/routing/route';
import { action } from '@ember-decorators/object';
import Teas from 'tea-time/src/teas';
import { getOwner } from '@ember/application';

export default class SettingsRoute extends Route {

  @action 
  restoreDefaults() {
    window.localStorage.setItem('teas', JSON.stringify(Teas));
    getOwner(this).lookup('route:application').refresh();
  }

}

Version 2: Bringing in Ember data, the ember-local-storage addon, and ember-changeset-validations

There were a few setup details required here that were not necessary in the version without Ember Data and ember-local storage. I had to add a model, adapter, serializer, and the storage ember-local-storage storage object.

I removed the id property from my preset list of teas as it was not necessary and caused problems with Ember Data…

src/teas.js

let Teas = [
  {name: "Green", ...etc   },
  ...etc
];
export default Teas;

storages/teas.js

This is a necessary setup step for using ember-local-storage

import StorageObject from 'ember-local-storage/local/object';
const Storage = StorageObject.extend();
export default Storage;

data/models/adapter.js

Necessary for ember-local-storage to work with Ember Data

export { default } from 'ember-local-storage/adapters/local';

data/models/serializer.js

Necessary for ember-local-storage to work with Ember Data

export { default } from 'ember-local-storage/serializers/serializer';

data/models/tea.js

import DS from 'ember-data';
const { Model, attr } = DS;
export default Model.extend({
  name: attr(),
  ...etc
});

Application route

Similar to the version 1 route above, the application route here checks to see if data exists in local storage. If it doesn’t it seeds it from JSON.

import Route from '@ember/routing/route';
import { storageFor } from 'ember-local-storage';
import Teas from 'tea-time/src/teas';

export default class ApplicationRoute extends Route {
  teas = storageFor('teas');

  model() {
    return this.store.findAll('tea').then(teas => {
      if (teas.length === 0) {
        Teas.map(tea => {
          let newTea = this.store.createRecord('tea', tea);
          newTea.save();
        });
        return this.store.findAll('tea');
      } else {
        return teas;
      }
    })
  }

}

Adding, editing or deleting items is typical Ember Data:

One thing I had to do when I started using Ember Data is ensuring that unsaved records were destroyed to prevent them from hanging around…. So I added this willTransition to my create item route…

import Route from '@ember/routing/route';
import { action } from '@ember-decorators/object';

export default class AddTeaRoute extends Route {

  model() {
    return {}
  }
  @action
  save(model) {
    model.save().then(()=> this.transitionToRoute('index'));    
  }
  @action
  willTransition() {
    if (this.get('controller.model.isNew')) {
      this.get('controller.model').deleteRecord();
    } else {
      return true;
    }
  }
}

Restoring to factory defaults

I had an option to restore local storage from the “factory default”, with Ember Data it looks like this:

import Route from '@ember/routing/route';
import { action } from '@ember-decorators/object';
import Teas from 'tea-time/src/teas';

export default class SettingsRoute extends Route {
  @action 
  restoreDefaults() {
    this.modelFor('application').map(tea=> tea.destroyRecord());
    Teas.map(tea => {
      let newTea = this.store.createRecord('tea', tea);
      newTea.save();
    });
  }
}

It had occurred to me when I started to feel a need to add validations that I might look into using a non-Ember javascript object validation library – validate.js looked promising – but I was already very familiar with ember-changeset-validations and decided to go that route instead.

The application can be found at Perfect Cup Tea Timer.

Any questions or comments welcome.

I am available for Ember.js consulting, as well as Ruby on Rails and Elixir/Phoenix + Elm, React, and Vue.js.

Gordon B. Isnor

Gordon B. Isnor writes about Ruby on Rails, Ember.js, Elm, Elixir, Phoenix, React, Vue and the web.
If you enjoyed this article, you may be interested in the occasional newsletter.

I am now available for project work. I have availability to build greenfield sites and applications, to maintain and update/upgrade existing applications, team augmentation. I offer website/web application assessment packages that can help with SEO/security/performance/accessibility and best practices. Let’s talk

comments powered by Disqus