Ember: How to get Router and Controller work together? - ember.js

For my login/logout scenario I implemented a conditional routing in ember:
App.Router = Ember.Router.extend({
//needs controller handling
//goLoggedIn: Ember.Route.transitionTo('loggedIn'),
goLoggedOut: Ember.Route.transitionTo('loggedOut'),
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
enter: function(router) {
var logged = getLoginState();
Ember.run.next(function() {
if (logged) {
router.transitionTo('loggedIn');
} else {
router.transitionTo('loggedOut');
}
});
}
}),
loggedIn: Ember.Route.extend({
connectOutlets: function(router, context){
...
}
}),
loggedOut: Ember.Route.extend({
connectOutlets: function(router, context){
...
}
})
...
My index.html says for loggedin view
<!-- Template for out -->
<script type="text/x-handlebars" data-template-name="out">
<hr /><br />
<h1>Logged Out</h1>
<span>Login with "test/test"</span><br /><br />
<label>Username: </label>{{view Ember.TextField valueBinding="App.OutController.username"}}<br />
<label>Password: </label>{{view Ember.TextField valueBinding="App.OutController.password" type="password"}}<br />
{{#if App.loginController.isError}}
<span class="login-error">Error: Invalid username or password.</span><br />
{{/if}}
<br /><button {{action goLoggedIn href=true}}>Login</button>
</script>
Now I am delegating this action simply to my Router. I know that I can delegate this to my controller as well with:
action login target="controller"
but after that, how to do the transitionTo function in my Router? Because I know that this shouldnt be done in my controller. So how to pass it to my Router?
Probably I am wrong and I have to let {{action goLoggedIn href=true}}. Then my Router delegates this with a function to my controller and I get a response. Instead of having goLoggedIn: Ember.Route.transitionTo('loggedIn') I need something like App.LoginController.doLogin and afterwards goLoggedIn: Ember.Route.transitionTo('loggedIn'). When this is the case how to implement it?
EdiT:
Like this?
App.Router = Ember.Router.extend({
//needs controller handling
goLoggedIn: Ember.Route.transitionTo('loggedIn'),
goLoggedOut: Ember.Route.transitionTo('loggedOut'),
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
enter: function(router) {
var logged = getLoginState();
Ember.run.next(function() {
if (logged) {
router.transitionTo('loggedIn');
} else {
router.transitionTo('loggedOut');
}
});
}
}),
loggedIn: Ember.Route.extend({
connectOutlets: function(router, context){
...
}
}),
loggedOut: Ember.Route.extend({
connectOutlets: function(router, context){
...
},
goLoggedIn: function(router, evt) {
router.get('inController').tryLogin()
router.transitionTo('loggedIn')
}
})
...
I get:Cannot call method 'split' of undefined
Edit 2:
It is working now. I had to remove href=true. Thanks

In a typical Ember app, the target property of each controller is set to the Router instance. If you want to send an action to the controller and then on to the router, you can do so within the controller method by saying this.get('target').send('goLoggedIn', optionalEvt)
I would recommend that you let the action be sent to the router directly. You can have a function handle the action that can call a method on one or more controllers before transitioning. e.g.:
...
{{action log href=true}}
...
logIn: function(router, evt) {
router.get('userController').prepareForLogin()
router.transitionTo('loggedIn')
}

Related

Propagate action from nested component to AppController

I have a component nested several levels down in other components. I'm trying to propagate an action all the way up to the AppController in order to open a modal.
The only way I know of doing this is to pass in the action to each component - but this seems extremely impractical. Is there a better way to access the AppController from a nested component?
See my jsbin for the code
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.AppController = Ember.Controller.extend({
actions: {
openModal: function(){
alert('this would open the modal')
}
}
})
App.MainComponentComponent = Ember.Component.extend({})
App.SubComponentComponent = Ember.Component.extend({
actions: {
triggerModal: function(){
// need to trigger the openModal action on the AppController
this.sendAction('openModal')
}
}
})
.
<script type="text/x-handlebars" data-template-name="index">
<h1>Index</h1>
{{main-component model=model}}
</script>
<script type="text/x-handlebars" data-template-name="components/main-component">
<h2>Main component</h2>
{{#each color in model}}
{{sub-component color=color}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="components/sub-component">
<button {{action "triggerModal"}}>{{color}}</button>
</script>
EDIT: I'm aware that I can render a template into the modal outlet:
this.render(modalName, {
into: 'application',
outlet: 'modal'
});
But I'm trying to access an action on the AppController.
You can utilize Ember.Instrumentation module, which can be used like a pub/sub.
Here is a working JS Bin example.
Solution outline:
1. On ApplicationController init, the controller subscribes to "openModal" event.
2. The neseted component instruments the event "openModal" within an action.
3. The instrumentation can be executed with a payload, so this would be the place to determine the modal content.
App.ApplicationController = Ember.Controller.extend({
actions: {
openModal: function(options) {
alert('this would open the modal with the content: ' + options.modalContent);
}
},
subscribeEvents: function() {
this.set('openModalSubscriber', Ember.Instrumentation.subscribe('openModal', {
before: Ember.K,
after: Ember.run.bind(this, function(name, timestamp, payload, beforeRet) {
this.send('openModal', payload);
}),
}, this));
}.on('init')
});
App.SubComponentComponent = Ember.Component.extend({
actions: {
triggerModal: function() {
Ember.Instrumentation.instrument('openModal.sub-component', {
modalContent: 'Inner content of modal'
}, Ember.K, this);
}
}
});
Components are supposed to be pretty isolated, therefore it probably doesn't make sense to be jumping over other components, going straight to their controllers... See the following discussion here
There is a targetObject property, which might be of use to you, although I am not 100% sure what you would set it to in this case.

Emberjs Application Routes and Controllers

Can someone explain why this works:
Code in App.js:
App.ApplicationRoute = Ember.Route.extend({
setupController : function (params) {
this.controllerFor('food').set('model', App.Food.find(params.food_id));
}
});
But the following won't, unless I explicitly declare App.FoodController = Ember.ObjectController.extend();
App.FoodRoute = Ember.Route.extend({
model : function(params) {
return App.Food.find(params.food_id);
}
});
This is the code I'm using in index.html and does not change between blocks of code
<script type="text/x-handlebars" data-template-name="application">
{{ outlet }}
</script>
<script type="text/x-handlebars" data-template-name="food">
{{name}}
</script>
Router:
App.Router.map(function() {
this.resource( 'foods' );
this.resource( 'food', { path : '/food/:food_id' } );
});
The code that you have shown seems OK. Here is a working fiddle that proves it:
http://jsfiddle.net/ebXeS/2/
The only thing wrong about the code is this part (which is excluded from the fiddle):
App.ApplicationRoute = Ember.Route.extend({
setupController : function (params) {
this.controllerFor('food').set('model', App.Food.find(params.food_id));
}
});
According to your Router definition, you should not have food_id in the parameters of your application route. More than that, you should access the controller for the food route in the uhm... FoodRoute. Read more about Ember and the way it does routing (http://emberjs.com/guides/).

EmberJS Router App: Views vs Controllers

I'm creating a router-based EmberJS app (strongly modelled on the excellent router guide). However, I'm quite muddled over what belongs in a view vs in a controller.
I totally get that {{action showFoo}} often indicates a state change and that the Router is the state machine for my app. But some of my actions don't fall into that category.
Here's an example from my actual code (html simplified but mustaches intact). I want to have a login form that works via ajax (i.e. the html form doesn't post directly to the server, it tells my ember app to attempt a login via json).
<form>
Email Name: {{view Ember.TextField valueBinding="email"}}
Password: {{view Ember.TextField valueBinding="password"}}
<button type="submit" {{ action logIn target="this" }}>Sign in</button>
</form>
The valueBindings are fields in my loginController but the logIn handler is in my view (because I couldn't figure out how to tell the template to call the controller). I feel like this is a weird distribution & I'm not sure what the right Ember approach is to this.
I don't think the router should be handling the action because requesting a login attempt isn't really a state change. The loginController feels like the right place to try the login. After a login response is received then that controller could trigger the state change.
I don't think the router should be handling the action because requesting a login attempt isn't really a state change.
I think that's exactly the case: attempting a login should transition to an authenticating state, where for example another click to "login" is ignored.
So IMHO this should be handled by the router. I'm thinking about something like this, see http://jsfiddle.net/pangratz666/97Uyh/:
Handlebars:
<script type="text/x-handlebars" >
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="login" >
<p class="info">{{message}}</p>
Login to view the admin area <br/>
Email: {{view Ember.TextField valueBinding="email" }} <br/>
Password: {{view Ember.TextField valueBinding="password" }} <br/>
<button {{action login}} >Login</button>
</script>
<script type="text/x-handlebars" data-template-name="authenticating" >
Communicating with server ...
</script>
<script type="text/x-handlebars" data-template-name="admin" >
Hello admin!
</script>
​
JavaScript:
App = Ember.Application.create();
App.ApplicationController = Ember.Controller.extend({
login: function() {
// reset message
this.set('message', null);
// get data from login form
var loginProps = this.getProperties('email', 'password');
// simulate communication with server
Ember.run.later(this, function() {
if (loginProps.password === 'admin') {
this.set('isAuthenticated', true);
this.get('target').send('isAuthenticated');
} else {
this.set('message', 'Invalid username or password');
this.set('isAuthenticated', false);
this.get('target').send('isNotAuthenticated');
}
}, 1000);
// inform target that authentication is in progress
this.get('target').send('authenticationInProgress');
},
logout: function() {
this.set('isAuthenticated', false);
}
});
App.ApplicationView = Ember.View.extend({
templateName: 'application'
});
App.LoginView = Ember.View.extend({
templateName: 'login'
});
App.AdminView = Ember.View.extend({
templateName: 'admin'
});
App.AuthenticatingView = Ember.View.extend({
templateName: 'authenticating'
});
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
loggedOut: Ember.Route.extend({
route: '/',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('login');
},
login: function(router) {
router.get('applicationController').login();
},
authenticationInProgress: function(router) {
router.transitionTo('authenticating');
}
}),
authenticating: Ember.State.extend({
enter: function(router) {
router.get('applicationController').connectOutlet('authenticating');
},
isAuthenticated: function(router) {
router.transitionTo('loggedIn');
},
isNotAuthenticated: function(router) {
router.transitionTo('loggedOut');
}
}),
loggedIn: Ember.Route.extend({
route: '/admin',
connectOutlets: function(router) {
if (!router.get('applicationController.isAuthenticated')) {
router.transitionTo('loggedOut');
}
router.get('applicationController').connectOutlet('admin');
},
logout: function(router) {
router.get('applicationController').logout();
}
})
})
})
});​
You can use the controller for this, the template your using have access to the controller.
<script type="text/x-handlebars" data-template-name="loginTemplate">
{{#if controller.login}}
Logged in!
{{else}}
Login failed
{{/if}}
</script>
This fiddle shows a small app, which does that: fiddle
Then after login has occured you can make an actioncall to your router, or show the user that login failed.
I have just made it done by change the codes as:
{{ action logIn target="controller" }}

How to access a specific context in ember 1.0 pre with action helper

In my ember view I want to get the person during this each and have it passed to the action but currently I only get a jquery event in the router (curious if this is bound to the context for free in pre 1.0 now)
template
<script type="text/x-handlebars" data-template-name="person">
{{#each person in controller}}
<li>
{{person.username}}
<input type="submit" value="delete" {{action removePerson person}}/>
</li>
{{/each}}
</script>
router w/ the method I was hoping to invoke w/ the person context
Router: Ember.Router.create({
root: Ember.Route.extend({
index: Em.Route.extend({
route: '/',
removePerson: function(router, context) {
router.get('personController').removePerson(context);
},
controller in more detail
PersonController: Ember.ArrayController.extend({
content: [],
addPerson: function (username) {
var person = PersonApp.Person.create({
username: username
});
this.pushObject(person);
},
removePerson: function (person) {
this.removeObject(person);
}
}),
The second variable passed to the router action handler is actually the event. The context is a variable of this event. Rewrite it like so:
Router: Ember.Router.create({
root: Ember.Route.extend({
index: Em.Route.extend({
route: '/',
removePerson: function(router, event) {
router.get('personController').removePerson(event.context);
},

How to route to the same nested state but have a different context?

I currently struggle to get a nested route to work, where one of the path elements is dynamic. That's the scenario I want to achieve:
The page contains the description of a project. Within the page is a tab menu to select different views. That should reflect in the URL as well. So I want to have different urls like:
url#/project1/info
url#/project1/status
url#/project1/...
To not repeat the :project parameter I added a nested project route that is not a leaf but only responsible for serialization/deserialization of the project itself.
Everything works fine as long as I use the initial project. But it can happen, that I want to link from one project to another project. That means the URL should change from url#/project1/info -> url#/project2/info and thus the view should change as well to display the infos about project2.
Sounds straightforward. However the deserialization method of the project route is not called when I link to project2 with an action helper
<a {{action changeProject context="App.project2" href=true}}>Go to project 2</a>
I guess that is because I am already in the info state. However how do I then propagate the context change? A simplified case you can find in the fiddle http://jsfiddle.net/jocsch/HYbZj/30/ or view it directly http://jsfiddle.net/jocsch/HYbZj/30/show/#/project1/info
Router: Ember.Router.extend({
enableLogging: true,
root: Ember.Route.extend({
changeProject: Em.State.transitionTo('project.info'),
index: Ember.Route.extend({
route: '/',
}),
project: Ember.Route.extend({
route: '/:project',
deserialize: function(router, params) {
var proj = App.get(params['project']);
router.get("applicationController").set("content", proj);
return proj;
},
serialize: function(router, context) {
return {project: context.id};
},
index: Ember.Route.extend({
route: '/',
redirectsTo: 'info'
}),
info: Ember.Route.extend({
route: '/info',
connectOutlets: function(router) {
var ctrl = router.get('applicationController');
ctrl.connectOutlet('project', ctrl.get('content'));
}
})
})
})
})
You do not need any custom serialization/deserialization.
One important missing thing in your code is the context passing in changeProject handler.
I would write the whole thing as follow:
JS
App = Ember.Application.create();
App.Project = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string')
});
App.Project.FIXTURES = [{
id: '36',
name: 'First project',
description: 'My very first project'
}, {
id: '42',
name: 'Another project',
description: 'My other favorite project'
}];
App.store = DS.Store.create({
adapter: DS.fixtureAdapter,
revision: 4
});
App.ApplicationController = Ember.Controller.extend();
App.ApplicationView = Ember.View.extend({
templateName: 'app-view'
})
App.ProjectsController = Ember.ArrayController.extend();
App.ProjectsView = Ember.View.extend({
templateName: 'projects-view'
})
App.ProjectController = Ember.ObjectController.extend();
App.ProjectView = Ember.View.extend({
templateName: 'project-view'
})
App.InfoController = Ember.ObjectController.extend();
App.InfoView = Ember.View.extend({
templateName: 'info-view'
})
App.Router = Ember.Router.extend({
enableLogging: true,
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/'
}),
showProjects: function(router) {
router.transitionTo('projects.index');
},
projects: Ember.Route.extend({
route: 'projects',
connectOutlets: function(router) {
var applicationController = router.get('applicationController');
applicationController.connectOutlet({
outletName: 'projectsList',
name: 'projects',
context: App.Project.find()
});
},
index: Ember.Route.extend({
route: '/'
}),
showProject: function(router, event) {
var project = event.context;
router.transitionTo('project.info', project);
},
project: Ember.Route.extend({
route: '/:project_id',
modelClass: 'App.Project',
connectOutlets: function(router, project) {
var applicationController = router.get('applicationController');
applicationController.connectOutlet('project', project);
},
info: Ember.Route.extend({
route: 'info',
connectOutlets: function(router) {
var projectController = router.get('projectController'),
project = projectController.get('content');
projectController.connectOutlet('info', project);
}
})
})
})
})
});
App.initialize();
Handlebars
<script type="text/x-handlebars" data-template-name='app-view'>
<h1>Welcome to projects app!</h1>
<a {{action showProjects}}>Projects home</a>
<hr/>
{{outlet projectsList}}
<hr/>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name='projects-view'>
{{controller.length}} projects:
<ul>
{{#each project in controller}}
<li>
<a {{action showProject context="project"}}>{{project.name}}</a>
</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name='project-view'>
<h2>Showing project <i>{{name}}</i></h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name='info-view'>
{{description}}
</script>
JSFiddle # http://jsfiddle.net/MikeAski/fRea6/