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'm creating an array in a service and fetch some data from a db and iterative over the data to add the group id as a key in the array
angular.module('tasksApp').
factory("data", ["$http", function ($http) {
var data = {};
data.allTasks = {};
data.today = [];
data.actioned = [];
data.complete = [];
data.todaysNextNumber = 0;
data.groups = [];
data.groupNames = [];
data.nextGroupPosition = [];
data.loadTasks = function () {
return $http({
method: 'GET',
url: '/loadTasks'})
.then(function(response) {
data.allTasks = response.data;
// Get groups & create the arrays
getGroups().then(function(result) {
var allGroups = result;
for (var key in allGroups) {
var group = allGroups[key];
data.groups[group.group_id] = [];
data.groupNames[group.group_id] = group.group_name;
data.nextGroupPosition[group.group_id] = 0;
}
The id's of the data start with 1 and go up to 9.
Then, in the component I load the data to the scope:
angular.module('tasksApp').
component('tasks', {
templateUrl: 'tasks/template.tasks.html',
controller: function startUp($scope, $rootScope, data) {
// Get tasks
data.loadTasks().then(function(response) {
$scope.groups = data.groups;
$scope.today = data.today;
$scope.actioned = data.actioned;
$scope.groupNames = data.groupNames;
console.log($scope.groups);
})
}
});
and use ng-repeat to go over the data in the template:
<!-- Start of Groups -->
<div ng-repeat="(key, group) in groups" class="col-md-3">
<div class="groupBox">
<div ng-attr-id="{{'groupCell-' + key}}" ng-click="showTitleInput(key)">
<h1 ng-attr-id="{{'groupTitle-' + key}}">{{groupNames[key]}} - {{key}}</h1>
<input class="groupTitleInput" ng-attr-id="{{'groupInput-' + key}}" ng-value="groupNames[key]"></input></div>
<div ng-attr-id="{{'div' + key}}">
<div ng-repeat="task in group | orderBy:'position'" ng-attr-id="{{'task-' + task.id}}" class="taskContainer">
<div ng-attr-id="{{'taskText-' + task.id}}" class="taskText" ng-click="displayActionBar = !displayActionBar">{{task.task}}</div>
<div ng-attr-id="{{'actionBar-' + task.id}}" class="actionBar" ng-show="displayActionBar">
<div class="actionButton glyphicon glyphicon-globe todaysTasks" ng-click="displayActionBar = !displayActionBar; addToToday(task.id, task.group_id)"></div>
<div class="actionButton glyphicon glyphicon-ok actioned" ng-click="displayActionBar = !displayActionBar; markAsActioned(task.id, task.group_id)"></div>
<div class="actionButton glyphicon glyphicon-thumbs-up complete" ng-click="displayActionBar = !displayActionBar; markAsComplete(task.id, task.group_id)"></div>
<div class="actionButton glyphicon glyphicon-trash delete" ng-click="displayActionBar = !displayActionBar; deleteTask(task.id, task.group_id)"></div>
<div class="actionButton glyphicon glyphicon-remove cancel" ng-click="displayActionBar = !displayActionBar"></div>
<div class="actionButton glyphicon glyphicon-menu-up up" ng-click="pushUp(task.id, task.group_id)"></div>
<div class="actionButton glyphicon glyphicon-menu-down down" ng-click="pushDown(task.id, task.group_id)"></div></div></div></div>
<form ng-submit="addTask(key)" ><input name={{key}} class="newTaskInput" type="text" ng-model="newTaskDescription" /><form></div></div>
All this works fine but when the data is displayed a div for an '0' element is shown. If I print the array out using console.log it doesn't show a '0' element. Also, if I change the data with the id of 1 to 0 it also iterates over a 1 element. I thought that ng-repeat iterated over the elements in the array using the assigned key, not going from 0 through to the number of elements.
Does anyone know why this is happening?
I'm creating a component using knockout and I'm having trouble getting the display to updated when I click a link. The change event is working however to change the currently selected button, the display is just not updating on the screen.
Component
ko.components.register('bs-dropdownbutton', {
template:
'<span class="btn-group pull-left" data-bind="visible: Loaded"> \
<button class="btn btn-default" data-bind="text: Selected.ButtonText, click: ButtonListener">Test</button> \
<button data-toggle="dropdown" class="btn btn-default dropdown-toggle"> \
<span class="caret"></span> \
</button> \
<ul class="dropdown-menu"> \
<!-- ko foreach: Buttons --> \
<li data-bind="click: $parent.ChangeSelected"> \
\
</li> \
<!-- /ko --> \
</ul> \
</span>',
viewModel: {
createViewModel: function (params, componentInfo) {
var self = this;
var vm = params.Buttons();
self.Selected = vm.SelectedButton();
self.Buttons = vm.Buttons();
self.ChangeSelected = function (btn, event) {
self.Selected = btn;
}
self.ButtonListener = function () {
vm.ButtonListener(self.Selected);
}
self.Loaded = self.Buttons !== undefined;
return self;
}
}
});
View Model
function recordManagement() {
var recordManagementVM = function () {
var self = this;
console.log("test");
var Button = function (btnValue, btnText) {
this.ButtonText = btnText;
this.ButtonValue = btnValue;
}
function printOption(option) {
switch (option) {
case "Excel":
alert("Printing Excel"); return;
case "Display":
alert("Printing Display"); return;
default:
alert("Unknown Document Type"); return;
}
}
var btnList = [
new Button('Display', "Display"),
new Button('Excel', "Excel"),
new Button('PDF', "PDF"),
new Button('Word', "Word"),
];
function ButtonGroup () {
var self = this;
self.Buttons = ko.observableArray(btnList),
self.SelectedButton = ko.observable(btnList[0]),
self.SelectedButton.subscribe(function () {
console.log(arguments);
});
self.GetSelected = function (btn, event) {
self.SelectedButton(btn[0]);
},
self.ButtonListener = function (btn) {
printOption(btn.ButtonValue);
}
return self;
};
self.Buttons = ko.observable(new ButtonGroup());
//self.SelectedButton = ko.observable(btnList[0]);
}; //--End VM --
ko.applyBindings(new recordManagementVM(), document.getElementById("recordmanagement"));
}
recordManagement();
JSFiddle
If you want the text here:
<button class="btn btn-default" data-bind="text: Selected.ButtonText, click: ButtonListener">Test</button>
to update as Selected changes, you'll have to make it observable. Right now, only the first Selection is rendered.
To make it observable, you can change self.Selected = vm.SelectedButton(); into self.Selected = ko.observable(vm.SelectedButton());
Set it with self.Selected(btn); instead of self.Selected = btn;
Data bind it with data-bind="text: Selected().ButtonText instead of data-bind="text: Selected.ButtonText
I'm using an Angular UI modal to update the calendar style grid UI. (on a drag and drop style app (using http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types)), to e.g. change the date of an order planningslot.
The modal is to provide a manual way of updating and I can’t save until the user hits the Save button.
This is fine (though I suspect it could be better) in updating the data in my parent scope object (scope.WokCentres), i.e. the date changes, great). What I’m stuck on is ‘moving’ the object to it’s new date within the 'calendar style grid'
Below is my JS and view html
JS:
$scope.EditWorkOrder = function (slot, max) {
var modalInstance = $uibModal.open({
animation: true,
templateUrl: '/app/WorkOrder/Views/EditWorkOrder.html',
controller: 'EditWorkOrderCtrl as vm',
size: 'lg',
resolve: {
data: function () {
return {
Slot: slot,
Max: max
}
}
}
});
//slotupdate is the returned object from the modal
modalInstance.result.then(function (slotupdate) {
for (var a = 0; a < scope.WorkCentres.length; a++) {
var wcs = scope.WorkCentres[a]
for (var b = 0; b < wcs.WorkOrderDates.length; b++) {
var wcd = wcs.WorkOrderDates[b]
for (var c = 0; c < wcd.PlanningSlots.length; c++) {
var slot = wcd.PlanningSlots[c]
if (slot.Id == slotupdate.Id) {
// This gets hit and updates the appropriate data object from the loop
scope.WorkCentres[a].WorkOrderDates[b].PlanningSlots[c] = slotupdate;
}
}
}
}
}, function () {
// do nothing
// $log.info('Modal dismissed at: ' + new Date());
});
};// END OF MODAL
VIEW:
<div ng-controller="workCentreCtrl as vm">
<div class="row">
<div class="workcentre-left">
<h3>Work Centre</h3>
</div>
<div class="workcentre-right">
<ul>
<li class="date-bar" ng-repeat="workdate in vm.WorkDates">{{workdate |date:'EEEE'}} {{workdate |date:'dd MMM'}}</li>
</ul>
</div>
</div>
<div>
<div class="row" ng-repeat="wc in vm.WorkCentres" ng-model="vm.WorkCentres">
<div class="workcentre-left">
<h5>{{wc.WorkCentreName}}</h5>
<button class="btn btn-default" ng-click="open(wc.WorkCentreId)" type="button">edit</button>
<p ng-if="wc.RouteTime != 0">{{wc.RouteTime}}</p>
</div>
<div class="workcentre-right dndBoxes">
<ul class="orderdate" ng-repeat="date in wc.WorkOrderDates" data-workdate="{{date.OrderDate}}">
<li id="parentorderdate" ng-class="{'four-slot': wc.max == 4, 'eight-slot': wc.max == 8, 'twelve-slot': wc.max == 12,'sixteen-slot': wc.max == 16}">
<ul dnd-list="date.PlanningSlots"
dnd-allowed-types="wc.allowedTypes"
dnd-disable-if="date.PlanningSlots.length >= wc.max"
dnd-dragover="dragoverCallback(event, index, external, type)"
dnd-drop="dropCallback(event, index, item, external, type, 'itemType')"
dnd-inserted="logEvent('Element was inserted at position ' + index, event)">
<li ng-repeat="slot in date.PlanningSlots" ng-model="date.PlanningSlots" ng-if="slot.WorkOrderNumber != '' "
dnd-draggable="slot"
dnd-type="wc.allowedTypes"
dnd-moved="date.PlanningSlots.splice($index, 1)"
dnd-effect-allowed="move" class="slot {{slot.css}}" title="{{slot.WOStatus}}">
<div>{{slot.SlotNumber}}</div>
<div>{{slot.WorkOrderNumber}} - {{slot.ProductDescription}}</div>
<div ng-if="slot.WOStatus != ''"><span class="float-right fa fa-edit fa-2x main-text edit-work-order" ng-click="EditWorkOrder(slot, wc.max)"></span></div>
</li>
<li ng-repeat="slot in date.PlanningSlots" ng-model="date.PlanningSlots" ng-if="slot.SlotBlocked == 'true'"
class="empty-slot">{{slot.SlotBlocked}}
<i class="fa fa-ban fa-2x main-text"></i>
</li>
<li class="dndPlaceholder">Drop work order here
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
Any help many appreciated.
itsdanny
The code in the OP, breaks the model (scope.WorkCentres).
There is an angular.forEach function which doesn't
modalInstance.result.then(function (slotupdate) {
// Remove it
angular.forEach(scope.WorkCentres, function (wc) {
angular.forEach(wc.WorkOrderDates, function (WorkOrderDate) {
angular.forEach(WorkOrderDate.PlanningSlots, function (slot) {
if (slot.Id == slotupdate.Id) {
WorkOrderDate.PlanningSlots.splice(index, 1);
}
})
})
})
// Add it back
angular.forEach(scope.WorkCentres, function (wc) {
if (wc.WorkCentreId == slotupdate.WorkCentreId) {
angular.forEach(wc.WorkOrderDates, function (WorkOrderDate) {
if (WorkOrderDate.OrderDate == slotupdate.OrderDate.getTime()) {
WorkOrderDate.PlanningSlots.push(slotupdate)
return;
}
})
}
})
}
I might actually cry out of joy!
Yesterday I make this question:
How can I refresh or load JSON to my viewModel on Knockout JS with complex models
Everything works OK with the fixes but when I try to use a complex json to load in the viewModel some of the buttons (specifically on Groups) doesn't work.
To resume the problem. I have a json with the previous serialized data. I use that json to fill the viewModel, this works, load correctly the data but the problem are in the "group" template, because the data is loaded but the buttons doesn't work, the only button which is working is the "remove group".
(Please refer to the image)
Any idea to fix this? Thanks.
Jsfiddle example with the problem
http://jsfiddle.net/y98dvy56/26/
!Check this picture.
The red circles indicates the buttons with problems.
The green circles indicates the buttons without problems.
Here is the body html
<div class="container">
<h1>Knockout.js Query Builder</h1>
<div class="alert alert-info">
<strong>Example Output</strong><br/>
</div>
<div data-bind="with: group">
<div data-bind="template: templateName"></div>
</div>
<input type="submit" value="Save" data-bind="click: Save"/>
</div>
<!-- HTML Template For Conditions -->
<script id="condition-template" type="text/html">
<div class="condition">
<select data-bind="options: fields, value: selectedField"></select>
<select data-bind="options: comparisons, value: selectedComparison"></select>
<input type="text" data-bind="value: value"></input>
<button class="btn btn-danger btn-xs" data-bind="click: $parent.removeChild"><span class="glyphicon glyphicon-minus-sign"></span></button>
</div>
</script>
<!-- HTML Template For Groups -->
<script id="group-template" type="text/html">
<div class="alert alert-warning alert-group">
<select data-bind="options: logicalOperators, value: selectedLogicalOperator"></select>
<button class="btn btn-xs btn-success" data-bind="click: addCondition"><span class="glyphicon glyphicon-plus-sign"></span> Add Condition</button>
<button class="btn btn-xs btn-success" data-bind="click: .addGroup"><span class="glyphicon glyphicon-plus-sign"></span> Add Group</button>
<button class="btn btn-xs btn-danger" data-bind="click: $parent.removeChild"><span class="glyphicon glyphicon-minus-sign"></span> Remove Group</button>
<div class="group-conditions">
<div data-bind="foreach: children">
<div data-bind="template: templateName"></div>
</div>
</div>
</div>
</script>
<!-- js -->
<script src="js/vendor/knockout-2.2.1.js"></script>
<script src="js/vendor/knockout-mapping.js"></script>
<script src="js/condition.js"></script>
<script src="js/group.js"></script>
<script src="js/viewModel.js"></script>
<script>
window.addEventListener('load', function(){
var json =
{"group":{"templateName":"group-template","children":[{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"group-template","children":[{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"}],"logicalOperators":["AND","OR"],"selectedLogicalOperator":"AND","text":"(Points = 0 AND Points = 0 AND Points = 0)"}],"logicalOperators":["AND","OR"],"selectedLogicalOperator":"AND","text":"(Points = 0 AND Points = 0 AND Points = 0 AND (Points = 0 AND Points = 0 AND Points = 0))"},"text":"(Points = 0 AND Points = 0 AND Points = 0 AND (Points = 0 AND Points = 0 AND Points = 0))"};
var vm = new QueryBuilder.ViewModel();
ko.mapping.fromJS(json.group, {}, vm.group);
ko.applyBindings(vm);
}, true);
</script>
Condition.js:
window.QueryBuilder = (function(exports, ko){
function Condition(){
var self = this;
self.templateName = 'condition-template';
self.fields = ko.observableArray(['Points', 'Goals', 'Assists', 'Shots', 'Shot%', 'PPG', 'SHG', 'Penalty Mins']);
self.selectedField = ko.observable('Points');
self.comparisons = ko.observableArray(['=', '<>', '<', '<=', '>', '>=']);
self.selectedComparison = ko.observable('=');
self.value = ko.observable(0);
}
exports.Condition = Condition;
return exports;
})(window.QueryBuilder || {}, window.ko);
Group.js
window.QueryBuilder = (function(exports, ko){
var Condition = exports.Condition;
function Group(){
var self = this;
self.templateName = 'group-template';
self.children = ko.observableArray();
self.logicalOperators = ko.observableArray(['AND', 'OR']);
self.selectedLogicalOperator = ko.observable('AND');
// give the group a single default condition
self.children.push(new Condition());
self.addCondition = function(){
self.children.push(new Condition());
};
self.addGroup = function(){
self.children.push(new Group());
};
self.removeChild = function(child){
self.children.remove(child);
};
}
exports.Group = Group;
return exports;
})(window.QueryBuilder || {}, window.ko);
ViewModel.js
window.QueryBuilder = (function(exports, ko){
var Group = exports.Group;
function ViewModel() {
var self = this;
self.group = ko.observable(new Group());
self.load = function (data) {
ko.mapping.fromJS(data, self);
}
self.Save = function () {
console.log(ko.toJSON(self));
}
}
exports.ViewModel = ViewModel;
return exports;
})(window.QueryBuilder || {}, window.ko);
Your issue is caused by the fact that the mapping plugin makes your data observable, but doesn't augment your data with the functions in your model such as the add, remove, etc... functions. If you do a console log for the json data when it's inserted into the view model you will notice that the data is observable but the functions are missing. You need to provide a mapping to customize your Group, Condition, etc.. constructors. Because the children array in your case is of mixed types (condition or group) Here is a custom mapping to take care of that:
var childrenMapping = {
'children': {
create: function(options) {
var data = options.data;
console.log(data);
var object;
switch(data.templateName) {
case 'condition-template':
object = new QueryBuilder.Condition(data);
break;
case 'group-template':
object = new QueryBuilder.Group(data);
break;
}
return object;
}
}
};
Then you simply need to provide this mapping in your initial mapping
ko.mapping.fromJS(json.group, childrenMapping, vm.group);
Then inside the constructor of the Group object:
function Group(data){
var self = this;
self.templateName = 'group-template';
...
ko.mapping.fromJS(data, childrenMapping, this);
}
You also need to update the Condition constructor to accept the data provided by the mapping, but since conditions don't have children you do not need to provide the childrenMapping here:
function Condition(data){
var self = this;
self.templateName = 'condition-template';
...
ko.mapping.fromJS(data, {}, this);
}
I've the mapping at the end of both function so that the mapped values override you initial value.
The updated jsfiddle here:
http://jsfiddle.net/omerio/y98dvy56/32/
This answer is related:
knockout recursive mapping issue