Search code examples
ember.jswebsocketember-data

Adding new data through websocket when using store.query in route


My app is using a websocket service based on ember-phoenix to push new records from the API to the store. I would like these new records to render in my template when they're added.

I have a route where the model hook returns a filtered query promise:

import Ember from 'ember';

const {
  get,
  inject,
} = Ember;

export default Ember.Route.extend({
  socket: inject.service(),

  model(params) {
    return this.store.query('my-model', {filter: {date: params.date}})
  },

  afterModel() {
    get(this, 'socket').joinSchedule();
  },

  resetController() {
    get(this, 'socket').leaveSchedule();
  },

});

When new records are pushed to the store through the websocket, they are not rendered by my template because of how store.query works. If I change store.query to store.findAll the new records are rendered, but I want my route to only load a subset of all the records based on the date query param.

It seems like my only option is to just reload the route's model when a new record is pushed to the store. Is it possible to do this from the service? If not, is there a different approach I might want to explore?

The relevant parts of my socket service are below:

import Ember from 'ember';
import PhoenixSocket from 'phoenix/services/phoenix-socket';

const {
  get,
  inject,
} = Ember;

export default PhoenixSocket.extend({
  session: inject.service(),
  store:   inject.service(),

  joinSchedule() {
    const channel = this.joinChannel(`v1:my-model`);

    channel.on('sync', (payload) => this._handleSync(payload));
  },

  _handleSync(payload) {
    get(this, 'store').pushPayload(payload);
  },
});

Solution

  • Option 1
    You can use Ember.Evented to subscribe and dispatch event. I have created twiddle for demonstration.

    In socket service,

    • socket should extend Ember.Evented class

      export default PhoenixSocket.extend(Ember.Evented, {

    • After updating store, you can just trigger myModelDataLoaded which will dispatch all the functions subscribed to myModelDataLoaded.

       _handleSync(payload) {
              get(this, 'store').pushPayload(payload);
              this.trigger('myModelDataLoaded'); //this will call the functions subscribed to myModelDataLoaded.        
          }

    In Route,

    • You can subscribe to myModelDataLoaded
    afterModel() {
            get(this, 'socket').joinSchedule();
            get(this, 'socket').on('myModelDataLoaded', this, this.refreshRoute); //we are subscribing to myModelDataLoaded
        }
    
    • Define refreshRoute function and call refresh function.

         refreshRoute() {
              this.refresh(); //forcing this route to refresh
          }
      
    • To avoid memory leak need to off subscribtion, you can do it either in resetController or deactivate hook.
      resetController() {
          get(this, 'socket').leaveSchedule();
          get(this, 'socket').off('myModelDataLoaded', this, this.refreshRoute);
      }
      

    Option 2.
    You can watch store using peekAll with observer and refresh route.

    In your controller,
    1. Define postModel computed property which will return live record array.
    2. Define postModelObserver dependant on postModel.[] this will ensure whenever store is updated with new row, it will be observed by myModelObserver and it will send action refreshRoute to route . where we will call refresh. As you know this will call beforeModel, model, afterModel method.

    As you know computed property is lazy, when you are accessing it only then it will be computed. so if you are not using it in template, then just add this.get('myModel') in init method

    Controller file

    import Ember from 'ember';
    const { computed } = Ember;
    export default Ember.Controller.extend({
        init() {
            this._super(...arguments);
            this.get('postModel');//this is just to trigger myModel computed property
        },
        postModel: computed(function() {
            return this.get('store').peekAll('post');
        }),
        postModelObserver: Ember.observer('postModel.[]', function() {
            this.send('refreshRoute');
        })
    });
    

    Route file - define action refreshRoute for refreshing, since refresh is available only in route.

    import Ember from 'ember';
    
    const {
        get,
        inject,
    } = Ember;
    
    export default Ember.Route.extend({
        socket: inject.service(),
        model(params) {
            return this.store.query('my-model', { filter: { date: params.date } })
        },
    
        afterModel() {
            get(this, 'socket').joinSchedule();
        },
    
        resetController() {
            get(this, 'socket').leaveSchedule();
        },
        actions:{
            refreshRoute() {
                this.refresh();
            },
        }
    });