Using Knockout observables derived from JSON to update the view dynamically - javascript

Hopefully this isn't bad practice, but I am trying to understand Knockout observables within the context of my previous question.
I want to update the view with 'red flower' or 'blue sky', depending on which button is clicked. Let's presume the JSON will be static. How can I go about using observables to update the view while only applying my bindings a single time?
Fiddle:
https://jsfiddle.net/ft8a6jbk/3/
HTML:
<button class="blue">Blue</button>
<button class="red">Red</button>
<div data-bind="text: name"></div>
<div data-bind="text: things()[0].item1"></div>
<script>
ko.applyBindings(viewModel);
</script>
JS:
var data = {
"colors": [{
"name": "blue",
"things": [{
"item1": "sky",
"item2": "ocean",
}, ]
}, {
"name": "red",
"things": [{
"item1": "flower",
"item2": "sun",
}, ]
}, ]
};
$('.blue').click(function() {
var viewModel = ko.mapping.fromJS(data.colors[0]);
});
$('.red').click(function() {
var viewModel = ko.mapping.fromJS(data.colors[1]);
});

How can I [...] while only applying my bindings a single time?
Like this:
function Sample(data) {
var self = this;
self.colors = ko.observableArray();
self.currentColor = ko.observable();
ko.mapping.fromJS(data, {}, self);
}
var sample = new Sample({
"colors": [{
"name": "blue",
"things": ["sky", "ocean"]
}, {
"name": "red",
"things": ["flower", "sun"]
}]
});
ko.applyBindings(sample);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div data-bind="foreach: colors">
<button data-bind="text: name, click: $root.currentColor"></button>
</div>
<div data-bind="with: currentColor">
<h4 data-bind="text: name"></h4>
<div data-bind="foreach: things">
<span data-bind="text: $data" />
</div>
</div>
Notes:
Don't write jQuery event handlers. Remove jQuery from your knockout code entirely. The two exceptions to this rule are: Using Ajax (since knockout has no Ajax functions) and writing custom binding handlers. Anything else, most prominently DOM manipulation, should be governed by Knockout completely.
An observable is a function. You can use it as an event handler, like I did above in the click binding. Here is how this works:
Knockout passes the context data, in this case a single "color" item, to the event handler function, in this case the currentColor observable.
When an observable is called with a value, it stores that value.
Effect: Instant event handler and application state storage - without writing a single function yourself.

Related

knockout observable array binding to view when there is a delay in assigning value not happening

I have a knockout observable array whose value assignment changes after a set value of time but do not see this reflecting in the view. Could someone tell where I am doing it wrong? I would expect the output to show
• GRE 1111
• TOEFL 111
but it shows
• GRE2 222
• TOEFL2 22
jsFiddle link: https://jsfiddle.net/4r37x9y5/
HTML:
console.clear();
function viewModel() {
this.plans = ko.observableArray([]);
var plans1 = [
{ id: 'GRE', count: '1111' },
{ id: 'TOEFL', count: '111' },
];
var plans2 = [
{ id: 'GRE2', count: '222' },
{ id: 'TOEFL2', count: '22' },
];
this.plans = plans2;
//this.plans = plans1;
setTimeout(function(){
console.log("In timeout before assigning plans");
this.plans = plans1;
console.log(this.plans);
}, 2000);
}
ko.applyBindings(viewModel());
// The above line equals:
// viewModel(); // updates window object and returns null!
// ko.applyBindings(); // binds window object to body!
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="panel panel-default">
<ul data-bind="foreach: plans" class="list-group">
<li class="list-group-item">
<span data-bind="text: id"></span>
<span data-bind="text: count"></span>
</li>
</ul>
</div>
There are couple of issues here. As mentioned by you in the comments, you are not binding an object with observables. You are simply adding a global variable plans. If knockout can't find a property in the viewModel, it will use the window object's property. That's why it works the first time
You need to change viewModel as a constructor function and use new viewModel() to create an object or an instance.
observables should be read and updated by calling them as functions. So, this.plans(plans1). If you set this.plans = plans2, it will simply overwrite the observable with a simple array without the subscribers to update the UI when the property changes
You need to use correct this inside setTimeout. Either by creating a self = this variable outside or using an arrow function as a callback
function viewModel() {
this.plans = ko.observableArray([]);
var plans1 = [{ id: "GRE", count: "1" }, { id: "TOEFL", count: "1" }];
var plans2 = [{ id: "GRE2", count: "2" }, { id: "TOEFL2", count: "2" }];
this.plans(plans2) // call it like a function
setTimeout(() => {
console.log("In timeout before assigning plans");
this.plans(plans1)
}, 2000);
}
ko.applyBindings(new viewModel()); // new keyword to create an object
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<ul data-bind="foreach: plans">
<li>
<span data-bind="text: id"></span>
<span data-bind="text: count"></span>
</li>
</ul>

Strange behaviour Angular driven select list

According to paper "How to set the initial selected value of a select element using Angular.JS ng-options & track by" by #Meligy which I used as a guidance to learn and solve my problem with implementing a select list (ng-options), I still encounter some strange collaterale behaviour.
Although the basic behaviour finally does what it should do, see Test Plunk, I still encounter strange behaviour on the selected item in that list. Not in my test plunk though, implemented in my developement site.
app.controller("TaskEditCtrl", function($scope) {
$scope.loadTaskEdit = loadTaskEdit;
function loadTaskEdit() {
taskLoadCompleted();
tasktypesLoadCompleted();
}
function taskLoadCompleted() {
$scope.tasks = [{
Id: 1,
Name: "Name",
Description: "Description",
TaskTypesId: 4
}
];
$scope.current_task_tasktypesid = $scope.tasks[0].TaskTypesId;
}
function tasktypesLoadCompleted() {
var tasktypes = [{ Id: 1, Name: "A" },
{ Id: 2, Name: "B" },
{ Id: 3, Name: "C" },
{ Id: 4, Name: "D" }];
$scope.available_tasktypes_models = tasktypes
}
$scope.submit = function(){
alert('Edited TaskViewModel (New Selected TaskTypeId) > Ready for Update: ' + $scope.tasks[0].TaskTypesId);
}
loadTaskEdit();
});
HTML
<form class="form-horizontal" role="form" novalidate angular-validator name="editTaskForm" angular-validator-submit="UpdateTask()">
<div ng-repeat="task in tasks">
<div>
<select ng-init="task.TaskTypes = {Id: task.TaskTypesId}"
ng-model="task.TaskTypes"
ng-change="task.TaskTypesId = task.TaskTypes.Id"
ng-options="option_tasttypes.Name for option_tasttypes in available_tasktypes_models track by option_tasttypes.Id">
</select>
</div>
</div>
<div class="">
<input type="submit" class="btn btn-primary" value="Update" ng-click="submit()" />
</div>
</form>
As said, see my test plunk which shows exactly what it supposed to do. Moreover, using 5 self-explaining images, I do hope to make my troulbe bit clearer what's the problem.
I'm a bit lost to figure out what's so troublesome. My 'water' is telling me something wrong or missing in css. Did have anybody out their ever have face comparable? What could cause me this trouble? Does have anybody out there have a clue?
Thanks in advance
[1
[]2
[]3
[]4
Apparently I'm a rookie on css. Any suggestion is welcome!
CSS
#region "style sheets"
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/css/site.css",
"~/content/css/bootstrap.css",
"~/content/css/bootstrap-theme.css",
"~/Content/css/font-awesome.css",
"~/Content/css/morris.css",
"~/Content/css/toastr.css",
"~/Content/css/jquery.fancybox.css",
"~/Content/css/loading-bar.css"));
#endregion "style sheets"
The key with the dropdown is to set the model to the object that was selected. I updated your code to behave the way that I believe you are asking for it to work.
The key differences are:
Set the ng-model of the dropdown to the selected object and not the id of the selected item. This will give you access to the full selected object and all it's properties.
Remove the ng-change binding - this is not necessary with 2 way data binding, and the value on the model (whatever is put in for ng-model) will automatically be updated.
In your HTML you were using properties that were never declared in the Controller $scope. I updated those to reflect the available variables that were in scope.
For more information on dropdowns please see the angular documentation. It's very useful for figuring these types of issues out - https://docs.angularjs.org/api/ng/directive/select
// Code goes here
var app = angular.module("myApp", []);
app.controller("TaskEditCtrl", function($scope) {
$scope.tasks = {};
$scope.current_task_tasktypesid = null;
$scope.selected_task_tasktype = null;
$scope.loadTaskEdit = loadTaskEdit;
function loadTaskEdit() {
taskLoadCompleted();
tasktypesLoadCompleted();
//EDIT: DEFAULT DROPDOWN SELECTED VALUE
$scope.selected_task_tasktype = $scope.available_tasktypes_models[2];
}
function taskLoadCompleted() {
$scope.tasks = [{
Id: 1,
Name: "Name",
Description: "Description",
TaskTypesId: 4
}
];
$scope.current_task_tasktypesid = $scope.tasks[0].TaskTypesId;
}
function tasktypesLoadCompleted() {
var tasktypes = [{ Id: 1, Name: "A" },
{ Id: 2, Name: "B" },
{ Id: 3, Name: "C" },
{ Id: 4, Name: "D" }];
$scope.available_tasktypes_models = tasktypes
}
$scope.submit = function(){
alert('submitted model: ' + $scope.selected_task_tasktype.Id);
}
loadTaskEdit();
});
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#*" data-semver="1.2.9" src="http://code.angularjs.org/1.2.9/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp" ng-controller="TaskEditCtrl as edit">
<form class="form-horizontal" role="form" novalidate angular-validator name="editTaskForm" angular-validator-submit="UpdateTask()">
<div ng-repeat="task in available_tasktypes_models">
<div>Task (Id): {{task.Id}}</div>
<div>Name: {{task.Name}}</div>
<div>Descripton: {{task.Description}}</div>
</div>
<p>Current Task.TaskTypesId: {{selected_task_tasktype.Id}}</p>
<div>
<select
ng-model="selected_task_tasktype"
ng-options="option_tasttypes.Name for option_tasttypes in available_tasktypes_models track by option_tasttypes.Id">
</select>
</div>
<p>{{task.TaskTypes}}</p>
<p>{{selected_task_tasktypesid = task.TaskTypes}}</p>
<div class="">
<input type="submit" class="btn btn-primary" value="Update" ng-click="submit()" />
</div>
</form>
</body>
</html>
First, I need to state the implementation of #Meligy and the suggested input of 'dball' are correct. So, go with the flow of your choice.
Keep notice on your style sheets.
Finally, I figured out that the style property 'color' with the value 'white' of selector #editTaskWrapper as identifier of the parent
<div id="editTaskWrapper">
acted as the bad guy. One way or the other, if I comment 'color: white' in
#editTaskWrapper {
background-color: #337AB7;
/*color: white;*/
padding: 20px;
}
the selected item in the selectlist becomes visible. All other controls and values are not affected, only the selected list item.

Dynamically access an array using ng-repeat

I need to access different arrays based on the users choice and then run through the array with a ng-repeat.
Controller:
$scope.allbooks={
book1:{price:"3.00",type:"non-fiction",chapters:book1chapters},
book2:{price:"4.00",type:"fiction",chapters:book2chapters},
};
$scope.pick = function(selectedBook) {
$rootScope.choice = selectedBook;
}
$scope.book1chapters=[
{title:"it begins"},
{title:"another one"}
];
$scope.book2chapters=[
{title:"hello"},
{title:"calling from the otherside"}
];
HTML:
<button ng-click="pick(allbooks.book1)">Book 1</button>
<button ng-click="pick(allbooks.book2)">Book 2</button>
<div ng-repeat:"m in choice.chapters"><-----this does not work
Chapter: {{m.title}}
</div>
This is a very simplified example just to make it easier to look at :) I don't know how t reference another array from inside an array. Thanks
It seems you did not define book1chapters and book1chapters for collection allbooks, instead you defined them in $scope which is not correct. Also change $rootScope to $scope since rootScope is not injected. The following code is working:
var book1chapters = [{
title: "it begins"
}, {
title: "another one"
}];
var book2chapters = [{
title: "hello"
}, {
title: "calling from the otherside"
}];
$scope.allbooks = {
book1: {
price: "3.00",
type: "non-fiction",
chapters: book1chapters
},
book2: {
price: "4.00",
type: "fiction",
chapters: book2chapters
},
};
$scope.pick = function(selectedBook) {
$scope.choice = selectedBook;
}
The code on plunker: http://plnkr.co/edit/83Ujp4R8BjIe39ROmp6n?p=preview
Short Answer
Basically you have incorrect ng-repeat syntax. It should have = instead of : before writing expression in front of ng-repeat directive like we do for value attribute
Markup
<div ng-repeat="m in choice.chapters"><-----this does not work
Chapter: {{m.title}}
</div>
Suggestions
You should not pollute $rootScope for sharing variables. For that you could create a shareable service which can share a data among-est various components of your app like controllers, directives, service, etc.
HTML
<button ng-click="sharableData.choice = 'book1'">Book 1</button>
<button ng-click="sharableData.choice = 'book2'">Book 2</button>
<div ng-repeat = "m in allbooks[sharableData.choice].chapters">
Chapter: {{m.title}}
</div>
Service
app.service('sharableData', function(){
var sharableData = this;
sharableData.sharedData = {
choice: undefined
};
});
Controller
app.controller('myCtrl', function($scope, sharableData){
//you other controller code
//add this additional line to expose service variable on html
$scope.sharableData = sharableData;
});

ObservableArray binding won't update

Edit: JSFiddle with comments
I'm developing my first SPA using knockoutjs. My situation is:
I have a list of items being displayed from which the user can select an item
With an item selected, the user can make changes to the selected item
After confirming the changes, the SPA sends the updated data to the web api
However, the list displaying all my entries does not reflect the updates made to the item
I created a simple fiddle.js (see here). It shows my problem better than 1000 words. I left out pagination logic for simplicity, but the observable for my list needs to be a computed for various reasons.
ViewMode.
var ViewModel = function() {
var self = this;
self.selectedItem = ko.observable();
self.items = ko.observableArray([
{
name: "Item A",
price: "12.99"
},
{
name: "Item B",
price: "13.99"
},
{
name: "Item C",
price: "90.99"
}]);
self.paginated = ko.computed(function() {
// This is where I do some pagination and filtering to the content
// It's left out here for simplicity. The binding for the list needs
// to be a computed though.
return self.items();
});
self.selectItem = function(item) {
self.selectedItem(item);
};
self.save = function(item) {
// Sending data to web api...
// After the saving, the displaying list does not update to reflect the changes
// I have made. However, switching entries and checking the changed item shows
// that my changes have been saved and are stored in the observable.
}
};
ko.applyBindings(new ViewModel());
View
<!-- ko foreach: paginated -->
<br />
<!-- /ko -->
<br />
<br />
<div data-bind="visible: selectedItem">
<!-- ko with: selectedItem -->
<form>
<input type="text" data-bind="value: name" />
<input type="text" data-bind="value: price" />
<br />
<button type="button" data-bind="click: $parent.save">Save</button>
</form>
<!-- /ko -->
</div>
I hope you can help me out, I don't want to reload all the data from the server for the sake of performance and speed.
you have to make the properties of the objects in your array observable properties in order to reflect the changes to the UI.
self.items = ko.observableArray([
{
name: ko.observable("Item A"),
price: ko.observable("12.99")
},
{
name: ko.observable("Item B"),
price: ko.observable("13.99")
},
{
name: ko.observable("Item C"),
price: ko.observable("90.99")
}]);

Big nested JSON data and updating DOM when changed

I'm dealing with pretty big amounts of json and the data is something like this:
{
"name": "John Smith",
"age": 32,
"employed": true,
"address": {
"street": "701 First Ave.",
"city": "Sunnyvale, CA 95125",
"country": "United States"
},
"children": [
{
"name": "Richard",
"age": 7,
"field": {
"field": "value"
}
}
]
}
Whenever I change anything I get a new response which is somewhat similar to the previous data, but where new properties might have been added, stuff might have been removed and so on.
My testcode is something like this (don't mind the infinite amount of bad practices here):
<div data-viewmodel="whatevz">
<span data-bind="text: stuff['nested-thingy']"></span>
</div>
<script>
function vm() {
var self = this;
this.stuff = ko.observable();
require(["shop/app"], function (shop) {
setTimeout(function () {
self.stuff(shop.stuff);
}, 1200);
});
}
ko.applyBindings(new vm(), $("[data-viewmodel]")[0]);
</script>
I want stuff['nested-thingy'] to be updated whenever stuff is updated. How do I do this without all kinds of mapping and making everything observable?
You should only have to update your biding:
<div data-viewmodel="whatevz">
<span data-bind="text: stuff()['nested-thingy']"></span>
</div>
You have to access the value of the observable with the (). That returns your object and then you can access it. The content of the binding is still dependent on the observable stuff therefore it should update whenever stuff is updated.
At least my fiddle is working that way: http://jsfiddle.net/delixfe/guM4X/
<div data-bind="if: stuff()">
<span data-bind="text: stuff()['nested-thingy']"></span>
</div>
<button data-bind="click: add1">1</button>
<button data-bind="click: add2">2</button>
Note the data-bind="if: stuff(). That is necessary if your stuff's content is empty at binding time or later...
function Vm() {
var self = this;
self.stuff = ko.observable();
self.add1 = function () {
self.stuff({'nested-thingy': "1"});
};
self.add2 = function () {
self.stuff({'nested-thingy': "2"});
};
}
ko.applyBindings(new Vm());
Any reason you can't use the mapping plugin to deal with the mapping for you? You can use the copy option for the properties that you don't want to be made observables:
var mapping = {
'copy': ["propertyToCopy"]
}
var viewModel = ko.mapping.fromJS(data, mapping);

Categories