I'm trying to figure out a way to structure my app in a way that the API interactions are independent from my views and viewmodels.
At the moment, my ajax calls (get, add, save, remove, etc) and models (User model, Message model) are inside my view models, but in the future, we'll have a mobile app that will be a bit different from the desktop app, so I'd like to keep these actions accessible in one place.
I've seen people use a 'services' folder where they have models that handle loading and storing data, but haven't seen a complete structure that also includes handling new and current data.
Let's say I have a separate 'profile page' shell that includes a 'messages' tab and a 'user details' tab. This section needs the following:
get user details
get messages
User Model
Message Model
add/edit/remove message
edit user details
How would I go about structuring this? Individually by component (messages with model + get + add/edit/remove and user with model + get + edit in separate files/folders) or by site area (everything in one file/folder)?
Hopefully this makes sense. Thank you!
I'm not experienced in Durandal but have some positive background working with KO. I would recommend you to apply module pattern and incapsulate all your API service methods into the separate class (lets call it Router) also putting it into separate file. And then use methods of the Router class inside viewmodels.
// file with Router class
(function ($, ko, app) {
app.Router = function () {
var self = this;
self.get = function (url, queryString, callBack) {
$.get(url, data, function(data) {
callBack(data);
});
};
self.post = function (url, queryString, callBack) {
$.post(url, data, function(data) {
callBack(data);
});
};
};
})(jQuery, ko, window.app)
// file with viewmodel
(function ($, ko, app) {
app.UserModel = function () {
var self = this;
//create instance of Router class.
//Create it here just for the example. Will be better to create it out of the models to have just one instance.
//Or convert Router class to singleton
var router = new app.Router();
self.getUserDetails = function() {
//use proper router method to GET data, providing params
router.get(properRestServiceUrl, {userID: 1}, self.showUserDetails);
};
self.addMessage = function() {
//use proper router method to POST data, providing params
router.post(properRestServiceUrl, {userID: 1, message: 'test message'}, self.showConfirmation);
};
//callback function
self.showUserDetails = function(data) {
alert(data);
};
//callback function
self.showConfirmation = function(data) {
alert("The message was added successfully!");
};
};
})(jQuery, ko, window.app)
I don't know what you use for back end, but in case it's ASP.NET MVC, I would strongly suggest checking out HotTowel template. Even if it's not ASP.NET MVC, it is still a good starting point to see how to efficiently structure your app.
http://www.asp.net/single-page-application/overview/templates/hottowel-template
Related
Here's the workflow:
A user's account page should list all of the object owned by that user.
Next to each object is a "delete" button that opens a Bootstrap modal. The modal asks the user if they truly want to delete the object, and if they confirm, then the modal should dismiss, the object should be deleted, and the view should update to reflect the deletion.
I am dismissing the modal using the data-dismiss attribute on the confirmation button inside of the modal.
Here is the function in my controller that deletes the object and (should) update the view:
$scope.deleteObject = function(object) {
object.destroy({
success: function(object) {
$scope.$apply();
},
error: function(object, error) {
// handle error
}
});
};
However, I have to refresh the page to see the updated view with the object removed.
Is there another way I should be using $scope.$apply?
EDIT: I found a workaround by creating a new $scope level function to load my collection of objects. Previously, this was done when the controller is loaded (not attached to any particular function.
In other words, my old code did this:
.controller('AccountCtrl', function($scope) {
var query = new Query('Object');
query.find().then(function(objects) {
$scope.objects = objects;
});
$scope.deleteObject = function(object) {
object.destroy({
success: function(object) {
// do something
}
});
}
});
Now I've wrapped the find code in a $scope level function, which I can call explicitly when an object is destroyed:
.controller('AccountCtrl', function($scope) {
$scope.getObjects = function() {
var query = new Query('Object');
query.find().then(function(objects) {
$scope.objects = objects;
});
}
$scope.getObjects(); // call when the view loads
$scope.deleteObject = function(object) {
object.destroy({
success: function(object) {
$scope.getObjects(); // call again when an object is deleted
}
});
}
});
I'm still hoping there is a cleaner solution to this, i.e. one where I don't have to manually update by object collection.
In your success you have to modify the local $scope.objects.
In you last exemple you should try this (code not tested, but this is how it should look):
$scope.deleteObject = function(object) {
object.destroy({
success: function(object) {
var objectIndex = $scope.objects.indexOf(object);
$scope.objects.splice(objectIndex,1);
}
});
}
In your controller you take the responsibility for updating your model. Angular take the responsibility for updating the view. Calling again the $scope.getObjects() is a way to do it. But the cleaner way is to implements your update of the model in case of success. You should also give an error method in case the server response is an error.
If you have correctly bind the collection to your view, i should update after a delete. Tell me if it helped you out.
I've tried to describe my problem in the below illustration.
When the page loads, a javascript object is parsed and becomes my backbone model called obj model. This obj model is passed along to many different independent and modular submodules that make use of the data in different ways.
Everything works great except for when I'm dealing with collections. To give the user the ability to manage "Photos" and "Comments" I need to create a separate collection/model data structure for them.
How should I sync back the changes to my "obj model"?
class Obj extends Backbone.DeepModel
class Comment extends Backbone.DeepModel
class CommentCollection extends Backbone.Collection
model: Comment
class Photo extends Backbone.DeepModel
class PhotoCollection extends Backbone.Collection
model: Photo
You're obj model should be externalized.
App.module("Entities",function(Entities,App,Backbone,Marionette,$,_){
"use strict";
//these are locally scoped, so they aren't accessable by the your app
var Obj = Backbone.Model.extend({
urlRoot:'api/obj',
});
var Data = {};
var API = {
//wrap request in deferred
getObjById : function(id, reset){
var deferred = $.Deferred();
//returned cached data if we don't request refreshed data
reset = reset || false;
if (Data[id] && !reset) {
return Data;
}
this._getObj(id, function(loadedObj) {
//cache object
Data[loadedObj.id] = loadedObj
deferred.resolve(loadedObj);
});
return deferred.promise();
},
_getObj: function(id, callback) {
var obj = new Obj({id:id});
obj.fetch({success:callback});
},
};
//Interface for mucking with Obj model
App.reqres.setHandler("obj:getObj", function(id) {
return API.getObjById(id);
});
});
You can get an obj like so. If reset isn't passed in, it will be the cached version (all sub modules can refer to same obj just by passing in ID or w/e your criteria is for loading):
$.when(App.request('obj:getObj', 123)).done(function(loadedObj) {
//show view, or do whatever
);
OR to get fresh data:
$.when(App.request('obj:getObj', 123, true)).done(function(loadedObj) {
//show view, or do whatever
);
These are just examples of how to GET data. You could extend the API and expose new handlers for UPDATING data. You could either have each sub module's controller listen to the obj (by all listening to the same, cached obj model), or just request the most up to date obj model each time you need it. This would be a round trip to the server if your cached data reflects the most recent changes. But then you have to worry about keeping things in sync.
Hope this helps
I'm trying to access and write/edit a property on a separate controller from an extended object. I've tried using this.get('controllers.photos'); and including the controller without luck. I've also tried this.controller('photos').get('uploadPhotos') and a number of obscure implementations. I've included an example of my code below.
I have a controller with a property uploadedPhotos
App.PhotosController = Ember.ArrayController.extend({
uploadedPhotos: [1,2,3,4,5]
}
I then have an object that handles my uploading and stores the IDs of the images.
App.Upload = Ember.Object.extend({
...some ajax call
success: function(file, response){
App.PhotosController.uploadedPhotos = response;
}
});
It is then created inside of a view called photos
App.PhotosView = Ember.View.extend({
didInsertElement: function(){
Upload.create();
}
});
You should move your upload methods into the router.
That being said you can inject the controller into your upload object on create.
App.Upload = Em.Object.extend
success: (file, response) ->
#get('controller').set('uploadedPhotos', response)
App.PhotosView = Em.View.extend
didInsertElement:
controller = #get('controller')
App.Upload.create(controller: controller)
Sorry about answering with coffeescript it is just faster to type.
I assume the Upload object just handles loading of photos. In that case, is there some reason for not having the functionality right on the PhotosController (making ajax call on init for example)?
Anyway, if you really need to wire controllers (I guess that Upload would need to extend Em.Controller instead of Em.Object) check this:
Dependecies Between Controllers.
I'm new to all the backend side of things, so hopefully this is one of the questions I'm gonna look back to and think "man, that was silly of me". Let me start. I want to make an application where you can draw rectangles on a screen.
Every rectangle is a model represented by an ItemView and every screen is a collection represented by a CollectionView. Rectangles are added to the collection once they are created by the user, dynamically.
When the user clicks on a "new screen" button, the following takes place
MyController = Backbone.Marionette.Controller.extend({
initialize:function() {
i=0;
},
newScreen: function() {
i=i+1;
this.collection = new Rectangles([],{id: i});
routestring = "screen/"+i;
router.navigate(routestring);
canvas.show(new ScreenView({collection: this.collection}));
},
...
I use an iterator for the different screens so that my backend looks like this
demo.firebaseio.com/screens/1
I am using Firebase's backbone bindings but I have absolutely no idea on how I can access a collection that's already stored on the server. The reason is that I want to be able to navigate through the different screens, fetch the collection from that url, and render it...
Here's my collection code
Rectangles = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
return 'https://demo.firebaseio.com/screen/'+this.id;
},
model: Rectangle,
firebase: function() {
return new Backbone.Firebase("https://demo.firebaseio.com/screen/"+this.id)
}
});
and here's my router code!
var Router = Backbone.Marionette.AppRouter.extend({
controller: myController,
appRoutes: {
'': 'newScreen',
'screen/:number': 'doSomething'
}
})
I'm open to any suggestions, and I do understand that I might have done something horribly wrong!
The following should work:
Rectangles = Backbone.Collection.extend({
firebase: new Backbone.Firebase("https://demo.firebaseio.com/screens/"+this.id),
});
However, you'll need to call Collection.sync() to initially fetch the data. The alternative is to use a truly realtime collection in the form of Backbone.Firebase.Collection:
Rectangles = Backbone.Firebase.Collection.extend({
firebase: new Firebase("https://demo.firebaseio.com/screens/"+this.id),
});
In this case you won't have to call fetch/sync/save etc. - the collection is always kept in sync with Firebase. There's more documentation on how this works here: http://firebase.github.io/backfire/
I am cleaning up a multi-page app of 65+ html pages and a central javascript library. My html pages have a ton of redundancies and the central js library has become spaghetti. I face limitations on consolidating pages because I am working within a larger framework that enforces a certain structure. I want to reduce the redundancies and clean up the code.
I discovered backbone, MVC patterns, microtemplating and requirejs, but they seem best for single page applications. Somehow I need to let the main module know what page is being loaded so it will put the right elements on the page. I am thinking of passing in the title of the html which will turn grab the correct collection of page elements and pass them into App.initialize as an object.
1) Can anyone validate this approach? If not are there alternate approaches recommended? How about extensions to backbone like marionette?
2) Can anyone recommend a means to get page specifics into the backbone framework?
Following backbone tutorials I built a successful test page with a main.js that calls an App.initialize method that calls a view.render method. My first thought is to read the html page title and use it to select a model for the specific page being loaded. I'd have to pass in an object with the specifics for each pages layout. Here's the view's render method so you can see what I am trying to do:
render: function () { // pass parameter to render function here?
var data = new InputModel,
pageTitle = data.pageTitle || data.defaults.pageTitle,
compiled,
template;
var pageElements = [
{ container: '#page_title_container', template: '#input_title_template' },
{ container: '#behavior_controls_container', template: '#behavior_controls_template' },
{ container: '#occurred_date_time_container', template: '#date_time_template' }]
for (var i = 0; i < pageElements.length; i++) {
this.el = pageElements[i].container;
compiled = _.template($(InputPageTemplates).filter(pageElements[i].template).html());
template = compiled({ pageTitle: pageTitle });
//pass in object with values for the template and plug in here?
$(this.el).html(template);
}
}
Your help will be greatly appreciated. I am having a lot of fun updating my circa 1999 JavaScript skills. There's a ton of cool things happening with the language.
Using the document title to choose the loaded scripts sounds a tad kludge-y. If it works, though, go for it.
Another idea worth exploring might be to utilize Backbone.Router with pushState:true to setup the correct page. When you call Backbone.history.start() on startup, the router hits the route that matches your current url, i.e. the page you are on.
In the route callback you could do all the page-specific initialization.
You could move the template and container selection out of the view into the router, and set up view in the initialize() function (the view's constructor). Say, something like:
//view
var PageView = Backbone.View.extend({
initialize: function(options) {
this.model = options.model;
this.el = options.el;
this.title = options.title;
this.template = _.template($(options.containerSelector));
},
render: function() {
window.document.title = title;
var html = this.template(this.model.toJSON());
this.$el.html(html);
}
});
Handle the view selection at the router level:
//router
var PageRouter = Backbone.Router.extend({
routes: {
"some/url/:id": "somePage",
"other/url": "otherPage"
},
_createView: function(model, title, container, template) {
var view = new PageView({
model:model,
title:title
el:container,
templateSelector:template,
});
view.render();
},
somePage: function(id) {
var model = new SomeModel({id:id});
this._createView(model, "Some page", "#somecontainer", "#sometemplate");
},
otherPage: function() {
var model = new OtherModel();
this._createView(model, "Other page", "#othercontainer", "#othertemplate");
}
});
And kick off the application using Backbone.history.start()
//start app
$(function() {
var router = new PageRouter();
Backbone.history.start({pushState:true});
}
In this type of solution the view code doesn't need to know about other views' specific code, and if you need to create more specialized view classes for some pages, you don't need to modify original code.
At a glance this seems like a clean solution. There might of course be some issues when the router wants to start catching routes, and you want the browser to navigate off the page normally. If this causes serious issues, or leads to even bigger kludge than the title-based solution, the original solution might still be preferrable.
(Code examples untested)