Accessing a service's promise from a route - ember.js

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

Related

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

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

PromiseProxy Service with Ember Data query

I'm trying to setup a PromiseProxy Service that returns an Ember Data model, but the result doesn't seem to set the content property.
My service looks like this:
import Ember from 'ember';
const { computed, inject, ObjectProxy, PromiseProxyMixin } = Ember;
export default ObjectProxy.extend(PromiseProxyMixin, {
isServiceFactory: true,
store: inject.service(),
promise: computed({
get() {
var store = this.get('store');
return store.findRecord('community', window.community.id);
}
})
});
I then inject this service into the following locations:
export function initialize(container, application) {
application.inject('controller', 'community', 'service:community');
application.inject('route', 'community', 'service:community');
application.inject('model', 'community', 'service:community');
application.inject('component', 'community', 'service:community');
}
export default {
name: 'community',
after: 'store',
initialize: initialize
};
And then I use it as a model in my application route as a sort of deferReadiness workaround, since my whole app depends on this one model
which is used throughout and expected to be there.
export default Ember.Route.extend({
model() {
return this.get('community');
}
});
The issue is that it goes on to other routes, and properties on the community object are not there, i.e. content isn't set. Also community.isPending is true. The CP does get hit and the data comes back (I tested with a then in the CP).
Here is a full gist example: https://gist.github.com/knownasilya/8c9f78d910ed50ec8d84
Edit
So I found a workaround:
promise: computed({
get() {
var store = this.get('store');
return store.findRecord('community', window.community.id)
.then(data => {
this.set('content', data);
return data;
})
}
})
Seems like it doesn't set the content because model is proxied already?
Ember Data already wraps its objects in an ObjectProxy, you could just set the object as your service.
Additionally, this syntax is deprecated in future versions syntax for initializers, since it's moved to instance initializers, but no big deal.
initialize: function (container, application) {
// the store will be available from the container,
// and the name of the store changes depending on which version you are using.
var store = container.lookup('service:store'),
community= store.find('community', id);
application.register("service:community", community, { instantiate: false });
application.inject("controller", "community", "service:community");
application.inject("route", "community", "service:community");
application.inject("component", "community", "service:community");
}
And then you can still return community from the model, beforeModel hook etc.

Ember: hooking into ActionHandler#send

I am trying to instrument this.send() in Ember, by hooking into ActionHandler#send as follows:
Ember.ActionHandler.reopen({
send() { console.log("hooked"); this._super(...arguments); }
}
When I call this from app.js, as the app is starting up, it works. When I call it from an initializer, it does not. When I call it after the app starts up, such as from the application controller, it doesn't work either. In both cases where it doesn't work, if I trace into a this.send() call, it goes directly into the original implementation of send.
I have a suspicion this has something to do with the way mixins are used when instantiating objects, but otherwise I'm stumped.
It does work when using initializer:
initializers/action-hook.js
import Ember from 'ember';
export function initialize() {
Ember.ActionHandler.reopen({
send() {
console.log("hooked");
this._super(...arguments);
}
});
}
export default {
name: 'action-hook',
initialize: initialize
};
Tested in application controller.
controllers/application.js
import Ember from 'ember';
export default Ember.Controller.extend({
afterInit: Ember.on('init', function() {
Ember.run.next(() => {
console.log('Send action.');
this.send('exampleAction');
});
}),
actions: {
exampleAction() {
console.log('exampleAction handled');
}
}
});
It outputs:
Send action.
hooked
exampleAction handled
Working demo and full code behind it.

Obversing route change to apply equivalent to onload

I'm trying to observe the route change to apply some common action once rendered. The idea is to have a feature similar to the onload but as we use a single-page app this needs to be triggered on each route changes. (could be scoped to the new view)
I found how to observe the currentPath changes:
App.ApplicationController = Ember.Controller.extend({
currentPathDidChange: function() {
prettyPrint()
}.observes('currentPath');
});
While this works good in some cases, it gets triggered when the route changes, but still to early to apply content changes as it seem to append before the content gets rendered.
Any idea on the best practice to achieve such goal?
Have you tried deferring the code with Ember.run.schedule? For instance,
App.ApplicationController = Ember.Controller.extend({
currentPathDidChange: function() {
Ember.run.schedule('afterRender', this, function() {
prettyPrint();
});
}.observes('currentPath')
});
Due to the deprecation of Controllers in Ember 1.x finding the url in the router would be a good way to future proof your apps. You can do this in ember-cli like so:
// similar to onLoad event behavior
export default Ember.Route.extend({
afterModel: function (model){
Ember.run.next(() => {
console.log(this.get('router.url'));
});
}
});
// hacky way to get current url on each transition
export default Ember.Route.extend({
actions: {
didTransition: function() {
Ember.run.next(() => {
console.log(this.get('router.url'));
});
}
}
});
This will log: /posts and /posts/3/comments ect.