I am wondering the appropriate way to access a route model from a different non nested route controller.
If I have my routes set up like this: (this works however, not sure if its proper)
App.Router.map(function() {
this.route('admin');
this.route('page1');
}
And the Page 1 route has a model like this:
App.page1Model = {content:'Content of simple model'};
App.Page1Route = Ember.Route.extend({
model(){
return App.page1Model;
});
Then the admin controller wants to access the page1 route, I can only do it like this:
App.AdminController = Ember.Controller.extend({
page1Model:App.page1Model,
Now do stuff with page1Model.....
});
Ive tried to use Ember.inject.controller() however that only works for me when my routes are nested and I want to access Parent controller from child. Is there a way to use that syntax to get what I want, or is there a better way than what im doing?
Thanks
There's an inherent problem with what you're asking for: when the user is on the admin page, they're not on the page1 page, so there's no page1 context. Some questions you might want to ask:
what happens if the user goes to /admin having never gone to /page1?
what happens if the user goes to /page1 then /page2 then /admin?
I can think of two Ember-esque ways of doing what you want:
A Page1ModelService. Here, you create an Ember.Service that holds an instance of Page1Model. You inject the service into route:page1 and route:admin and let them each pull off the instance. Whether they can change which instance of the model is showing is up to you.
Return a Page1Model instance in the model hook for route:application. This route sits above both route:page1 and route:admin, so they can both look up the model as follows:
// route:application
model() {
return App.Page1Model.create();
}
// route:page1
model() {
return this.modelFor('application');
}
I was able to achieve my goal through using registers and injection. Can someone please take a look and let me know if this is 'proper' through Ember standards or if there is a better way ( #James A. Rosen :) )?
OH! If there is a better way to attach the model to the page1 route, please let me know. This worked though I am not sure if i like the .model after create().
JSBIN: http://jsbin.com/tikezoyube/1/edit?html,js,output
JS of that:
var App = Ember.Application.create();
var page1Model = {title:'Old Title'};
var page1ModelFactory = Ember.Object.extend({
model : page1Model
});
App.Router.map(function(){
this.route('page1');
this.route('admin');
});
App.register('model:page1', page1ModelFactory);
App.inject('controller:admin','page1Model','model:page1');
App.Page1Route = Ember.Route.extend({
model(){ return page1ModelFactory.create().model; }
});
App.AdminController = Ember.Controller.extend({
actions:{
updateTitle:function(){
console.log(this.get('page1Model').model.title);
this.get('page1Model').set('model.title','THE NEW TITLE!');
console.log(this.get('page1Model').model.title);
this.transitionToRoute('page1');
}
}
});
Thanks!
I have created a full example for the purpose of describing this issue. My actual application is even bigger than the presented demo and there are more services and directives operated by every controller. This leads to even more code repetition. I tried to put some code comments for clarifications,
PLUNKER: http://plnkr.co/edit/781Phn?p=preview
Repetitive part:
routerApp.controller('page1Ctrl', function(pageFactory) {
var vm = this;
// page dependent
vm.name = 'theOne';
vm.service = 'oneService';
vm.seriesLabels = ['One1', 'Two1', 'Three1'];
// these variables are declared in all pages
// directive variables,
vm.date = {
date: new Date(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
}
//default call
vm.update();
})
Basically I moved all the logic I could to factories and directives. But now in every controller that uses certain directive I need, for example, a field that keeps the value that directive is modifying. And it's settings. Later I need similar field to keep the data that comes from dataservice, and the call itself (method) is the same as well.
This leads to a lot of repetition.
Graphically I see the current example to look like this:
While I believe the proper design should look more like this:
I tried to find some solution here, but none seem to be confirmed. What I have found:
AngularJS DRY controller structure, suggesting I pass the $scope or vm and decorate it with extra methods and fields. But many sources say it is dirty solution.
What's the recommended way to extend AngularJS controllers? using angular.extend, but this have problems when using controller as syntax.
And then I have found also the answer (in the link above):
You don't extend controllers. If they perform the same basic functions then those functions need to be moved to a service. That service can be injected into your controllers.
And even when I did there is still a lot of repetition. Or is it the way it just has to be? Like John Papa sais (http://www.johnpapa.net/angular-app-structuring-guidelines/):
Try to stay DRY (Don't Repeat Yourself) or T-DRY
Did you face a similar issue? What are the options?
From a over all design perspective I don't see much of a difference between decorating a controller and extending a controller. In the end these are both a form of mixins and not inheritance. So it really comes down to what you are most comfortable working with. One of the big design decisions comes down to not just how to pass in functionality to just all of the controllers, but how to also pass in functionality to say 2 out of the 3 controllers also.
Factory Decorator
One way to do this, as you mention, is to pass your $scope or vm into a factory, that decorates your controller with extra methods and fields. I don't see this as a dirty solution, but I can understand why some people would want to separate factories from their $scope in order to separate concerns of their code. If you need to add in additional functionality to the 2 out of 3 scenario, you can pass in additional factories. I made a plunker example of this.
dataservice.js
routerApp.factory('pageFactory', function() {
return {
setup: setup
}
function setup(vm, name, service, seriesLabels) {
// page dependent
vm.name = name;
vm.service = service;
vm.seriesLabels = seriesLabels;
// these variables are declared in all pages
// directive variables,
vm.date = {
date: moment().startOf('month').valueOf(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = get(vm.service);
}
//default call
vm.update();
}
});
page1.js
routerApp.controller('page1Ctrl', function(pageFactory) {
var vm = this;
pageFactory.setup(vm, 'theOne', 'oneService', ['One1', 'Two1', 'Three1']);
})
Extending controller
Another solution you mention is extending a controller. This is doable by creating a super controller that you mix in to the controller in use. If you need to add additional functionality to a specific controller, you can just mix in other super controllers with specific functionality. Here is a plunker example.
ParentPage
routerApp.controller('parentPageCtrl', function(vm, pageFactory) {
setup()
function setup() {
// these variables are declared in all pages
// directive variables,
vm.date = {
date: moment().startOf('month').valueOf(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
}
//default call
vm.update();
}
})
page1.js
routerApp.controller('page1Ctrl', function($controller) {
var vm = this;
// page dependent
vm.name = 'theOne';
vm.service = 'oneService';
vm.seriesLabels = ['One1', 'Two1', 'Three1'];
angular.extend(this, $controller('parentPageCtrl', {vm: vm}));
})
Nested States UI-Router
Since you are using ui-router, you can also achieve similar results by nesting states. One caveat to this is that the $scope is not passed from parent to child controller. So instead you have to add the duplicate code in the $rootScope. I use this when there are functions I want to pass through out the whole program, such as a function to test if we are on a mobile phone, that is not dependent on any controllers. Here is a plunker example.
You can reduce a lot of your boilerplate by using a directive. I've created a simple one to replace all of your controllers. You just pass in the page-specific data through properties, and they will get bound to your scope.
routerApp.directive('pageDir', function() {
return {
restrict: 'E',
scope: {},
controller: function(pageFactory) {
vm = this;
vm.date = {
date: moment().startOf('month').valueOf(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
};
vm.update();
},
controllerAs: 'vm',
bindToController: {
name: '#',
service: '#',
seriesLabels: '='
},
templateUrl: 'page.html',
replace: true
}
});
As you can see it's not much different than your controllers. The difference is that to use them, you'll use the directive in your route's template property to initialize it. Like so:
.state('state1', {
url: '/state1',
template: '<page-dir ' +
'name="theOne" ' +
'service="oneService" ' +
'series-labels="[\'One1\', \'Two1\', \'Three1\']"' +
'></page-dir>'
})
And that's pretty much it. I forked your Plunk to demonstrate.
http://plnkr.co/edit/NEqXeD?p=preview
EDIT: Forgot to add that you can also style the directive as you wish. Forgot to add that to the Plunk when I was removing redundant code.
I can't respond in comment but here what i will do :
I will have A ConfigFactory holding a map of page dependent variables :
{
theOne:{
name: 'theOne',
service: 'oneService',
seriesLabels: ['One1', 'Two1', 'Three1']
},
...
}
Then i will have a LogicFactory with a newInstance() method to get a proper object each time i need it.
The logicFactory will get all the data / method shared betwwen controllers.
To this LogicFactory, i will give the view-specific data. and the view will have to bind to this Factory.
And to retrieve the view-specific data i will pass the key of my configuration map in the router.
so let say the router give you #current=theOne, i will do in the controller :
var specificData = ServiceConfig.get($location.search().current);
this.logic = LogicFactory.newInstance(specificData);
Hope it help
I retouch your example, here is the result : http://plnkr.co/edit/ORzbSka8YXZUV6JNtexk?p=preview
Edit: Just to say this way, you can load the specific configuration from a remote server serving you the specific-view data
I faced completely the same issues as you described. I'm a very big supporter of keeping things DRY. When I started using Angular there was no prescribed or recommended way to do this, so I just refactored my code as I went along. As with many things I dont think their is a right or wrong way to do these things, so use whichever method you feel comfortable with. So below is what I ended up using and it has served me well.
In my applications I generally have three types of pages:
List Page - Table list of specific resource. You can
search/filter/sort your data.
Form Page - Create or Edit resource.
Display Page - Detailed view-only display page of resource/data.
I've found there are typically a lot of repetitive code in (1) and (2), and I'm not referring to features that should be extracted to a service. So to address that I'm using the following inheritance hierarchy:
List Pages
BaseListController
loadNotification()
search()
advancedSearch()
etc....
ResourceListController
any resource specific stuff
Form Pages
BaseFormController
setServerErrors()
clearServerErrors()
stuff like warn user is navigating away from this page before saving the form, and any other general features.
AbstractFormController
save()
processUpdateSuccess()
processCreateSuccess()
processServerErrors()
set any other shared options
ResourceFormController
any resource specific stuff
To enable this you need some conventions in place. I typically only have a single view template per resource for Form Pages. Using the router resolve functionality I pass in a variable to indicate if the form is being used for either Create or Edit purposes, and I publish this onto my vm. This can then be used inside your AbstractFormController to either call save or update on your data service.
To implement the controller inheritance I use Angulars $injector.invoke function passing in this as the instance. Since $injector.invoke is part of Angulars DI infrastructure, it works great as it will handle any dependencies that the base controller classes need, and I can supply any specific instance variables as I like.
Here is a small snippet of how it all is implemented:
Common.BaseFormController = function (dependencies....) {
var self = this;
this.setServerErrors = function () {
};
/* .... */
};
Common.BaseFormController['$inject'] = [dependencies....];
Common.AbstractFormController = function ($injector, other dependencies....) {
$scope.vm = {};
var vm = $scope.vm;
$injector.invoke(Common.BaseFormController, this, { $scope: $scope, $log: $log, $window: $window, alertService: alertService, any other variables.... });
/* ...... */
}
Common.AbstractFormController['$inject'] = ['$injector', other dependencies....];
CustomerFormController = function ($injector, other dependencies....) {
$injector.invoke(Common.AbstractFormController, this, {
$scope: $scope,
$log: $log,
$window: $window,
/* other services and local variable to be injected .... */
});
var vm = $scope.vm;
/* resource specific controller stuff */
}
CustomerFormController['$inject'] = ['$injector', other dependencies....];
To take things a step further, I found massive reductions in repetitive code through my data access service implementation. For the data layer convention is king. I've found that if you keep a common convention on your server API you can go a very long way with a base factory/repository/class or whatever you want to call it. The way I achieve this in AngularJs is to use a AngularJs factory that returns a base repository class, i.e. the factory returns a javascript class function with prototype definitions and not an object instance, I call it abstractRepository. Then for each resource I create a concrete repository for that specific resource that prototypically inherits from abstractRepository, so I inherit all the shared/base features from abstractRepository and define any resource specific features to the concrete repository.
I think an example will be clearer. Lets assume your server API uses the following URL convention (I'm not a REST purest, so we'll leave the convention up to whatever you want to implement):
GET -> /{resource}?listQueryString // Return resource list
GET -> /{resource}/{id} // Return single resource
GET -> /{resource}/{id}/{resource}view // Return display representation of resource
PUT -> /{resource}/{id} // Update existing resource
POST -> /{resource}/ // Create new resource
etc.
I personally use Restangular so the following example is based on it, but you should be able to easily adapt this to $http or $resource or whatever library you are using.
AbstractRepository
app.factory('abstractRepository', [function () {
function abstractRepository(restangular, route) {
this.restangular = restangular;
this.route = route;
}
abstractRepository.prototype = {
getList: function (params) {
return this.restangular.all(this.route).getList(params);
},
get: function (id) {
return this.restangular.one(this.route, id).get();
},
getView: function (id) {
return this.restangular.one(this.route, id).one(this.route + 'view').get();
},
update: function (updatedResource) {
return updatedResource.put();
},
create: function (newResource) {
return this.restangular.all(this.route).post(newResource);
}
// etc.
};
abstractRepository.extend = function (repository) {
repository.prototype = Object.create(abstractRepository.prototype);
repository.prototype.constructor = repository;
};
return abstractRepository;
}]);
Concrete repository, let's use customer as an example:
app.factory('customerRepository', ['Restangular', 'abstractRepository', function (restangular, abstractRepository) {
function customerRepository() {
abstractRepository.call(this, restangular, 'customers');
}
abstractRepository.extend(customerRepository);
return new customerRepository();
}]);
So now we have common methods for data services, which can easily be consumed in the Form and List controller base classes.
To summarize the previous answers:
Decorating controllers: as you said, this is a dirty solution; Imagine having different factories decorating the same controller, it will be very difficult (especially for other developers) to prevent collision of properties, and equally difficult to trace which factory added which properties. It's actually like having multiple inheritance in OOP, something that most modern languages prevent by design for the same reasons.
Using a directive: this can be a great solution if all your controllers are going to have the same html views, but other than that you will have to include fairly complex logic in your views which can be difficult to debug.
The approach I propose is using composition (instead of inheritance with decorators). Separate all the repetitive logic in factories, and leave only the creation of the factories in the controller.
routerApp.controller('page1Ctrl', function (Page, DateConfig, DataService) {
var vm = this;
// page dependent
vm.page = new Page('theOne', 'oneService', ['One1', 'Two1', 'Three1']);
// these variables are declared in all pages
// directive variables,
vm.date = new DateConfig()
// dataservice
vm.dataService = new DataService(vm.page.service);
//default call
vm.dataService.update();
})
.factory('Page', function () {
//constructor function
var Page = function (name, service, seriesLabels) {
this.name = name;
this.service = service;
this.seriesLabels = seriesLabels;
};
return Page;
})
.factory('DateConfig', function () {
//constructor function
var DateConfig = function () {
this.date = new Date();
this.dateOptions = {
formatYear: 'yy',
startingDay: 1
};
this.format = 'dd-MMMM-yyyy';
this.opened = false;
this.open = function ($event) {
this.opened = true;
};
};
return DateConfig;
})
This code is not tested, but I just want to give an idea. The key here is to separate the code in the factories, and add them as properties in the controller. This way the implementation is not repeated (DRY), and everything is obvious in the controller code.
You can make your controller even smaller by wrapping all the factories in a larger factory (facade), but this may make them more tightly coupled.
I have a simple action that can be attached to list items in an {{#each}} loop, and when that action is triggered, it will link to that instance of the model.
This is what it looks like now
VpcYeoman.SuperTableController = Ember.ArrayController.extend({
actions: {
goTo: function(input) {
this.transitionToRoute('someModel', input);
}
}
});
The action is called on an HTML element like this
{{action 'goTo' this bubbles=false}}
You can see the problem with this in that 'goTo' cannot be reused on other models because it is specifically looking at the 'someModel' model.
Please help me make this action work for whatever the current model is
I tried replacing 'someModel' with a generic 'model' & even 'this.model' but they didn't work.
Do not reply with 'use {{#link-to}}' please. I am aware that this exists and
Before you read this, you should know that I do recommend you use the link-to helper. I normally pass a computed property to the helper when I need it to change based on the model...
I am not sure where you have that action in your code, but you could just compute that path as needed. For example, take this item controller:
App.ItemController = Ember.ObjectController.extend({
getTransitionPath: function () {
return this.get('foo') + '_bar';
},
transitionPath: function () {
return this.get('foo') + '_bar';
}.property('foo'),
actions: {
goTo: function(input) {
//this.transitionToRoute(this.getTransitionPath(), input); // Regular method
this.transitionToRoute(this.get('transitionPath'), input); // Computed property
}
}
});
I also don't know what kind of logic you are looking for inside of those methods, but this pattern should work on a per model basis.
Good luck!
I'd like to know if there is a way to check if a controller exists using Ember, then associate it to a view?
I'm going through a list of element coming from an array, and depending of those element, I generate a view, and sometimes I need to associate a controller to this view. I'm using Ember AppKit.
export default Ember.CollectionView.extend({
init: function () {
this._super();
var self = this;
myList = [{name: 'element-1'}, {name: 'element-2'}];
myList.forEach(function (element) {
self.push(Ember.View.create({
templateName: 'path/to/template/'+element.name,
controller: 'path/to/controller/'+element.name //Associate the controller only if it exists, I don't know how to do that.
}));
});
}
});
Thanks
As mentioned in the comment of your question, the View knows its Controller so I'd go the other way and tell the Controller beforehand which other controllers are needed through its needs property which could then be aliased and observed by the view.
Edit 2013-03-02
This appears to be resolved in RC1
In previous versions of Ember.js, controllers would keep state assigned to them, but this seems to be an issue in Pre4.
So if I were to have this controller
App.UsersController = Ember.ArrayController.extend({
content: ['mike', 'jen', 'sofia'],
_content_observer: (function(){
/* I'm called, but my author doesn't know why */
console.log('Content was altered! But why? And by whom?');
}).observes('content')
});
The content is overwritten for some unexplained reason. I don't want to use ember data, but it seems like I'm being forced that direction.
This JS Fiddle exemplifies the issue.
What's going on? How do I stop it or is this so engrained in embers opinionatedness that I need to just accept it and go with the flow?
Edit
Taking this a bit further, it appears that whatever is setup as the model will be set to the content value, even if you override the setupController hook.
e.g.:
UsersRoute = Ember.Route.extend({
model: function() {
/*I should never be called, but I am. How curious.*/
return ['This','Shouldnt','Be','Assigned'];
},
setupController: function() {
/* According to http://emberjs.com/guides/routing/specifying-a-routes-model/, I should prevent the model from being assigned to content, but I don't */
}
});
The UsersController.content will end up with the value ['This','Shouldnt','Be','Assigned']
See this updated fiddle
This isn't really an ember-data thing. The new router sets controller's content property automatically. Instead of setting content from within the controller dedinition, customize the model that will be used for your route by overriding the model hook. For example:
App.UsersRoute = Ember.Route.extend({
model: function() {
return ['mike', 'jen', 'sofia', 'greta']
}
}
I modified your jsfiddle here: http://jsfiddle.net/WGYmg/
You may use the setupController method to set the controller's contents as you like:
setupController: function(controller) {
controller.set('content', []);
}
See this fiddle
Edit
You can use the model method to return the original content:
model: function () {
var c = this.controllerFor('users');
return c.get('content');
}
This is a bit hackish, but still..:)
See updated fiddle