Hi, I'm about 2 weeks into building my application with ember.js, and the time has come to pull together my project layout into its final shape. To that end, I started looking into using Ember's register / inject mechanism instead of just creating global singletons and attaching them to my App object (for an excellent description of dependency injection in Ember, see here)
I'm stuck with standard dependency injection dilemma - circular references.
Let's say, I have two manager-like classes that I need available throughout application. Let's call them AuthManager and DataManager.
App.AuthManager = Ember.Object.extend({
logIn: function (user) {
var promise = this.dataManager.post("/session/new", user);
//...
}
});
App.DataManager = Ember.Object.extend({
getJSON: function (url) {
if (!this.authManager.get("isLoggedIn")) {
return false;
}
//...
}
});
So, as you see dataManager needs access to authManager and vice-versa.
My naive take at a solution was something like this:
App.initializer({
name: "dataManager",
initialize: function (container, application) {
application.register("my:dataManager", application.DataManager);
application.inject("my:authManager", "dataManager", "my:dataManager");
}
});
App.initializer({
name: "authManager",
initialize: function (container, application) {
application.register("my:authManager", application.AuthManager);
application.inject("my:dataManager", "authManager", "my:authManager");
}
});
Predictably, this results in a dead loop. I was hoping dependency injection system would try some crafty juggle, like node's require does, but no.
I have tried:
Moving the first inject into the second initializer, after my:authManager is registered.
Moving the first inject into its own initializer, after the first two
Putting any combination of these into the Ember.onLoad('Ember.Application', ...) from the linked article
Unfortunately, everything I tried ended in stack overflow (pun intended :-)).
Am I missing something? The documentation is pretty sparse in this area.
Of course, I can always manually lookup instance after the 'official' injection, but I was hoping for some more elegant solution.
You definitely have a circular dependency, and if you were using a different language I'd tell you to use the inversion of control pattern, but it's a little difficult using your problem and the container.
Solution 1
If you're fine adding them under a namespace such as manager or something like that then here's a solution (it's tightly coupled, but the code is tightly coupled already, almost enough that they could be together or a mixin on the other).
App.Manager = Ember.Object.extend({
init: function(){
// late fake injection
this.authManager.dataManager = this.dataManager;
this.dataManager.authManager = this.authManager;
}
});
App.initializer({
name: "manager",
after:['dataManager', 'authManager'],
initialize: function (container, application) {
application.register("my:manager", application.Manager);
application.inject("my:manager", "dataManager", "my:dataManager");
application.inject("my:manager", "authManager", "my:authManager");
application.inject("controller", "manager", "my:manager");
application.inject("route", "manager", "my:manager");
}
});
App.initializer({
name: "dataManager",
initialize: function (container, application) {
application.register("my:dataManager", application.DataManager);
}
});
App.initializer({
name: "authManager",
initialize: function (container, application) {
application.register("my:authManager", application.AuthManager);
}
});
And an example:
http://emberjs.jsbin.com/mopaquko/2/edit
Solution 2
On another note, this creates a new instance on each route/controller. If you only need one instance. You can do it like so, much easier and doesn't need the namespace.
App.initializer({
name: "joinManagers",
after:['dataManager', 'authManager'],
initialize: function (container, application) {
var dataManager = container.lookup('my:dataManager'),
authManager = container.lookup('my:authManager');
authManager.dataManager = dataManager;
dataManager.authManager = authManager;
application.register("my:jointDataManager", dataManager, {instantiate:false});
application.register("my:jointAuthManager", authManager, {instantiate:false});
application.inject("controller", "dataManager", "my:jointDataManager");
application.inject("controller", "authManager", "my:jointAuthManager");
application.inject("route", "dataManager", "my:jointDataManager");
application.inject("route", "authManager", "my:jointAuthManager");
}
});
App.initializer({
name: "dataManager",
initialize: function (container, application) {
application.register("my:dataManager", application.DataManager);
}
});
App.initializer({
name: "authManager",
initialize: function (container, application) {
application.register("my:authManager", application.AuthManager);
}
});
http://emberjs.jsbin.com/mopaquko/3/edit
Solution 3
As was pointed out, Ember's container create's singletons by default, you can eagerly create the copies then allow ember to still resolve based on the original namespace.
App.initializer({
name: "joinManagers",
after:['dataManager', 'authManager'],
initialize: function (container, application) {
var dataManager = container.lookup('my:dataManager'),
authManager = container.lookup('my:authManager');
authManager.dataManager = dataManager;
dataManager.authManager = authManager;
application.inject("controller", "dataManager", "my:dataManager");
application.inject("controller", "authManager", "my:authManager");
application.inject("route", "dataManager", "my:dataManager");
application.inject("route", "authManager", "my:authManager");
}
});
App.initializer({
name: "dataManager",
initialize: function (container, application) {
application.register("my:dataManager", application.DataManager);
}
});
App.initializer({
name: "authManager",
initialize: function (container, application) {
application.register("my:authManager", application.AuthManager);
}
});
http://emberjs.jsbin.com/mopaquko/7/edit
Related
I'm working on a Backbone.js app which utilizes a 'master view' which all views and subviews extend from.
Master view
define(['backbone'], function (Backbone) {
return Backbone.View.extend({
events: {
},
showSuccess: function (msg) {
alert(msg);
}
});
});
I then have a Main View which generates the page, this can call on sub views for smaller parts:
define(['backbone','masterView', 'mySubView'], function (Backbone, mView, mySubView) {
var mainView = mView.extend({
events: function () {
return _.extend({}, coreView.prototype.events, {
});
},
render: function () {
var sub = new mySubView({'foo': 'bar'});
}
});
return new mainView();
});
Finally, my subview, which when it's initialised, it says that options is undefined.
define(['backbone','masterView', 'mySubView'], function (Backbone, mView, mySubView) {
var subView = mView.extend({
events: function () {
return _.extend({}, coreView.prototype.events, {
});
},
initialize: function (options) {
console.log(options);
}
});
return new subView();
});
In this setup, why is options undefined when I passed them in my MainView? If the subview doesn't extend masterView, but Backbone.view instead, it works fine.
Your last line in the subview file:
return new subView();
You're returning a new instance instead of returning the constructor in the subview module. It should be:
return subView;
Note that as a convention, JavaScript code should use PascalCase for types (constructor, classes, etc.), and instances (variables, properties, etc.) should use camelCase.
Also, I'm sharing tricks on how to design a good base class with Backbone. You could shift the responsibility of merging the events to the base class instead of each child class.
I have a base class that I would like to extend in a service to help get data in to the angular scope. I have searched around the net for a solution, but have not found one that I like. I have a base class that is used to access the File systems of devices
the class structure:
var cOfflineStorageBase = Class.extend({
init: function(){
},
CreateFolderDir: function(){
},
DeleteAll: function(){
},
DeleteDirectories: function(){
},
DeleteItem: function(){
},
GetFiles: function(){
},
FileExists: function(){
},
GetPath: function(){
},
GetObject: function(){
},
SaveObject: function(){
},
});
I would like to be able to extend this class in several different angular services (ie offlineCart, offlineCustomLists, ect...) where each service would be able to use the storage base to store the various different data types. I am looking for the best, most appropriate way to do this in angular. In vanilla JavaScript one would just do something like this:
var newClass = cOfflineStorageBase.extend({
//add new stuff here
});
but I want to do this same thing the angular way.
The approach I have been considering are to use the angular.extend functionality, but I am not sure this is appropriate or would something like this be a more appropriate approach:
app.factory('someChild', ['$http' , 'cOfflineStorageBase',
function($http, cOfflineStorageBase){
var SomeClass = cOfflineStorageBase.extend({
init: function(){
this._super.init()
},
//Add more stuff here
});
return SomeClass;
}]);
I would like some advice if theses approaches are correct or if there might be another that is better for what I am wanting to accomplish. I would also like or rather need to use promises in much of this code as it would be async.
I pulled off this trick recently.
I will start by defining a plain JavaScript constructor. This does not need to be an angular service. What I do is that, later, the extending constructors can pass any necessary injections by parameter. So, this will be the base "class" of my angular services. This is where I would expose anything I want all angular services to inherit.
function ParentService($http) {
this.$http = $http;
}
ParentService.prototype.foo = function () {
alert("Hello World");
};
Then I will proceed to define a child constructor using prototypal inheritance. This constructor will indeed be an angular service (you can tell by my use of $inject at the end).
function ChildService($http) {
Parent.call(this, $http);
}
ChildService.prototype = new ParentService();
ChildService.prototype.baz = function() {
return this.$http.get('/sample/rest/call');
}
ChildService.$inject = ['$http'];
Then I will proceed to register the services à la carte in the corresponding angular modules:
var app = angular.module('SampleApp', []);
app.service('child', ChildService);
Finally, in my controller I will simply inject my service, which will be an instance of my ChildService constructor, which in turn extends my ParentService constructor:
app.controller('MainCtrl', ['$scope', 'child', function ($scope, child) {
child.foo(); //alert("Hello World")
var promise = child.bar();
}]);
You can see a JSFiddle here
Also there is an interesting video in Youtube from ngConf called Writing A Massive Angular App which covers some of these topics and a few other ideas on code reusability with angular.
This question was asked, and answered, 18 months ago. I recently went through the same issue on a project. I wanted to have a base Model defined that I could use to build factories off of. Angular has a very simple Provider to assist with this called the Value Provider, which Angular implements using the Value Recipe.
I'm not sure what version of Angular you may have been using at the time, but this dates back (AFAIK) to version 1.3.0. (As of this writing, current stable is 1.4.8)
I'm also using John Resig's Simple Inheritance Script.
http://ejohn.org/blog/simple-javascript-inheritance/
Here's a snippet of my code (with most of the application specific logic removed).
var MyApp = angular.module( 'MyApp',['ngResource','ngAnimate','ngSanitize'] );
/* ==================================================================================== */
// - Base Model Class -------------------------------------------------------------
/* ==================================================================================== */
MyApp
/**
* BaseModel - Value Provider
*
*/
.value( 'BaseModel',Class.extend({
attribs: {},
init: function(){
var self = this;
_active = true;
_new = true;
_origs = {};
_loadByObject = function( obj ){ ... }
},
get: function( key ){ ... },
set: function( key,val ){ ... },
isNew: function(){ ... },
keep: function(){ ... },
remove: function(){ ... },
load: function( obj ){ ... }
verify: function(){ ... },
save: function(){ ... },
}))
.factory( 'UserFactory',
[ '$http', '$q', 'BaseModel',
function( $http, $q, BaseModel ){
var UserFactory = BaseModel.extend({
init: function(){
this._super( false );
_fields = [
'first', 'last', 'email',
'phone', 'password', 'role'
];
_permitted = [
'first', 'last', 'email',
'phone', 'password', 'role'
];
_required = [
'first', 'last', 'email', 'role'
];
_resource = "users";
_api = "users";
}
});
return UserFactory;
}])
I'd love to hear anyone's feedback, too.
Here's the Angular
Docs:
https://code.angularjs.org/1.3.0/docs/guide/providers
I have the following situation:
app.js: Singleton Marionette.Application() where I define a nav, a footer, and a main region. In the initializer I construct Marionette.Contoller's and attach them to the app's this.controller object for later control. I might not construct all the Controller's here, just the ones I want to Eagerly Load. Some are Lazy Loaded later. I also instantiate a Backbone.Router here, and pass in a reference to my app object:
var theApp = new TP.Application();
theApp.addRegions(
{
navRegion: "#navigation",
mainRegion: "#main",
footerRegoin: "#footer"
});
theApp.addInitializer(function()
{
// Set up controllers container and eagerly load all the required Controllers.
this.controllers = {};
this.controllers.navigationController = new NavigationController({ app: this });
this.controllers.loginController = new LoginController({ app: this });
this.controllers.calendarController = new CalendarController({ app: this });
this.router = new Router({ app: this });
});
**Controller.js: this is a general use controller that handles view & model intsantiation and eventing. Each Controller owns its own Marionette.Layout, to be filled into the App.mainRegion. Each Controller binds to the layout's "show" event to fill in the layout's regions with custom views. Each Controller offers a getLayout() interface that returns the controller's associated layout.
Marionette.Controller.extend(
{
getLayout: function() { return this.layout; },
initialize: function()
{
this.views.myView = new MyView();
...
this.layout.on("show", this.show, this);
...
},
show: function()
{
this.layout.myViewRegion.show(myView);
}
});
router.js: the router uses the app singleton to load a Controller's layout into the App's main region:
...
routes:
{
"home": "home",
"login": "login",
"calendar": "calendar",
"": "calendar"
},
home: function ()
{
var lazyloadedController = new LazyLoadController();
this.theApp.mainRegion.show(lazyLoadController.getLayout());
},
login: function (origin)
{
this.theApp.mainRegion.show(this.theApp.controllers.loginController.layout);
}
As it is, everything works fine except for reloading the same layout / controller twice. What happens is that the DOM events defined in the LoginView do not re-bind on second show. Which is easily solved by moving the LoginView initialization code into the "show" event handler for that Controller:
LoginController = Marionette.Controller.extend(
{
...
show: function()
{
if (this.views.loginView)
delete this.views.loginView.close();
this.views.loginView = new LoginView({ model: this.theApp.session });
this.views.loginView.on("login:success", function()
{
});
this.layout.mainRegion.show(this.views.loginView);
}
Now everything works fine, but it undoes part of the reason I created Controller's to begin with: I want them to own a View and its Models, create them once, and not have to destroy & recreate them every time I switch layouts.
Am I missing something? Is this not how I should be using Layouts? Isn't the whole point of Layouts and Regions that I can switch in & out Views at will?
Obviously I wouldn't jump back to LoginController/Layout often, but what about between a HomeController/Layout, CalendarController/Layout, SummaryController/Layout, etc... in a single page application I might switch between those 'top-level' layouts rather often and I would want the view to stay cached in the background.
I think your problem is that you don't maintain a single instance of the controller. The recommended way to handle routing/controllers (based on Brian Mann's videos) is like this
App.module('Routes', function (Routes, App, Backbone, Marionette, $, _) {
// straight out of the book...
var Router = Marionette.AppRouter.extend({
appRoutes: {
"home": "home",
"login": "login",
"calendar": "calendar"
}
});
var API = {
home: function () {
App.Controller.go_home();
},
login: function () {
App.Controller.go_login();
},
calendar: function () {
App.Controller.go_calendar();
}
};
App.addInitializer(function (options) {
var router = new Router({controller: API});
});
});
... and the controller:
App.module("Controller", function (Controller, App, Backbone, Marionette, $, _) {
App.Controller = {
go_home: function () {
var layout = new App.Views.Main();
layout.on('show', function () {
// attach views to subregions here...
var news = new App.Views.News();
layout.newsRegion.show(news);
});
App.mainRegion.show(layout);
},
go_login: function () {
....
},
go_calendar: function () {
....
}
};
});
I suspect your problem is the lazy-loaded controller...
We are in the process of learning Ember.js. We do all our development TDD, and want Ember.js to be no exception. We have experience building Backbone.js apps test-driven, so we are familiar with testing front-end code using Jasmine or Mocha/Chai.
When figuring out how to test views, we ran into a problem when the template for the view uses has a #linkTo statement. Unfortunately we are unable to find good test examples and practices. This gist is our quest to get answers how to decently unit-test ember applications.
When looking at the test for linkTo in Ember.js source code, we noticed it contains a full wiring of an ember app to support #linkTo. Does this mean we cannot stub this behaviour when testing a template?
How do you create tests for ember views using template renders?
Here is a gist with our test and a template that will make the test pass, and a template that will make it fail.
view_spec.js.coffee
# This test is made with Mocha / Chai,
# With the chai-jquery and chai-changes extensions
describe 'TodoItemsView', ->
beforeEach ->
testSerializer = DS.JSONSerializer.create
primaryKey: -> 'id'
TestAdapter = DS.Adapter.extend
serializer: testSerializer
TestStore = DS.Store.extend
revision: 11
adapter: TestAdapter.create()
TodoItem = DS.Model.extend
title: DS.attr('string')
store = TestStore.create()
#todoItem = store.createRecord TodoItem
title: 'Do something'
#controller = Em.ArrayController.create
content: []
#view = Em.View.create
templateName: 'working_template'
controller: #controller
#controller.pushObject #todoItem
afterEach ->
#view.destroy()
#controller.destroy()
#todoItem.destroy()
describe 'amount of todos', ->
beforeEach ->
# $('#konacha') is a div that gets cleaned between each test
Em.run => #view.appendTo '#konacha'
it 'is shown', ->
$('#konacha .todos-count').should.have.text '1 things to do'
it 'is livebound', ->
expect(=> $('#konacha .todos-count').text()).to.change.from('1 things to do').to('2 things to do').when =>
Em.run =>
extraTodoItem = store.createRecord TodoItem,
title: 'Moar todo'
#controller.pushObject extraTodoItem
broken_template.handlebars
<div class="todos-count"><span class="todos">{{length}}</span> things to do</div>
{{#linkTo "index"}}Home{{/linkTo}}
working_template.handlebars
<div class="todos-count"><span class="todos">{{length}}</span> things to do</div>
Our solution has been to essentially load the whole application, but isolate our test subjects as much as possible. For example,
describe('FooView', function() {
beforeEach(function() {
this.foo = Ember.Object.create();
this.subject = App.FooView.create({ foo: this.foo });
this.subject.append();
});
afterEach(function() {
this.subject && this.subject.remove();
});
it("renders the foo's favoriteFood", function() {
this.foo.set('favoriteFood', 'ramen');
Em.run.sync();
expect( this.subject.$().text() ).toMatch( /ramen/ );
});
});
That is, the router and other globals are available, so it's not complete isolation, but we can easily send in doubles for things closer to the object under test.
If you really want to isolate the router, the linkTo helper looks it up as controller.router, so you could do
this.router = {
generate: jasmine.createSpy(...)
};
this.subject = App.FooView.create({
controller: { router: this.router },
foo: this.foo
});
One way you can handle this is to create a stub for the linkTo helper and then use it in a before block. That will bypass all the extra requirements of the real linkTo (e.g. routing) and let you focus on the contents of the view. Here's how I'm doing it:
// Test helpers
TEST.stubLinkToHelper = function() {
if (!TEST.originalLinkToHelper) {
TEST.originalLinkToHelper = Ember.Handlebars.helpers['link-to'];
}
Ember.Handlebars.helpers['link-to'] = function(route) {
var options = [].slice.call(arguments, -1)[0];
return Ember.Handlebars.helpers.view.call(this, Em.View.extend({
tagName: 'a',
attributeBindings: ['href'],
href: route
}), options);
};
};
TEST.restoreLinkToHelper = function() {
Ember.Handlebars.helpers['link-to'] = TEST.originalLinkToHelper;
TEST.originalLinkToHelper = null;
};
// Foo test
describe('FooView', function() {
before(function() {
TEST.stubLinkToHelper();
});
after(function() {
TEST.restoreLinkToHelper();
});
it('renders the favoriteFood', function() {
var view = App.FooView.create({
context: {
foo: {
favoriteFood: 'ramen'
}
}
});
Em.run(function() {
view.createElement();
});
expect(view.$().text()).to.contain('ramen');
});
});
I am new in SPA's with backbone and I am trying to develop a small app by using backbone and requireJs.
The problem I faced is that I can't extend a view by passing a collection.
Well, this is the view with name MenuView.js
define([
'Backbone'
], function (Backbone) {
var MenuView = Backbone.View.extend({
tagName: 'ul',
render: function () {
_(this.collection).each(function (item) {
this.$el.append(new MenuListView({ model: item }).render().el);
}, this);
return this;
}
});
return new MenuView;
});
and this is the router.js in which the error is appeared
define([
'Underscore',
'Backbone',
'views/menu/menuView',
'views/createNew/createNew',
'collections/menu/menuCollection',
], function (_, Backbone, MenuView, CreateNewView,Menucollection) {
var AppRouter = Backbone.Router.extend({
routes: {
'index': 'index',
'action/:Create': 'Create'
},
index: function () {
CreateNewView.clear();
//----------- HERE IS THE PROBLEM ------------
$('#menu').html(MenuView({ collection: Menucollection.models }).render().el);
},
Create: function () {
CreateNewView.render();
}
});
var initialize = function () {
var appRouter = new AppRouter();
Backbone.history.start();
appRouter.navigate('index', { trigger: true });
};
return {
initialize: initialize
};
});
The error message is "object is not a function". I agreed with this since the MenuView is not a function. I tried to extend the MenuView (MenuView.extend({collection:Menucollection.models})) and the error message was "objet[object,object] has no method extend".
I suppose that the way I am trying to do this, is far away from the correct one.
Could anyone suggest how to do this?
Thanks
#Matti John's solution will work, but it's more of a workaround than a best practice IMHO.
As it is, you initializing your view just by requiring it, which:
Limits you to never accept arguments
Hits performance
Makes it really hard to unit-test if you relay on assigning properties ater constructing an instance.
A module should be returning a 'class' view and not an instance on that view.
In MenuView.js I would replace return new MenuView with return MenuView; and intitalzie it when required in router.js.
Your MenuView.js returns an initialized MenuView, so you could just do:
MenuView.collection = Menucollection
Note I haven't selected the models - I think it's better if you don't use the models as a replacement for your view's collection, since it would be confusing to read the code and not have a Backbone collection as the view's collection. You would also lose the method's contained within the collection (e.g. fetch/update).
If you do this, then you would need to update your loop (each is available as a method for the collection):
this.collection.each(function (item) {
this.$el.append(new MenuListView({ model: item }).render().el);
}, this);