Knockout.js - How to toggle between views - javascript

Alright, newbie Knockout question here.
In this example: http://learn.knockoutjs.com/WebmailExampleStandalone.html#Inbox
How does the mail detail view replace the folder list view?
That is, what feature causes the divs to be toggled? In inspecting the dom, I see that what happens is the div's are actually rendered empty when not displayed.
Can someone enlighten me? I know this is rather basic, but its the last piece I need to click into place for my understanding.
Just to be 100% clear: when you click on a row in the folder list, what causes the folder view to be emptied and the mail detail to display? Is it the with binding? (That doesn't seem right.)

You are on the right track with the with binding: in this example the views are changed using the with binding relaying on this feature of the binding:
The with binding will dynamically add or remove descendant elements depending on whether the associated value is null/undefined or not
So in the viewmodel code you will see something like this:
this.get('#:folder', function () {
self.chosenFolderId(this.params.folder);
self.chosenMailData(null);
$.get("/mail", { folder: this.params.folder }, self.chosenFolderData);
});
this.get('#:folder/:mailId', function () {
self.chosenFolderId(this.params.folder);
self.chosenFolderData(null);
$.get("/mail", { mailId: this.params.mailId }, self.chosenMailData);
});
So the functions which are "chaining" the view nulls out one of the properties while filling in the other which toggles the views defined as:
<!-- Chosen mail -->
<div class="viewMail" data-bind="with: chosenMailData">
...
<div/>
<!-- Mails grid -->
<table class="mails" data-bind="with: chosenFolderData">
</table>
This is not the nicest solution but don't forget that Knockout is a Databind/MVVM library and not a full blown SPA framework so it does not have concepts for layouting and higher level view composition.
However this could be made nicer with using the template binding:
<div id="mainView" data-bind="{template: {name: templateName, data: activeView}}">
</div>
And turning the views into templates:
<script type="text/html" id="ChosenMail">
<div class="viewMail">
...
<div/>
</script>
<script type="text/html" id="MailsGrid">
<table class="mails">
...
</table>
</script>
And in the routing only set the activeView property and lookup the corresponding template name for it:
this.get('#:folder', function () {
$.get("/mail", { folder: this.params.folder }, function(data) {
self.activeView(data);
self.templateName('ChosenMail');
});;
});
this.get('#:folder/:mailId', function () {
$.get("/mail", { mailId: this.params.mailId }, function(data) {
self.activeView(data);
self.templateName('MailsGrid');
});
});
But because this is quite much manual and error prone work I would use something like Durandal.js which is a real SPA framework and it was designed for this kind scenarios.

That is just a Demo on a light weight SPA scenario, with binding is just a inline template binding. Not very useful for a dynamic SPA. Like Nemesv suggests use the template binding.
The problem with the template binding is that its very verbose to use, I have addressed this in my Binding convention library (One of many features)
Instead of doing
<div id="mainView" data-bind="{template: {name: templateName, data: activeView}}">
</div>
You do
<div id="mainView" data-name="activeView">
</div>
My library will do the rest, check out the wiki on templates here
https://github.com/AndersMalmgren/Knockout.BindingConventions/wiki/Template-convention
And a little fiddle
http://jsfiddle.net/xJL7u/11/

Related

Open modal dialog from within ember-table cell

I'm at my wit's end trying to accomplish what should be very straightforward behavior: I have an Ember table component (from Addepar), I would like to have buttons inside that table that trigger a modal dialog.
Since I'm new to Ember, I started with the Ember table starter kit jsbin available here: http://jsbin.com/fasudiki/9/edit
I added a custom cell view so I can use my own template:
columns: function() {
var firstColumn;
firstColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 350,
textAlign: 'text-align-right',
headerCellName: 'Column 1',
tableCellViewClass: 'App.EmberTableMyCustomCell',
getCellContent: function(row) {
return row.get('myRandomValue').toFixed(2);
}
});
return [firstColumn];
}.property(),
with
App.EmberTableMyCustomCell = Ember.Table.TableCell.extend({
templateName: 'custom-table-cell',
classNames: 'custom-table-cell'
});
and
<script type="text/x-handlebars" data-template-name="custom-table-cell">
<span class="ember-table-content">
{{ view.cellContent}}
<button {{action 'openModal' 'modal'}}>This one doesn't</button>
<button {{action 'myCellAction' 'modal'}}>This one doesn't either</button>
</span>
</script>
I then tried following the official Ember guide for modal dialogs: http://emberjs.com/guides/cookbook/user_interface_and_interaction/using_modal_dialogs/
In Ember terminology, I'd like to be able to trigger an action on the Index route from within the ember-table component.
I tried triggering the action directly from the template which didn't work:
<button {{action 'openModal' 'modal'}} >Open modal</button>
I then tried what is suggested in the "Sending Actions From Components To Your Application" guide:
http://emberjs.com/guides/components/sending-actions-from-components-to-your-application/
by creating an 'actions' map on the App.EmberTableMyCustomCell view and then using both
this.send('openModal', 'modal');
and
this.sendAction('openModal', 'modal');
Again, no success.
I then tried what's recommended in this SO question:
Ember component sendAction() not working
by setting the action name in a custom attribute on my ember-table and using it at a parameter for triggerAction(...) using:
<div class="table-container">
{{table-component
hasFooter=false
columnsBinding="columns"
contentBinding="content"
myCustomActionName="openModal"
}}
</div>
and
actions : {
myCellAction : function () {
this.triggerAction('myCustomActionName', 'modal');
}
}
Again, no success.
Any ideas what I'm doing wrong?
I have put the code in jsbin: http://jsbin.com/yovikaviseve/2/edit
I believe that (unfortunately) your action in App.EmberTableMyCustomCell is not being called. I'm not sure if it's the best solution, but I was able to work around the problem by extending Ember.Table.EmberTableComponent and defining my action there. Once the action was called there, I was able to use the method from your linked SO post to propagate the action to the ApplicationController.
I actually sent the primary action instead to make things a bit simpler, as described here: http://emberjs.com/guides/components/sending-actions-from-components-to-your-application/.
Here's the working example: http://emberjs.jsbin.com/yemebu/3/edit.
Thanks for including a JS Bin - made it much easier for me to take a look. Let me know if you have more questions or if this approach doesn't work for you!

Knockout SPA: Different Layouts(Master pages)

I'm building SPA application with knockout.js .
Basically what i do is bind as current page a ko component depending on what is route.
It looks something like
<div id="currentPage" data-bind="component: { name: currentRoute.page,
attr: currentRoute }>
</div>
This is for the current page and the whole picture whit the layout looks like:
<html>
<head></head>
<body>
<div data-bind="component: {name: "nav"}></div>
<div data-bind="component: {name: "aside"}></div>
<div id="currentPage" data-bind="component: { name: currentRoute.page,
attr: currentRoute}">
</div>
</body
</html>
The problem is that i dont have one layout.. the other one looks like this (pseudo used)
<html>
<body>
<wrapper>
<currentPage>
</wrapper>
</body
</html>
So basically the first layout is not direct parent of the currentPage module but the second is..
The variants for dynamic layout changing that i can think of are
Specify the layout components in each page.. But i dont think it is good idea cause i will be writing the same code over and over and will not be able to persist the state in the layout component because of when i change the next page the layout will be recreated( not fully but enough to loose the state )
The layout is separate from the currentPage component so only the currentComponent binding will be changed.. this is perfect for persisting state in layout but not good when i have variant where i want different layout for example which is wrapper around the componentBinding..
I'll be very happy if some one shares fresh ideas how to solve such problems.
In knockout if any part of view is to be dynamically changed you got to have bindings attached to the view. You say that the outer layout of the page is dependent on the inner page being displayed, so logically I consider it part of that page view definition. That is why I would not look for some special way to handle this case.
If I am not missing something, the reasonable thing to do in this case is to just include layout in each page. You could then handle repetitive code by using "traditional" approach using knockout templates (especially Note 5 on dynamic templates from http://knockoutjs.com/documentation/template-binding.html) and code extraction.
Completely other approach you might want to take is to put all layouts in master page and then control them by using bindings like "if" and "visible".

KnockoutJS how to make different ViewModels work toghether?

Suppose i have a main view that contains
A bookable item
A shopping cart
Both elements are in the same page, each of them has its own VM, like this:
<div id='page'>
<div id='item'>
<span data-bind='text: item().name'></span>
<span data-bind='text: item().price'></span> EUR
<!-- What to bind on this click handler? -->
<button>Add</button>
</div>
<hr>
<div id='cart'>
You have 0 items in your cart.
</div>
</div>
Javascript
function ItemVM() {
var self = this;
self.item = ko.observable({id: 1, name:'test', price: 3.99});
}
function CartVM() {
var self = this;
// Adds an item to cart.
self.add = function(item) {
// Business logic here
}
// And so on, other methods here.
self.remove = function(item) {}
self.checkout = function() {}
}
ko.applyBindings(new ItemVM(), document.getElementById('item'));
ko.applyBindings(new CartVM(), document.getElementById('cart'));
I have 2 questions.
1) How to use a click handler, within the 'item' context, that is defined elsewhere? In other words, how to make the button use CartVM.add() as the click handler?
2) Is there something wrong i am doing in reference to KO or MVVM itself?
Fiddle Here
The idea of the MVVM pattern is to have a view bound to a single viewModel. Then you will have data objects described in the model.
In the situtation you have I tend to favour composition. So if I have a view composed of functional elements I tend to compose the viewModel of these separate elements.
I do find this overall is easier. It's likely the container VM will have some elements that are actually part of the individual page. It's probably hard to follow if there are a lot of individual viewModels that are part of the page. The other issue is that cart is coupled to one container div. In the case of cart this may be acceptable. In other cases your functional component may have view elements which may be hard to contain under one div so it becomes hard to segregate viewModels like this.
I've amended your design with:
Note I add a call to add in the itemVM as shown.
function ItemVM() {
this.add = function(data, e) {
viewModel.CartVM.add(data);
};
}
function VM() {
this.ItemVM = new ItemVM();
this.CartVM = new CartVM();
}
var viewModel = new VM();
ko.applyBinding(viewModel);
fiddle to illustrate this here: http://jsfiddle.net/q8uWW/4/
HTH
An example using my binding convention library, it makes it easy to work with multiple view models. https://github.com/AndersMalmgren/Knockout.BindingConventions
To communicate between models you can use a Event Aggregate pattern, I have one in a library called SignalR.EventAggregatorProxy, if you have no use for SignalR you can extract the eventaggregatorn part. https://github.com/AndersMalmgren/SignalR.EventAggregatorProxy/
The idea with my convention library is to use template bindings for each view model. Like
<div id='page'>
<div id='item' data-name="item"></div>
<hr>
<div id='cart' data-name="cart"></div>
</div>
<script id="ItemView" type="text/html">
<span data-name='name'></span>
<span data-name='price'></span> EUR
<button data-name="add">Add</button>
</script>
<script id="CartView" type="text/html">
You have <span data-name="count"></span> items in your cart.
</script>
Fiddle
http://jsfiddle.net/hL5rY/

KO cannot find template with ID

I've used Knockout templates before, so I'm not sure why this isn't working for me.
I tried two different styles of ko markup, neither work.
<!-- more nesting levels -->
<div class="cal-day-tps" data-bind="foreach: timePeriods">
<div class="cal-day-tp-cont">
<div data-bind="template: { name: 'tp-ed-templ', data: $data }"></div>
//both of these methods fail
<!-- ko template: { name: 'tp-ed-templ', data: $data } -->
<!-- /ko -->
</div>
</div>
<!-- /more nesting levels -->
<script type="text/html" id="tp-ed-templ">
<!-- bunch of markup -->
</script>
I just get the error "Cannot find template with ID tp-ed-templ".
Probably just a typo, but I haven't been able to find it.
I'm using KO in the context of Durandal, though this shouldn't make a difference.
Tried declaring the template before usage, didn't help.
Someone else ran into the same thing with no solution either
It seems to be an issue with Durandal, not Knockout.
I tried some extremely simple cases in vanilla durandal setups, and it still does the same thing. Even tried putting the script in the same nested location as the binding, no dice.
The short answer: You can't currently use Knockout templates inside of Durandal.
However, as nemesv pointed out, if you put your named template outside of Durandal, ko is able to find them. For example, anywhere outside of the <div id="applicationHost"></div> element.
The other workarounds are to either use Durandal's compose functionality, or just inline the templates as anonymous.
Knockout templates will probably be supported in the near future.
I finally dug these answers up on the Durandal google group,
Mixing knockout templates with durandal compose
knockout can't find templates inside of views
The issue is that the KO template element must exist in the DOM before the Durandal view is bound. This is because the view is bound before it is inserted into the DOM so any contained templates cannot be resolved by ID.
Using a function that returns an observable can be used to later re-trigger a template binding .. it works, but is wonky. (An if binding could be used for similar effect.)
// bind to this in markup:
// <div data-bind="template: {name: $root.templateName, .. }">
vm.templateName = function () {
return vm.TemplateId();
};
// Changing this will trigger an observable in the KO template binding;
// don't ask me why we have to pass in a function to 'name' ..
vm.TemplateId = ko.observable("dummy-template-id-that-exists");
// After the view is attached the correct template element is in the DOM
// so we can trigger the template to (re-)bind and it will find it.
function viewAttached () {
vm.TemplateId("the-real-template-id");
}

AngularJS bind specific data to template

I'm currently making a switch from Knockout to Angular. The main problem I'm having right now is in transferring my original templates to something Angular will recognise.
Specifically, here's a bit of code I'm having trouble transferring:
<!-- ko template: { name: 'SquareTempl', data: Squares[5] } --><!-- /ko -->
In Knockout, this will attach Squares[5] to SquareTempl, so that when the template gets rendered, it does so using the members within Squares[5](or whatever data that gets attached).
I need to repeat the process for Squares[0]~Squares[11]. I can't use ng-repeat though since I won't be iterating through them in numerical order.
Ideally, it would be nice if I could do something along the lines of
<td class="Square" id="five" ng-include src="'SquareTempl.html'" ng-data="Squares[5]">
Any ideas?
Here's a JSFiddle I've written to outline a failed attempt I've tried using ng-model.
http://jsfiddle.net/fZz3W/9/
Two things: First, you can make ng-data be available by implementing it yourself YourApp.directive("ngData", function() {}) Secondly, do you need the HTML to be part of another file? An easy way to accomplish what you're looking for in Angular is with ng-repeat like:
<td ng-repeat="item in Square">
<div>{{item.name}}</div>
</td>
When the Square array is updated an additional post will be made.
Review your modified JSFiddle: http://jsfiddle.net/TdWMF/1/
So this is mostly a hack to achieve what you want, until I can offer you a better solution:
Second update using mixed order ng-repeat: http://jsfiddle.net/TdWMF/3/
Basically:
<div ng-repeat="index in [4, 2, 0, 3, 1]">
square index: {{index}}<br />
square: {{Squares[index]}}
</div>
Pretty ugly, and non-ideal, I know. I'd also recommend performing the order array generate in a function and then doing: ng-repeat="index in displayOrder(Squares)"

Categories