I have a view that displays data from a foreach loop in different categories depending on the type. Each category will contain a number of users - I created an object that will check to see if the number of users in a category are more than 10 then the text for the visible bind will show. And for the category that doesn't have more than 10 it will not show the text.
My question: if the first category doesn't have 10 it won't show text does that mean that it won't also show text for the remaining categories?
Help with: the visible binding is not working even though a category would contain more than 10 and not sure why.
Here is my JSFiddle: http://jsfiddle.net/xNdJk/1/
JavaScript:
var userViewModel = function (data) {
var _self = this;
_self.Name = ko.observable(data.Name);
_self.Letter = ko.observable(data.Letter);
_self.ShowLetter = ko.computed(function () {
return (roleViewModel.UserCount > 13);
});
};
var typeViewModel = function (data) {
var _self = this;
_self.ContentType = ko.observable(data.ContentType);
_self.Name = ko.observable(data.Name);
_self.Rank = ko.observable(data.Rank);
_self.UserCount = ko.observable(data.UserCount);
_self.Users = ko.observableArray([]);
};
View:
<div class="collapse in" data-bind="template: { name: 'list', foreach: $data.Users }">
</div>
<div id="letter" data-bind="visible:ShowLetter, text: Letter"></div>
You are mixing classes and instances, you have created a secondModel class but you never instance it, here is a working example
http://jsfiddle.net/xNdJk/2/
var viewModel = function(){
this.Letter = ko.observable('Hello, World!');
this.secondModel = new secondModel();
this.shouldShowMessage = ko.computed(function() {
return (this.secondModel.UserCount() > 13);
}, this);
}
var secondModel = function(){
var self = this;
self.UserCount = ko.observable(153);
}
Related
I've got a complex object in a JSON format. I'm using Knockout Mapping, customizing the create callback, and trying to make sure that every object that should be an observable - would actually be mapped as such.
The following code is an example of what I've got:
It enables the user to add cartItems, save them (as a JSON), empty the cart, and then load the saved items.
The loading part fails: It doesn't display the loaded option (i.e., the loaded cartItemName). I guess it's related to some mismatch between the objects in the options list and the object bounded as the cartItemName (see this post), but I can't figure it out.
Code (fiddle):
var cartItemsAsJson = "";
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.availableProducts = ko.observableArray([]);
self.language = ko.observable();
self.init = function () {
self.initProducts();
self.language("english");
}
self.initProducts = function () {
self.availableProducts.push(
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
);
}
self.getProducts = function () {
return self.availableProducts;
}
self.getProductName = function (product) {
if (product) {
return self.language() == "english" ?
product.productName().english : product.productName().french;
}
}
self.getProductValue = function (selectedProduct) {
// if not caption
if (selectedProduct) {
var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
return product.productName().english == selectedProduct.productName().english;
});
return matched;
}
}
self.getProductColours = function (selectedProduct) {
selectedProduct = selectedProduct();
if (selectedProduct) {
return selectedProduct.availableColours();
}
}
self.addCartItem = function () {
self.cartItems.push(new cartItemVM());
}
self.emptyCart = function () {
self.cartItems([]);
}
self.saveCart = function () {
cartItemsAsJson = ko.toJSON(self.cartItems);
console.log(cartItemsAsJson);
}
self.loadCart = function () {
var loadedCartItems = ko.mapping.fromJSON(cartItemsAsJson, {
create: function(options) {
return new cartItemVM(options.data);
}
});
self.cartItems(loadedCartItems());
}
}
var productVM = function (name, availableColours, data) {
var self = this;
self.productName = ko.observable({ english: name, french: name + "eux" });
self.availableColours = ko.observableArray(availableColours);
}
var cartItemVM = function (data) {
var self = this;
self.cartItemName = data ?
ko.observable(ko.mapping.fromJS(data.cartItemName)) :
ko.observable();
self.cartItemColour = data ?
ko.observable(data.cartItemColour) :
ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://rawgit.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.js
"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $parent.getProducts(),
optionsText: function (item) { return $parent.getProductName(item); },
optionsValue: function (item) { return $parent.getProductValue(item); },
optionsCaption: 'Choose a product',
value: cartItemName"
>
</select>
</div>
<div>
<select data-bind="options: $parent.getProductColours(cartItemName),
optionsText: $data,
optionsCaption: 'Choose a colour',
value: cartItemColour,
visible: cartItemName() != undefined"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'empty cart', click: emptyCart" />
<button data-bind="text: 'save cart', click: saveCart" />
<button data-bind="text: 'load cart', click: loadCart" />
</div>
</div>
What needs to be changed to fix it?
P.S.: I've got another piece of code (see it here) that demonstrates a persistance of the selected value even after changing the options - though there optionsValue is a simple string, while here it's an object.
EDIT:
I figured out the problem: the call ko.mapping.fromJS(data.cartItemName) creates a new productVM object, which is not one of the objects inside availableProducts array. As a result, none of the options corresponds to the productVM contained in the loaded cartItemName, so Knockout thereby clears the value altogether and passes undefined.
But the question remains: how can this be fixed?
In the transition from ViewModel -> plain object -> ViewModel you loose the relation between the products in your cart and the ones in your handlerVM.
A common solution is to, when loading a plain object, manually search for the existing viewmodels and reference those instead. I.e.:
We create a new cartItemVM from the plain object
Inside its cartItemName, there's an object that does not exist in handlerVM.
We look in handlerVM for a product that resembles this object, and replace the object by the one we find.
In code, inside loadCart, before setting the new viewmodels:
loadedCartItems().forEach(
ci => {
// Find out which product we have:
const newProduct = ci.cartItemName().productName;
const linkedProduct = self.availableProducts()
.find(p => p.productName().english === newProduct.english());
// Replace the newProduct by the one that is in `handlerVM`
ci.cartItemName(linkedProduct)
}
)
Fiddle: https://jsfiddle.net/7z6010jz/
As you can see, the equality comparison is kind of ugly. We look for the english product name and use it to determine the match. You can also see there's a difference in what is observable and what isn't.
My advice would be to use unique id properties for your product, and start using those. You can create a simpler optionsValue binding and matching new and old values happens automatically. If you like, I can show you an example of this refactor as well. Let me know if that'd help.
I currently have a list of students and a list of classes in a school object. I would like each class object to be able to display a filtered list of students based on the class id property.
I have tried to access the parent object via custom binding but have not had any success.
Perhaps I am looking at the problem the wrong way? I have spent a couple days on this and whichever way I tackle it I always need to access a value on a parent object.
Are there any methods of accessing what I need? I am beginning to think that it is not possible to access a global style variable.
function School()
{
var self = this;
self.ClassVMs = ko.observableArray([]).indexed('Number');
self.ChildVMs = ko.observableArray([]).indexed('Number');
}
function ClassVM(classId, text)
{
var self = this;
self.Number = ko.observable();
self.Text = ko.observable(text);
self.ClassId = ko.observable(classId);
}
function ChildVM(classId, text)
{
var self = this;
self.Number = ko.observable();
self.ClassId = ko.observable(classId);
self.Text = ko.observable(text);
}
I have a Fiddle with my setup. Any and all guidance is appreciated. Thanks
You do not need global variables to solve this. Knockout has $root and $parent to step slightly outside of the scope you're in inside a foreach. In addition, if really needed, you can always make sure the view models get another type of view model as its dependency. In fact, if one view model has a list of sub view models it already has such a dependency.
What you need to think about is what your UI/UX is going to be like. Are you designing your view models to support a view where the user is "viewing" a student and enrolls him/her in classes? Or is the app user viewing a class and adding students one by one?
Here's a variant that shows a little bit of both:
function School(classes) {
var self = this;
self.classes = ko.observableArray(classes);
self.students = ko.observableArray([]);
self.enroll = function(child, someClass) {
if (self.students().indexOf(child) < 0) {
self.students.push(child);
}
if (someClass.students().indexOf(child) < 0) {
someClass.students.push(child);
}
};
self.enrollNewChild = function(someClass) {
if (!!someClass.childToBeEnrolled()) {
self.enroll(someClass.childToBeEnrolled(), someClass);
someClass.childToBeEnrolled(null);
}
};
self.enrollInClass = function(child) {
if (!!child.classToBeEnrolledIn()) {
self.enroll(child, child.classToBeEnrolledIn());
child.classToBeEnrolledIn(null);
}
};
}
function Class(id, txt) {
var self = this;
self.id = ko.observable(id);
self.txt = ko.observable(txt);
self.students = ko.observableArray([]);
self.studentsCsv = ko.computed(function() {
return self.students().map(function(s) { return s.txt(); }).join(", ");
});
self.childToBeEnrolled = ko.observable(null);
}
function Child(id, txt) {
var self = this;
self.id = ko.observable(id);
self.txt = ko.observable(txt);
self.classToBeEnrolledIn = ko.observable(null);
}
var english = new Class(1, "English 1");
var math1 = new Class(2, "Mathematics 1");
var math2 = new Class(2, "Mathematics 2");
var john = new Child(1, "John Doe");
var mary = new Child(1, "Mary Roe");
var rick = new Child(1, "Rick Roll");
var marc = new Child(1, "Marcus Aurelius");
var school = new School([english, math1, math2]);
ko.applyBindings(school);
// Method 1:
school.enroll(john, english);
school.enroll(john, math2);
school.enroll(marc, english);
school.enroll(mary, math2);
school.enroll(mary, english);
school.enroll(rick, english);
school.enroll(rick, math1);
td { background-color: #eee; padding: 2px 10px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<h3>Classes:</h3>
<table>
<tbody data-bind="foreach: classes">
<tr>
<td><span data-bind="text: txt"></span></td>
<td><span data-bind="text: studentsCsv"></span></td>
<td>
Add student
<select data-bind="options: $root.students, value: childToBeEnrolled, optionsText: 'txt', optionsCaption: 'Choose...'"></select>
<button data-bind="click: $root.enrollNewChild">enroll now</button>
</td>
</tr>
</tbody>
</table>
<h3>School Students</h3>
<table>
<tbody data-bind="foreach: students">
<tr>
<td><span data-bind="text: txt"></span></td>
<td>
Enroll in:
<select data-bind="options: $root.classes, optionsText: 'txt', value: classToBeEnrolledIn, optionsCaption: 'Choose...'"></select>
<button data-bind="click: $root.enrollInClass">enroll now</button>
</td>
</tr>
</tbody>
</table>
It does not answer your question directly ("display a filtered list of students based on the class id property") because I think that's an XY-problem, and you're better off trying to find a solution like the above where you have proper references instead of having to use id and some kind of lookup mechanism.
Yes I can see how it could be considered a bit of an XY problem. You both made look at it differently which was a great help.
The way I originally would have liked wasn't looking feasible. My compromise was to add a computed to the root view model and display the list this way.
function School()
{
var self = this;
self.ClassVMs = ko.observableArray([]).indexed('Number');
self.ChildVMs = ko.observableArray([]).indexed('Number');
self.DisplayClassId = ko.observable(1);
self.Display = function(x)
{
console.log(x);
self.DisplayClassId(x);
}
}
var viewModel = new School();
viewModel.filteredItems = ko.computed(function () {
var filter = viewModel.DisplayClassId();
if (!filter) {
return viewModel.ChildVMs();
} else {
var filtered = ko.utils.arrayFilter(viewModel.ChildVMs(), function (item) {
return (item.ClassId() === filter);
});
return filtered;
}
})
Fiddle for reference
I am building a JS framework to simulate AngularJS models, only for educational purposes.
The thing is: I assign a callback to run when the models(inputs) are updated, but after they run by the first time, they "disappear".
Before trying to do this using jQuery, I was trying with querySelectorAll and got stuck on the same problem.
Here it is: http://jsfiddle.net/YmY2w/
HTML
<div class="container" jd-app="test">
<input type="text" jd-model="name" value="foo" /><br />
<input type="text" jd-model="email" value="foo#foo" />
<hr />
<p>Name: {{ name }}</p>
<p>Email: {{ email }}</p>
</div>
JAVASCRIPT
(function($, window, document, undefined) {
'use strict';
// registering models
(function() {
var $app = $('[jd-app]');
var $models = $('[jd-model]', $app);
$models.each(function(index) {
UpdateModel.apply(this);
});
function UpdateModel() {
var model = { name: $(this).attr('jd-model'), value: this.value }
var re = new RegExp('{{\\s+('+ model.name +')\\s+}}', 'gm');
$('*', $app).each(function(index) {
var $this = $(this);
$this.text( $this.text().replace(re, model.value) );
});
$(this).on('keyup', UpdateModel);
}
})();
})(jQuery, window, document);
What am I doing wrong? Is there a better way to accomplish this?
The problem you're having is that when your script first sees {{ name }} it converts it to the value of the model "name". But then in your html, you have the text "foo" instead of {{ name }} (the thing you're trying to search/replace), so you aren't able to update the displayed text.
What you have to do is keep track of the individual DOM nodes (as attached to particular models) which contain the text that initially has the "{{ }}" in them with the proper model name.
I came up with a kind of ugly prototype here:
http://jsfiddle.net/YmY2w/11/
I'm not sure how flexible it is in terms of identifying the proper templated locations, but it serves as a demo for the kind of implementation I'm talking about. Also - not very efficient I would guess. (But then neither is angularjs really...)
(function($, window, document, undefined) {
'use strict';
// From http://stackoverflow.com/questions/298750/how-do-i-select-text-nodes-with-jquery
function getTextNodesIn(node, includeWhitespaceNodes) {
var textNodes = [], nonWhitespaceMatcher = /\S/;
function getTextNodes(node) {
if (node.nodeType == 3) {
if (includeWhitespaceNodes || nonWhitespaceMatcher.test(node.nodeValue)) {
textNodes.push(node);
}
} else {
for (var i = 0, len = node.childNodes.length; i < len; ++i) {
getTextNodes(node.childNodes[i]);
}
}
}
getTextNodes(node);
return textNodes;
}
// registering models
(function() {
var $app = $('[jd-app]');
var $models = $('[jd-model]', $app);
var models = [];
$models.each(function(index) {
registerModel.apply(this);
});
function registerModel() {
var model = { name: $(this).attr('jd-model'), value: this.value, domElements: [] };
var re = new RegExp('{{\\s+('+ model.name +')\\s+}}', 'gm');
$app.contents().each(function(index) {
if ($(this).text().match(re)) {
var textNodes = getTextNodesIn(this, false);
console.log(textNodes);
$.each(textNodes, function(index, elem) {
if (elem.nodeValue.match(re)) {
var text = elem.nodeValue;
var myArray = re.exec(text);
var match = myArray[0];
var firstIndex = text.indexOf(match);
var newElem = elem.splitText(firstIndex);
newElem.splitText(match.length);
model.domElements.push(newElem);
}
});
}
});
models.push(model);
$(this).on('keyup', updateModel);
$(this).trigger('keyup');
}
function updateModel() {
var $input = $(this);
$.each(models, function(index, model) {
if (model.name === $input.attr('jd-model')) {
var newVal = $input.val();
$.each(model.domElements, function(index, elem) {
elem.nodeValue = newVal;
});
}
});
}
})();
})(jQuery, window, document);
I have this div which displays a letter, but I want to add an if statement of when to show this div based on the following condition:
if usersCount() > 3 then show letter
<div class=" header" id="letter" data-bind="text: Letter">
....
</div>
How could i add the if statement along with the text - expression statement?
data-bind="if: UserCount() > 13 then text:Letter"` ....??
var userViewModel = function (data) {
var _self = this;
_self.Letter = ko.observable(data.Letter);
};
var roleViewModel = function (data) {
var _self = this;
_self.UserCount = ko.observable(data.UserCount);
};
Check out the Visible Binding. You'll want to create a property in you View Model to handle the logic of hiding/showing the div. Here is a JSFiddle to demonstrate.
<div data-bind="visible: shouldShowMessage, text: Letter">
</div>
<script type="text/javascript">
var viewModel = function(){
var self = this;
self.Letter = ko.observable('Hello, World!');
self.UserCount = ko.computed(function() {
return Math.floor((Math.random() * 20) + 1);
});
self.shouldShowMessage = ko.computed(function() {
return (self.UserCount() > 13);
});
};
</script>
I am new at knockoutjc library, and can you help me? I have created a new model in javascript like this.
The code is here:
<h2>Category : Throusers</h2>
<h3>Sizes</h3>
<ul data-bind="foreach: products">
<li>
<input type="checkbox" data-bind="value: size.id" />
<label data-bind="text: size.name"></label>
</li>
</ul>
<h3>Colors</h3>
<ul data-bind="foreach: products">
<li>
<input type="checkbox" data-bind="value: color.id" />
<label data-bind=" text: color.name"></label>
</li>
</ul>
<h3>Products</h3>
<ul data-bind="foreach: products">
<li>
<label data-bind="text: name"></label> -
<label data-bind="text: size.name"></label>-
<label data-bind="text: color.name"></label>
</li>
</ul>
<script type="text/javascript">
function Color(id, name) {
return {
id: ko.observable(id),
name: ko.observable(name)
};
};
function Size(id, name) {
return {
id: ko.observable(id),
name: ko.observable(name)
};
}
function Product(id,name, size, color) {
return {
id: ko.observable(),
name: ko.observable(name),
size: size,
color: color
};
};
var CategoryViewModel = {
id: ko.observable(1),
name: ko.observable("Throusers"),
products: ko.observableArray([
new Product(1,"Levi's 501", new Size(1, "30-32"), new Color(1, "Red")),
new Product(2,"Colins 308", new Size(2, "32-34"), new Color(2, "Black")),
new Product(3,"Levi's 507", new Size(1, "30-32"), new Color(3, "Blue"))
])
};
ko.applyBindings(CategoryViewModel);
</script>
And now,
I wanna this: duplicated Sizes and colors should not list.
When I select a color from colors, selected color products should list and others should be disabled
If model is wrong?
I made an attempt to solve your problem.
I wrote everything from scratch, take a look. There might be a few bugs, didn't have time to test it a lot. But you can now add easily any custom filters you want.
http://jsfiddle.net/blackjim/8y5PP/12/
// jQuery 1.10 loaded
var myAPP = (function($,ko){
// trouserModel constructor
var TrouserModel = function(id,name,color,size,visible){
// maybe better if fields are ko observable, depends on other details
this.id = id||0,
this.name = name||'',
this.color = color||'',
this.size = size||'',
this.visible = visible||true;
return ko.observable(this);
}
// main viewmodel
var trouserProducts = {
data: ko.observableArray(),
filters: ko.observableArray()
}
trouserProducts.sizeFilter = ko.computed(setFilter('size'));
trouserProducts.colorFilter = ko.computed(setFilter('color'));
trouserProducts.updateFilter = function(element, valueAccessor, allBindingsAccessor) {
var ar = trouserProducts.data();
if(!ar[0]) return true;
var activeFilters = trouserProducts.filters().filter(function(el){return el().on;});
for(var i=0; i<ar.length; i++){
for(var j=0; j<activeFilters.length; j++){
var thisProp = ar[i]()[activeFilters[j]().prop].toLowerCase();
if( thisProp===activeFilters[j]().value ){
var that = ar[i]();
ar[i]({
id: that.id,
name: that.name,
color: that.color,
size: that.size,
visible: true
});
break;
}
}
if( j===activeFilters.length ){
var that = ar[i]();
ar[i]({
id: that.id,
name: that.name,
color: that.color,
size: that.size,
visible: false
});
}
}
return true;
};
// helper functions
var makeFilter = function(prop,value){
var ar = trouserProducts.filters()
value = value.toLowerCase(); // normalize values (OPTIONAL)
for(var i=0; i < ar.length ;i++){
var that = ar[i]();
if(that.prop===prop && that.value===value){
that.on = true;
return false;
}
}
// add filter
trouserProducts.filters.push(ko.observable({
prop: prop,
value: value,
on: true,
updateFilter: function(){
trouserProducts.updateFilter();
return true;
}
}));
}
// return a function with a specific filter
function setFilter(prop){
var prop = prop,
propFilter = function(el,i,ar){
// el is ko observable filter here
return el().prop === prop;
};
return function(){
return trouserProducts.filters().filter(propFilter);
}
};
var addTrouser = function(id,name,color,size){
var newTrouser = new TrouserModel(id,name,color,size);
color && makeFilter('color',color); // make color filter
size && makeFilter('size',size); // make size filter
trouserProducts.data.push(newTrouser); // add new trouserModel
}
return {
trouserProducts: trouserProducts,
addTrouser: addTrouser
}
}(jQuery,ko));
// add your initial products here
myAPP.addTrouser(1,"Levi's 501","Red","30-32");
myAPP.addTrouser(2,"Levi's 507","Black","32-34");
myAPP.addTrouser(3,"Levi's 507","Black","30-32");
myAPP.addTrouser(4,"Dirty jeans","Blue","32-34");
myAPP.addTrouser(5,"Dirty jeans","Red","32-34");
ko.applyBindings(myAPP.trouserProducts);
window.myAPP = myAPP;
Instead of storing your sizes and colors on your product model, you should store them separately -- like a normalized database.
Store only the id of the size and the color for what is available in the product model.
The foreach for the color list and the size list should then be iterating over all sizes in the size model and all colors in the color model.
Add a visible binding to the product list. Return true if the product has the size id and color id.
Lastly, I'd probably also make the size and color properties of your product model into arrays so that each product can have multiple colors and sizes associated with it.