HotTowel Durandal Inject different views based on the user - javascript

In the shell.html for HotTowel template we have:
<!--ko compose: {model: router.activeItem,
afterCompose: router.afterCompose,
transition: 'entrance'} -->
<!--/ko-->
which will automatically insert the proper view by convention. I am trying to inject different views based on the user's role in a HotTowel/Durandal App. For example,
I have two Views,
productEditor_Admin.html
productEditor_Superviser.html
(instead of these two views, I used to have only productEditor.html, by convention everything worked)
and only a single ViewModel:
productEditor.js
Now, I want to have a function in productEditor.js that will let me decide which view to insert based on user's role. I see in the Composition documentation, we can do function strategy(settings) : promise but I am not sure what's the best way to accomplish this in the HotTowel template. Anyone have already tried and got an answer for that?

It's possible to return a 'viewUrl' property in the view model, so hopefully something like the following will crack the door open ;-).
define(function () {
viewUrl = function () {
var role = 'role2'; //Hardcoded for demo
var roleViewMap = {
'default': 'samples/viewComposition/dFiddle/index.html',
role1: 'samples/viewComposition/dFiddle/role1.html',
role2: 'samples/viewComposition/dFiddle/role2.html'
};
return roleViewMap[role];
}
return {
viewUrl: viewUrl(),
propertyOne: 'This is a databound property from the root context.',
propertyTwo: 'This property demonstrates that binding contexts flow through composed views.'
};
});

Did you take a look at John Papa's JumpStart course on PluralSight.
Look at the source code from that app here: https://github.com/johnpapa/PluralsightSpaJumpStartFinal
In App/Config.js file he adds other routes which are visible by default as :
var routes = [{
url: 'sessions',
moduleId: 'viewmodels/sessions',
name: 'Sessions',
visible: true,
caption: 'Sessions',
settings: { caption: '<i class="icon-book"></i> Sessions' }
}, {
url: 'speakers',
moduleId: 'viewmodels/speakers',
name: 'Speakers',
caption: 'Speakers',
visible: true,
settings: { caption: '<i class="icon-user"></i> Speakers' }
}, {
url: 'sessiondetail/:id',
moduleId: 'viewmodels/sessiondetail',
name: 'Edit Session',
caption: 'Edit Session',
visible: false
}, {
url: 'sessionadd',
moduleId: 'viewmodels/sessionadd',
name: 'Add Session',
visible: false,
caption: 'Add Session',
settings: { admin: true, caption: '<i class="icon-plus"></i> Add Session' }
}];
You can add routes to both the views here using the same logic and then in your productEditor.js you can decide which view to navigate and navigate to that using router.navigateTo() method.

Related

Binding ResourceBundle property to list item

In SAPUI5, you can often bind resource bundle properties to items in several ways. I'm attempting to do it in JavaScript, using data provided by an Odata service, but the methods I've found here so far haven't worked. I've tried two different ways:
How to Use Internalization i18n in a Controller in SAPUI5?
binding issue with sap.m.List and model configured in manifest.json
And neither of these have worked. I feel this is because I'm inside a items list, inside of a dialog that's causing my issue:
My code is:
var resourceBundle = this.getView().getModel("i18n");
if (!this.resizableDialog) {
this.resizableDialog = new Dialog({
title: 'Title',
contentWidth: "550px",
contentHeight: "300px",
resizable: true,
content: new List({
items: {
path: path,
template: new StandardListItem({
title: resourceBundle.getProperty("{Label}"),//"{ path : 'Label', fomatter : '.getI18nValue' }",
description: "{Value}"
})
}
}),
beginButton: new Button({
text: 'Close',
press: function () {
this.resizableDialog.close();
}.bind(this)
})
});
getI18nValue : function(sKey) {
return this.getView().getModel("i18n").getProperty(sKey);
},
Using the 2nd method above, never calls the JavaScript method. I didn't think it would work, as it's more JSON based. The first method, loads the data fine, but instead of showing the resource bundle text, it shows just the text found inside of the {Label} value.
The value found inside of {Label} matches the key found inside of the resouce bundle i.e. without the i18n> in front, like you would have in a view.
Anyone have any suggestions?
Use of formatter will solve your problem, but the way you're doing it is wrong. Try this, it will solve your problem.
var resourceBundle = this.getView().getModel("i18n");
if (!this.resizableDialog) {
this.resizableDialog = new Dialog({
title: 'Title',
contentWidth: "550px",
contentHeight: "300px",
resizable: true,
content: new List({
items: {
path: path,
template: new StandardListItem({
title: {
parts: [{ path: "Label" }],
formatter: this.getI18nValue.bind(this)
},
description: "{Value}"
})
}
}),
beginButton: new Button({
text: 'Close',
press: function () {
this.resizableDialog.close();
}.bind(this)
})
});
}
And the formatter function getI18nValue will be like this,
getI18nValue: function(sKey) {
return this.getView().getModel("i18n").getResourceBundle().getText(sKey);
},

Best Practice: Handle read-only data with Redux

I am building an application with React and Redux and I have a question about design.
My application uses React Router. It has a Navigation Bar (on the left) that displays the routes from React Router configuration file with a Material Design Menu.
I would know what is the best practice to handle the static data of this LeftNav component.
This data has the following shape:
const menuItems = [
{
icon: 'home',
label: 'Home',
url: '/home',
withDivider: true,
access: 'all',
},
{
icon: 'settings',
label: 'Settings',
url: '/settings',
access: 'admin',
},
{
icon: 'power_settings_new',
label: 'Log Out',
url: '/logout',
access: 'user',
},
];
To respect the smart and dumb component proposal, my LeftNav component is dumb and stateless.
I also have a smart component (right now it's just my AppContainer) that renders the LeftNav component and provides the menuItems array to it via props.
I wonder if I must include this read-only data into my redux state tree. In this case, I would have a constant reducer like this:
export default handleActions({}, [
{
icon: 'home',
label: 'Home',
url: '/home',
withDivider: true,
access: 'all',
},
{
icon: 'settings',
label: 'Settings',
url: '/settings',
access: 'admin',
},
{
icon: 'power_settings_new',
label: 'Log Out',
url: '/logout',
access: 'user',
},
]);
Is it a good practice to have a constant reducer with no action handler? If not, what should I do?
Thanks in advance.
I'm not recognizing how your reducer is working. What is handleActions?
I think a constant reducer seems like a reasonable solution, but I would implement it by using a default first parameter. Assuming you're using combineReducers to scope all your reducers to the part of the state they control, I'd do something like:
/* reducers.js */
const menuItemsInitialState = [
{
icon: 'home',
label: 'Home',
url: '/home',
withDivider: true,
access: 'all',
},
{
icon: 'settings',
label: 'Settings',
url: '/settings',
access: 'admin',
},
{
icon: 'power_settings_new',
label: 'Log Out',
url: '/logout',
access: 'user',
},
];
export function menuItems(state = menuItemsInitialState, _) {
return state;
}
Personally I prefer my reducers to focus on dealing with application / system-wide state. The configuration in your example would feel more at home living as a constant inside the component which makes use of it (LeftNav?)
#Cnode I agree #acjay that it should be in reducer state, even without action creators.
I would go one step further and suggest that you should fetch the data after the app loads rather than including the data in your built files. Generally data does not belong in built files.
This is because, in the real world this particular set (and others - possibly much larger sets) would be statically available on an API as you probably would want the values configurable by a CMS.
Add a static reducer to host this state
Fetch the data after the app initialises and then push it on state - meaning you will have at least one action creator.

Durandal 2.0 "routes"

I'm a Beginner trying to learn SPA / Durandal, Knockout etc...
Need some help with my routes for a Admin drop down button.
Here are my routes in the shell.js so far:
var routes = [
{ route: '', moduleId: 'home', title: 'Home', nav: 1 },
{ route: 'downtime', moduleId: 'downtime', title: 'Downtime', nav: 2 },
{ route: 'downtimeadd', moduleId: 'downtimeadd', title: 'Add A New Downtime', nav: false, settings: { admin: true } },
{ route: 'production', moduleId: 'production', title: 'Production', nav: 4 }];
I've also created adminRoutes in the shell.js to bind to the view with KO:
var adminRoutes = ko.computed(function () {
return router.routes.filter(function (r) {
return r.settings.admin;
});
});
From research, they say that router.routes is an array, but when I bind this to my view the button shows 0 items for the drop down.
When I do (below) I can get all the routes, but I only need the admin routes...
var adminRoutes = ko.computed(function () {
return router.routes;
});
How should I proceed? It seems like router.routes is not actually an array?
If I try to print it out to the console:
console.log(router.routes[0]); //Chrome says its undefined...
console.log(router.routes); //Shows array of size 4...
Yup no clue... Help would be appreciated!
Update:-------------------------------------------------------------------
Even after RainerAtSpirit's suggestions I still get an empty array when I filter in code.
var router = require('plugins/router');
//Array size 4
console.log(router.routes);
//Array size 0
console.log(router.routes.filter(function (r) { return r; }));
However when I run this in the "chrome console":
var router = require('plugins/router')
router.routes.filter(function (r) { return r; })
I do get the array back, so I don't know why in code it doesn't work.
I got this to work, although maybe not as perfectly as I would like.
The first problem I noticed was an issue that others were not seeing. In my example (with druandal 2.0 from the HotTowel template version 1.1 for VS 2013) I noticed that this code for adding the adminRoutes was called prior to the activate method. Therefore, my route.routes was an empty array. To fix this problem, I switched to just using the array of routes that the routes are getting mapped from the config.js with (note: config.routes rather than route.routes):
var adminRoutes = ko.computed(function () {
return config.routes.filter(function(r) {
return r.admin;
});
});
Finally, I'm return "r.admin". This is the part that would like to make work better because I added an admin property to the admin route without the "settings" group, see "admin: true":
var routes = [
{ route: '', moduleId: 'equipment', title: 'Equipment', nav: 1 },
{ route: 'testresults', moduleId: 'testresults', title: 'Test Results', nav: 2 },
{ route: 'testresultdetail/:id', moduleId: 'testresultdetail', title: 'View a test result', nav: false },
{ route: 'testresultadd', moduleId: 'testresultadd', title: 'Add a Test Result', nav: false, caption: '<i class="fa fa-plus"></i> Add Test Result', admin: true }
];
With these two changes, the menu item showed up.
In knockout when you make a property to be observable it turns in to a function wrapping ,so when you need to get the actual value you need to access it as a function. That means
router.routes[0] will be undefined but router.routes()[0] will work correct !
You can try
var adminRoutes = ko.computed(function () {
return router.routes().filter(function (r) {
return r.settings.admin;
});
});

Where does Durandal router set page title

I am using Durandal for a very simple website. In all of my browser tabs the page title is coming up with "undefined|" appended to the front of the application's title. Where is Durandal getting and setting that value?
pthalacker
Ultimately Durandal's router plugin is setting the document.title.
https://github.com/dFiddle/dFiddle-1.2/blob/gh-pages/App/durandal/plugins/router.js#L254
onNavigationComplete: function (routeInfo, params, module) {
if (app.title) {
document.title = routeInfo.caption + " | " + app.title;
} else {
document.title = routeInfo.caption;
}
},...
Typically Durandal is able to construct a missing caption propterty on the route object, so maybe there's something different in the way the routes are set up.
https://github.com/dFiddle/dFiddle-1.2/blob/gh-pages/App/samples/shell.js#L6
router.map([
{ url: 'hello', moduleId: 'samples/hello/index', name: 'Hello World', visible: true },
{ url: 'hello/:name', moduleId: 'samples/hello/index', name: 'Examples' },...
]);
You can set the page title when the viewmodel gets activated:
activate: function (params) {
// Setting page title
params.routeInfo.caption = "My Page Title";
return true;
}
Replacing page titles in Durandal 2.1.0
(if you want to the page title to be something distinct from the last level of the route)
Slight modification to RainerAtSpirit's answer: Specify 'title' in the route instead of 'name'.
router.map([
{ url: 'hello', moduleId: 'samples/hello/index', title: 'Hello World', visible: true },
{ url: 'hello/:name', moduleId: 'samples/hello/index', title: 'Examples' },...
]);

Insert a node at a specific location in Ext.tree.panel

I'm trying to change this ext js treegrid sample:
http://dev.sencha.com/deploy/ext-4.0.1/examples/tree/treegrid.html
All I want is to add a node at a specific location.
I tried this:
var treePanel = Ext.getCmp('treePanel');
var selNode = treePanel.getSelectionModel().getSelection()[0];
var appendedChild = selNode.appendChild({
task: 'Task1',
user: 'Name',
duration: '10',
leaf: true
});
It gives no errors, but the UI doesn't update. Same thing with selNode.insertChild(0, .. It runs with no errors, but the UI doesn't update.
Somehow this works:
var node = treePanel.store.getRootNode();
node.appendChild({
task: 'Task3',
user: 'Name',
duration: '10',
leaf: true
});
So how do I append/insert at a specific location based on user selection.
Here is what my full code looks like: http://jsfiddle.net/4T68s/
I changed the store to static data because a crossdomain request to get the json data wouldn't work.
You should change leaf property to false on node before you add child to it:
selNode.set('leaf', false);
Working sample: http://jsfiddle.net/lolo/4T68s/6/
If you want to append nodes, the new parent must not be a leaf. I've update your code to give you an example of how you add the node at the right location and how you change the icon so it doesn't look like the parent folder icon, which is what I assume you're trying to avoid.
http://jsfiddle.net/4T68s/5/
Setup your store like this so that the parent nodes are expanded and loaded.
var store = Ext.create('Ext.data.TreeStore', {
model: 'Task',
root: {
expanded: true,
children: [
{
task: 'Settings',
leaf: false,
expanded: true,
children: [
{
task: 'System Settings',
expanded: true,
loaded: true,
icon: 'icon-leaf'},
{
task: 'Appearance',
expanded: true,
loaded: true,
}
]}
]
}
});
Then insert your a new model like this.
var selNode = treePanel.getSelectionModel().getSelection()[0];
var newTask = Ext.create('Task');
newTask.set({
task: 'Task1',
user: 'Name',
duration: '10',
expanded: true,
loaded: true,
leaf: false,
icon: 'icon-leaf'
});
selNode.insertChild(0, newTask);
That will create a new parent node which you will then be able to add more tasks to. Use the icon property to set the icon image src or iconCls to do it with CSS. Although there's a bug with the CSS way. It retains the x-tree-icon-parent class for some reason.

Categories

Resources