KnockOutJS - Multiple ViewModels in a single View - mvvm

I'm thinking that my application is getting quite large now, too large to handle each View with a single ViewModel.
So I'm wondering how difficult it would be to create multiple ViewModels and load them all into a single View. With a note that I also need to be able to pass X ViewModel data into Y ViewModel data so the individual ViewModels need to be able to communicate with each other or at least be aware of each other.
For instance I have a <select> drop down, that select drop down has a selected state which allows me to pass the ID of the selected item in the <select> to another Ajax call in a separate ViewModel....
Any points on dealing with numerous ViewModels in a single View appreciated :)

Knockout now supports multiple model binding. The ko.applyBindings() method takes an optional parameter - the element and its descendants to which the binding will be activated.
For example:
ko.applyBindings(myViewModel, document.getElementById('someElementId'))
This restricts the activation to the element with ID someElementId and its descendants.
See documentation for more details.

If they all need to be on the same page, one easy way to do this is to have a master view model containing an array (or property list) of the other view models.
masterVM = {
vmA : new VmA(),
vmB : new VmB(),
vmC : new VmC(),
}
Then your masterVM can have other properties if needed, for the page itself. Communication between the view models would not be difficult in this situation as you could relay through the masterVM, or you could use the $parent / $root in bindings, or some other custom options.

This is my answer after completing very large project with lots of ViewModels in single view.
Html View
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<div id="container1">
<ul>
<li >Container1 item</li>
<!-- ko foreach: myItems -->
<li>Item <span data-bind="text: $data"></span></li>
<!-- /ko -->
</ul>
</div>
<div id="container2">
<ul>
<li >Container2 item</li>
<!-- ko foreach: myItems -->
<li>Item <span data-bind="text: $data"></span></li>
<!-- /ko -->
</ul>
</div>
<script src="js/jquery-1.11.1.js"></script>
<script src="js/knockout-3.0.0.js"></script>
<script src="js/DataFunction.js"></script>
<script src="js/Container1ViewModel.js"></script>
<script src="js/Container2ViewModel.js"></script>
</body>
</html>
For this view I'm creating 2 view models for id=container1 and id=container2 in two separate javascript files.
Container1ViewModel.js
function Container1ViewModel()
{
var self = this;
self.myItems = ko.observableArray();
self.myItems.push("ABC");
self.myItems.push("CDE");
}
Container2ViewModel.js
function Container2ViewModel() {
var self = this;
self.myItems = ko.observableArray();
self.myItems.push("XYZ");
self.myItems.push("PQR");
}
Then after these 2 viewmodels are registering as separate viewmodels in DataFunction.js
var container1VM;
var container2VM;
$(document).ready(function() {
if ($.isEmptyObject(container1VM)) {
container1VM = new Container1ViewModel();
ko.applyBindings(container1VM, document.getElementById("container1"));
}
if ($.isEmptyObject(container2VM)) {
container2VM = new Container2ViewModel();
ko.applyBindings(container2VM, document.getElementById("container2"));
}
});
Like this you can add any number of viewmodels for separate divs. But make sure do not create separate view model for a div inside registered div.

Check MultiModels plugin for Knockout JS - https://github.com/sergun/Knockout-MultiModels

We use components to achieve that. (http://knockoutjs.com/documentation/component-overview.html)
For example, we have this component library we are developing: https://github.com/EDMdesigner/knobjs
If you dig into the code, you will see that for example we reuse the knob-button component in several places.

Related

Jquery cannot find selector hidden by IziModal

IziModal is beautiful but Jquery selectors cannot see or find the modal elements it hides. So this code: var nextmodal = $(this).next('.izra'); $(nextmodal).iziModal('open'); , which can see any div hidden by display:none, can find any element EXCEPT divs hidden by Izimodal.
https://jsfiddle.net/zmbth7u9/4/ (Stack code snippet below)
Izimodal can find its own hidden div with class .izra but no Jquery selector can?
I've tried everything including div: $(this).next($(":hidden:first"));
console.log($(hiddenstuff))
The above code SKIPS OVER the Izimodal hidden div and finds the second!
I need to activate modals with generic classes repeated throughout documents for footnotes, saving code, markup and time.
Any idea how I can get Jquery selectors to find the div with the .izra class so I can act on it?
Since we cannot find siblings because IziModal hides them beyond reach, perhaps modal divs should begin without the .izra class (set to display:none by css) and dynamically add the .izra modal class upon hitting the click trigger? We could find the divs first with next(), then add the izra class, or would that put us back at square one?
Thank you!
https://jsfiddle.net/zmbth7u9/4/ The modal and finding divs in this fiddle works, and shows the lines that inexplicably don't work.
//How things should Work generically
$('.generictrigger').click(function () {
var nextmodal = $(this).next('.genericmodal');
$(nextmodal).modal('show');
});
//IziModal Initialize and fire on open
$(function () {
$(".izra").iziModal();
$('.izra').iziModal('open'); // This works!
});
//Proof hidden divs easily found when NOT hidden by IziModal
$(".trigger2").click(function () {
var hiddenstuff = $(this).next($(":hidden:first")); // This works!
console.log($(hiddenstuff))
});
//Proof IziModal divs cannot be found by Jquery selectors
$(document).on('click', '.trigger', function (event) {
event.preventDefault();
// $('.izra').iziModal('open'); // THIS WORKS!
var nextmodal = $(this).next('.izra'); // Should work but does not
console.log($(nextmodal));
$(nextmodal).modal('open'); // Should work but does not <<<<<<
});
.hidden { display: none; }
.c1 { background-color:#33ccff; }
.c2 { background-color:#ffff99; }
.c3 { background-color:#80ff80; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.5.1/css/iziModal.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.5.1/js/iziModal.js"></script>
<!-- Finding divs hidden by IziModal Begin -->
<!-- How things should generically work calling Next() -->
<div class="containerdivforchildren">
<div class="c1 generictrigger">Click for Generic Modal FOUND by NEXT Selector </div>
<div class="genericmodal modal fade modal-body modal-content"><a title="Close"><i data-dismiss="modal" class="glyphicon glyphicon-remove icon-arrow-right pull-left"></i></a>Generic Bootstrap Modal</div>
</div>
<!-- This section proves any div can be found EXCEPT divs touched by IziModal -->
<div class="modaltriggercontainer">
<div class="trigger2 c3">Click to Log var hiddenstuff = $(this).next($(":hidden:first"));to console</div>
<div class="trigger c2">Click to Open Izra (will fail but logs results of $(this).next() to console</div>
<div class="izra">Unretrievable modal div when hidden by IziModal class</div>
<!--Above DIV IS SKIPPED AND CANNOT BE FOUND!-->
<div id="incognito" class="c1 oddlynamed hidden">hidden by style but found using $(this).next()</div>
</div>
<!-- Above div is the first div found by any selector looking for the .next()-->

knockout.js nested foreach on nested array

I've been looking at examples of nested foreach loops in knockout all afternoon, and I haven't been able to get anything working. My current setup, at least the relevant parts, are below.
ViewModel:
var sample = {
this.pageData = ko.observable();
this.panels = ko.observableArray();
};
ko.utils.extend(sample.prototype, {
activate: {
this.pageData(sampleData);
this.panels([
{
name: 'column1',
keys: ['key1', 'key2', 'key3'],
loadedWidgets: ko.observableArray()
},
{
name: 'column2',
keys: ['key4', 'key5'],
loadedWidgets: ko.observableArray()
},
{
name: 'column3',
keys: ['key6'],
loadedWidgets: ko.observableArray()
}
]);
this.loadWidgetPanels(this.panels(), this.pageData());
},
loadWidgetPanels: function (panels, pageData) {
for (var i = 0; i < panels.length; i++) {
var screens = filterContentByKey(pageData.Content, panels[i].keys);
if (screens) {
panels[i].loadedWidgets.push(widgetFactory.getWidgets(screens));
}
}
}
}
View:
<div>
<!-- ko foreach: panels -->
<div class="3columnStyle">
<!-- ko foreach: loadedWidgets -->
<!--ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
<!-- /ko -->
</div>
I've confirmed that I'm getting back the right data in the right format in my loadedWidgets, but they don't display in the view. I can tell that the view at least knows how much data is there, because my DOM has a ko compose element for each widget. For example, the first column has a handful of widgets, and that div gets created with a handful of widgets in it. Column 2 has 2 widgets, and it gets 2 compose elements. Column 3 has 1 widget and gets one element. It's just not displaying the widgets themselves. Do I need additional elements or additional binding somewhere?
I have a working model of this that doesn't rely on nested loops. Instead of using the array of objects, it just creates each of the observable arrays. In other words, it's not looping. Instead of one array containing three objects, each with its own array, I have three arrays:
this.column1Widgets();
this.column2Widgets();
this.column3Widgets();
They're constructed the same way, just manually instead of looping. And the View looks more like this:
<div class="3columnStyle">
<!-- ko foreach: column3Widgets -->
<!-- ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
<div class="3columnStyle">
<!-- ko foreach: column3Widgets -->
<!-- ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
<div class="3columnStyle">
<!-- ko foreach: column3Widgets -->
<!-- ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
I'm not sure why it's not working with the nested loop, but since the data is identical, I'm sure there's something I'm missing in setting up the View.
Without seeing the rest of your code, it's difficult to tell for sure. I'm a little suspicious of the sample object in your viewmodel. But it seems to me that you're not actually nesting your foreach's.
In your view, replace
foreach: loadedWidgets
with
foreach: $data.loadedWidgets
You need to reference the parent foreach in some way. $data represents the current item in the parent foreach, and that item, if I understand your model correctly, contains a loadedWidgets key.
Also, there's no need for containerless binding in your case.
As Eric Taylor suggested, it must have something to do with the containerless binding. I created a jsfiddle with some oversimplified object models, but changing my DOM from the above to the following immediately fixed the issue:
<div>Test</div>
<div data-bind="foreach: panels">
<ul data-bind="foreach: loadedWidgets">
<li data-bind="text: $data"></li>
</ul>
</div>
I don't think I have a good grasp of how the containers interact with the binding yet.

ember.js: how to delete a record using a view

in ember's official guide, they say it's possible to delete a record using a view, but they don't provide an example of how to do it. i can't understand how views can get the id of the object do destroy.
maybe i didn't understand what's the view purpose? i think it's an event handler (but i see sometimes it's used to render chunks of hbl... maybe that's why i'm confusing)
is there an example of the whole process of deletion anywhere?
thank you
Generally, what you want to do is create an {{action}} in your view that sends an event to where it should actually be handled: either the controller or the route. (In my case, a little of both)
Note: Generally, you don't need to write a View class for templates, unless the view needs a particular event handler. Ember generates a generic view on-the-fly. You can see this through {{log view}}:
<script type="text/x-handlebars" data-template-name="app">
{{log view}}
</script>
If you look in the console you will find that the template app is associated with a view class:
For example, in the following view template, I'm defining a "Delete" button, which will trigger the action remove in the controller.
<script type="text/x-handlebars" data-template-name="product/remove">
<fieldset>
<legend>Remove</legend>
<div class="row-fluid">
Are you sure you want to delete <strong>{{content.name}}</strong>?
</div>
</fieldset>
<ht />
{{#linkTo products class="btn"}}Back to List{{/linkTo}}
<button {{action remove target="controller"}} class="btn btn-danger">
Delete
</button>
</script>
The controller simply gets the content property and signals the route to fire the confirmRemove event, passing its content as the argument
App.ProductRemoveController = Em.ObjectController.extend({
remove: function() {
this.get('target').send('confirmRemove', this.get('content'));
}
});
And the route actually handles it like this:
App.ProductRemoveRoute = Em.Route.extend(App.NotifyHandler, {
setupController: function(controller, model) {
var c = this.controllerFor('product');
controller.set('content', c.get('content'));
},
events: {
confirmRemove: function(record) {
record.deleteRecord();
// should commit here
// this.get('store').commit();
this.controllerFor('application').set(
'notification', 'Product has been removed'
);
this.transitionTo('products');
}
}
});
(see fiddle)
If you want to handle the event directly in the Route, without talking to the controller, in your view template, you simply omit the target="controller", and the framework will look up for a handler of that event in the controller, and if doesn't find, it will look up in the route. In this approach, you have to pass the event argument via Handlebars, if any argument is required. In this case, I know that this represents the content in that template:
<script type="text/x-handlebars" data-template-name="product/remove">
<fieldset>
<legend>Remove</legend>
<div class="row-fluid">
Are you sure you want to delete <strong>{{content.name}}</strong>?
</div>
</fieldset>
<ht />
{{#linkTo products class="btn"}}Back to List{{/linkTo}}
<button {{action confirmRemove this}} class="btn btn-danger">
Delete
</button>
</script>
With this approach, you don't need to define anything in your controller as it will fire the event directly in the route:
App.ProductRemoveController = Em.ObjectController.extend();
(see fiddle)
Update: In order to have the event handled in the object controller, the itemController property has to specify a controller, which should extend Em.ObjectController:
Depot.TransportDocumentsController = Ember.ArrayController.extend
itemController: 'transportDocument'
Depot.TransportDocumentController = Ember.ObjectController.extend
removeItem: ->
alert("aoooo")
The only thing that would be changed in a template would be the mention of the itemController in the {{each}} helper:
{{#each doc in controller itemController="transportDocument"}}
{{doc.number}}
<!-- rest of the template removed to make this short. -->
<button {{action removeItem}} class='btn btn-danger btn-small'>
<i class="icon-white icon-remove"></i>
</button>
{{/each}}
In the action, you don't need to say where the handler is located, as the framework can find the target on its own.
(see fiddle)

jQuery next() selector when divs are not neighboring

Ive been using the following to change the width of the div.my-div that appears after the one you've clicked:
$(".my-div").click(function () {
$(this).next().css({'width':'500px'});
});
As my divs were neighboring, this worked fine:
<div class="my-div">stuff</div>
<div class="my-div">stuff</div>
<div class="my-div">stuff</div>
However now the structure has changed so they are no longer neighboring:
<div>
<div class="my-div">stuff</div>
</div>
<div>
<div>
<div class="my-div">stuff</div>
</div>
</div>
<div class="my-div">stuff</div>
Whats the simplest way to select the next element of the same class?
Thanks
jQuery will return elements in order of their appearance in the DOM.
As such, you could cache all the .my-div elements, use the index()[docs] method to get the index of the one that received the event, increment it and use the eq()[docs] method to get the next one.
var divs = $(".my-div"); // cache all of them
divs.click(function () {
var idx = divs.index( this ); // get the index in the set of the current one
divs.eq( idx + 1 ).css({'width':'500px'}); // get the one at the next index
});
This saves you from doing a bunch of unnecessary DOM selection and traversing.
Example: http://jsfiddle.net/VrATm/1/
EDIT: Posted wrong example link. Fixed.
You can traverse the tree hierarchy. That is, you can first jump to parent, then to next, then to children, like this:
$(this).parent().next().find(' > div').css({'width':'500px'});

How to use "this" and not "this" selectors in jQuery

I have 4 divs with content like below:
<div class="prodNav-Info-Panel">content</div>
<div class="prodNav-Usage-Panel">content</div>
<div class="prodNav-Guarantee-Panel">content</div>
<div class="prodNav-FAQ-Panel">content</div>
And a navigation list like this:
<div id="nav">
<ul id="navigation">
<li><a class="prodNav-Info" ></a></li>
<li><a class="prodNav-Usage" ></a></li>
<li><a class="prodNav-Guarantee"></a></li>
<li><a class="prodNav-FAQ" ></a></li>
</ul>
</div>
When the page is first displayed I show all the content by executing this:
$('div.prodNav-Usage-Panel').fadeIn('slow');
$('div.prodNav-Guarantee-Panel').fadeIn('slow');
$('div.prodNav-FAQ-Panel').fadeIn('slow');
$('div.prodNav-Info-Panel').fadeIn('slow');
Now, when you click the navigation list item it reveals the clicked content and hides the others, like this:
$('.prodNav-Info').click( function() {
$('div.prodNav-Info-Panel').fadeIn('slow');
$('div.prodNav-Usage-Panel').fadeOut('slow');
$('div.prodNav-Guarantee-Panel').fadeOut('slow');
$('div.prodNav-FAQ-Panel').fadeOut('slow');
});
So what I have is 4 separate functions because I do not know which content is currently displayed. I know this is inefficient and can be done with a couple of lines of code. It seems like there is a way of saying: when this is clicked, hide the rest.
Can I do this with something like $(this) and $(not this)?
Thanks,
Erik
In your particular case you maybe able to use the .sibilings() method something like this:
$(this).fadeIn().sibilings().fadeOut()
Otherwise, lets say that you have a set of elements stored somewhere that points to all of your elements:
// contains 5 elements:
var $hiders = $(".prodNavPanel");
// somewhere later:
$hiders.not("#someElement").fadeOut();
$("#someElement").fadeIn();
Also, I would suggest changing the classes for your <div> and <a> to something more like:
<div class="prodNavPanel" id="panel-Info">content</div>
....
<a class="prodNavLink" href="#panel-Info">info</a>
This gives you a few advantages over your HTML. First: the links will have useful hrefs. Second: You can easily select all your <div>/<a> tags. Then you can do this with jQuery:
$(function() {
var $panels = $(".prodNavPanel");
$(".prodNavLink").click(function() {
var m = this.href.match(/(#panel.*)$/);
if (m) {
var panelId = m[1];
$panels.not(panelId).fadeOut();
$(panelId).fadeIn();
return false; // prevents browser from "moving" the page
}
});
});