Durandal observable array not updating - javascript

I copied the pagination example from the Durandal Samples into my code and I have noticed that the function gets called just once. Changing the values on the observable does not have any effect on the items observable array and does not make a fresh call to SimpleGrid . What am I doing wrong ?
My gridViewModel does not get called with any data in it the first time and does not get called again.
Thanks a lot.
start.html
<html snipped>
<div data-bind='compose: gridViewModel'></div>
start.js
define(['durandal/system', 'plugins/http', 'durandal/app', './simpleGrid', 'knockout', 'moment'], function (system, http, app, SimpleGrid, ko, moment) {
//Note: This module exports an object.
//That means that every module that "requires" it will get the same object instance.
//If you wish to be able to create multiple instances, instead export a function.
//See the "welcome" module for an example of function export.
var self = this;
self.start_date = ko.observable("");
self.end_date = ko.observable("");
self.records_per_page = ko.observable("");
self.records = ko.observableArray([]);
self.pages = ko.observableArray([]);
var initialData = [];
self.getShippersData = function () {
var that = this;
var obj = {
start_date: this.start_date(),
end_date: this.end_date(),
search_for: this.search_for(),
records_per_page: this.records_per_page()
};
http.get('shippers.php/records', obj).then(function (response) {
that.records(response.dbrecs);
that.initialData = response.pages;
that.pages(response.pages);
console.log("updating pages");
}, function (error) {
console.log("Error State : Call to shippers.php Failed");
});
};
self.search_for = ko.observable("").extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" } });
search_for.subscribe(function (searchTerm) {
//make use of searchTerm i.e. search_for variable to make ajax call.
//Whenever you change value of search_for, this method will invoke
//console.log("This is a proxy for an AJAX call");
self.getShippersData();
});
console.log("This is after the shipper call");
console.log(initialData);
self.items = ko.observableArray(initialData);
self.gridViewModel = new SimpleGrid({
data: self.pages,
pageSize: self.records_per_page
});
return {
items: items,
start_date: start_date,
end_date: end_date,
records_per_page: records_per_page,
search_for: search_for,
records: records,
pages: pages,
getShippersData: getShippersData,
gridViewModel: gridViewModel,
SimpleGrid: SimpleGrid,
activate: function () {
//the router's activator calls this function and waits for it to complete before proceeding
var begin = moment().startOf('day').format('DD/MM/YYYY HH:mm:ss');
var end = moment().endOf('day').format('DD/MM/YYYY HH:mm:ss');
begin = "15/05/2014 00:00:00";
end = "18/05/2014 00:00:00";
this.start_date(begin);
this.end_date(end);
this.getShippersData();
},
select: function (item) {
//the app model allows easy display of modal dialogs by passing a view model
//views are usually located by convention, but you an specify it as well with viewUrl
},
/*
canDeactivate: function () {
//the router's activator calls this function to see if it can leave the screen
//return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
}
*/
};
});
simpleGrid.js
define(['knockout'], function (ko) {
var SimpleGrid = function (configuration) {
console.log("I am in SimpleGrid");
console.log(configuration);
this.data = configuration.data;
this.currentPageIndex = ko.observable(0);
this.pageSize = configuration.pageSize || 5;
this.itemsOnCurrentPage = ko.computed(function () {
var startIndex = this.pageSize * this.currentPageIndex();
return this.data.slice(startIndex, startIndex + this.pageSize);
}, this);
this.maxPageIndex = ko.computed(function () {
return Math.ceil(ko.utils.unwrapObservable(this.data).length / this.pageSize) - 1;
}, this);
};
return SimpleGrid;
});
simpleGrid.html
<div>
<ul class="pagination">
<!-- ko foreach: ko.utils.range(0, maxPageIndex) -->
<li>
</li>
<!-- /ko -->
</ul>
</div>

I think you need to prefix your functions in your object return with self..
Like this:
return {
items: self.items,
start_date: self.start_date,
end_date: self.end_date,
records_per_page: self.records_per_page,
search_for: self.search_for,
records: self.records,
pages: self.pages,
getShippersData: self.getShippersData,
gridViewModel: self.gridViewModel
And then in your activate function, call self.functionName instead of this..
I've had similar problems when doing just like you and calling this. in the object wich I'm returning.

Related

Rendering the view returns undefined

I've got a collection view with two filter methods, and a render method which takes a parameter. The problem I'm stuck with is that when rendering the view for the first time it returns me an error. Here's my collection:
var ResumeCollection = Backbone.Collection.extend({
url: 'http://localhost:3000',
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
}
});
And my view:
var ResumeList = Backbone.View.extend({
events { // hash array of filter events },
initialize: function () {
this.collection.fetch();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function (ev) {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function (ev) {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
},
showAll: function (ev) {
this.render(this.collection);
}
});
This view gets rendered for the first time in my router by passing a collection:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function () {
this.layout = new LayoutView();
}
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
});
And this is the layout view within which all the other views are rendered:
var LayoutView = Backbone.View.extend({
el: $('#outlet'),
render: function (view) {
if (this.child && this.child !== view) {
this.child.undelegateEvents();
}
this.child = view;
this.child.setElement(this.$el).render();
return this;
}
});
When I just refresh my page, I get filtered.toArray is not a function error and nothing is rendered respectively. After inspecting everything in the debugger, I found out that when the view gets rendered for the first time, the filtered attribute receives an empty collection, assigns it to data variable, which becomes an empty array and goes to the body of render function, becoming undefined after that. The mysteries go here: whenever I click items, that are bound to my show* events, they act exactly as expected and render either models where interviewed === false, or true or the whole collection. This looks kinda magic to me and I haven't got the faintest idea what can I do with that.
ADDED: GitHub repo with this project
Your home function on the AppRouter has a typo. You have an extra semi-colon.
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection();
}));
}
Should be
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
I needed to remove it to get the JSFiddle working: https://jsfiddle.net/4gyne5ev/1/
I'd recommend adding some kind of linting tool into your IDE or Build process (http://eslint.org/)
You need to add home url content to your db.json file like this
"" : [
{
'somthing': 'somthing'
}
]
After a piece of advice from my mentor I realized that the core of the problem was in asynchronous origin of fetch method -- as I passed this.collection.fetch in my initialize function, it executed after my render method, not before it, so my render method had just nothing to render when the view was called for the first time. So, this fix worked:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection();
// removed .fetch() method from here
},
render: function (filtered) {
var self = this;
var data;
// and added it here:
this.collection.fetch({
success: function (collection) {
if (!filtered) {
data = collection.toArray();
} else {
data = filtered.toArray();
}
self.$el.html(self.template(collection.toJSON()));
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
})
}
});
}
});
And this worked perfectly and exactly as I needed.

Failed to load routed module requirejs? durandal bug?

I created an Asp.Net MVC and used nuget to add HotTowel (V2.0.1 of 9/11/2013). I created a couple of ViewModel, Models. However, I got the following error.
"Failed to load routed module (viewmodels/myVM). Details: Load timeout for modules: durandal/plugins/router\nhttp://requirejs.org/docs/errors.html#timeout"
Is it the problem of durandal/plugins/router? Or it can be caused by some code I added?
The error occurred at Scripts/durandal/system.js.
var logError = function(error) {
if(error instanceof Error){
throw error;
}
throw new Error(error);
};
The following is the VM code.
define(['services/datacontext', 'durandal/plugins/router', 'services/logger'],
// Remove the durandal/plugins/router and the functions will get rid of the error.
function (datacontext, router, logger) {
var title = 'Event';
var vm = {
activate: activate,
deactivate: deactivate,
refresh: refresh,
events: events,
title: title
};
return vm;
//#region Internal Methods
var events = ko.observableArray();
function activate() {
logger.log(title + ' View Activated', null, title, true);
return datacontext.getEventPartials(events);
}
var deactivate = function () {
events([]);
};
var refresh = function () {
return datacontext.getEventPartials(events, true);
};
//#endregion
});
The following is the call stack
logError [system.js] Line 92 Script
Anonymous function [router.js] Line 359 Script
[External Code]
Anonymous function [system.js] Line 260 Script
[External Code]
[Async Call]
....
Code at router.js,
isProcessing(true);
router.activeInstruction(instruction);
if (canReuseCurrentActivation(instruction)) {
ensureActivation(activator.create(), currentActivation, instruction);
} else {
system.acquire(instruction.config.moduleId).then(function(module) {
var instance = system.resolveObject(module);
ensureActivation(activeItem, instance, instruction);
}).fail(function(err){
system.error('Failed to load routed module (' + instruction.config.moduleId + '). Details: ' + err.message);
});
}
}
And previous one in system.js.
acquire: function() {
var modules,
first = arguments[0],
arrayRequest = false;
if(system.isArray(first)){
modules = first;
arrayRequest = true;
}else{
modules = slice.call(arguments, 0);
}
return this.defer(function(dfd) {
require(modules, function() {
var args = arguments;
setTimeout(function() {
if(args.length > 1 || arrayRequest){
dfd.resolve(slice.call(args, 0));
}else{
dfd.resolve(args[0]);
}
}, 1);
}, function(err){
dfd.reject(err);
});
}).promise();
},
Based on the comments I'd recommend to modify the vm code slightly, so that all variables that are returned via vm are defined before use. In addition 'plugins/router' is used instead of 'durandal/plugins/router'.
define(['services/datacontext', 'plugins/router', 'services/logger'],
// Remove the durandal/plugins/router and the functions will get rid of the error.
function (datacontext, router, logger) {
var title = 'Event';
var events = ko.observableArray();
var deactivate = function () {
events([]);
};
var refresh = function () {
return datacontext.getEventPartials(events, true);
};
var vm = {
activate: activate,
deactivate: deactivate,
refresh: refresh,
events: events,
title: title
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return datacontext.getEventPartials(events);
}
//#endregion
});
BTW the name Internals methods is misleading as everything in that region is returned via vm. I prefer to work with named function instead, which get created before the return statement if they are returned and place them below the return statement in a Internal methods region if they are not returned.
define(['services/datacontext', 'plugins/router', 'services/logger'],
function( datacontext, router, logger ) {
var title = 'Event';
var events = ko.observableArray();
function deactivate () {
events([]);
}
function refresh () {
return datacontext.getEventPartials(events, true);
}
function activate () {
logger.log(title + ' View Activated', null, title, true);
return datacontext.getEventPartials(events);
}
return {
activate: activate,
deactivate: deactivate,
refresh: refresh,
events: events,
title: title
};
//#region Internal Methods
//#endregion
});

Custom modal Durandal

I have been trying to work with the durandal framework and must say I'm very pleased. I'm using the Movie tutorial by http://stephenwalther.com/ and have made some changes. With help of the Ryan Keeters Youtube video's. I have accomplished editing movies with modals.
But I can't get it to work to add new movies via a modal.
I have created a add.html and add.js but when the link for creating a new movie is clicked and the modal shows it shows the page i'm at in the middle (like a loop when I click further).
THis is my binding on show.html:
<a data-bind="click: viewAddMovieModal">Add Movie</a>
And this is the javascript:
define(function (require) {
var self = this;
var vm = {
activate: activate,
title: 'movies page',
movies: ko.observableArray([])
};
//return vm;
// self.moviesRepository = require("repositories/moviesRepository");
self.router = require('durandal/plugins/router');
self.system = require('durandal/system');
self.app = require('durandal/app');
self.movie = require('viewmodels/movie');
self.addmovie = require('viewmodels/add');
//self.Movies = ko.observableArray([]);
self.viewMovieModal = function (movie, element) {
self.app.showModal(movie).then(function (result) {
if (result) {
//self.app.showMessage(result.toString(), result.toString());
vm.movies.remove(movie);
}
}).fail(function (result) {
self.app.showMessage(result.toString(), "Something went wrong!");
});
};
self.viewAddMovieModal = function (addmovie,element) {
// self.app.showModal(addmovie).then(function (result) {
// self.app.showMessage(result.toString(), result.toString());
// });
self.app.showModal(null, element, addmovie);
};
init();
function init() {
vm.movies.push(new movie("Star wars", "Piet"));
vm.movies.push(new movie("Harry Potter", "Jan"));
vm.movies.push(new movie("Hangover", "Klaas"));
}
function activate() {
self.system.log("I get in the activate function!");
}
return {
activate: activate,
movie: movie,
addmovie:addmovie,
vm:vm,
viewMovieModal: viewMovieModal
};
});

Backbone - Validation not working on create, only update/edit?

So, I am able to validate just fine when I am editing an existing item. However, if I want to create, validation for some reason is not getting kicked off. Instead, I am seeing the errors below:
//this is if the field I want to validate is empty
Uncaught TypeError: Object #<Object> has no method 'get'
//this is if everything in the form is filled out
Uncaught TypeError: Cannot call method 'trigger' of undefined
Here is(what I think is) the relative portion of my js. Sorry if its an overload, I wanted to add as much as I can to be as specific as possible:
Comic = Backbone.Model.extend({
initialize: function () {
this.bind("error", this.notifyCollectionError);
this.bind("change", this.notifyCollectionChange);
},
idAttribute: "ComicID",
url: function () {
return this.isNew() ? "/comics/create" : "/comics/edit/" + this.get("ComicID");
},
validate: function (atts) {
if ("Name" in atts & !atts.Name) {
return "Name is required";
}
if ("Publisher" in atts & !atts.Publisher) {
return "Publisher is required";
}
},
notifyCollectionError: function (model, error) {
this.collection.trigger("itemError", error);
},
notifyCollectionChange: function () {
this.collection.trigger("itemChanged", this);
}
});
Comics = Backbone.Collection.extend({
model: Comic,
url: "/comics/comics"
});
comics = new Comics();
FormView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, "render");
this.template = $("#comicsFormTemplate");
},
events: {
"change input": "updateModel",
"submit #comicsForm": "save"
},
save: function () {
this.model.save(
this.model.attributes,
{
success: function (model, response) {
model.collection.trigger("itemSaved", model);
},
error: function (model, response) {
model.trigger("itemError", "There was a problem saving " + model.get("Name"));
}
}
);
return false;
},
updateModel: function (evt) {
var field = $(evt.currentTarget);
var data = {};
var key = field.attr('ID');
var val = field.val();
data[key] = val;
if (!this.model.set(data)) {
//reset the form field
field.val(this.model.get(key));
}
},
render: function () {
var html = this.template.tmpl(this.model.toJSON());
$(this.el).html(html);
$(".datepicker").datepicker();
return this;
}
});
NotifierView = Backbone.View.extend({
initialize: function () {
this.template = $("#notifierTemplate");
this.className = "success";
this.message = "Success";
_.bindAll(this, "render", "notifySave", "notifyError");
comics.bind("itemSaved", this.notifySave);
comics.bind("itemError", this.notifyError);
},
events: {
"click": "goAway"
},
goAway: function () {
$(this.el).delay(0).fadeOut();
},
notifySave: function (model) {
this.message = model.get("Name") + " saved";
this.render();
},
notifyError: function (message) {
this.message = message;
this.className = "error";
this.render();
},
render: function () {
var html = this.template.tmpl({ message: this.message, className: this.className });
$(this.el).html(html);
return this;
}
});
var ComicsAdmin = Backbone.Router.extend({
initialize: function () {
listView = new ListView({ collection: comics, el: "#comic-list" });
formView = new FormView({ el: "#comic-form" });
notifierView = new NotifierView({el: "#notifications" });
},
routes: {
"": "index",
"edit/:id": "edit",
"create": "create"
},
index: function () {
listView.render();
},
edit: function (id) {
listView.render();
$(notifierView.el).empty();
$(formView.el).empty();
var model = comics.get(id);
formView.model = model;
formView.render();
},
create: function () {
var model = new Comic();
listView.render();
$(notifierView.el).empty();
$(formView.el).empty();
formView.model = model;
formView.render();
}
});
jQuery(function () {
comics.fetch({
success: function () {
window.app = new ComicsAdmin();
Backbone.history.start();
},
error: function () {
}
});
})
So, shouldnt my create be getting validated too? Why isnt it?
When creating a new instance of a model, the validate method isn't called. According to the backbone documentation the validation is only called before set or save.
I am also struggling with this problem and found solutions in related questions:
You could make a new model and then set its attributes (see question 9709968)
A more elegant way is calling the validate method when initializing the model (see question 7923074)
I'm not completely satisfied with these solutions because creating a new instance of the model like described in the backbone documentation shouldn't happen when an error is triggered. Unfortunately, in both solutions you're still stuck with a new instance of the model.
edit: Being stuck with a new instance of the model is actually quite nice. This way you can give the user feedback about why it didn't pass the validator and give the opportunity to correct his/her input.
OK. So, I'm having some mild success here.
First, I wrote my own validation framework, Backbone.Validator since I didn't like any of the ones out there that I found.
Second, I am able to get the validation framework to set off the validation routine by setting silent: false with in the object provided during the new Model creation.
Along with using the use_defaults parameter from my validation framework I am able to override bad data during setup in initial testing. I'm still working on doing some more tests on this, but it seems to be going OK from from the Chrome browser console.

Backbonejs when to initialize collections

I'm building small one page application with rails 3.1 mongodb and backbonejs.
I have two resources available through json api. I created two models and collections in backbone which look like this
https://gist.github.com/1522131
also I have two seprate routers
projects router - https://gist.github.com/1522134
notes router - https://gist.github.com/1522137
I generated them with backbonejs-rails gem from github so code inside is just template. I initialize my basic router inside index.haml file
#projects
:javascript
$(function() {
window.router = new JsonApi.Routers.ProjectsRouter({projects: #{#projects.to_json.html_safe}});
new JsonApi.Routers.NotesRouter();
Backbone.history.start();
});
I don't want fetch notes when application is starting, because there is big chance that user will never look inside notes. So there isn't good reason to fetch it on start. Inside NotesRouter in all action I rely on #notes variable but without .fetch() method this variable is empty. Also I should can reproduce notes view from url like
/1/notes/5
project_id = 1
note_id = 5
What is best practices in backbonejs to solve this kind of problem ?
Why don't you lazy load the notes when it's requested? Here's an example:
var State = Backbone.Model.extend({
defaults: {
ready: false,
error: null
}
});
var Note = Backbone.Model.extend({
initialize: function () {
this.state = new State();
}
});
var Notes = Backbone.Collection.extend({
model: Note,
initialize: function () {
this.state = new State();
}
});
var NoteCache = Backbone.Model.extend({
initialize: function () {
this._loading = false;
this._loaded = false;
this._list = new Notes();
},
_createDeferred: function (id) {
var note = new Note({ id: id });
this._list.add(note);
this._load();
return note;
},
getNote: function (id) {
return this._list.get(id) || this._createDeferred(id);
},
getNotes: function () {
if (!this._loaded)
this._load();
return this._list;
},
_load: function () {
var that = this;
if (!this._loading) {
this._list.state.set({ ready: false, error: null });
this._loading = true;
$.ajax({
url: '/api/notes',
dataType: 'json',
cache: false,
type: 'GET',
success: function (response, textStatus, jqXHR) {
_.each(response.notes, function (note) {
var n = that._list.get(note.id);
if (n) {
n.set(note);
} else {
that._list.add(note, { silent: true });
n = that._list.get(note.id);
}
n.state.set({ ready: true, error: null });
});
that._list.state.set({ ready: true, error: null });
that._list.trigger('reset', that._list);
that._loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
that._list.state.set({ error: 'Error retrieving notes.' });
that._list.each(function (note) {
note.state.set({ error: 'Error retrieving note.' });
});
},
complete: function (jqXHR, textStatus) {
that._loading = false;
}
});
}
}
});
In this example, I'm defining a NoteCache object that manages the lazy loading. I also add a "state" property to the Note model and Notes collection.
You'll probably want to initialize NoteCache somewhere (probably inside your route) and whenever you want a note or notes, just do this:
var note = noteCache.getNote(5);
var notes = noteCache.getNotes();
Now inside your view, you'll want to listen for state changes in case the note/notes is not loaded yet:
var NoteView = Backbone.View.extend({
initialize: function(){
this.note.state.bind('change', this.render, this);
},
render: function(){
if (this.note.state.get('error') {
// todo: show error message
return this;
}
if (!this.note.state.get('ready') {
// todo: show loader animation
return this;
}
// todo: render view
return this;
}
});
I haven't tested this, so there may be some bugs, but I hope you get the idea.

Categories

Resources