Ember localStorage
The addon provides a storageFor
computed property that returns a proxy and persists the changes to localStorage or sessionStorage. It works with objects and arrays and has a generator to create the proxy objects or arrays.
It ships with an ember-data adapter that works almost the same as the JSONAPIAdapter with some relationship sugar added.
The idea was taken from Tom Dale's gist Ember Array that writes every change to localStorage and extended to objects.
The storageFor
API was inspired by Ember State Services.
Installation
ember install ember-local-storage
Compatibility
Ember | Addon | Node |
---|
>= 3.4 | >= 2.0 | >= 12.x |
>= 2.12 | < 2.0 | >= 10.x |
Changelog
See the CHANGELOG
The documentation in this README is for versions >= 2.0.0
If you upgrade from a version <= 0.1.5
you need to set a legacyKey
on the computed storageFor
:
export default Ember.Component.extend({
settings: storageFor('settings', { legacyKey: 'your-old-key' })
});
Usage
Configuration
Namespace & keyDelimiter
In you apps config/environment.js
you can set a namespace
and a keyDelimiter
. For backward compatibility this is a opt-in feature.
Important: Don't turn this feature on for existing apps. You will lose access to existing keys.
To activate it there are the following options:
namespace
can be true
or a string. If set to true
it will use modulePrefix
as the namespacekeyDelimiter
is a string. The default is :
module.exports = function() {
var ENV = {
modulePrefix: 'my-app',
'ember-local-storage': {
namespace: true,
namespace: 'customNamespace',
keyDelimiter: '/'
}
}
};
ember-data support
This addon autodetects if you use ember-data and will include the support for ember-data adapters and serializes by default. You can opt out of this behavior by setting the includeEmberDataSupport
option to false
:
module.exports = function() {
var ENV = {
modulePrefix: 'my-app',
'ember-local-storage': {
includeEmberDataSupport: false
}
}
};
NOTE: However, there are environments where the detection fails and the support is disabled when you really do want it. A common place you might run into this is on https://ember-twiddle.com. For that case you can force including support for ember-data by turn this option on. Edit the twiddle.json
to include the following:
"ENV": {
"ember-local-storage": {
"includeEmberDataSupport": true
}
},
Object & Array
Object
Run ember g storage -h
for all options.
ember g storage stats
// will generate a localStorage object
ember g storage stats -s
// will generate a sessionStorage object
import StorageObject from 'ember-local-storage/local/object';
const Storage = StorageObject.extend();
Storage.reopenClass({
initialState() {
return { counter: 0 };
}
});
export default Storage;
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { storageFor } from 'ember-local-storage';
export default class ApplicationController extends Controller {
@storageFor('stats') stats;
@action
countUp() {
this.incrementProperty('stats.counter');
}
@action
resetCounter() {
this.get('stats').clear();
}
}
{{! app/templates/application.hbs}}
<button {{on "click" this.countUp}}>Page Visits: {{stats.counter}}</button>
<button {{on "click" this.resetCounter}}>X</button>
Array
Run ember g storage -h
for all options.
ember g storage anonymous-likes -a
// will generate a localStorage array
ember g storage anonymous-likes -a -s
// will generate a sessionStorage array
import StorageArray from 'ember-local-storage/local/array';
const Storage = StorageArray.extend();
export default Storage;
import Component from '@glimmer/component';
import { storageFor } from 'ember-local-storage';
import { action } from '@ember/object';
export default class LikeItemComponent extends Component {
anonymousLikes: storageFor('anonymous-likes'),
get isLiked() {
return this.get('anonymousLikes').includes(this.get('id'));
}),
@action
like(id) {
this.get('anonymousLikes').addObject(id);
}
}
{{! app/templates/components/like-item.hbs}}
{{#unless this.isLiked}}
<button {{on "click" (fn this.like this.id)}}>Like it</button>
{{else}}
You like it!
{{/unless}}
storageFor options
storageFor(key, model, options)
key
String - The filename of the storage (e.g. stats)
model
Optional string - The dependent property. Must be an ember data model or an object with modelName
and id
properties. (It is still experimental)
options
are:
Methods
The following methods work on StorageObject
and StorageArray
.isInitialContent()
You can call .isInitialContent()
to determine if content
is equal to initialState
.
Returns a boolean.
.reset()
You can invoke .reset()
to reset the content
to the initialState
.
.clear()
You can invoke .clear()
to remove the content
from xStorage.
Adapter & Serializer
Important: The Adapter works with ember-data versions >= 1.13
because it depends on JSONAPIAdapter
.
If your app is a pure LocalStorage app you just need to create the application adapter and serializer:
export { default } from 'ember-local-storage/adapters/local';
export { default } from 'ember-local-storage/serializers/serializer';
If you already use Ember Data for non LocalStorage models you can use a per type adapter and serializer.
export { default } from 'ember-local-storage/adapters/local';
export { default } from 'ember-local-storage/serializers/serializer';
If you use namespaced models e.g. blog/post
you have to add the modelNamespace
property to the corresponding adapter:
import Adapter from 'ember-local-storage/adapters/local';
export default class BlogPostAdapter extends Adapter {
modelNamespace = 'blog';
}
Model
Your model is a DS.Model
with two new relationship options
import Model, { attr, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr('string') name;
@hasMany('comment', { dependent: 'destroy' }) comments;
}
import Model, { attr, belongsTo } from '@ember-data/model';
export default class CommentModel extends Model {
@attr('string') name;
@belongsTo('post', { autoSave: true }) post;
});
Options
dependent
can be used in hasMany
relationships to destroy the child records when the parent record is destroyed.autoSave
can be used in belongsTo
relationships to update the association on the parent. It's recommended to use it.
.query() & .queryRecord()
As per ember guides you can query for attributes:
this.store.query('post', { filter: { name: 'Just a name' } });
this.store.query('post', { filter: { name: /^Just(.*)/ } });
Querying relationships also works:
this.store.query('post', { filter: { user: '123' } });
this.store.query('post', { filter: { user: { id: '123' } } });
this.store.query('post', { filter: { user: /^12/ } });
this.store.query('post', { filter: { user: { id: /^12/ } } });
this.store.query('post', { filter: { user: { type: 'editor' } } });
this.store.query('post', { filter: { user: { type: /^ed(.*)ors$/ } } });
this.store.query('post', { filter: { user: { id: '123', type: 'editor' } } });
this.store.query('post', { filter: { user: { id: '123', type: /^ed(.*)ors$/ } } });
this.store.query('user', { filter: { projects: '123' } });
this.store.query('user', { filter: { projects: { id: '123' } } });
this.store.query('user', { filter: { projects: /^12/ });
this.store.query('user', { filter: { projects: { id: /^12/ } } });
this.store.query('user', { filter: { pets: { type: 'cat' } } });
this.store.query('user', { filter: { pets: { id: '123', type: 'cat' } }) };
this.store.query('user', { filter: { pets: [{ type: 'cat' }, { type: 'dog' }] } });
this.store.query('user', { filter: { pets: { type: /cats|dogs/ } } });
You can use queryRecord
to return only one record. See the guides for an example.
Import & Export
The addon ships with utility functions that enables export and import of you LocalStorage data.
You have to add fileExport
option to the environment.js
:
module.exports = function() {
var ENV = {
'ember-local-storage': {
fileExport: true
}
}
};
Import exportData()
and importData()
from ember-local-storage/helpers/import-export
.
Both return a Promise.
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { importData, exportData } from 'ember-local-storage/helpers/import-export';
export default class IndexRoute extends Route {
@service store;
@action
exportData() {
exportData(
this.store,
['posts', 'comments'],
{ download: true, filename: 'my-data.json' }
);
}
@action
importData(event) {
this.readFile(event.target.files[0]).then((file) => {
importData(this.store, file.data);
});
}
readFile(file) {
const reader = new FileReader();
return new Promise((resolve) => {
reader.onload = function(event) {
resolve({
file: file.name,
type: file.type,
data: event.target.result,
size: file.size,
});
};
reader.readAsText(file);
});
}
}
importData(store, content, options)
store
the ember data store
content
can be a JSON API compliant object or a JSON string
options
are:
json
Boolean (default true
)truncate
Boolean (default true
) if true
the existing data gets replaced.
exportData(store, types, options)
store
the ember data store
types
Array of types to export. The types must be pluralized.
options
are:
json
Boolean (default true
)download
Boolean (default false
)filename
String (default ember-data.json)
Test Helpers
ember-local-storage
provides a helper to reset the storage while testing. This could be very useful when part of the
logic you are testing depends on the information in the storage.
Take a look at the following acceptance tests.
import { describe, afterEach } from 'mocha';
import { setupApplicationTest } from 'ember-mocha';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import resetStorages from 'ember-local-storage/test-support/reset-storage';
describe('Acceptance | login page', function() {
let hooks = setupApplicationTest();
setupMirage(hooks);
afterEach(function() {
if (window.localStorage) {
window.localStorage.clear();
}
if (window.sessionStorage) {
window.sessionStorage.clear();
}
resetStorages();
});
it('visiting a place', async function() {
});
});
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL } from '@ember/test-helpers';
import resetStorages from 'ember-local-storage/test-support/reset-storage';
module('basic acceptance test', function(hooks) {
let hooks = setupApplicationTest(hooks);
hooks.afterEach(function() {
if (window.localStorage) {
window.localStorage.clear();
}
if (window.sessionStorage) {
window.sessionStorage.clear();
}
resetStorages();
});
test('can visit /', async function(assert) {
await visit('/');
assert.strictEqual(currentURL(), '/');
});
});
Deprecations
ember-local-storage.initializers.local-storage-adapter
until: 3.0.0
The initializer has been deprecated and will be removed in version 3.0.0. This is due to the fact that ember-data >= 4.12
will no longer allow to reopen
the Store
. To remove the deprecation message you need to use the utility functions provided by the addon:
import { importData, exportData } from 'ember-local-storage/helpers/import-export';
See the Export & Import example. When you are done you need to set loadInitializer
to false
:
module.exports = function() {
var ENV = {
'ember-local-storage': {
loadInitializer: false
}
}
};
This will be the default behaviour for apps that use ember-data >= 4.12
.
ember-local-storage.mixins.adapters.import-export
until: 3.0.0
Using the import-export mixin has been deprecated and will be removed in version 3.0.0. You should use the utility functions provided by the addon:
import { importData, exportData } from 'ember-local-storage/helpers/import-export';
See the Export & Import example.
ember-local-storage.storageFor.options.legacyKey
until: 2.0.0
Using legacyKey
has been deprecated and will be removed in version 2.0.0. You should migrate your key to the new format. For storageFor('settings')
that would be storage:settings
.
Contributing
See the Contributing guide for details.
Publishing
npx release-it
npm publish --tag latest
License
This project is licensed under the MIT License.