I am trying to implement an application-level error handler to catch failed promises within my application. Most of the suggestions I've seen around this have to do with error logging, but I actually want to trigger a modal window to let my users know they're no longer connected to the internet + can only read data.
I'd like to trigger an action from within the RSVP.onerror handler. The only way I've managed to do this so far is with something like this:
Ember.RSVP.configure('onerror', function(e) {
window.App.__container__
.lookup('controller:application')
.send('showModal', 'disconnected');
});
but it seems a bit hacky to me.
Is there somewhere more appropriate to write this code? More generally, are there any best practices around notifying users of a lost internet connection?
Ember has a built in error route (docs here).
Depending on how you want to handle other kinds of errors in your app, you could add views/error.js and run a method on willInsertElement in that view that tests whether the user is offline and, if true, sends the showModal action.
Alternatively, failed promises can automatically be caught in the original promise and trigger some method that has been injected into the controller or whatever instance is making the promise:
Controller:
this.set('thing').then(function() {
// Success
}, this.offlineError()); // Failure
Initializer (note the syntax is for ember-cli):
export default {
name: 'offline-error',
initialize: function(container, app) {
var offlineError = function() {
// or something like this
container.lookup('controller:application').send('showModal', 'disconnected');
};
app.register('offlineError', offlineError, { instantiate: false });
// Inject into routes, controllers, and views.
Em.A(['route', 'controller', 'view']).forEach(function(place) {
app.inject(place, 'offlineError', 'offlineError:main');
});
}
};
The above code makes the offlineError method available in all routes, controllers, and views but the beauty of it is that the method has access to the container without using the hacky private property.
I ended up wrapping Offline.js in a service. I'm using ember-cli.
// services/connection.js
/* global Offline */
import Ember from 'ember';
// Configure Offline.js. In this case, we don't want to retry XHRs.
Offline.requests = false;
export default Ember.Object.extend({
setup: function() {
if (Offline.state === 'up') {
this._handleOnline();
} else {
this._handleOffline();
}
Offline.on('down', this._handleOffline, this);
Offline.on('up', this._handleOnline, this);
}.on('init'),
_handleOffline: function() {
this.set('isOffline', true);
this.set('isOnline', false);
},
_handleOnline: function() {
this.set('isOnline', true);
this.set('isOffline', false);
}
});
I injected the service into controllers and routes:
// initializers/connection.js
export default {
name: 'connection',
initialize: function(container, app) {
app.inject('controller', 'connection', 'service:connection');
app.inject('route', 'connection', 'service:connection');
}
};
Now in my templates, I can reference the service:
{{#if connection.isOffline}}
<span class="offline-status">
<span class="offline-status__indicator"></span>
Offline
</span>
{{/if}}
(Offline.js also has some packaged themes, but I went with something custom here).
Also, in my promise rejection handlers I can check if the app is offline, or if it was another unknown error with the back-end, and respond appropriately.
If anyone has any suggestions on this solution, chime in!
Related
I'm new to Ember.js and I've got some problems to understand its philosophy. I know actions up, data down but in real life, lets say I have Fotorama initialized in my-gallery component (I don't know if that is ok, but I did it in didInsertElement method). This library has its own events. They could look like this in plain JS:
$('.fotorama').on('fotorama:ready', function (e, fotorama) {});
or:
$('.fotorama').on('fotorama:show', function () {});
but I feel in Ember, those should be somehow mapped into actions in component.
My question is: how? I need to fire some actions (to be catched by another components or maybe a router) inside those actions. So I think it should be like this: this.sendAction('actionName', actionParams);
You can keep component reference to call sendAction method.
didInsertElement(){
this._super(...arguments);
var _this=this;
this.$('.fotorama').on('fotorama:show', function () {
_this.sendAction('actionName', actionParams);
});
}
willDestroyElement(){
this._super(...arguments);
this.$('.fotorama').off('fotorama:show')
}
If we find an better answer to this question. I will happily remove my answer.
I have a similar problem. I need to have a third party library talk to my ember app so I registered a custom event in my ember app:
const App = Ember.Application.extend({
customEvents: {
foo: 'foo',
},
});
Then triggered it from third party code:
<a href="..." onclick="$(this).trigger('foo'); return false;">
And, in a component, write the handler for that event according to the docs:
export default Ember.Component.extend({
foo(event) {
console.warn('event happened', event);
},
});
See:
* https://guides.emberjs.com/v3.0.0/components/handling-events/
* https://www.emberjs.com/api/ember/release/classes/Application/properties/customEvents?anchor=customEvents
Hey I'm facing a problem with removing a view.
The view is used as navbar
{{view "inner-form-navbar" navbarParams=innerNavObject}}
Where params look like this
innerNavObject: {
...
routeToReturn: 'someroute.index',
...
},
On the navbar there's a small "back" button when it's clicked the parent index route is opened.
It currently works like this:
this.get('controller').transitionToRoute(routeToReturn);
But this won't work in a component and is sketchy anyways. Do i need to somehow inject router to component? Or has anyone gotten a solution for this? The navbar is used in so many places so adding a property to navbarObject to have certain action defined is not a really good solution imo.
Went for this solution :
export default {
name: 'inject-store-into-components',
after: 'store',
initialize: function(container, application) {
application.inject('component', 'store', 'service:store');
application.inject('component', 'router', 'router:main');
}
};
Now i can do
this.get('router').transitionTo('blah')
Well you can try to use a service that provides the routing capabilities and then inject into the component.
There's an addon that seems to do just that - ember-cli-routing-service
Example taken from the link, adapted for you scenario:
export default Ember.Component.extend({
routing: Ember.inject.service(),
someFunc () {
this.get('routing').transitionTo(this.get('innerNavObject'). routeToReturn);
}
});
Having a component control your route/controller is typically bad practice. Instead, you would want to have an action that lives on your route or controller. Your component can then send that action up and your route or controller will catch it (data down, actions up).
In your controller or route, you would have your transition action:
actions: {
transitionFunction(route) {
this.transitionTo(route);
}
}
You would also define the the current route name in your route or controller and pass that to your nav bar component. Controller could then look like:
export default Controller.extend({
application: inject.controller(),
currentRoute: computed('application.currentRouteName', function(){
return get(this, 'application.currentRouteName');
}),
actions: {
transitionFunction(route) {
this.transitionTo(route);
}
}
});
Then call your component and pass the currentRoute CP to it:
{{nav-bar-component currentRoute=currentRoute action='transitionFunction'}}
Then, in your component, you can have a function that finds the parent route from the currentRoute:
export default Component.extend({
click() { // or however you are handling this action
// current route gives us a string that we split by the . and append index
const indexRoute = get(this, currentRoute).split('.')[0] + '.index';
this.sendAction('action', indexRoute);
}
});
Extending a route
Per your comment, you may want to have this across multiple routes or controllers. In that case, create one route and have your others extend from it. Create your route (just as I created the Controller above) with the action. Then import it for routes you need:
import OurCustomRoute from '../routes/yourRouteName';
export default OurCustomRoute.extend({
... // additional code here
});
Then your routes will have access to any actions or properties set on your first route.
I am using the latest Ember, 2.1, and am wondering how to set some application wide variables, such as user id, username, email, etc, presumably on the application controller.
While ember applications have tons of files, I haven't really done a lot yet, I'm somewhat confident I'm sharing just the right code. I don't have a login route file. I have the ember simple auth plugin installed, but I'm not actually using/invoking it any special way, except for mixing it into my application route:
import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin';
export default Ember.Route.extend(ApplicationRouteMixin)
My router:
this.route('login')
My login template:
<button {{action 'forceLogin'}}>Force login of devinrhode2#gmail.com by clicking this action button</button>
<p>Your account id is: {{account_id}}</p>
My login controller:
export default Ember.Controller.extend({
actions: {
forceLogin: () => {
var data = {
"account_id": 123,
"email":"devinrhode2#gmail.com",
"name":"Devin Rhode"
}
this.setProperties(data)
}
}
});
I have the forceLogin controller action being called, but, the {{account_id}} is not populating into the template. How could I get the account_id to render back into the template? How could I make the account_id globally accessible to my ember application by calling this.get('account_id') wherever I need it?
Currently I get the error:
Cannot read property 'setProperties' of undefined
You get the error because of the way you define forceLogin. Arrow functions are bound to the context where they're defined. Here's what your code compiles to:
var _this = this; // we capture `this` from out here!
export default Ember.Controller.extend({
actions: {
forceLogin() {
...
_this.setProperties(data) // `_this` is the window!
}
}
});
That's no good because this should be the instance of the controller and instead it's the window.
Instead you should define forceLogin like this:
export default Ember.Controller.extend({
actions: {
forceLogin() {
...
this.setProperties(data) // `this` is our controller instance
}
}
});
To get the account_id from somewhere else, you can inject the login controller:
// in some other controller
export default Ember.Controller.extend({
login: Ember.inject.controller(),
actions: {
doSomethingWithTheAccountId() {
var accountId = this.get('login.account_id');
...
}
}
});
It would be cleaner to move those properties to a service, which you can inject anywhere with Ember.inject.service()
I'm developing a webapp to display and search documents. I've laid out the main wrapper div like this:
<div class="wrapper">
{{searchComponent searchComponent=searchComponent}}
{{pdfViewer pdfSearchComponent=searchComponent}}
</div>
This allows me to later add other types of viewers in there, like so:
{{otherViewer otherSearchComponent=searchComponent}}
The outer wrapper is an ember component as well. So it's controller looks like:
Ember.controller.extend({
searchComponent: null,
.
.
otherProperties,
actions: { }
});
And the searching component binds itself on itialization, as inspired from this source: http://www.samselikoff.com/blog/getting-ember-components-to-respond-to-actions/
Ember.controller.extend({
searchComponent: null,
.
.
onStart: function(){
this.searchComponent = this;
}.on('init'),
.
.
actions: {
someAction: function() {
//do something
}
}
}
So I can now reference the component from the main pdfViewer like this:
this.get('searchComponent').send('someAction')
To get the response, right now I bind another property to all the controllers / templates, and then watch for a change on that property in the viewer controller, after which I can place the results where they need to be.
Is there a way to send a 'message' from my 'pdfViewer' to my 'searchComponent' and receive a 'response' without explicitly binding them together, as above?
You could consider using pub/sub through a Service event bus, in which your searchComponent and pdfViewer both emit and listen for messages, so can talk to each other. Sure, there is a dependency on the service, but from what I see your components are pretty application-specific anyway.
Something like:
_listen: function() {
this.get('eventBus').on('message', this, 'handleMessage');
}.on('init'),
actions: {
click() { this.get('eventBus').trigger('message', message); }
}
A few weeks ago I evaluated several approaches to parent-children component communication: http://emberigniter.com/parent-to-children-component-communication/, perhaps this helps somewhat.
Quick summary/tldr:
It seems that Ember's container lookup process + Ember-CLI's module resolver doesn't allow manually un-registering a service and then registering a replacement if the original service can be resolved using the resolver (I want to do the method described here, but it doesn't work)
How can I mock an Ember-CLI service in an acceptance test without using a hacky, custom resolver? (example project/acceptance test here)
Detailed explanation + example
Create a new service that is injected into a controller:
ember generate service logger
services/logger.js
export default Ember.Object.extend({
log: function(message){
console.log(message);
}
});
initializers/logger-service.js
export function initialize(container, application) {
application.inject('route', 'loggerService', 'service:logger');
application.inject('controller', 'loggerService', 'service:logger');
}
The service is accessed through its injected name, loggerService, in an action handler on the application controller:
Use the service in a controller
templates/application.hbs
<button id='do-something-button' {{action 'doSomething'}}>Do Something</button>
controllers/application.hs
export default Ember.Controller.extend({
actions: {
doSomething: function(){
// access the injected service
this.loggerService.log('log something');
}
}
});
Attempt to test that this behavior occurs correctly
I created an acceptance test that checks that the button click triggered the service. The intent is to mock out the service and determine if it was called without actually triggering the service's implementation -- this avoids the side-effects of the real service.
ember generate acceptance-test application
tests/acceptance/application-test.js
import Ember from 'ember';
import startApp from '../helpers/start-app';
var application;
var mockLoggerLogCalled;
module('Acceptance: Application', {
setup: function() {
application = startApp();
mockLoggerLogCalled = 0;
var mockLogger = Ember.Object.create({
log: function(m){
mockLoggerLogCalled = mockLoggerLogCalled + 1;
}
});
application.__container__.unregister('service:logger');
application.register('service:logger', mockLogger, {instantiate: false});
},
teardown: function() {
Ember.run(application, 'destroy');
}
});
test('application', function() {
visit('/');
click('#do-something-button');
andThen(function() {
equal(mockLoggerLogCalled, 1, 'log called once');
});
});
This is based on the talk Testing Ember Apps: Managing Dependency by mixonic that recommends unregistering the existing service, then re-registering a mocked version:
application.__container__.unregister('service:logger');
application.register('service:logger', mockLogger, {instantiate: false});
Unfortunately, this does not work with Ember-CLI. The culprit is this line in Ember's container:
function resolve(container, normalizedName) {
// ...
var resolved = container.resolver(normalizedName) || container.registry[normalizedName];
// ...
}
which is part of the container's lookup chain. The issue is that the container's resolve method checks the resolver before checking its internal registry. The application.register command registers our mocked service with the container's registry, but when resolve is called the container checks with the resolver before it queries the registry. Ember-CLI uses a custom resolver to match lookups to modules, which means that it will always resolve the original module and not use the newly registered mock service. The workaround for this looks horrible and involves modifying the resolver to never find the original service's module, which allows the container to use the manually registered mock service.
Modify Resolver to avoid resolving to original service
Using a custom resolver in the test allows the service to be successfully mocked. This works by allowing the resolver to perform normal lookups, but when our service's name is looked up the modified resolver acts like it has no module matching that name. This causes the resolve method to find the manually registered mock service in the container.
var MockResolver = Resolver.extend({
resolveOther: function(parsedName) {
if (parsedName.fullName === "service:logger") {
return undefined;
} else {
return this._super(parsedName);
}
}
});
application = startApp({
Resolver: MockResolver
});
This seems like it shouldn't be necessary and doesn't match the suggested service mocking from the above slides. Is there a better way to mock this service?
The ember-cli project used in this question be found in this example project on github.
Short version of the solution: your registered mock service must have a different service:name than the "real" service you're trying to mock.
Acceptance test:
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from 'container-doubling/tests/helpers/start-app';
var application;
let speakerMock = Ember.Service.extend({
speak: function() {
console.log("Acceptance Mock!");
}
});
module('Acceptance | acceptance demo', {
beforeEach: function() {
application = startApp();
// the key here is that the registered service:name IS NOT the same as the real service you're trying to mock
// if you inject it as the same service:name, then the real one will take precedence and be loaded
application.register('service:mockSpeaker', speakerMock);
// this should look like your non-test injection, but with the service:name being that of the mock.
// this will make speakerService use your mock
application.inject('component', 'speakerService', 'service:mockSpeaker');
},
afterEach: function() {
Ember.run(application, 'destroy');
}
});
test('visit a route that will trigger usage of the mock service' , function(assert) {
visit('/');
andThen(function() {
assert.equal(currentURL(), '/');
});
});
Integration test (this is what I was originally working on that caused me issues)
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';
let speakerMock = Ember.Service.extend({
speak: function() {
console.log("Mock one!");
}
});
moduleForComponent('component-one', 'Integration | Component | component one', {
integration: true,
beforeEach: function() {
// ember 1.13
this.container.register('service:mockspeaker', speakerMock);
this.container.injection('component', 'speakerService', 'service:mockspeaker');
// ember 2.1
//this.container.registry.register('service:mockspeaker', speakerMock);
//this.container.registry.injection('component', 'speakerService', 'service:mockspeaker');
}
});
test('it renders', function(assert) {
assert.expect(1);
this.render(hbs`{{component-one}}`);
assert.ok(true);
});
You can register your mock and inject it instead of the original service.
application.register('service:mockLogger', mockLogger, {
instantiate: false
});
application.inject('route', 'loggerService', 'service:mockLogger');
application.inject('controller', 'loggerService', 'service:mockLogger');
I use this approach for mocking the torii library in my third-party login acceptance tests. I hope there will be a nicer solution in the future.
The existing answers work well, but there's a way that avoids renaming the service and skips the inject.
See https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/helpers/module-for-acceptance.js#L14 and usage at https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/acceptance/keyboard-shortcuts-test.js#L13
I'll present it here as an update to the test helper I previously had here, so it's a drop-in replacement, but you may just want to follow the links above instead.
// tests/helpers/override-service.js
// Override a service with a mock/stub service.
// Based on https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/helpers/module-for-acceptance.js#L14
// e.g. used at https://github.com/ember-weekend/ember-weekend/blob/fb4a02/tests/acceptance/keyboard-shortcuts-test.js#L13
//
// Parameters:
// - newService is the mock object / service stub that will be injected
// - serviceName is the object property being replaced,
// e.g. if you set 'redirector' on a controller you would access it with
// this.get('redirector')
function(app, newService, serviceName) {
const instance = app.__deprecatedInstance__;
const registry = instance.register ? instance : instance.registry;
return registry.register(`service:${serviceName}`, newService);
}
Plus performing the jslint and helper registration steps from https://guides.emberjs.com/v2.5.0/testing/acceptance/#toc_custom-test-helpers
I can then call it like this, in my example stubbing out a redirect (window.location) service, which we want to do because redirecting breaks Testem:
test("testing a redirect's path", function(assert) {
const assertRedirectPerformed = assert.async();
const redirectorMock = Ember.Service.extend({
redirectTo(href) {
assert.equal(href, '/neverwhere');
assertRedirectPerformed();
},
});
overrideService(redirectorMock, 'redirector');
visit('/foo');
click('#bar');
});