Ember 1.0 Final Internet Explorer 8 and 9 (History API - pushState and replaceState) - ember.js

I was under the impression that Ember was well tested under some older versions of IE. However, upon finally firing up my near complete app (form wizard). I'm noticing IE is complaining about replaceState and pushState, two methods that are not supported according to http://caniuse.com/#search=pushState
Are there any workarounds for this?
SCRIPT438: Object doesn't support property or method 'replaceState'
get(this, 'history').replaceState(state, null, path);

UPDATE: As of Ember 1.5.0+ I can confirm that they added 'auto' which should eliminate the need for the example below.
App.Router.reopen({
location: 'auto'
});
Original Answer:
Apparently you need to feature detect the history API:
if (window.history && window.history.pushState) {
App.Router.reopen({
location: 'history'
});
}

There is no work around. If you need to use real URLs (/users) instead of hash URLs (/#/users) then you have to either exclude IE 8 & 9 from your list of supported browsers, or you need to use Ember in a "progressive enhancement" manner, still serve valid content from the server for each real URL that is visited, and use feature detection to selectively enable your Ember app.

To properly support both pushSate and non-pushState browsers, you need to have a translator between the 2 different state mechanisms.
For example, let's say your rootURL is '/admin/' and you start with this URL:
/admin/users/123
For IE8/9, you'll want to redirect the user to '/admin/#/users/123' before Ember's routing mechanism takes over. Likewise, if you start with this URL:
/admin/#/users/123
...then for browsers that support pushState, you'll want to replace the state to '/admin/users/123' before Ember's routing mechanism takes over.
This is the default behavior of Backbone's router and it works fairly well.
To achieve this result in Ember, you can do something like this, which was inspired by Backbone's source code:
App.Router.reopen({
rootURL: '/admin/',
init: function () {
this.translateRoute();
this._super();
},
translateRoute: function () {
var hasPushState = window.history && window.history.pushState;
var atRoot = window.location.pathname === this.rootURL;
var fragment = decodeURIComponent(window.location.pathname);
if (!fragment.indexOf(this.rootURL))
fragment = fragment.substr(this.rootURL.length);
if (hasPushState)
this.location = 'history';
// If we started with a route from a pushState-enabled browser,
// but we're currently in a browser that doesn't support it...
if (!hasPushState && !atRoot) {
window.location.replace(this.rootURL + '#/' + fragment);
return;
}
// If we started with a hash-based route,
// but we're currently in a browser that supports pushState...
if (hasPushState && atRoot && window.location.hash) {
fragment = window.location.hash.replace(/^(#\/|[#\/])/, '');
window.history.replaceState({}, document.title, window.location.protocol + '//' + window.location.host + this.rootURL + fragment);
}
}
});

Related

Get two versions of ember-simple-auth to play well together

We're working with two ember applications that each run different version of ember and ember-simple-auth, and want to get ember-simple-auth to work well with both version.
The old app
Ember 1.8.1
Ember-simple-auth 0.7.3
The new app
Ember 2.3.1
Ember-simple-auth 1.0.1
Uses cookie session store
We trying to change the session API for the older version so that it stores the access and refresh tokens correctly so the new app can use it.
So far, we’ve tried overriding the setup and updateStore methods to work with the authenticated nested object but are still running into issues.
Disclaimer - Patrick Berkeley and I work together. We found a solution after posting this question that I figured I would share.
In order for a 0.7.3 version of ember-simple-auth's cookie store to play nicely with a 1.0.0 version, we did have to normalize how the cookie was being formatted on the app with the earlier version in a few key places, mostly centered around the session object (the 0.7.3 session is an ObjectProxy that can be extended in the consuming app to create your own custom session).
The methods that we needed to override, centered around the structure of data being passed to the cookie store to persist and what was being returned when a session was being restored. The key difference is on version 0.7.3, the access_token, etc is stored top-level on the content object property of the session. With 1.0.0. this is nested inside another object inside content with the property name of authenticated. We therefore needed to ensure that everywhere we were making the assumption to set or get the access_token at the top level, we should instead retrieve one level deeper. With that in mind, we came up with these methods being overridden in our custom session object:
// alias access_token to point to new place
access_token: Ember.computed.alias('content.authenticated.access_token'),
// overridden methods to handle v2 cookie structure
restore: function() {
return new Ember.RSVP.Promise((resolve, reject) => {
const restoredContent = this.store.restore();
const authenticator = restoredContent.authenticated.authenticator;
if (!!authenticator) {
delete restoredContent.authenticated.authenticator;
this.container.lookup(authenticator).restore(restoredContent.authenticated).then(function(content) {
this.setup(authenticator, content);
resolve();
}, () => {
this.store.clear();
reject();
});
} else {
this.store.clear();
reject();
}
});
},
updateStore: function() {
let data = this.content;
if (!Ember.isEmpty(this.authenticator)) {
Ember.set(data, 'authenticated', Ember.merge({ authenticator: this.authenticator }, data.authenticated || {}));
}
if (!Ember.isEmpty(data)) {
this.store.persist(data);
}
},
setup(authenticator, authenticatedContent, trigger) {
trigger = !!trigger && !this.get('isAuthenticated');
this.beginPropertyChanges();
this.setProperties({
isAuthenticated: true,
authenticator
});
Ember.set(this, 'content.authenticated', authenticatedContent);
this.bindToAuthenticatorEvents();
this.updateStore();
this.endPropertyChanges();
if (trigger) {
this.trigger('sessionAuthenticationSucceeded');
}
},
clear: function(trigger) {
trigger = !!trigger && this.get('isAuthenticated');
this.beginPropertyChanges();
this.setProperties({
isAuthenticated: false,
authenticator: null
});
Ember.set(this.content, 'authenticated', {});
this.store.clear();
this.endPropertyChanges();
if (trigger) {
this.trigger('sessionInvalidationSucceeded');
}
},
bindToStoreEvents: function() {
this.store.on('sessionDataUpdated', (content) => {
const authenticator = content.authenticated.authenticator;
this.set('content', content);
if (!!authenticator) {
delete content.authenticated.authenticator;
this.container.lookup(authenticator).restore(content.authenticated).then((content) => {
this.setup(authenticator, content, true);
}, () => {
this.clear(true);
});
} else {
this.clear(true);
}
});
}.observes('store'),
This took us most of the way there. We just needed to ensure that the authenticator name that we use matches the name on 1.0.0. Instead of 'simple-auth-authenticator:oauth2-password-grant', we needed to rename our authenticator via an initializer to 'authenticator:oauth2'. This ensures that the apps with the newer version will be able to handle the correct authenticator events when the cookie session data changes. The initializer logic is simple enough:
import OAuth2 from 'simple-auth-oauth2/authenticators/oauth2';
export default {
name: 'oauth2',
before: 'simple-auth',
initialize: function(container) {
container.register('authenticator:oauth2', OAuth2);
}
};
The above satisfies our needs- we can sign in to an app using ember-simple-auth 0.7.3 and have the cookie session stored and formatted properly to be handled by another app on ember-simple-auth 1.0.0.
Ideally, we would just update the Ember and Ember Simple Auth versions of the app though business needs and the fact that we wanted to focus our energies on the v2 versions (which are completely fresh and new code bases) propelled us to go down this path.

Reload model/update template on createRecord save

I see this question is being ask all over again still don't find solution that works for such a trivial task.
This url displays a list of navigations tabs for workspaces.
http://localhost:4200/users/1/workspaces
Each of tab resolves to
http://localhost:4200/users/1/workspaces/:wid
Also on the I have a button that suppose to create a new workspace as well as new tab.
Here how controller for looks:
export default Ember.Controller.extend({
actions: {
newWorkspace: function () {
this.get('currentModel').reload();
var self = this;
var onFail = function() {
// deal with the failure here
};
var onSuccess = function(workspace) {
self.transitionToRoute('dashboard.workspaces.workspace', workspace.id);
};
this.store.createRecord('workspace', {
title: 'Rails is Omakase'
}).save().then(onSuccess, onFail);
}
}
});
When I click on button I see in ember inspector new record indeed created as well as url redirected to id that represents newly created workspace.
My question is how to force model/template to reload. I have already killed 5h trying model.reload() etc. Everything seem not supported no longer. Please please help.
UPDATE
When adding onSuccess
model.pushObject(post);
throws Uncaught TypeError: internalModel.getRecord is not a function
I believe you should call this.store.find('workspace', workspace.id) for Ember Data 1.12.x or earlier. For 1.13 and 2.0 there are more complicated hooks that determine whether or not the browser should query the server again or use a cached value; in that case, call this.store.findRecord('workspace', workspace.id, { reload: true }).
I do not know if this help. I had a similar problem. My action was performed in the route. Refresh function took care of everything.

Adding headers after RESTAdapter initialization

I am trying to add an Authorization header to my adapter's request after the adapter has been initialized and used. I can add headers in a static way at the time I create my ApplicationAdapter, but I can't seem to get it use the headers in subsequent REST calls. I am trying this:
var auth= "Basic " + hash;
App.ApplicationAdapter.reopen({
headers: {
Authorization: auth
}
});
I have debugged RESTAdapter in the ajax method, and the test for adapter.headers is always undefined.
The accepted answer doesn't address the fact that the recommended approach is not working in ember-data. I say recommended since:
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L88
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L162
and other places in that file.
Further, the issue the OP brings up with of undefined specifically happens here:
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L619
So, the following simply does not work:
App.ApplicationAdapter.reopen({
headers: {token: 'reopen_token (NO WORK)' }
});
I've tried to point to this out as an issue but it got closed within an hour:
https://github.com/emberjs/data/issues/1820
Hopefully core will decide to either fix this or remove the comments. But, yes, for now it seems you have to hijack jQuery ajax setup, Ember.$.ajaxPrefilter, or override the ajax on the adapter yourself.
EDIT: So after getting some more feedback from Ember devs, it looks like the core of this issue is trying to reopen an instance already created. So using a computered property when it's defined (so it will update as desired) seems to be the advised approach. Hope that helps (there's a recently merged pull request that makes this more obvious in the comments of referenced file:https://github.com/emberjs/data/pull/1818/files#diff-1d7f5a5b77898df15de501c3c38d4829R108 )
EDIT 2: Got this working in my app so here's the code in case someone else gets stuck:
//app.js
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
namespace: 'api/v1',
headers: function() {
return {
token: this.get('App.authToken') || localStorage.getItem('token')
};
}.property("App.authToken")
});
//login-controller.js (only action shown..assume `data` has user/pass)
actions: {
login: function() {
$.post('/token/', data).done(function(user) {
App.set('authToken', user.token);
//Above will trigger adapters's header computed property to update
// Transition to previous attempted route
var attemptedTransition = self.get('attemptedTransition');
if(attemptedTransition) {
attemptedTransition.retry();
}
else {
self.transitionToRoute('yourapproute');
}
})
.fail(function(response) {
//fail handling omitted
});
The answers are already introduced in official API document.
http://emberjs.com/api/data/classes/DS.RESTAdapter.html#toc_headers-customization
Use computed property with session injection
or just use volatile computed property
You should be able to use $.ajaxPrefilter to add custom headers (or params).
See: http://api.jquery.com/jQuery.ajaxPrefilter/
Ember.$.ajaxPrefilter(function( options, oriOptions, jqXHR ) {
var auth= "Basic " + hash;
jqXHR.setRequestHeader("Authorization", auth);
});

emberjs handle 401 not authorized

I am building an ember.js application and am hung up on authentication. The json rest backend is rails. Every request is authenticated using a session cookie (warden).
When a user first navigates to the application root rails redirects to a login page. Once the session is authorized the ember.js app is loaded. Once loaded the ember.js app makes requests to the backend using ember-data RESTadapter and the session for authorization.
The problem is the session will expire after a predetermined amount of time. Many times when this happens the ember.js app is still loaded. So all requests to the backend return a 401 {not autorized} response.
To fix this problem I am thinking the ember.js app needs to notify the user with a login modal every time a 401 {not autorized} response is returned from the server.
Does anyone know how to listen for a 401 {not autorized} response and allow the user to re-login without losing any changes or state.
I have seen other approaches such as token authorization but I am concerned with the security implications.
Anybody have a working solution to this problem?
As of the current version of Ember Data (1.0 beta) you can override the ajaxError method of DS.RESTAdapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR && jqXHR.status === 401) {
#handle the 401 error
}
return error;
}
});
Note that you should call #_super, especially if you are overriding one of the more complex adapters like DS.ActiveModelAdapter, which handles 422 Unprocessable Entity.
AFAIK this is not addressed by the current implementation of ember-data and the ember-data README states that "Handle error states" is on the Roadmap.
For the time being, you can implement your own error handling adapter. Take a look at the implementation of the DS.RestAdapter . By using that as a starter, it should not be too difficult to add error handling in there (e.g simply add an error function to the the data hash that is passed to the jQuery.ajax call).
For those willing to accept a solution that does lose changes and state you can register a jQuery ajaxError handler to redirect to a login page.
$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError) {
// You should include additional conditions to the if statement so that this
// only triggers when you're absolutely certain it should
if (jqXHR.status === 401) {
document.location.href = '/users/sign_in';
}
});
This code will get triggered anytime any jQuery ajax request completes with an error.
Of course you would never actually use such a solution as it creates an incredibly poor user experience. The user is yanked away from what they're doing and they lose all state. What you'd really do is render a LoginView, probably inside of a modal.
An additional nicety of this solution is that it works even if you occasionally make requests to your server outside of ember-data. The danger is if jQuery is being used to load data from other sources or if you already have some 401 error handling built-in elsewhere. You'll want to add appropriate conditions to the if statement above to ensure things are triggered only when you're absolutely certain they should.
It's not addressed by ember-data (and probably won't be), but you can reopen the DS class and extend the ajax method.
It looks like this:
ajax: function(url, type, hash) {
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.contentType = 'application/json; charset=utf-8';
hash.context = this;
if (hash.data && type !== 'GET') {
hash.data = JSON.stringify(hash.data);
}
jQuery.ajax(hash);
},
You can rewrite it with something like this (disclaimer: untested, probably won't work):
DS.reopen({
ajax: function(url, type, hash) {
var originalError = hash.error;
hash.error = function(xhr) {
if (xhr.status == 401) {
var payload = JSON.parse(xhr.responseText);
//Check for your API's errorCode, if applicable, or just remove this conditional entirely
if (payload.errorCode === 'USER_LOGIN_REQUIRED') {
//Show your login modal here
App.YourModal.create({
//Your modal's callback will process the original call
callback: function() {
hash.error = originalError;
DS.ajax(url, type, hash);
}
}).show();
return;
}
}
originalError.call(hash.context, xhr);
};
//Let ember-data's ajax method handle the call
this._super(url, type, hash);
}
});
What we're doing here is essentially deferring the call that received the 401 and are preserving the request to be called again when login is complete. The modal's ajax call with have the original error applied to it from the original ajax call's hash, so the original error would still work as long as it's defined :-)
This is a modified implementation of something we're using with our own data-persistence library, so your implementation might vary a bit, but the same concept should work for ember-data.

How to bind content with JSON in Ember.js

All the examples are using fixed data source in the arraycontroller.content, while I am using dynamic data source which is generated from anther web service and returns a JSON, it won't create an object that I declared in Ember,
here is the code sample:
ET.AppYear = Ember.Object.extend({
text:null,
value:null
});
ET.EmailTypes = Ember.Object.extend();
ET.appYearController = Ember.ArrayController.create({
content: [],
loadYears: function (year) {
if (year != null) {
for (var i = -5; i < 5; i++) {
this.pushObject({ text: year + i, value: year + i });
//.AppYear.create({ text: year + i, value: year + i });
}
}
}
});
ET.selectedAppYearController = Ember.Object.create({
selectedAppYear: '2011',
alertChange: function(){
alert("selected App Year is now " + this.get('selectedAppYear'));
}.observes('selectedAppYear'),
isChanged: function () {
if (this.appYear != null) {
this.set('selection',this.get('content'));
//LoadETByAppYearETTypeID(this.selectedAppYear, ET.selectedEmailTypesController.emailType.email_template_type_id);
}
} .observes('selectedAppYear'),
AppYear: function() {
var ay = ET.appYearController.findProperty('text',this.get('selectedAppYear'));
return ay;
}.property('selectedAppYear')
});
As you can see, I will call ET.appYearController.loadYears(JSON) in an AJAX post back, which will create the content by using this.pushObject, but this is not the ET.AppYear object schema, while I call ET.selectedAppYearController.selectedAppYear, it returns an undefined object, and which really returns an object with {text:xx,value,xx} schema. That's why the selectionBinding also won't work in this case.
So what's the typical solution for this to import JSON elements as defined object and put into content?!
Take a look at the Ember-data library, it was made for your use-case and related use-cases:
https://github.com/emberjs/data
[2014-02-19: Deprecated - I no longer support ember-rest because it is overly simplistic, and would recommend using ember-data for most ember projects. Check out the Ember guides for an overview of ember-data as well as this example project ]
I've created a very simple library to handle working with REST interfaces from Ember:
https://github.com/cerebris/ember-rest
I'm also writing a series of posts about using this library from Rails. The first is here:
http://www.cerebris.com/blog/2012/01/24/beginning-ember-js-on-rails-part-1/
My next post, hopefully coming later today, deals precisely with loading data via AJAX into a collection of Ember objects. For now, you can see this behavior in the sample app as well as the ember-rest.js readme.
If you'd like to avoid using this lib, you might still want to check out the source. The deserialize() method on Ember.Resource is used to import JSON and could also be used with a generic Ember.Object.