javascript: durandaljs, applyBindings inside a viewmodel - javascript

I'm trying to make a single page application with dynamic content, using durandaljs. For example, if you change the language in your settings, then the UI gets updated. I'm using SignalR to load the objects from the server, and everything works fine apart from when I navigate. The first time I load the view, I'm getting the following error:
Uncaught Error: Unable to parse bindings.
Message: ReferenceError: router is not defined;
Bindings value: compose: {
model: router.activeItem, //wiring the router
afterCompose: router.afterCompose, //wiring the router
transition:'entrance', //use the 'entrance' transition when switching views
cacheViews:true //telling composition to keep views in the dom, and reuse them (only a good idea with singleton view models)
}
but if I reload the page, then the view is displayed correctly.
Here is an example of the viewmodel:
define(function (require) {
var p = require('hubs/myhub'),
rep = require('repositories/myrepository');
var myViewModel = function(data, proxy, cookie) {
var self = this;
self.proxy = proxy;
self.cookie = cookie;
self.Labels = ko.observableArray([]);
try {
self.proxy
.invoke('Setup', self.cookie.Username, self.cookie.Language)
.done(function (res) {
if (res.Result) {
console.log(JSON.stringify(res.Object, null, 4));
self.Labels(res.Object.Labels);
} else {
console.log(res.Error);
}
});
} catch (e) {
console.log(e.message);
}
};
return {
activate: function () {
var cookie = JSON.parse($.cookie(rep.cookieName));
ko.applyBindings(myViewModel({}, p.proxy, cookie), document.getElementById('my_container'));
}
};
});
If I take off the applyBinding of the activate function, then there is no more issue within the navigation. Would there be proper way to do this?
I've modified the return statement for:
return {
myModel: new myViewModel ({ }, p.proxy, JSON.parse($.cookie(rep.cookieName))),
activate: function () {
this.myModel.init();
}
};
and wrapped the signalr call inside an init() function. everything works great now.

That is exactly the right way! DUrandal calls the Ko.applybindings for YOU ;) Meaning Durandal does the binding!
Hot Towel SPA Durandal Knockout and Dynatree

Related

Waiting on collection data prior to rendering a subview

I am building what should be a fairly simple project which is heavily based on Ampersand's starter project (when you first run ampersand). My Add page has a <select> element that should to be populated with data from another collection. I have been comparing this view with the Edit page view because I think they are quite similar but I cannot figure it out.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
Below you can see that I am trying to fetch the app.brandCollection and set its value to this.model, is this correct? I will need to modify the output and pass through the data to an ampersand-select-view element with the correct formatting; that is my next problem. If anyone has suggestions for that I would also appreciate it.
var PageView = require('./base');
var templates = require('../templates');
var ProjectForm = require('../forms/addProjectForm');
module.exports = PageView.extend({
pageTitle: 'add project',
template: templates.pages.projectAdd,
initialize: function () {
var self = this;
app.brandCollection.fetch({
success : function(collection, resp) {
console.log('SUCCESS: resp', resp);
self.brands = resp;
},
error: function(collection, resp) {
console.log('ERROR: resp', resp, options);
}
});
},
subviews: {
form: {
container: 'form',
waitFor: 'brands',
prepareView: function (el) {
return new ProjectForm({
el: el,
submitCallback: function (data) {
app.projectCollection.create(data, {
wait: true,
success: function () {
app.navigate('/');
app.projectCollection.fetch();
}
});
}
});
}
}
}
});
This is only the add page view but I think that is all that's needed.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
This string represents path in a current object with fixed this context. In your example you've waitFor: 'brands' which is nothing more than PageView.brands here, as PageView is this context. If you'd have model.some.attribute, then it'd mean that this string represents PageView.model.some.attribute. It's just convenient way to traverse through objects.
There's to few informations to answer your latter question. In what form you retrieve your data? What do you want to do with it later on?
It'd be much quicker if you could ping us on https://gitter.im/AmpersandJS/AmpersandJS :)

reloading ember-data models

I'm working on a controller that reloads data when data is received from a websocket. I've got it working up to the point of reloading the data. I'm not sure why this isn't working, but I'm getting an error when I call self.get('contact').reload(); below. 'object has no method reload'. I'm pretty sure I'm calling this incorrectly, but I'm not sure how to reload the data from the store. Could someone help me out?
CallMonitor.ContactsRoute = Ember.Route.extend({
model: function(){
return this.store.find('contact');
},
setupController: function(controller, contacts) {
var socket = io.connect('http://localhost:3000');
controller.set('socket', socket);
controller.set('contact', contacts);
}
});
CallMonitor.ContactsController = Ember.ArrayController.extend({
socketDidChange: function(){
var socket = this.get('socket'),
self = this;
if(socket)
{
socket.on('call', function (data) {
var contactToUpdate = self.contact.filter(function(item) {
return item.id == data.contactId;
});
if(contactToUpdate.length)
{
contactToUpdate.reload();
}
else
{
// reload all the contacts
self.get('contact').reload();
}
});
}
}.observes('socket')
});
I ended up just doing another fetch from the store and then setting the controller's contact property again. Couldn't find an easy way to do a reload for multiple records. For single records, it's easy to just do a "reload()" but for new records and such it's apparently not that easy.
var contactPromise = self.contact.store.find('contact');
contactPromise.then(function(data){
self.set('contact', data);
}, undefined);
Kind of a bummer. Also couldn't find a good way to remove records in ember data.

Render a View of Backbone Model returns undefined

Lets say i have a JSON like this:
JSON example, my json is validated on jsonlint and it works.
json_object = {
"texts_model": {
"hello": "hola",
"icon_text": "Icon!"
},
"collection_vias": {
"text": "hola",
"icon_text": "Icon!"
}
};
I have made a Collection that parse the contents of the json and generates model and collections from this json.
App.TemplatesCollection = Backbone.Model.extend({
model: App.TemplateModel,
url: TEMPLATES_SERVICE,
initialize: function(){
this.fetch({
success: (function () {
console.log(' Success ');
}),
error:(function (e) {
//console.log(' Error: ' + e);
}),
complete:(function (e) {
console.log(' Fetch completado con exito. ');
})
});
},
//Here i generate all my models and collections.
parse: function(response){
App.texts = new App.TemplateModel(response.text_model);
App.vias = new App.ViasCollection(response.collection_vias);
return response;
},
//I was trying with the get function but i the only thing i got was undefined.
plain_texts: function(){
return( this.get('plain_texts') ) ;
}
});
And the view is like this:
App.TemplateView = Backbone.View.extend({
el: App.$main_content,
initialize: function(){
_.bindAll(this, 'render');
},
//Here i pass the template(html source) i want to render.
render: function(template){
var html = render(template, this.model.toJSON() );
App.$main_content.html(html);
return this;
}
});
And my start.js where they live all the declarations of my models and views:
//app
App = {
init: function(){
console.log('Iniciando...');
//variables y constantes
App.$main_content = $('#main-content-content');
App.$main_header = $('#main-header-content')
App.$main_navigation = $('#main-navigation-content');
//data
App.templates = new App.TemplatesCollection();
//views
App.templateView = new App.TemplateView({model: App.texts});
//router
App.router = new App.Router();
},
start: function(){
//init
App.init();
//router
Backbone.history.start();
}
}
And the router:
//router
App.Router = Backbone.Router.extend({
routes:{
"" : "index",
":web" : "url"
},
index: function(){
console.log("index");
//Here i do not know what to do, i mean do i have to instiate the View each time i go to index? or only render?
App.templateView = new App.TemplateView({model: App.texts});
App.templateView.render("sections/login/form");
},
url: function(web){
console.log(web);
}
});
//on document ready
$(function(){
App.start();
});
My problem is that when the html is loaded the only thing i have is:
"Uncaught TypeError: Cannot call method 'toJSON' of undefined "
But when i put this on the developer console:
App.templateView = new App.TemplateView({model: App.texts});
App.templateView.render("sections/login/form");
My view is rendered correctly.
Why my view isn't rendered on the load and only when i put my code on the developer console?
How can i render my model on the view on the router url?
Why do i have undefined on the html loaded on the developer console?
----EDIT---
All right,
I think i understand. Maybe I'm generating a problem of a thing that does not have to have a problem.
Now my Model is like this:
App.TemplatesCollection = Backbone.Model.extend({
model: App.TemplateModel,
url: TEMPLATES_SERVICE,
plain_texts: function(){
return this.get('texts') ;
},
initialize: function(){
this.fetch();
}
});
And the View:
App.TemplateView = Backbone.View.extend({
el: App.$main_content,
initialize: function(){
console.log(this.collection);
var ea = this.collection.get('texts');
console.log(ea);
},
render: function(template){
console.log(this.collection);
return this;
}
});
Now i see my collection inside my View.
But when i try to do this to get only the text version on my View:
var ea = this.collection.get('texts');
console.log(ea);
Im getting the error of undefined:
Uncaught TypeError: Cannot call method 'get' of undefined
Any idea about how can i resolve this?
I'm trying to solve this by myself. I do not want to look like im asking to develop my solution.
Thanks in advance.
It's a little hard to read, but at a quick glance: your App.texts = is in in the parse() function of your Collection. As a result, it gets called once the .fetch() on the collection is performed... until then, your App.texts is undefined!
If App.texts is undefined when you create the TemplateView, then the view's model will actually be undefined, and so, in the render, when the template engine you use is doing a toJSON(), it will say that it has an undefined value...
There may be other problems, but this one is the most glaring. Here is a quick&dirty fix: once the fetch() is done, your collection will trigger a reset event. That's your cue for doing the rendering. So, what you can do, is instead of passing the model to the View, you can pass the collection instead:
App.templateView = new App.TemplateView({collection: App.templates});
Now, in your View's initialize, you can do something like:
if(App.texts) {
//Your collection has already fetched and already went through parse()
this.model = App.texts;
this.render("sections/login/form");
} else {
//Your collection hasn't done the fetch yet
view = this;
this.collection.one("reset", function(){
view.model = App.texts;
view.render("sections/login/form");
});
}
If you give a collection as a param to a View's construction, it'll be stored in this.collection, same as with model. The idea here is to use the events to know when to do the rendering, and also let the view tell you when it's ready to render. You could also do something in your render() function to check if the model is defined!
To see if this analysis is correct, you can put a console.log(App.texts); in your index function in the router.
One way to make the code a bit more obvious is to initialize your App.texts and App.vias directly in your App's init. And give a reference to them to your AppTemplatesCollection if you really need to side-load them in the parse of AppTemplates' fetch(). The difference that makes is that you can bind to events from the App.vias collection ('add', 'remove', 'reset') or to the App.texts model ('change').
Another thing I noticed is that you have a collection of App.TemplateModel but you are still creating a App.texts where you put the result of the fetch into your own instance of App.TemplateModel? That doesn't seem right, maybe you have a reason for doing so, but in the most general case, the collection is suppose to handle the creation of the models, especially after a fetch!
The usual use case of the parse() method is to side-load data (other models/collection), change the format (from XML to something JS can understand) or to remove useless keys (for instance user: {id: ..., name: ... }, you'll return response.user so that Backbone can play with the correct hash directly). What you are doing here seems to fall out of this pattern so maybe it's a cause for worry?
In your code you have created collection as :
App.TemplatesCollection = Backbone.Model.extend({
//rest of the code
If you want to create a collection you need to extend Backbone.Collectionand not Backbone.Model.
App.TemplatesCollection = Backbone.Collection.extend({
//rest of the code

amplify.js - amplify.store not storing data

I am using amplify.js with Knockout.js and I want to store data locally. I tried using this code: amplify guide
but it isn't working for me.
My view model
define(['services/datacontext'], function (dataContext) {
var store = amplify.store("offlineData"); // Why is agency undefined after retrieving from the store?!?!?!
var agency = ko.observableArray([]);
var initialized = false;
var save = function (agency) {
return dataContext.saveChanges(agency);
};
var vm = { // This is my view model, my functions are bound to it.
//These are wired up to my agency view
activate: activate,
agency: agency,
title: 'agency',
refresh: refresh, // call refresh function which calls get Agencies
save: save
};
return vm;
function activate() {
if (initialized) {
return;
}
initialized = true;
if (initialized == true) {
amplify.store("offlineData", vm.agency);
}
return refresh();
}
function refresh() {
return dataContext.getAgency(agency);
}
});
After refresh retrieves the data, I save this data to the local store. So when I make another request for this page. I would expect var store to contain this data but it is undefined.
Does anyone know how to use amplify?
amplify.store("offlineData", vm.agency);
vm.agency is a function, therefore you need to invoke it to get its value
amplify.store("offlineData", vm.agency());

When do I need a model in backbone.js?

I'm new to Backbone.js, and someone who comes out of the 'standard' model of JS development I'm a little unsure of how to work with the models (or when).
Views seem pretty obvious as it emulates the typical 'listen for event and do something' method that most JS dev's are familiar with.
I built a simple Todo list app and so far haven't seen a need for the model aspect so I'm curious if someone can give me some insight as to how I might apply it to this application, or if it's something that comes into play if I were working with more complex data.
Here's the JS:
Todos = (function(){
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
}
});
var TodoView = Backbone.View.extend({
el: $('#todos'),
newitem: $('#new-item input'),
noitems: $('#no-items'),
initialize: function(){
this.el = $(this.el);
},
events: {
'submit #new-item': 'addItem',
'click .remove-item': 'removeItem'
},
template: $('#item-template').html(),
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
return this;
},
removeItem: function(e){
$(e.target).parent('.item-wrap').remove();
}
});
self = {};
self.start = function(){
new TodoView();
};
return self;
});
$(function(){
new Todos(jQuery).start();
});
Which is running here: http://sandbox.fluidbyte.org/bb-todo
Model and Collection are needed when you have to persist the changes to the server.
Example:
var todo = new TodoModel();
creates a new model. When you have to save the save the changes, call
todo.save();
You can also pass success and error callbacks to save . Save is a wrapper around the ajax function provided by jQuery.
How to use a model in your app.
Add a url field to your model
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
},
url: {
"http://localhost";
}
});
Create model and save it.
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
var todo = new TodoModel({'content':this.newitem.val()});
todo.save();
return this;
},
Make sure your server is running and set the url is set correctly.
Learning Resources:
Check out the annotated source code of Backbone for an excellent
explanation of how things fall into place behind the scenes.
This Quora question has links to many good resources and sample apps.
The model is going to be useful if you ever want to save anything on the server side. Backbone's model is built around a RESTful endpoint. So if for example you set URL root to lists and then store the list information in the model, the model save and fetch methods will let you save/receive JSON describing the mode to/from the server at the lists/<id> endpoint. IE:
ToDoListModel = Backbone.model.extend( {
urlRoot : "lists/" } );
// Once saved, lives at lists/5
list = new ToDoListModel({id: 5, list: ["Take out trash", "Feed Dog"] });
list.save();
So you can use this to interact with data that persists on the server via a RESTful interface. see this tutorial for more.
I disagree with the idea that model is needed only to persist changes (and I am including LocalStorage here, not only the server).
It is nice to have representation of models and collections so that you have object to work with and not only Views. In your example you are only adding and removing divs (html) from the page, which is something you can do normally with jQuery. Having a Model created and added to a Collection everytime you do "add" and maybe removed when you clear it will allow you some nice things, like for example sorting (alphabetically), or filtering (if you want to implement the concept of "complete" to-do).
In your code, for example:
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
complete: false
}
});
var Todos = Backbone.Collection.extend({
model: TodoModel
})
In the View (irrelevant code is skipped):
// snip....
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
var newTodo = new TodoModel({ content: this.newitem.val() });
this.collection.add(newTodo); // you get the collection property from free from the initializer in Backbone
this.el.append(templ({model: newTodo})); // change the template here of course to use model
this.newitem.val('').focus();
return this;
},
Initialize like this:
self.start = function(){
new TodoView(new Todos());
};
Now you have a backing Collection and you can do all sort of stuff, like filtering. Let's say you have a button for filtering done todos, you hook this handler:
_filterDone: function (ev) {
filtered = this.collection.where({ complete: true });
this.el.html(''); // empty the collection container, I used "el" but you know where you are rendering your todos
_.each(filtered, function (todo) {
this.el.append(templ({model: todo})); // it's as easy as that! :)
});
}
Beware that emptying the container is probably not the best thing if you have events hooked to the inner views but as a starter this works ok.
You may need a hook for setting a todo done. Create a button or checkbox and maybe a function like this:
_setDone: function (ev) {
// you will need to scope properly or "this" here will refer to the element clicked!
todo = this.collection.get($(ev.currentTarget).attr('todo_id')); // if you had the accuracy to put the id of the todo somewhere within the template
todo.set('complete', true);
// some code here to re-render the list
// or remove the todo single view and re-render it
// in the simplest for just redrawr everything
this.el.html('');
_.each(this.collection, function (todo) {
this.el.append(templ({model: todo}));
});
}
The code above would not have been so easy without Models and Collections and as you can see it does not relate in any way with the server.

Categories

Resources