durandaljs child router deactivate event - durandal

How to fire deactivate event when parent route changing.
For instance in 'HTML Samples', when Master-Detail page is active, change view to another one.
How to force dialog "Do you want to leave...." here?
Thanks
Vladimir.
UPD:
Code from HTML Samples with replaced dialogs
project.js
define(['durandal/system', 'durandal/app'], function (system, app) {
var ctor = function (name, description) {
this.name = name;
this.description = description;
};
ctor.prototype.canActivate = function () {
return true; //!!! CHANGED!!!
};
ctor.prototype.activate = function () {
system.log('Model Activating', this);
};
ctor.prototype.canDeactivate = function () {
return false; //!!! CHANGED!!!
};
ctor.prototype.deactivate = function () {
system.log('Model Deactivating', this);
};
return ctor;
});
Now you can not change detail view using select control.
But easily can change whole module via navigation panel.

What you're looking for is the canDeactivate attribute. You can use the dialog plugin to display a dialog. If your canDeactivate function returns true, the view will deactivate. If it returns false, the view will remain.
Update
There is a bug in the router, that is a wontfix, where a child viewModel is not properly deactivated when the parent viewModel is deactivated / navigated away from. See here: https://github.com/BlueSpire/Durandal/issues/570
See
http://durandaljs.com/documentation/Using-Activators/
http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks/

This is a super late answering. I am maintaining an existing durandal app. The quick fix of mine is to manually call deactivate of child viewmodel in the deactivate of the parent. However, the child deactivate may be called multiple times.
Shell.prototype.deactivate = function(){
this.router.activeItem().deactivate();
};

Related

Testing a ReactiveVar in Meteor

I am trying to test a event in a Template. Within the click event, there is a Reactive Var that gets set.
The test is blocked by the Reactive Var.
Any ideas on how I stub or mock a reactive var in a template? I tried adding it to the template (template.reactiveVar = "whatever"), but no go, as I don't think it's writeable.
The ?? indicate where I am wondering what to put.
it('sets a reactive var when a button is clicked', function(){
Template.button.fireEvent('click ', {
context: { some: 'values'},
event: {'target': {'text': 'randomText' } },
templateInstance:new Template('upload', function(e) {
this.reactiveVar = new ReactiveVar("not clicked)
})
});
chai.assert.equal(Template.reactiveVar, "clicked")});
// The actual template is below..
Template.button.events{(
'click button': function(evt, template){
var text = evt.target.text
template.reactiveVar.set(text)
}
});
The result is...
TypeError: Cannot read property 'set' of undefined
Any ideas?
I suspect the event is actually firing and getting through to your event handler before the callback that creates the reactive var has run.
You should create the reactive var when you first create the template independent of your test:
Template.button.onCreated(function(){
this.reactiveVar = new ReactiveVar("not clicked");
});

Durandal Composition Binding with canDeactivate

I am using Durandal 2.1, and I am having a problem with view composition. I have a view for managing many types of items. I also want a view to manage a subset of those types. So I created a manage view and a managesubset view. The managesubset view just composes the manage view and passes it an array containing the subset of items. This way the user can go to /100/manage or 100/managesubset where managesubset will only allow the user to manage a subset of items. I am using this pattern because I will have multiple different versions of managesubset.
My problem is that the canDeactivate method is not fired when going to managesubset. Is there anyway to fire the canDeactivate and Deactivate lifecycle events when composing?
According to #3 under Activator Lifecycle Callbacks here, I should be able to do this, but I cannot find any good examples.
Code:
manage.js
define(['durandal/app', 'plugins/router'], function (app, router) {
var constructor = function () {
var self = this;
//...variable creation and assignment
//life cycle events
self.activate = function (viewmodel) {
self.recordId(viewmodel.recordId);
self.assignableTypes(viewmodel.assignableTypes);
self.pageHeaderTitle = viewmodel.pageHeaderTitle;
self.pageHeaderIcon = viewmodel.pageHeaderIcon;
};
self.canActivate = function (id) {
var deferred = $.Deferred();
//check if user has access to manage equipment
};
self.canDeactivate = function () {
if (!self.saveSuccessfull() && this.isDirty()) {
return app.showMessage("You have unsaved changes, are you sure you want to leave?", "Unsaved Changes", ["Yes", "No"]);
}
else {
return true;
}
}
};
return constructor;
});
managesubset.js
define([], function () {
var recordId = ko.observable();
var manageRecord = ko.observable();
return {
recordId: recordId,
manageRecord: manageRecord,
activate: function (id) {
recordId(id);
manageRecord({
pageHeaderTitle: 'Manage Subset',
pageHeaderIcon: 'cb-subset',
assignableTypes: [102],
recordId: recordId()
});
},
canActivate: function (id) {
var deferred = $.Deferred();
//check if user has access to manage equipment
}
}
});
managesubset.html
<div data-bind="compose: { model: 'manage', activationData: manageRecord() }"></div>
The activate is called correctly each time. The deactivate and canDeactive are what don't work, and they are never called.

durandal, pass parameters to widget during navigation

i have several singleton views in my SPA, each of these view contain the same widget.
When the view is activated i take some parameters from the activate callback and pass it to the widget and it works fine.
But if i navigate the second time into the view (with different parameters into the activate callback)
the activate method of the widgets is rightly not raised.
How can i pass the fresh data to the widgets ?
I tried to make the parameter observable and subscribe it into the widget (settings.params.subscribe) and it works, but i don't think it's a good solution.
This should be pretty simple assuming you are returning a constructor from your widget -
View model -
var thisWidget = new widget(someArbitraryData)
function createWidget() {
dialog.show(thisWidget);
}
// later
function updateWidget() {
thisWidget.refreshData(newArbitraryData);
}
Widget module -
define([], function () {
var ctor = function () {
var self = this;
self.data = ko.observable();
};
ctor.prototype.refreshData = function (newData) {
var self = this;
self.data(newData);
};
ctor.prototype.activate = function (activationData) {
var self = this;
self.data(activationData);
};
});

TableView 'click' listener being ignored

I am dynamically building a TableView in my controller, which works fine: The initial table displays the initial collections data as expected.
The problem is that the TableView's 'click' event listener is ignored when I click on the table rows. I am testing in the browser, and I never even see the console event file (see comments in controller file). All relevant code snippets below:
In my alloy.js I setup a backbone collection:
function defaultTodo(name) { return {name: name, done: false}; }
function doneTodo(name) { return {name: name, done: true}; }
Alloy.Collections.todos = new Backbone.Collection();
Alloy.Collections.todos.reset([
defaultTodo('Apples'), // create not yet done todo
defaultTodo('Banana'),
defaultTodo('Paper Towels'),
defaultTodo('Broccoli'),
doneTodo('Beans'), // create already done todo
doneTodo('Water'),
doneTodo('Blueberries'),
doneTodo('Stir Fry')
])
Here is my index.js controller:
var todos = Alloy.Collections.todos;
function redrawTable() {
// clear all the old data
// See http://developer.appcelerator.com/question/49241/delete-all-rows-in-a-tableview-with-a-single-click
$.table.setData([]);
// Create and add the TableViewSections
var alreadyDone = Ti.UI.createTableViewSection({ headerTitle: "Already Done" });
var needsDoing = Ti.UI.createTableViewSection({ headerTitle: "Needs Doing" });
$.table.appendSection(needsDoing);
$.table.appendSection(alreadyDone);
// Add the todo to the appropriate sections
todos.forEach(function(todo) {
var section = todo.get('done') ? alreadyDone : needsDoing;
addEntry(todo, section);
});
// Add the click listener
// THIS LISTENER IS IGNORED ********************************
$.table.addEventListener('click', function(e) {
console.log(e);
todos.at(e.index).set('done', true);
todos.trigger('change');
});
// Helper function to add a row to a section
function addEntry(todo, section) {
var row = Ti.UI.createTableViewRow({
title: todo.get('name'),
className: "row"
});
section.add(row);
}
}
// Redraw our table each time our todos collection changes
todos.on('change', redrawTable);
// Trigger a change event to draw the initial table
todos.trigger('change');
$.index.open();
And here is index.xml view file:
<Alloy>
<Window class="container">
<Label id="test" class="header">My Grocery List</Label>
<TextField id="newItem"/>
<TableView id="table">
</TableView>
</Window>
</Alloy>
UPDATE: Working Code
In addition to the changes below, I also added onClick="markDone" to the xml.
function markDone(e) {
console.log(e.row.todo);
e.row.todo.set('done', true);
todos.trigger('change');
}
function redrawTable() {
// clear all the old data
// See http://developer.appcelerator.com/question/49241/delete-all-rows-in-a-tableview-with-a-single-click
$.table.setData([]);
var rows = [];
var done = [];
var doing = [];
// Add the todo to the appropriate sections
todos.forEach(function(todo) {
var row = Ti.UI.createTableViewRow({
title: todo.get('name'),
className: "row"
});
row.todo = todo;
todo.get('done') ? done.push(row) : doing.push(row);
});
// Create and add the TableViewSections
rows.push(Ti.UI.createTableViewSection({ headerTitle: "Needs Doing" }));
rows = rows.concat(doing);
rows.push(Ti.UI.createTableViewSection({ headerTitle: "Already Done" }));
rows = rows.concat(done);
$.table.setData(rows);
};
I created brand new project using files which you provided and eventListener is working perfectly fine. However there are couple other bugs:
Creating listener inside redrawTable() function, which is executed every time you click on something in TableView. As a result at the beginning you have one eventListener but after every click all listeners are duplicated.
Using index property in event handler to find index of Backbone model object to update. index property is indicating at which place given row was displayed in your TableView. When you are moving rows between sections their index are changing. In your case it's better to check e.row.name property and use Backbone.Collection.findWhere() method. If user can have two items with the same name on the list, then you have to create additional property to determine which object in model should be changed.
You should add rows to section before section are added to table. In your case table is very simple so instead of doing loops you can just create one simple array of objects (with title and optional header properties) and pass it to $.table.setData().
It's good to wait for postlayout event triggered on main view before triggering any custom events to make sure that the whole view was created and all objects are initiated.
Check rewrote index.js code below to see how it could be done.
var todos = Alloy.Collections.todos;
function redrawTable() {
var done = todos.where({done: true}).map(function(elem) {
return { title: elem.get('name') };
});
if (done.length > 0) {
done[0].header = 'Already Done';
}
var notdone = todos.where({done: false}).map(function(elem) {
return { title: elem.get('name') };
});
if (notdone.length > 0) {
notdone[0].header = 'Needs Doing';
}
$.table.setData( done.concat(notdone) );
}
$.table.addEventListener('click', function(e) {
todos.where({ name: e.row.title }).forEach(function(elem){
elem.set('done', !elem.get('done'));
});
todos.trigger('change'); // Redraw table
});
// Redraw our table each time our todos collection changes
todos.on('change', redrawTable);
// Trigger a change event to draw the initial table
$.index.addEventListener('postlayout', function init(){
todos.trigger('change');
// Remove eventListener, we don't need it any more.
$.index.removeEventListener('postlayout', init);
})
$.index.open();
redrawTable() could have a little more refactor but I left it so it's easier to read.
To read more about manipulating TableView check this Appcelerator documentation page.

Durandal, get path of the current module

Is there a way in Durandal to get the path of the current module? I'm building a dashboard inside of a SPA and would like to organize my widgets in the same way that durandal does with "FolderWidgetName" and the folder would contain a controller.js and view.html file. I tried using the getView() method in my controller.js file but could never get it to look in the current folder for the view.
getView(){
return "view"; // looks in the "App" folder
return "./view"; // looks in the "App/durandal" folder
return "/view"; // looks in the root of the website
return "dashboard/widgets/htmlviewer/view" //don't want to hard code the path
}
I don't want to hardcode the path inside of the controller
I don't want to override the viewlocator because the rest of the app still functions as a regular durandal spa that uses standard conventions.
You could use define(['module'], function(module) { ... in order to get a hold on the current module. getView() would than allow you to set a specific view or, like in the example below, dynamically switch between multiple views.
define(['module'], function(module) {
var roles = ['default', 'role1', 'role2'];
var role = ko.observable('default');
var modulePath = module.id.substr(0, module.id.lastIndexOf('/') +1);
var getView = ko.computed(function(){
var roleViewMap = {
'default': modulePath + 'index.html',
role1: modulePath + 'role1.html',
role2: modulePath + 'role2.html'
};
this.role = (role() || 'default');
return roleViewMap[this.role];
});
return {
showCodeUrl: true,
roles: roles,
role: role,
getView: getView,
propertyOne: 'This is a databound property from the root context.',
propertyTwo: 'This property demonstrates that binding contexts flow through composed views.',
moduleJSON: ko.toJSON(module)
};
});
Here's a live example http://dfiddle.github.io/dFiddle-1.2/#/view-composition/getView
You can simply bind your setup view to router.activeRoute.name or .url and that should do what you are looking for. If you are trying to write back to the setup viewmodels property when loading you can do that like below.
If you are using the revealing module you need to define the functions and create a module definition list and return it. Example :
define(['durandal/plugins/router', 'view models/setup'],
function(router, setup) {
var myObservable = ko.observable();
function activate() {
setup.currentViewName = router.activeRoute.name;
return refreshData();
}
var refreshData = function () {
myDataService.getSomeData(myObservable);
};
var viewModel = {
activate: activate,
deactivate: deactivate,
canDeactivate: canDeactivate
};
return viewModel;
});
You can also reveal literals, observables and even functions directly while revealing them -
title: ko.observable(true),
desc: "hey!",
canDeactivate: function() { if (title) ? true : false,
Check out durandal's router page for more info on what is available. Also, heads up Durandal 2.0 is switching up the router.
http://durandaljs.com/documentation/Router/
Add an activate function to your viewmodel as follows:
define([],
function() {
var vm = {
//#region Initialization
activate: activate,
//#endregion
};
return vm;
//#region Internal methods
function activate(context) {
var moduleId = context.routeInfo.moduleId;
var hash = context.routeInfo.hash;
}
//#endregion
});