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/
Related
I am trying to write a reusable component library, and have come up with this issue which is bugging me for a time now.
I have overriden the template engine using the example that is provided. I think my problem is mentioned in his 'A quick note about rewriting templates' paragraph (I am not sure though, my problem might be different). But I don't have any idea how to implement it. Any help is appreciated. Here's the problem:
My template engine basically registers templates given as 'templateName' and 'markUp'. Firstly, I have a product model, which goes as:
var Product = function(img, brand, description){
this.img = img;
this.brand = brand;
this.description = description;
};
Then, I have my parent view as:
{
name: 'productlist-view',
template: '<productlist-view class="product-list">\
<ul data-bind="foreach: {data: productlist, as: \'product\'}">\
<product-box params="parent: $parent, productInfo: product" data-bind="click: getProduct"></product-box>\
</ul>\
</productlist-view>'
}
Now in the viewModel of this productlist-view view, productlist property is defined as an array of Product instances. The product-box component is supposed to create a viewModel and an associated template for each of these Products. The product-box component is registered to knockout using:
ko.components.register('product-box', {
'viewModel': function(params){
this.product = params.product;
this.getProduct = function(){
//Will perform an ajax call here
};
},
'template': '<div class="product-box" data-bind="click: getProduct">\
<img class="product-img fl" data-bind="attr: {src: product.img}"/>\
<p data-bind="text: product.brand"></p>\
<p data-bind="text: product.description"></p>\
</div>'
})
I know, in the code above there are two bindings for the getProduct method. I will come to that. For now, just imagine the one in the productlist-view was not there.
The code above generates an html that goes like:
<productlist-view ....
<ul ....
<product-box ....
<div class="product-box" ....
<img .../>
<p ... />
<p ... />
</div>
</product-box>
<product-box ..... goes on
</ul>
</productlist-view>
In the above-code, the wrapper div in the product-box element is totally unnecessary since it just wraps the element. Moreover, the element is already wrapped within product-box element. So I want to remove the wrapper div. The problem here is that, I need the whole product visual to be clickable, but I cannot bind the click event to the getProduct method from the productlist-view template. When the foreach loop iterates in productlist-view template, the $data points to model of the product, not the viewModel (aka product-box).
How can I set this getProduct method, from the parent view?
Is there anyway to remove that unnecessary wrapper div in product-box?
In other words how can I have a clickable product-box component which goes like:
<productlist-view ....
<ul ....
<product-box data-bind="click: getProduct"....
<img .../>
<p ... />
<p ... />
</product-box>
...
</ul>
</productlist-view>
You can make a custom binding handler that would attach a click binding to the parent, but that strikes me as too clever by half (and violates encapsulation). If your click binding is associated with your component, it makes sense that the div be part of the component.
Instead of using custom product-box tags, you can use a virtual tag and the component binding, so you don't have an extraneous wrapper.
I understand from this question & answer that I can use KnockoutJS to bind 2 different view models to 2 different elements.
var viewModel1={...}
var viewModel2={...}
<div id="one"></div>
<div id="two"></div>
ko.applyBindings(viewModel1, document.getElementById('one'));
ko.applyBindings(viewModel2, document.getElementById('two'));
But, is there a way to bind 2 partial view models to the same element?
// This did not work for me
<div id='container'>
<span data-bind='text: firstname' />
<span data-bind='text: lastname' />
</div>
var partialVM1={
firstname:'John',
}
var partialVM2={
lastname:'Adams',
}
var container=document.getElementById('container');
ko.applyBindings(partialVM1,container);
ko.applyBindings(partialVM2,container);
Or alternatively, does KnockoutJS have a way of combining partial viewmodels into a single view model?
Prior Stackoverflow answers appear to say "no way", but I'm asking if there are there newly added Knockout options or if someone has a nice workaround for my dilemma.
// Pseudo-code:
var combinedVM = ko.combine(partialVM1,partialVM2);
ko.applyBindings(combinedVM,container);
I ask because the 2 partial view models are being created by separate processes (In Visual Studio, there are 2 different t4 scripts that create the 2 partial views separately).
If there is no easy workaround, I could refactor the 2 t4 scripts into a single script, but that would require a modest bit of rewiring (I'd rather avoid it, if possible).
Any other alternatives?
You could create a single view model and use data-binding with to change context.
Edit: jsFiddle
Example:
<div id='container'>
<div id='context1' data-bind='with: partial1'>
<span data-bind='text: firstname' />
</div>
<div id='context2' data-bind='with: partial2'>
<span data-bind='text: lastname' />
</div>
</div>
var partialVM1={
firstname:'John',
}
var partialVM2={
lastname:'Adams',
}
var combined = (function () {
function combinedVM() {
this.partial1 = ko.observable(partialVM1);
this.partial2 = ko.observable(partialVM2);
}
return combinedVM;
})();
var container=document.getElementById('container');
ko.applyBindings(combined,container);
I have problem with scopes of controllers. I'm using controller as directive and I have code similar to this example:
<div ng-controller="ItemsController as itemCtrl">
<table> .. some data ... </table>
<a ng-click="itemCtrl.createItem()">Create new item</a>
</div>
<div id="create-form" ng-controller="ItemFormController as itemFormCtrl">
<form ng-submit="itemFornCtrl.saveItem()">... form inputs ...</form>
</div>
<div id="edit-items" ng-controller="MultipleItemsEdit as multiEditCtrl">
... table with some data ....
<!-- I need this -->
<a ng-click="itemCtrl.createItem()">Create new item</a>
<!-- -->
</div>
Basically there are 3 isolated scopes. But I need to break this isolation and call methods from one scope on another.
I'm currently using ugly "delegate" kind of hack.
Controllers and their methods are not so interesting, only interesting methods are ItemsController.createItem():
this.createItem = function(dataCollection) {
angular.element( $("#create-form) ).controller().createNewItem(dataCollection);
}
and ItemFormController.createNewItem(dataCollection):
this.createNewItem = function(dataCollection) {
... some initialization ....
$("#add-item").dialog( "open" );
}
I need to call createNewItem method on ItemFormController to show modal box. But I cannot do it directly, so I'm using method createItem which gets the create-form element and its controller and calls createNewItem method on it. It is kind of a delegate. But I don't like it, because I need to call createNewItem from many places of my code and I don't want to populate all my controllers with this kind of delegate methods.
Maybe I could make these delegates on some kind of root controller, but isn't there any better solution?
You can nest the edit controller scope in the list controller scope by simply nesting the divs (move the div with ng-controller="MultipleItemsEdit as multiEditCtrl" into the div with ng-controller="ItemsController as itemCtrl"). That way the you can call the method directly.
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/
I'm building an interface with a lot of toggles to control what data is being filtered in a different part of an App's search results. Here is a codepen of it: Here
Coming from a jQuery/Backbone background, what is the most Angular way of toggling the 'active' state of any/all of the filter items? Essentially, almost any <li> tag presented here is a toggle-able feature.
In jQuery, I would put a listener on the view and wait for any click events to bubble up and toggle an 'active' class on the event.target. I want to do it the best way with Angular.
(Also, this is my first Angular project.. I am probably doing all sorts of things the wrong way. Apologies in advance.)
Edit: To clarify the question, I have an App Interface with 20+ possible filter attributes to control a separate module on the page. Every time someone toggles one of these filter attributes, I want to add/remove an 'active' class. Do I put an 'ng-click="function(...)"' in the ng-repeat for each controller? Or is there an easier way to manage this module-wide behavior (a la event bubbling, like in Backbone/jQuery) ?
Thanks!
You can do something like this:
<section ng-init="active = 'areaFoo'">
<div ng-class="{active:active == 'areaFoo'}" ng-click="active = 'areaFoo'"></div>
<div ng-class="{active:active == 'areaBar'}" ng-click="active = 'areaBar'"></div>
</section>
It will populate $scope.active for you, and is very angular as it leverages existing directives, manages the state on scope, and does not leverage dom api's or events outside of directives. There is really no need to involve the controller here, as its display logic.
Learn more about ng-class here.
Multiple active elements
<section>
<div ng-class="{active:areaFoo}" ng-init="areaFoo = true">
<button ng-click="areaFoo = true">activate</button>
<button ng-click="areaFoo = false">de activate</button>
</div>
<div ng-class="{active:areaBar}" ng-init="areaBar = false">
<button ng-click="areaBar = true">activate</button>
<button ng-click="areaBar = false">de activate</button>
</div>
<div ng-class="{active:areaBar}" ng-init="areaBaz = false">
<button ng-click="areaBaz = true">activate</button>
<button ng-click="areaBaz = false">de activate</button>
</div>
</section>
you could also toggle with something like this ng-click="areaFoo = !areaFoo"
I was able to come up with a solution I'm ok with, for anyone curious you can see a demo Here.
Here are the relevant code snippets:
<li ng-repeat='category in data' ng-class='{active: category.isActive}' ng-click='toggleActive(category)' >
<span class='solr-facets-filter-title'>{{category.catTitle}}</span>
<span class='solr-facets-filter-count'>{{category.catResults}}</span>
</li>
An ng-click calls a method on the Controller, toggleActive(category). The current data model gets sent to the method. In the JS:
$scope.toggleActive = function(category){
category.isActive = !category.isActive;
}
The function returns the opposite of the isActive attribute back to the li in question: an ng-class adds the active class for a truthy state of isActive.
I'm not a huge fan of how I have to adjust the data model with flags for active/inactive states like this, but it ends up working out for the best in this case. I can push those isActive states back to the $scope so that other parts of the App can run queries based on that information.