funkensturm / ember-local-storage

The addon provides a storageFor computed property that returns a proxy and persists the changes to localStorage or sessionStorage. It ships with an ember-data adapter.
https://www.funkensturm.com/ember-local-storage/
MIT License
218 stars 76 forks source link

Any way of having nested data without Ember Data? #244

Closed monovertex closed 7 years ago

monovertex commented 7 years ago

Hi! We have a particular use case in our app which I'm having trouble implementing and I'm look for suggestions on how can I achieve this using ember-local-storage.

We want to use the local storage as a repository for various preferences of the current user, on multiple pages of the app.

Initially, we had one storage object for each page, so we had a file structure like this:

storages
  preferences
    page1.js
    page2.js
    ...

However, this is problematic because we wanted to automatize working with preferences, and even if we didn't need to specify an initialState for the storages, we still had to create the files, which creates a pitfall. Every new page requires a new storage file, otherwise it breaks, which is not ideal. It can also lead to a huge file structure, with each file having just 3-4 lines and the same content in all of them.

After that I tried to create a single storage, preferences.js, which would hold a nested structure, each property in this Storage Object being an Object, corresponding to a page. I quickly realized then that nested data doesn't work.

Reading the docs, I figured I should create an adapter / serializer pair that would take care of this problem, but the provided adapter / serializer are based on Ember Data, and we implemented our own data layer, without using Ember Data at all in the app.

Is there any way to use the local storage without having to create a storage file for each page and without having to install Ember Data?

LE: It would also be an option if we could use different storages for each page, but without needing to create a file for each of them. Sort of like an auto-generation of storages, if it's possible.

fsmanuel commented 7 years ago

@monovertex I think in your case you should try the model argument for storageFor(key, model, options)

model Optional string - The dependent property. Must be an ember data model or an object with modelName and id properties. (It is still experimental)

Don't worry about "It is still experimental" - I put it there because I didn't figure out a way to do 'garbage collection' yet. Imagin you use it with models and have a lot of records it could exceed the localStorage quota... But in your case your are in control of the keys/pages and can do garbage collection if you think you need it. But probably it's a limited amount of pages and you don't have to worry about it at all...

An example would be:

Ember.Object.extend({
  page1preferences: storageFor('preferences', { modelName: 'page', id: 1 }),
  page2preferences: storageFor('preferences', { modelName: 'page', id: 2 })
});

Let me know if that helps.

monovertex commented 7 years ago

I'm sorry for not responding, It's been a few really busy days and I didn't find the time for this.

I think it might work. Could the id be a string? So I could write a function that wraps storageFor, something like this:

function preferencesFor(page) {
  return storageFor('preferences', { modelName: 'pagePreferences', id: page };
}

And page would be a string identifier for a particular page.

fsmanuel commented 7 years ago

@monovertex id can be a string. I don't think that you can use a function to wrap it but you can use:

computed('page', function() {
  return storageFor('preferences', { modelName: 'pagePreferences', id: page };
})
monovertex commented 7 years ago

@fsmanuel, thanks for the help! Two things:

  1. It doesn't seem to work with an inline Object, only with a string, pointing to another property on this, that is an Object. I looked through the helpers source and maybe I misunderstood something, but I dohn't think the logic is ok there when you pass an Object. The modelName ends up completely ignored in that specific case.

  2. Is it possible to have different initial states depending on the ID? If I set the initialState for the preferences storage, it would set that initial state for all the instances of the object.

fsmanuel commented 7 years ago

@monovertex you are completely right about about 1. You need to provide a property on the context.

  1. That is not possible. As far as I understand you setting I would recommend a thin wrapper that does the setup. Something like that should work:
const pagePreferencesDefaults = {
  page1: { some: 'defaults', for: 'page1' },
  page2: { other: 'drfaults', for: 'page2' }
};

export default Ember.Component.extend({
  preferences: computed('page', function() {
    let page = this.get('page');
    this.set('pagePreferencesObject', { modelName: 'pagePreferences', id: page });
    let preferences = storageFor('preferences', 'pagePreferencesObject');

    if (preferences.isInitialContent()) {
      preferences.setProperties(pagePreferencesDefaults[page];
    }

    return preferences;
  })
});

If you are familiar with computed properties you know that you can have your own CPs if not have a look how to create them. It's just a function that returns a CP:

// app/utils/cps/page-preferences.js

const pagePreferencesDefaults = {
  page1: { some: 'defaults', for: 'page1' },
  page2: { other: 'drfaults', for: 'page2' }
};

export default function pagePreferences(dependendKey) {
  return computed(dependendKey, function() {
    let page = this.get(dependendKey);
    this.set('_pagePreferencesObject', { modelName: 'pagePreferences', id: page });
    let preferences = storageFor('preferences', '_pagePreferencesObject');

    if (preferences.isInitialContent()) {
      preferences.setProperties(pagePreferencesDefaults[page];
    }

    return preferences;
  })
};

// component.js
import pagePreferences from 'app/utils/cps/page-preferences';

export default Ember.Component.extend({
  preferences: pagePreferences('page')
});

That way all your defaults are at one place.

monovertex commented 7 years ago

Thanks for all the help!

I went with a similar approach, a preferencesFor function that returns a CP which attaches the Object and the storageFor result internally on the context. There was a weird error when simply returning the storageFor result like in your example, so I also attached that on the context with Ember.defineProperty (I figure returning a floating CP at runtime like that is not supported).

However, I used a second parameter for the preferencesFor function, which describes the initial state, so I don't have to use globals.

Also, I did not know about the isInitialContent method, so I'll use that, thanks!

fsmanuel commented 7 years ago

@monovertex happy to help. Have a look at the public api: https://github.com/funkensturm/ember-local-storage/blob/master/addon/mixins/storage.js#L119 They are just convenient methods to help you with the objects/arrays.