Context
In my shell.html I have put a language selection into the header section. It looks like this:
<select class="form-control select2"
data-bind="options:languages,
optionsText:'label',
optionsValue:'code',
value:selectedLanguage">
</select>
My shell.js:
define(['plugins/router', 'durandal/app', 'knockout'], function (router, app, ko) {
return {
router: router,
selectedLanguage: ko.observable(),
languages: [
{code: 'cs', label: 'čeština'},
{code: 'sk', label: 'slovenský'},
{code: 'en', label: 'english'},
{code: 'de', label: 'deutsch'},
{code: 'ru', label: 'русский'}
],
activate: function () {
return router.map([
{ route: ['', 'dashboard'], moduleId: 'core/dashboard/pages/index', title: 'Dashboard', icon: 'fa fa-dashboard fa-fw', nav: true },
{ route: 'attributes*details', moduleId: 'core/attribute/pages/index', title: 'Attribute', icon: 'fa fa-puzzle-piece fa-fw', nav: true, hash: '#/attributes' }
]).buildNavigationModel()
.mapUnknownRoutes('core/dashboard/pages/index', 'not-found')
.activate();
}
};
});
When I load other views, this always stays in the header and does not change.
Task
Whenever the user changes the language, I want to be able to register/notice this on any sub-page and then react accordingly - like showing the text in another language.
Question
I suppose this is more of a conceptual question. If I change the language, then I am sure that my observable in the shell.js will pick this up. What I do not understand is how to achieve this for my sub-views. Do I need to register some global-subscriber?
It is obviously not enough to just react in the shell.js because that view-model does not know which content needs to be loaded. Only the actual sub-views / modules know this. But in order to react, they need to know that the language has changed.
It would be great if someone could help me on this and give me a hint on how to best deal with these type of features in a durandal/knockout context.
[EDIT] Current Solution
I have tried a few things out now and this is what I have come up with:
main.js: creates an AppViewModel and adds it to the global myapp Object (that all pages require).
define([
'durandal/system',
'durandal/app',
'durandal/viewLocator',
'plugins/router',
'myapp/myapp',
'myapp/appViewModel'
],
function (
system,
app,
viewLocator,
router,
myapp,
AppViewModel
) {
system.debug(true);
// Global view-model which will be required by sub-pages.
myapp.app = new AppViewModel({
defaultLanguage: 'cs'
});
app.title = 'Control Panel';
app.configurePlugins({
router:true,
dialog: true,
widget: true
});
app.start().then(function() {
viewLocator.useConvention();
app.setRoot('shell', 'entrance');
});
});
AppViewModel: handles the language events
//------------------------------------------------------------------
// Global AppViewModel which holds the current language,
// listens for changes and notifies the current sub-page
// if is has registered a listener-function.
//------------------------------------------------------------------
define([ 'jquery', 'underscore', 'knockout' ], function($, _, ko) {
"use strict";
function AppViewModel(options) {
if (!(this instanceof AppViewModel)) {
throw new TypeError("AppViewModel constructor cannot be called as a function.");
}
this.options = options || {};
// Set the initial language.
this.selectedLanguage = ko.observable(options.defaultLanguage);
_.bindAll(this, 'languageChanged', 'onLanguageChange');
}
AppViewModel.prototype = {
constructor: AppViewModel,
//---------------------------------------------------------------
// Calls listener when language changes. See shell.
//---------------------------------------------------------------
languageChanged: function(data, event) {
if(!_.isNull(this.onLanguageChange)
&& !_.isUndefined(this.onLanguageChange)
&& _.isFunction(this.onLanguageChange)) {
this.onLanguageChange(data, event);
}
},
//---------------------------------------------------------------
// Listener that can be overridden by view-models of sub-pages.
//---------------------------------------------------------------
onLanguageChange: function(data, event) {
// Override by ViewModel.
},
//---------------------------------------------------------------
// Clear() should be called in deactivate callback of the
// view-model to stop being notified when the language changes.
//---------------------------------------------------------------
clear: function() {
// Reset to empty function.
this.onLanguageChange = function(data, event) {}
}
}
return AppViewModel;
});
shell.js:
define(['plugins/router', 'durandal/app', 'knockout', 'underscore', 'myapp/myapp'], function (router, app, ko, _, myapp) {
return {
router: router,
languages: [
{code: 'cs', label: 'čeština'},
{code: 'sk', label: 'slovenský'},
{code: 'en', label: 'english'},
{code: 'de', label: 'deutsch'},
{code: 'ru', label: 'русский'}
],
app: myapp.app, // make app available to shell.html for the event handler.
activate: function () {
return router.map([
{ route: ['', 'dashboard'], moduleId: 'core/dashboard/pages/index', title: 'Dashboard', icon: 'fa fa-dashboard fa-fw', nav: true },
{ route: 'attributes*details', moduleId: 'core/attribute/pages/index', title: 'Attribute', icon: 'fa fa-puzzle-piece fa-fw', nav: true, hash: '#/attributes' }
]).buildNavigationModel()
.mapUnknownRoutes('core/dashboard/pages/index', 'not-found')
.activate();
}
};
});
shell.html: languageChanged() is now being called on the global AppViewModel
<select class="form-control select2"
data-bind="event: { change: app.languageChanged }, options:languages,
optionsText:'label',
optionsValue:'code',
value:app.selectedLanguage"></select>
And finally, one of the page view-models (which is now notified when the selecbox in the shell.html changes)
define(['jquery', 'knockout', 'myapp/myapp'], function ($, ko, myapp) {
return {
activate: function(data) {
myapp.app.onLanguageChange = function(data, event) {
// Handle language stuff ...
}
},
deactivate : function() {
// Stop listening for language changes
myapp.app.clear();
}
}
});
Hmm ... I wonder whether anyone reads enough to get to this line. If you do, thanks for your time.
So, is this a feasable way of doing it or is there a more "best practise" durandal way?
[EDIT] Improved solution with durandal publish/subscribe mechanism:
AppViewModel: now just sends message - no need for listener logic.
AppViewModel.prototype = {
constructor : AppViewModel,
// ---------------------------------------------------------------
// Trigger message when language has changed.
// ---------------------------------------------------------------
languageChanged : function(data, event) {
app.trigger('language:change', data);
}
}
index.js: now just receives message - no need to register listener on the AppViewModel.
define(['durandal/app', 'jquery', 'knockout'], function (app, $, ko) {
return {
_langSubscriber: {},
activate: function(data) {
_langSubscriber = app.on('language:change').then(function(language) {
// Handle language stuff ...
});
},
deactivate : function() {
_langSubscriber.off();
}
}
});
As Eric Taylor pointed out, it is important to unsubscribe again. If you don't you will end up with A LOT of subscriptions and most likely a memory leak.
Best regards, Michael