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.
Related
I recently tried extended the EmberRouter to include the following piece of information.
router.js
import EmberRouter from '#ember/routing/router';
const Router = EmberRouter.extend({
lastVisitedURL: null,
init() {
this._super(...arguments);
this.on('routeWillChange', () => {
this._super(...arguments);
this.lastVisitedURL = this.currentURL;
});
}
// Router.map code (not important)
})
I would now like to extract lastVisitedURL from a controller file. However, I'm not sure how to do it. Some of the things I've tried include importing the EmberRouter directly (I.E.):
import Router from '../router';
export default Controller.extend({
someFunction() {
console.log(Router.lastVisitedURL); // returns undefined
}
});
I'm not perfectly sure what the problem is with this approach, but it appears to be returning me some sort of other object or function that doesn't truly contain the state of the router.
So the next approach, that seems to be a little more accepted, was to try to use the RouterService object that I believe is meant to provide an API to the EmberRouter.
import Router from '../router';
export default Controller.extend({
router: service(),
someFunction() {
console.log(this.router.lastVisitedURL) // returns undefined
}
});
The problem I encountered with this solution though is that even though the routerService can store the state of the EmberRouter, it doesn't store my specific new variable. So I now need a way to add this specific pice of data to the RouterService as well as the EmberRouter.
I'm not really sure how to do this or if there is a better approach to the problem I'm trying to solve. Any thoughts appreciated!
I'm a little bit confused about your use case to be honest. The current URL is available on the RouterService, which is shipped with Ember by default. You could access it like this:
import Controller from '#ember/controller';
import { inject as service } from '#ember/service';
export default Controller.extend({
router: service(),
someFunction() {
console.log(this.router.currentURL);
}
});
It seems like you are trying to reinvent that feature.
If you want to go with declaring a property on the EmberRouter instance and use it at other places you need to look up the router on the container. It's available as router:main. You can't import it directly as it's neither a service nor a controller. The code would look like:
import Controller from '#ember/controller';
import { getOwner } from '#ember/application';
export default Controller.extend({
someFunction() {
let owner = getOwner(this);
let router = owner.lookup('router:main');
console.log(router.currentURL);
}
});
I would not recommend such a pattern. I don't think it's officially supported. As far as I'm aware router:main is private API. So it might be broken in a minor release.
This could be way better addressed by a service:
// app/services/location-history.js
import Service from '#ember/service';
import { inject as service } from '#ember/service';
import { action } from '#ember/object';
export default Service.extend({
router: service(),
updateRoute: action(function() {
this.visitedURLs = [...this.visitedRoutes, this.router.currentURL];
}),
init() {
this.router.on('routeDidChange', this.updateRoute);
this.set('visitedURLs', []);
},
willDestroy() {
this.router.off('routeDidChange', this.updateRoute);
}
});
If you find that syntax hard to read I recommend switching to native ECMAScript classes, which is the default syntax since Ember Octance:
// app/services/location-history.js
import Service from '#ember/service';
import { inject as service } from '#ember/service';
import { action } from '#ember/object';
export default class LocationHistoryService extends Service {
#service router;
visitedURLs = [];
#action
updateRoute() {
this.visitedURLs = [...this.visitedRoutes, this.router.currentURL];
}
constructor() {
this.router.on('routeDidChange', this.updateRoute);
},
willDestroy() {
this.router.off('routeDidChange', this.updateRoute);
}
});
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')});
}
}
I have recently started switching from syntax like this:
Ember.Object.extend({
someMethod: function(){
// do something
}.on('init')
});
to this:
Ember.Object.extend({
someMethod: Ember.on('init', function() {
// do something
})
});
Because it seems to be the convention nowadays and I hear it works better in add-ons.
However, occasionally I want to chain behaviors of the method like this:
Ember.Object.extend({
someMethod: function(){
// do something
}.on('init').observes('someProperty')
});
Is there a way to achieve this with the syntax that doesn't count on having function prototypes modified to include on and observes methods?
function() {}.on('init') does not work in addons because Function prototype extensions are disabled by default in addons. This was done so they work with applications that also have them turned off.
In this case, Ember.on('init', function() {}).observes('someProperty') is not considered a prototype extension because you are calling the .observes() function on whatever is returned from the .on() function.
You should avoid using .on() with lifecycle hooks, specifically .on('init'). The .on() event listener is asynchronous, leading to unexpected event execution ordering. The better alternative is to override the init method:
Ember.Object.extend({
init() {
this._super(...arguments);
}
});
Another suggested improvement is that starting with Ember 1.13 you have new lifecycle hooks available. This allows you to replace the .on() + .observes() with for example didReceiveAttrs:
Ember.Object.extend({
didReceiveAttrs() {
// check if `someProperty` changed
// do something
}
});
I just don't think it's possible with chaining, but if you need to do that with non-deprecated syntax this may work:
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Observe and Chain Demo',
clicks: 0,
someProp: 'undefined',
bindOnInitAndPropChange: Ember.on('init', function() {
this.set('someProp', `Initialized. I see ${ this.get('clicks') } clicks.`);
Ember.addObserver(this, 'clicks', null, function() {
this.set('someProp', `Observed, I now see ${ this.get('clicks') } clicks.`);
});
}),
actions: {
incrementClicks() {
this.incrementProperty('clicks');
}
}
});
Working Twiddle
How can transitionToRoute be called cleanly from within an Ember component?
It works with injecting a controller into the component and calling the controller's transitionToRoute function, however I'd like something a little more elegant if possible.
What it currently looks like inside the component's javascript:
// this.controller is injected in an initializer
this.controller.transitionToRoute("some.target.route.name");
What would be nicer in the component's javascript:
transitionToRoute("some.target.route.name");
One goal is do this without using sendAction as this particular component has a single purpose and should always transition to the same route. There's no need for any other Ember artifacts to be aware of the route this component always transitions to, there's no need for the associated indirection. The responsibility for the target route is owned by this component.
UPDATE Please see the other more recent answers for how to achieve this with less code in newer Ember versions, and vote those up if they work for you - Thanks!
Inject the router into the components and call this.get('router').transitionTo('some.target.route.name').
To inject the router into all components, write an initializer at app/initializers/component-router-injector.js with the following contents:
// app/initializers/component-router-injector.js
export function initialize(application) {
// Injects all Ember components with a router object:
application.inject('component', 'router', 'router:main');
}
export default {
name: 'component-router-injector',
initialize: initialize
};
Sample usage in a component:
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
submit: function() {
this.get('router').transitionTo('some.target.route.name');
}
}
});
Jan 22, 2018 update
As of Ember 2.15, phase 1 of the public router service is implemented.
Transition to a route from inside a component:
import { inject as service } from '#ember/service';
export default Ember.Component.extend({
router: service(),
actions: {
someAction() {
this.get('router').transitionTo('index');
}
}
});
Use
router: service()
instead of
router: service('-routing')
import Component from '#ember/component';
import {inject as service} from '#ember/service';
export default Component.extend({
router: service(),
actions: {
onClick(params) {
let route = this.getMyRoute(params);
this.get('router').transitionTo(route);
}
}
});
If you want to use the router only in a specific component or service or controller, you may try this:
Initialize an attribute with the private service -routing. The - because it's not a public API yet.
router: service('-routing'),
And then inside any action method or other function inside the service or component:
this.get('router').transitionTo(routeName, optionalParams);
Note: It'll be transitionToRoute in a controller.
You can use container to get access to any needed part of application. To get application controller :
this.container.lookup('controller:application')
But what about structure of application - components should generate events - so my opinion it's better to use sendAction. Cause in future you can get situation, when you need to filter such behavior ( for example ) or other application-specific logic before transition
I am using Ember's Need Api to call a method of a controller in another controller. I am able to get the instance of the controller but when I am calling it method it returns me this error TypeError: Object [object Object] has no method.
This is how I am calling it:
Cards.CardsIndexController = Ember.Controller.extend({
needs: 'account_info',
actions: {
accountInfoStart:function(){
console.log(this.get('controllers.account_info').test()); // error here
}
}
});
This is the controller whose function I want to call
Cards.AccountInfoController = Ember.Controller.extend({
actions:{
test: function(){
alert(1);
}
}
});
How can I solve it?
test is not technically a method, but an action or event. Use the send method instead:
this.get('controllers.account_info').send('test', arg1, arg2);
As per Ember documentation; create a property that lazily looks up another controller in the container. This can only be used when defining another controller.
legacy ember application example:
App.PostController = Ember.Controller.extend({
accountInfo: Ember.inject.controller()
this.get('accountInfo').send('test')
});
modern ember application example:
// in an ember app created with ember-cli
// below snippet would be the app/controllers/post.js file
import Ember from 'ember';
export default Ember.Controller.extend({
appController: Ember.inject.controller('application')
});
You can find more documentation about Ember.inject here
From the Updated Ember Documentation :
import { inject } from '#ember/controller';
export default Ember.Controller.extend({
appController: inject('application')
});
For further reference, you can find out by this link https://guides.emberjs.com/release/applications/dependency-injection/#toc_ad-hoc-injections