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

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);
},
});

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();
},
}
});

You can trigger an event from your websocket service when you receive a message on the socket, then subscribe to it in your route and then call refresh() to reload your model.
There is also https://github.com/ember-data/ember-data-filter - which retuns live array.

It is not a better way, but one way to do with your existing code is using a callback.
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() {
let cb = (myModelRecord) => {
this.get('model').push(myModelRecord);
};
get(this, 'socket').joinSchedule(cb);
},
resetController() {
get(this, 'socket').leaveSchedule();
},
});
Call callback method in socket service,
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(cb) {
const channel = this.joinChannel(`v1:my-model`);
channel.on('sync', (payload) => cb(this._handleSync(payload)));
},
_handleSync(payload) {
return get(this, 'store').pushPayload(payload);
},
});

Related

Trigger alert when there is a change to ember data / model

I have the following route that will poll a model and refresh the data at a given interval. What I'm trying to do is trigger an alert when a new record is available in the model. I'm new to this, so I'm having some trouble figuring out how to trigger an alert site-wide without simply triggering it each time the model refreshes. I tried using 'didCreate' in the model, but it doesn't seem to recognize new records.
import Route from '#ember/routing/route';
import Ember from 'ember'
export const pollInterval = 8000 // time in milliseconds
export default Route.extend({
model() {
return Ember.RSVP.hash({
pat: this.store.findAll('pat'),
appt: this.store.findAll('appt')
})
},
getSMS () {
return this.get('store').findAll('smstext')
},
onPoll () {
return this.getSMS()
.then((users) => {
this.set('currentModel', users)
})
},
afterModel () {
let smsPoller = this.get('smsPoller')
if (!smsPoller) {
smsPoller = this.get('pollboy').add(this, this.onPoll, pollInterval)
this.set('smsPoller', smsPoller)
}
},
setupController(controller, models) {
controller.set('huddle', models.huddleappt);
controller.set('pat', models.pat);
}
})
I would recommend to use a service for this use case. You can inject your service wherever you need the data, and in the service you can handle the polling.
You can then display your data like this.
In your component file:
import Component from '#ember/component';
import { inject as service } from '#ember/service';
import { computed } from '#ember/object';
export default Component.extend({
smsService: service(),
smsData: computed('smsService.data')
// ...
And in your template you can access your data with the computed property from your components js file

A model id come from another model, how can I set in route by ember

My ember version:
DEBUG: -------------------------------
Ember : 2.10.2
Ember Data : 2.11.0
jQuery : 2.2.4
Ember Simple Auth : 1.1.0
Model Fragments : 2.3.2
DEBUG: -------------------------------
And my route code:
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import RSVP from 'rsvp';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model() {
console.log(1);
return RSVP.hash({
...,
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
});
},
afterModel(model, transition) {
return this.store.findRecord('company', model.user.get('companyId')).then(company => {
console.log(2);
this.set('company', company);
});
},
setupController(controller, model) {
console.log(3);
controller.set('user', model.user);
controller.set('company', this.get('company'));
}
});
Look at console.log code, I think the correct order should be 1->2->3. But sometimes it turns out to be 1->3->2.
But my company id must come from user api. So what is way I set it in route? Thanks.
I am writing just another solution, From RSVP.hash api docs
Returns a promise that is fulfilled when all the given promises have been fulfilled, or rejected if any of them become rejected. The returned promise is fulfilled with a hash that has the same key names as the promises object argument. If any of the values in the object are not promises, they will simply be copied over to the fulfilled object.
So you can write your requirement like the below code,
model() {
var promises = {
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
};
return Ember.RSVP.hash(promises).then(hash => {
//then method will be called once all given promises are fulfilled, or rejected if any of them become rejected.
return this.store.findRecord('company', hash.user.get('companyId')).then(company => {
hash.company = company; // Here i am setting company property inside model itself, so you dont need to set it in route and carry over to controller
return hash;
});
})
}
Note:I am curious to know if you can reproduce 1->3->2 behavior in ember-twiddle.
Actually the right way to do this is to put all your model fetching in your model hook:
model() {
return RSVP.hash({
...,
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
}).then(hash => {
hash.company = this.store.findRecord('company', hash.user.get('companyId'));
return RSVP.hash(hash);
})
},
setupController(controller, model) {
controller.set('user', model.user);
controller.set('company', model.company);
}

Computed property on a service that accesses the store

I wrote a service for loading notifications:
import Ember from 'ember';
export default Ember.Service.extend({
sessionUser: Ember.inject.service(),
store: Ember.inject.service(),
read() {
let currentUserId = this.get('sessionUser.user.id');
return this.get('store').query('notification', {
userId: currentUserId,
read: true
});
},
unread() {
let currentUserId = this.get('sessionUser.user.id');
return this.get('store').query('notification', {
userId: currentUserId,
read: false
});
}
});
I want to change the colour of an icon in the navigation bar when there are unread notifications. The navigation bar is a component:
import Ember from 'ember';
export default Ember.Component.extend({
notifications: Ember.inject.service(),
session: Ember.inject.service(),
hasUnreadNotifications: Ember.computed('notifications', function() {
return this.get('notifications').unread().then((unread) => {
return unread.get('length') > 0;
});
})
});
And the template then uses the hasUnreadNotifications property to decide if the highlight class should be used:
<span class="icon">
<i class="fa fa-bell {{if hasUnreadNotifications 'has-notifications'}}"></i>
</span>
However, it doesn't work. Although the store is called and notifications are returned, the hadUnreadNotifications doesn't resolve to a boolean. I think this is because it returns a promise and the template can't deal with that, but I'm not sure.
Questions
Is it idiosyncratic ember to wrap the store in a service like this. I'm doing this because it feels clumsy to load the notifications in the application route just to show the count.
Why doesn't hasUnreadNotifications return a boolean?
Is it possible to make read and unread properties instead of functions, so a computed property can be created in the service to calculate the count?
Returning promise from computed property will not work. Computed properties are not Promise aware. to make it work you need to return DS.PrmoiseObject or DS.PromiseArray.
You can read other options available from this igniter article.
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Component.extend({
notifications: Ember.inject.service(),
session: Ember.inject.service(),
hasUnreadNotifications: Ember.computed('notifications', function() {
return DS.PromiseObject.create({
promise: this.get('notifications').unread().then((unread) => {
return unread.get('length') > 0;
})
});
})
});

Accessing a service's promise from a route

I am struggling to a Service's promised data in a Route. The problem occurs when I am transitioning to the Route at application init; that is, if I load the application, then transition, everything is fine, because the promise is already fulfilled, but if I hit browser reload on that Route, the offending lines won't run. The Service is:
// services/lime-core.js
import Ember from 'ember';
export default Ember.Service.extend({
store: Ember.inject.service(),
resources: null,
init() {
this.set('resources', []);
this.get('store').findAll('resource').then(resources => {
this.set('resources', resources);
});
}
});
This service works perfectly in a template, assuming I have injected the service into the component. I access this service in the route as follows: (assume slug has a meaningful value)
// dashboard/route.js
export default Ember.Route.extend({
limeCore: Ember.service.inject(),
...
model(params) {
...
this.set('resource', this.get('limeCore.resources').findBy('slug', slug));
...
}
}
When the model() hook is called, the limeCore service's init() method is still waiting for the Promise to fulfill. I tried to be clever, but changing the code to something like:
this.get('limeCore.resources').then(resources => {
this.set('resource', resources.findBy('slug', slug))
});
doesn't work, because this.get('limeCore.resources') does not return a Promise. This logic has to be in the route (i.e. can't be moved to a template), because I'm dependent on the slug value to determine an ID which loads a different set of ember-data.
Again, this code works properly once the Promise has been fulfilled — that is, on a future transition to this route, but not on initial application load.
I'm sure there is a correct way to do this... either the Service needs to return a Promise (while still being usable in templates), or I need to make sure that the Promise is fulfilled before the Route.model() method can be executed.
Thanks!
An approach i would use
app/misc/lime_core.js
function getResources(store) {
return store.findAll('resource')
}
export { getResources };
random route
import { getResources } from 'app/misc/lime_core';
export default Ember.Route.extend({
model: function() {
const store = this.get('store');
const sourcePromise = getResources(store);
}
})
But if you're still looking for service approach i would use it like this
export default Ember.Service.extend({
resources: null,
store: Ember.inject.service(),
getResources: function() {
return this.get('store').findAll('source')
}
});
route
limeCore: Ember.inject.service(),
model: function() {
const store = this.get('store');
const sourcePromise = this.get('limeCore').getResources(); // sourcePromise.then(...
}
" My route's model() method result depends on the id of the resource"
model: function() {
this.get('limeCore').getResources().then(sources => {
return Ember.RSVP.hash({
artifact: store.find('artifact', { source: source.get('firstObject.id)})
})
})
}
Or solution 2
model: function() {
return Ember.RSVP.hash({
artifact: this.get('limeCore').getResources().then(sources => {
return store.find('artifact', {source: xx})
})
})
})
}
Also your getResources function can be modified by your criteria
function getResources(store) {
return store.findAll('resource').then(r => r.findBy('id', 1))
}
I think my question was poorly phrased, so I apologize to the reader for that. This happened because if I'm at the point of asking a question, it's often because I'm having trouble expressing the problem.
The approach suggested above didn't work for me, although it gave a few helpful hints. The significant requirement was that (as I mentioned in the comment) I needed to use the resource.id value in the model query. kristjan's approach addressed that, but my question didn't sufficiently show how complicated the model() method was.
An unwritten second requirement was that the ajax request is only made once, because the data rarely changes and is required in a lot of places on application load.
In the end, I was able to use a blend of kristjan's approach — creating a limeCore.getResource() method that loads the data in a Promise, and then require that promise in my route's beforeModel() hook. The key thing I realized was that beforeModel(), like model(), will wait until a Promise resolves before calling the next hook. In my application, model() should never run until these core objects have been loaded (model() is dependent upon them), so it made sense to have them loaded before. Perhaps there is a more elegant approach (which I'm open to hearing), but at this point I feel the issue has been resolved!
// services/lime-core.js
import Ember from 'ember';
export default Ember.Service.extend({
store: Ember.inject.service(),
resources: null,
clients: null,
init() {
this.set('resources', []);
this.set('clients', []);
},
// getCoreObjects() needs to be called at least once before the resources, clients and projects are available in the application. Ideally this method will be called in the route's beforeModel() hook. It cannot be called from the application route's beforeModel() hook because the code will not succeed if the user isn't authenticated.
getCoreObjects() {
if (this.get('resources').length === 0 || this.get('clients').length === 0) {
return Ember.RSVP.hash({
resources: this.get('store').findAll('resource').then(resources => {
this.set('resources', resources);
}),
clients: this.get('store').findAll('client', {include: 'projects'}).then(clients => {
this.set('clients', clients);
})
});
} else {
return Ember.RSVP.hash({});
}
}
});
and in my route:
// routes/dashboard.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
limeCore: Ember.inject.service(),
session: Ember.inject.service(),
...
beforeModel(transition) {
this._super(...arguments);
if (this.get('session.isAuthenticated')) {
return this.get('limeCore').getCoreObjects();
}
},
model(params) {
...
this.set('resource', this.store.peekAll('resource').findBy('slug', slug));
...
return this.store.query('artifact', {'resource-id': this.get('resource.id')});
}
}

Can Route model observe applicationController props?

I'm building ember app, and I have date selector at the top and a few tabs. Each tab represents a model to work with, but all models need date selector. So I store the date selector values as applicationController properties, and I've reached the point where I need to load data with store.query("Model", {date: applicationController.date}) and now I'm lost. If I use ModelController with hooks like:
export default Ember.Controller.extend({
appController: Ember.inject.controller('application'),
myNeedData: function() {
this.store.findAll('myNeedData',
{date: this.get('appController').get('selectedUrlDate')}
);
}.property('appController.selectedUrlDate')
})
everything actually works, but it is a hack. So I need to load model data through Route's model(). But how can I pass applicationController property to Route and make it observe the changes?
Thanks, Kitler for pointing out the path of researching. So I've made the service
export default Ember.Service.extend({
store: Ember.inject.service(),
loadModel(date) {
// some important actions
}
});
then the controller functions:
export default Ember.Controller.extend({
nextMonth() {
var date = this.get('selectedDate');
date.setMonth(date.getMonth() + 1);
this.set('selectedDate', new Date(date));
},
prevMonth() {
var date = this.get('selectedDate');
date.setMonth(date.getMonth() - 1);
this.set('selectedDate', new Date(date));
},
});
and the route:
export default Ember.Route.extend({
modelService: Ember.inject.service('my-service'),
model() {
return this.prepareModel();
},
setupController: function(controller, model) {
controller.set('model', model);
},
prepareModel() {
const date = this.controllerFor('application').get('selectedUrlDate');
return this.get('modelService').loadModel(date);
},
actions: {
nextMonth() {
const self = this;
self.controllerFor('application').nextMonth();
self.refresh();
},
prevMonth() {
const self = this;
self.controllerFor('application').prevMonth();
self.refresh();
},
}
});
So now I have data manipulation in route, not important repeatable property currentDate and its manipulation in application controller, and route accordingly changes model on user interactions due to refresh!