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.
Related
I am altering an observableArray, modifying some data in a subscribe event. First I am converting the ObservableArray using ko.toJS(), mapping trough the data, and altering. At the end I call self.menuCategories(jsArray) to set the observableArray again.
It seems like I lose the "connection" to the observableArray in some way, since the foreach statement in my code suddenly breaks.
Either there is a very much easier way to handle this, or I am not handling the observables correctly.
CODE :
var MenuWizardModel = function() {
var self = this;
self.menuCategories = ko.observableArray();
self.commonDiscount = ko.observable(0);
// Handling adding items to menuCategories.
self.addNewSubMenuItem = function () {
var newSubMenuItem = new SubMenuItemViewModel(self.newSubMenuItemName(), []);
self.menuCategories.push(newSubMenuItem);
self.newSubMenuItemName(null);
self.createNewSubMenu(false);
}
function SubMenuItemViewModel(name, foodItemList) {
var self = this;
self.name = ko.observable(name);
self.foodItemList = ko.observableArray(foodItemList);
}
self.commonDiscount.subscribe(function(val) {
var discount = parseInt(val) / 100;
var jsArray = ko.toJS(self.menuCategories);
console.log(jsArray)
jsArray = ko.toJS(jsonArray[0].foodItemList.map(item => {
item.price = parseInt(item.price) - (parseInt(item.price) * discount);
return item;
}));
self.menuCategories(jsArray);
});
MARKUP :
<div data-bind="foreach: menuCategories">
<h4 data-bind="text: name"></h4>
<div data-bind="foreach: foodItemList" class="list-group">
...
DATA :
I think the best way to handle this type of thing is to add a computed observable to the fooditem that captures the global discount and calculates the discounted price.
something like the following.
var MenuWizardModel = function() {
var self = this;
self.menuCategories = ko.observableArray([{
name: 'Main Meals'
}]);
self.commonDiscount = ko.observable(0);
self.newSubMenuItemName = ko.observable();
// Handling adding items to menuCategories.
self.addNewSubMenuItem = function() {
var newSubMenuItem = new SubMenuItemViewModel(self.newSubMenuItemName(), [{name: 'Oranges', price: 3.99}]);
self.menuCategories.push(newSubMenuItem);
self.newSubMenuItemName(null);
//self.createNewSubMenu(false);
}
function mapFoodItem(item){
item.discountedPrice= ko.pureComputed(function(){
var discount = parseInt(self.commonDiscount()) / 100
return parseInt(item.price) - (parseInt(item.price) * discount);
});
return item;
}
function SubMenuItemViewModel(name, foodItemList) {
var self = this;
self.name = ko.observable(name);
self.foodItemList = ko.observableArray(foodItemList.map(mapFoodItem));
}
};
ko.applyBindings(new MenuWizardModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<label>Discount <input data-bind="value: commonDiscount"></label>
<label>Sub Menu Name: <input data-bind="value: newSubMenuItemName" /></label>
<button data-bind="click: addNewSubMenuItem">Add Sub Menu</button>
<div data-bind="foreach: {data: menuCategories, as: 'menu' }">
<h4 data-bind="text: menu.name"></h4>
<div data-bind="foreach: {data: menu.foodItemList, as: 'food'}" class="list-group">
<div class="list-group-item">
Name: <span data-bind="text: food.name"></span>
Price: <span data-bind="text: food.price"></span>
Discounted Price: <span data-bind="text: food.discountedPrice"></span>
</div>
</div>
</div>
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 am trying to get my head around using Knockout.js to create a product cart. Each item outputs a plus and minus button as well as a remove button. My aim is to be able to have the plus and minus increment or decrement the quantity, and the remove button to remove the product. My constraints are that I can't use JQuery.
I've attempted to separate my app concerns so that I have ShopView, ShopModel and ShopItem (ShopItem is the individual item that is pushed to an observable array within the ShopModel). The buttons are rendered, however when clicking on an individual remove/add/minus button and logging the value of this to the console I only am able to see my JS class, not the individual element to remove or alter. Any insight would be greatly appreciated. I've included the bare-bones snippets of the key parts:
index.html
<script type="text/html" id="itemsList">
{{ _.each(items(), function(item) { }}
<a href="#" data-bind="click: minus" class='left-minus'>–</a>
<p class="qty" data-bind="text: item.quantity"></p>
Remove
<a href="#" data-bind="click: plus" class='right-plus'>+</a>
{{ }) }}
</script>
<section data-bind="template: { name: 'itemsList' }" class="items-inner"></section>`
shopView.js
class shopView {
constructor() {
this.setupShop()
}
setupShop(){
this.model.items.push(new Item(97, 'cover-3', '/media/img/cover-3.jpg', 'Issue 5', 'Spring Summer 17', 1, 10));
ko.applyBindings(this.model);
}
}
module.exports = shopView
shopView.js
let ko = require('knockout');
class shopItem{
constructor (id, url, thumbnail, title, edition, quantity, price) {
this.id = ko.observable(id)(),
this.thumbnail = ko.observable(url)(),
this.title = ko.observable(title)(),
this.edition = ko.observable(edition)(),
this.quantity = ko.observable(quantity)(),
this.price = ko.observable(price)();
this.add = function(){
};
this.minus = function(){
};
}
}
module.exports = shopItem;
shopModel
Shop Item
class shopModel {
constructor() {
this.items = ko.observableArray([]);
this.minus = function(item){
console.log(item);
};
this.plus = function(){
};
this.remove = (item) => {
this.items.remove(item);
};
}
}
module.exports = shopModel;
The click binding provides the current $data value to the callback function. But because you are using Underscore for the loop, $data isn't the item. You can change your click binding to something like this:
<a href="#" data-bind="click: function() {minus(item)}" class='left-minus'>–</a>
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);
}
I have a simple model object with one array property.
Example:
{name: 'Foo', tags = ['fun', 'cool', 'geek']};
I add more one of these models to an obervableArray in my view model.
//pseudo code
oa.add({name: 'Foo', tags: ['fun', 'cool', 'geek']});
oa.add({name: 'Bar', tags: ['sad', 'dorky', 'uncool']});
oa.add({name: 'Qwerty', tags: ['keys', '101', 'geek']});
Now when I filter an item based on a tag I would like to display a message that there are no more items with a certain tag.
Filter code:
// self = this;
self.filter = ko.observable('');
self.filterItems = ko.dependentObservable (function() {
var filter = this.filter();
if (!filter) {
return this.items();
} else {
return ko.utils.arrayFilter(this.items(), function(item) {
try {
if (compareAssociativeArrays(item.tags, filter)) {
return true;
}
} catch (e) {}
self.items.remove(item);
});
}
}, this);
Is it possible to data-bind a given length of items with an indexOf a tag value ?
UPDATE
I did come up with a solution, but not sure if best. With it I can modify and retrieve totals as well:
self.hasGeek = ko.computed(function () {
var sum = 0;
var item;
for (var i=0; i<self.items().length; i++) {
var item = self.items()[i];
if (item.tags().indexOf('geek') != -1) {
sum++;
}
}
return (sum > 0) ? true : false;
});
I am not sure about your exact structure, but I would setup a computed observable to represent your filtered items. Then, you can include a section that has its visibility controlled by the length of the filtered items computed observable.
Would be like: http://jsfiddle.net/rniemeyer/aVtpc/
Tag Filter: <input data-bind="value: tagFilter" />
<hr/>
<div data-bind="visible: !filteredItems().length">
No items found
</div>
<ul data-bind="foreach: filteredItems">
<li data-bind="text: name"></li>
</ul>
js:
var ViewModel = function() {
this.tagFilter = ko.observable();
this.items = ko.observableArray([
{name: 'Foo', tags: ['fun', 'cool', 'geek']},
{name: 'Bar', tags: ['sad', 'dorky', 'uncool']},
{name: 'Qwerty', tags: ['keys', '101']}
]);
this.filteredItems = ko.computed(function() {
var filter = this.tagFilter();
if (!filter) {
return this.items();
}
return ko.utils.arrayFilter(this.items(), function(item) {
return ko.utils.arrayFirst(item.tags, function(tag) {
return tag === filter;
});
});
}, this);
};
ko.applyBindings(new ViewModel());
Quick psuedo code off my head ... After you filter down the oa, perhaps using a computed function, you can do something like this:
<span data-bind="text: filterOa().length"></span>
<!-- ko: foreach filterOa -->
<span data-bind="text: name"></span>
<!-- /ko -->
<!-- if: filterOa().length === 0 -->
You got nothing
<!-- /ko -->