How to use asynchronous service in ember.js? - ember.js

Before my ember.js app start, I need to access a entry api to get all those api endpoints and csrf token.
I'm trying to do it in service so the response can be accessed everywhere:
import Service from '#ember/service';
import $ from 'jquery'
export default Service.extend({
pageData: null,
init () {
this._super(...arguments)
var self = this
$.ajax({
type: 'get',
url: '/page_data',
dataType: 'json'
}).then((response) => {
self.set('pageData', response)
})
}
});
And I'm injecting the service in initializer:
export function initialize (application) {
application.inject('route', 'api', 'service:page-data');
application.inject('controller', 'api', 'service:page-data');
application.inject('component', 'api', 'service:page-data');
}
export default {
initialize
};
But I can't use the service in my router directly with this.get('api').pageData because it's asynchronous.
I found something on ember.js documentation here:
Operations performed in initializers should be kept as lightweight as possible to minimize delays in loading your application. Although advanced techniques exist for allowing asynchrony in application initializers (i.e. deferReadiness and advanceReadiness), these techniques should generally be avoided. Any asynchronous loading conditions (e.g. user authorization) are almost always better handled in your application route's hooks, which allows for DOM interaction while waiting for conditions to resolve.
But it 's used in initializers, not service.
Any suggests what I can do with my service now?

You can use application's deferReadiness method, to stall your app load.
However, if you require a loading UI, i would suggest to set a promise object to your pageData variable, and as soon as that promise resolves/rejects you can change the state of your app accordingly.

Here's what I did at last:
import Service from '#ember/service';
import $ from 'jquery'
import { Promise } from 'rsvp'
export default Service.extend({
pageData: null,
setup () {
return new Promise((resolve, reject) => {
var self = this
$.ajax({
type: 'get',
url: '/page_data',
dataType: 'json'
}).then((response) => {
self.set('pageData', response)
resolve()
}).catch(() => {
reject()
})
})
}
});
Then in the index route, run the setup in beforeModel hook:
import Route from '#ember/routing/route';
import { all } from 'rsvp'
export default Route.extend({
beforeModel () {
return all([this.get('api').setup()])
},
model () {
window.console.log(this.get('api').pageData)
}
});

Related

Download files in ember: using Ember-cli-file-saver

It might be plain ignorance from my part but I have only managed to download a file generated by the api using the model method mention in the documentation. Using a component I am quite blind.
The specific question would be: where do I pass the mention arraybuffer:true to the application adapter or to a custom ajax request? Do you have a working example?
Here is a simple try using an ajax service:
import Component from '#ember/component';
import FileSaverMixin from 'ember-cli-file-saver/mixins/file-saver';
import { inject as service } from '#ember/service';
export default Component.extend(FileSaverMixin, {
tagName: 'div',
ajax: service(),
store: service(),
click() {
this.get('ajax').request('/excel', {
options: {
arraybuffer: true
}
}
).then((content) => {
console.log(content);
this.saveFileAs(this.get('filename'), content, this.get('contentType'));
}).catch((error) => {
console.log(error);
})
}
});
And this is my adapter:
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
import AdapterArrayBufferMixin from 'ember-cli-file-saver/mixins/adapter-arraybuffer-mixin';
import ENV from 'efac-front/config/environment';
export default DS.JSONAPIAdapter.extend(
DataAdapterMixin,
AdapterArrayBufferMixin,
{
authorizer: 'authorizer:token',
namespace: 'api',
host: ENV.host
}
);
I keep getting an error of SyntaxError: Unexpected token P in JSON at position 0... because it is trying to interpret an array buffer or binary response as json data.
I very much appreciate any light you can throw here
Well it was super easy, but not very well documented. I just needed to add the dataType key to my ajax request like this:
this.get('ajax').request('/excel', {
dataType: 'arraybuffer',
options: {
arraybuffer: true
}
}
).then((content) => {
this.saveFileAs('reporte-asistencia.xlsx', content, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
}).catch((error) => {
console.log(error);
});
It can also be done with dataType: 'blob'.

Ember simple auth doesn't send auth header to api

I'm trying make authorization in my ember application.
Its working on client side, but ember doesn't attach Bearer token to api request.
My adapter
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
export default DS.JSONAPIAdapter.extend(DataAdapterMixin, {
host: 'http://localhost/money-app-api/web/app_dev.php/api',
authorizer: 'authorizer:application'
});
My authorizer:
import Ember from 'ember';
import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';
const { isEmpty } = Ember;
export default OAuth2Bearer.extend({
authorize(data, block) {
const accessToken = data['access_token'];
if (!isEmpty(accessToken)) {
block('Authorization', `Bearer ${accessToken}`);
}
accessToken in authorizer is exists and is correct.
My api is correct too, i tested it by Postman.
I am writing you a full tutorial please just follow that hope it works for you.
//folders and files tree
adapters
--- application.js
authenticators
--- oauth2.js
authorizers
---- oauth2-bearer.js
Adapters/application.js
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
export default DS.JSONAPIAdapter.extend(DataAdapterMixin,{
authorizer: 'authorizer:oauth2-bearer',
host: 'http://localhost/money-app-api/web/app_dev.php',
namespace: 'api'
});
authenticators/oauth2.js
import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';
export default OAuth2PasswordGrant.extend({
serverTokenEndpoint: 'http://localhost/money-app-api/web/app_dev.php/token'
});
authorizers/oauth2-bearer.js
export { default } from 'ember-simple-auth/authorizers/oauth2-bearer';
so now in your route, application.js, you are able to use the following code: this is just for demo purpose you need to modify as you need.
this.get('session').authorize('authorizer:oauth2-bearer', (headerName, headerValue) => {
headers[headerName] = headerValue;
});
I am writing an authentication in route/application.js to clarify more. in this example I am getting Account and User information based on session which has already authenticated .
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
import config from '../config/environment';
export default Ember.Route.extend(ApplicationRouteMixin, {
model() {
return Ember.RSVP.hash({
account: new Ember.RSVP.Promise((resolve, reject) => {
if (!this.get('session.isAuthenticated')) {
resolve({});
return;
}
let store = this.store,
session = this.get('session');
let headers = {};
this.get('session').authorize('authorizer:oauth2-bearer', (headerName, headerValue) => {
headers[headerName] = headerValue;
});
return Ember.$.ajax(config.apiUrl + '/api/account', {
headers: headers
}).then(data => {
if (data) {
store.pushPayload(data);
resolve(store.peekRecord('user', data.data.id));
} else {
reject({});
session.invalidate();
}
}).fail(() => {
session.invalidate();
});
})
});
},
sessionAuthenticated() {
this.refresh();
this._super();
}
});
I hope, this can solve your problem.
an important tip:
The REST adapter allows your store to communicate with an HTTP server by
transmitting JSON via XHR. Most Ember.js apps that consume a JSON API
should use the REST adapter.
### Headers customization
Some APIs require HTTP headers, e.g. to provide an API key. Arbitrary
headers can be set as key/value pairs on the `RESTAdapter`'s `headers`
object and Ember Data will send them along with each ajax request.
```app/adapters/application.js
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
headers: {
"API_KEY": "secret key",
"ANOTHER_HEADER": "Some header value"
}
});
```
`headers` can also be used as a computed property to support dynamic
headers. In the example below, the `session` object has been
injected into an adapter by Ember's container.
```app/adapters/application.js
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
headers: Ember.computed('session.authToken', function() {
return {
"API_KEY": this.get("session.authToken"),
"ANOTHER_HEADER": "Some header value"
};
})
});
```
Source
My only problem was that i have not authorization in my accept headers in api
allow_headers: ['origin', 'X-Custom-Auth', 'Content-Type', 'Authorization']
That is strange, because when tested by Postman all was working.

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

Custom post-request with RESTAdapter

My models work with the server via Ember's default RESTAdapter.
I just created a custom endpoint /mail on my server which sends an e-mail if provided a name, valid e-mail-adress and text.
How do I make Ember send that custom post-request? Is it possible without Ember.ajax at all?
For me personally, I wouldn't use Ember-Data to handle that scenario; I generally only use Ember-Data to handle my persisted models. If you try to use Ember-Data for other AJAX calls, it's just going to become a mess. Remember that Ember-Data's job is to manage your persisted data and one way that it can do that is with AJAX calls. That doesn't mean that anything that requires an AJAX call should be handled with Ember-Data.
I have this same issue and I wrote a utility module that has functions for all of my non-model AJAX stuff. This makes it really easy to swap out for testing. Here's a small example:
// utils/ajax.js
export function sendHelpEmail(comment) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'POST',
url: '/api/contact_us',
contentType: 'application/json',
data: JSON.stringify({ comment }),
processData: false,
statusCode: {
200: () => Em.run(null, resolve),
500: () => Em.run(null, reject)
}
});
});
}
Then, I can do something like this in my controller:
import { sendHelpEmail} from '../utils/ajax.js';
export default Em.Controller.extend({
actions: {
sendEmail() {
sendHelpEmail(this.get('comment'));
}
}
});

How to mock an Ember-CLI service in an acceptance test?

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