Custom modal Durandal - javascript

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
};
});

Related

JS works with .include but fails with .extend

I made this JS to add a functionality on a form (backend) that computes a field when the event click is triggered. So far the code recomputes when I use ".include" but the whole JS in all views fail since I'm using ".include". When I try to use extend my code does nothing. Looks like Odoo doesn't add the extended code to the JS engine so my question is, what am I doing wrong here? Is there something else I need to add so my code works as extended?
odoo.define('med_care.TestRenderer', function (require) {
"use strict";
var viewRegistry = require('web.view_registry');
var FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
var TestFormRenderer = FormRenderer.extend({
events: _.extend({}, FormRenderer.prototype.events, {
'click .sign_selector': '_onSignSelectorClicked',
}),
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.fields = state.fields;
this._onSignSelectorClicked = _.debounce(this._onSignSelectorClicked, 300, true);
},
confirmChange: function (state, id, fields, e) {
var self = this;
if (state.model == 'med.test') {
return this._super.apply(this, arguments).then(function () {
self.canBeSaved(self.state.id);
});
}
},
_onSignSelectorClicked: function (event) {
this.state.data.telephone = '333';
if (this.state.model == 'med.test') {
var info_test = {
dataPointID: this.state.id,
changes: {telephone: '333'},
viewType: "form",
notifyChange: true
};
var odoo_event = this.trigger_up('field_changed', info_test);
this.confirmChange(this.state, this.state.id, "telephone",
odoo_event)
}
},
});
var TestFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Renderer: TestFormRenderer,
}),
});
viewRegistry.add('test_form', TestFormView);
return TestFormView;
});

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.

Durandal observable array not updating

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.

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
});

angular-ui/bootstrap: Testing a controller that uses a dialog

I've a controller that uses a Dialog from angular-ui/bootstrap:
function ClientFeatureController($dialog, $scope, ClientFeature, Country, FeatureService) {
//Get list of client features for selected client (that is set in ClientController)
$scope.clientFeatures = ClientFeature.query({clientId: $scope.selected.client.id}, function () {
console.log('getting clientfeatures for clientid: ' + $scope.selected.client.id);
console.log($scope.clientFeatures);
});
//Selected ClientFeature
$scope.selectedClientFeature = {};
/**
* Edit selected clientFeature.
* #param clientFeature
*/
$scope.editClientFeature = function (clientFeature) {
//set selectedClientFeature for data binding
$scope.selectedClientFeature = clientFeature;
var dialogOpts = {
templateUrl: 'partials/clients/dialogs/clientfeature-edit.html',
controller: 'EditClientFeatureController',
resolve: {selectedClientFeature: function () {
return clientFeature;
} }
};
//open dialog box
$dialog.dialog(dialogOpts).open().then(function (result) {
if (result) {
$scope.selectedClientFeature = result;
$scope.selectedClientFeature.$save({clientId: $scope.selectedClientFeature.client.id}, function (data, headers) {
console.log('saved.');
}, null);
}
});
};
});
I'm almost completely new to testing, and figured that maybe I need to test two things:
That a dialog opens when $scope.editClientFeature() is called
That $save is called successfully after a dialog is closed and returns a 'result'
My really messed up test now looks like this:
describe('ClientFeatureController', function () {
var scope, $dialog, provider;
beforeEach(function () {
inject(function ($controller, $httpBackend, $rootScope, _$dialog_) {
scope = $rootScope;
$dialog = _$dialog_;
//mock client
scope.selected = {};
scope.selected.client = {
id: 23805
};
$httpBackend.whenGET('http://localhost:3001/client/' + scope.selected.client.id + '/clientfeatures').respond(mockClientFeatures);
$controller('ClientFeatureController', {$scope: scope});
$httpBackend.flush();
});
});
it('should inject dialog service from angular-ui-bootstrap module', function () {
expect($dialog).toBeDefined();
console.log($dialog); //{}
});
var dialog;
var createDialog = function (opts) {
dialog = $dialog.dialog(opts);
};
describe('when editing a clientfeature', function () {
createDialog({});
console.log(dialog); //undefined
// var res;
// var d;
// beforeEach(function () {
// var dialogOpts = {
// template: '<div>dummy template</div>'
// };
// console.log(dialog);
// d = $dialog.dialog(dialogOpts);
// d.open();
// });
//
// it('should open a dialog when editing a client feature', function () {
// expect(d.isOpen()).toBe(true);
// });
});
});
The immediate problem now is that I'm unable to create/open a dialog. I get the following error:
Chrome 25.0 (Mac) ClientFeatureController when editing a clientfeature encountered a declaration exception FAILED
TypeError: Cannot call method 'dialog' of undefined
It would be great if someone has already written a test for a similar use case and can provide me with an example as I'm pretty lost.
Thanks,
Shaun
I was struggling with the same problem until right now, after trolling the the github repo i found the dialog tests and used that as a starting point :
var $dialog,$scope,$httpBackend;
beforeEach(module('ui.bootstrap.dialog'));
beforeEach(function(){
inject(function (_$dialog_, _$httpBackend_, $controller){
$dialog = _$dialog_;
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/appServer/list')
.respond([{
id:1,
name:'test1'
},
{
id:2,
name:'test2'
},
{
id:3,
name:'test3'
}]);
//setup controller scope
scope = {};
ServerCtrl = $controller('ServerCtrl', {
$scope: scope,
$dialog:$dialog
});
});
});
I also prefer a proper mock. When it is not available, i patch the service
To test this:
$dialog.messageBox(title, msg, btns)
.open()
.then(function (result) {
if (result == 'ok') {
// block executed if user click OK
}
});
You can patch $dialog like this:
$dialog.messageBox = function (title, msg, btns) {
return {
open: function () {
return {
then: function (callback) {
callback('ok'); // 'ok' will be set to param result
}
}
}
}
};
Personally I try to mock all services out. If the ui-bootstrap project does not provide a $dialog mock, you should open a bug ticket there and ask them for one. However creating one is as easy.
The mock service should have fake methods that do nothing but return promises. It should also give you a method to flush all asynchronous methods to make it easier to do synchronous testing.
I find it clearest to write my own mock of the dialog. Here's an example of mocking out a dialog to simulate "yes" being chosen.
Code under test
.controller('AdminListingCtrl', function AdminListingController($scope, $dialog, houseRepository) {
$scope.houses = houseRepository.query();
$scope.remove = function (house) {
var dlg = $dialog.messageBox('Delete house', 'Are you sure?', [
{label: 'Yep', result: 'yes'},
{label: 'Nope', result: 'no'}
]);
dlg.open().then(function (result) {
if (result == 'yes') {
houseRepository.remove(house.id);
$scope.houses = houseRepository.query();
}
});
};
}
Tests
describe('when deleting a house', function () {
var fakeDialog = {
open: function()
{
return {
then: function(callback) {
callback("yes");
}
};
}
};
beforeEach(inject(function($dialog) {
spyOn($dialog, 'messageBox').andReturn(fakeDialog);
}));
it('should call the remove method on the houseRepository', function () {
scope.remove({id: 99});
expect(houseRepository.remove).toHaveBeenCalledWith(99);
});
// etc
});

Categories

Resources