knockout.js 3.3 - rerendering component in foreach binding - javascript

My viewModel consist of observable array with observable elements.
// viewmodel
var viewModel = function () {
this.o = ko.observableArray();
for (var i = 0; i < 3; i++)
this.o.push(ko.observable(0));
};
I need to change the values of these elements. And for this purpose I create the component. Simple example of it is below:
//custom element <component>
ko.components.register("component", {
viewModel: function (params) {
var self = this;
this.value = params.value;
console.log("init component");
this.i = 1;
this.change = function () {
self.value(self.i++);
console.log("change to " + self.value());
}
},
template: "<span data-bind='text: value'></span> <button data-bind='click:change'>Change</button>"
});
This component can change value of observable element which come in params.value.
My view is very simple:
<!--ko foreach:o-->
<component params="value: $rawData"></component>
<!--/ko-->
Full example: http://jsfiddle.net/tselofan/xg16u5cg/7/
Problem is when value of observable element in observable array is changed, component is rendered again, because it is located inside foreach binding. You can see this in logs. What the best practice can I use in this situation? Thank you

The component is being recreated each time the number changes because the context of the component is the number.
http://jsfiddle.net/Crimson/xg16u5cg/8/
<!-- ko foreach: o -->
<component params="value: $data.myNumber"></component>
<!-- /ko -->
//Test how components work in foreach binding
//custom element <component>
ko.components.register("component", {
viewModel: function (params) {
var self = this;
this.value = params.value;
console.log("init component");
this.i = 1;
this.change = function () {
self.value(self.i++);
console.log("change to " + self.value());
}
},
template: "<span data-bind='text: value'></span> <button data-bind='click:change'>Change</button>"
});
// viewmodel
var viewModel = function () {
this.o = ko.observableArray();
for (var i = 0; i < 3; i++)
this.o.push({myNumber: ko.observable(0)});
};
ko.applyBindings(new viewModel());

Knockout-Repeat (https://github.com/mbest/knockout-repeat) is an iterative binding that does not create a new binding context, and has a foreach mode, so it should work as you expect with your component.

Related

Observable is changed only for one viewModel

I have collection of Tags (array of some string)
For each of Tags I create knockout viewModel TagsViewModel
var TagsViewModel = function() {
var vm = this;
vm.showTags = ko.observable(false);
window.shouter.subscribe(function(newValue) {
vm.showTags(newValue);
}, vm, 'toggleReviewTags');
}
And I have another "toggler" to show/hide tags in another partial view. For it I've created separate viewModel TagFiltersViewModel and use knockout pubSub to communicate with TagsViewModel
var TagFiltersViewModel = function() {
var vm = this;
vm.isTagsVisible = ko.observable(false);
vm.isTagsVisible.subscribe(function(newValue) {
window.shouter.notifySubscribers(newValue, 'toggleReviewTags');
});
vm.toggleTags = function() {
vm.isTagsVisible(!vm.isTagsVisible());
}
}
Each TagsViewModel I bind to container with calculated id "tag-container-"+ {tagId}
and for each do next thing
var element = document.getElementById(tagModel.tagsContainer);
ko.cleanNode(element);
ko.applyBindings(new TagsViewModel(tagModel), element);
Problem - locally only one tag from collection is shown after click on toggle button. I have created jsFiddle, but there I can't reproduce my problem.
Any thoughts what is the problem in my case?
I would suggest doing something like the following, it should make it much easier to manage.
There may be a specific reason you are binding each tag seperately using theapplyBindings method but you'll have to elaborate on that.
var arrayOfTags = ['tag1', 'tag2', 'tag3'];
var ViewModel = function() {
var self = this;
// Return an array TagsViewModels using the map method
this.Tags = ko.observableArray(arrayOfTags.map(function(tag) {
return new TagsViewModel(tag);
}));
// Observable to track if all tags are hidden/shown
this.TagsVisible = ko.observable(true);
// Loop through tags, show and set flag to true
this.ShowTags = function() {
self.Tags().forEach(function(tag) {
tag.Visible(true);
})
self.TagsVisible(true);
};
// Loop through tags, hide and set flag to false
this.HideTags = function() {
self.Tags().forEach(function(tag) {
tag.Visible(false);
})
self.TagsVisible(false);
};
};
var TagsViewModel = function(name) {
this.Name = ko.observable(name)
this.Visible = ko.observable(true);
};
var model = new ViewModel();
ko.applyBindings(model);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="click: ShowTags, visible: !TagsVisible()">Show Tags</button>
<button data-bind="click: HideTags, visible: TagsVisible">Hide Tags</button>
<hr>
<!-- ko if: Tags() -->
<!-- ko foreach: Tags -->
<span data-bind="text: Name, visible: Visible"></span>
<!-- /ko -->
<!-- /ko -->

Checked event not firing sometimes on checkboxes rendered by knockout in chrome

I have create a simple jsfiddle example: https://jsfiddle.net/c2wus7qg/3/
Here you will see four list items with checkboxes. If you uncheck any of them the change event will not fire. Then if you uncheck the remaining item it will fire.
Also if you check any unchecked item and uncheck again the change event will not fire in this case as well.
This seem to happen in chrome only. It works as expected in firefox and safari.
Here are code examples (simplified):
html:
<ul>
<!-- ko foreach: categoriesComputed -->
<li>
<input data-bind="attr:{value: id}, checked: state,
event: {change: $root.categoryChanged}" type="checkbox">
<span data-bind="text: name"></span>
</li>
<!-- /ko -->
</ul>
javascript:
function viewModel() {
var self = this;
self.categories = ko.observableArray();
self.Category = function(id, name, state) {
this.id = id;
this.name = name;
this.state = ko.observable(state);
};
self.categoriesComputed = ko.computed(function() {
return self.categories()
.sort(function(a, b) {
return a.state() < b.state() ? 1 : -1;
})
.slice(0, 4);
});
self.categoryChanged = function() {
console.log('Yep, definitely changed!');
}
}
var KO = new viewModel();
ko.applyBindings(KO);
setTimeout(function() {
var categories = [];
for ( var i = 1; i <= 20; i++ ) {
//set state to true only for some elements
var state = i >= 5 && i <= 6;
categories.push(new KO.Category(i, 'category #' + i, state));
}
KO.categories(categories);
}, 1000);
I think what is happening here is that you are unbinding the checkbox before its "change" event can fire. Your computed is only showing the top 4, but as soon as you uncheck one of the options it gets sorted out of those top 4 and removed from the DOM.
If you remove the .slice(0, 4); from your computed function you should see the event fire properly each time.
Edit:
One possible solution depending on your use-case is to use an array on the root model to store the selected options rather than a state observable on each individual option.
self.selectedCategories = ko.observableArray([5,6]);
The checked binding will use the array to store the selected models, or, when paired with the checkedValue binding will use the array to store just that property value of the selected option. For example if you use:
<input data-bind="attr:{value: id}, checked: $root.selectedCategories, checkedValue: id" type="checkbox">
the selectedCategories array will contain the IDs of the option objects (5 and 6 to begin with).
This way you can create a single subscription on that observable array to watch for changes to any of the checkboxes, and bypass the event binding which relies on the model being rendered in the DOM.
self.categoryChanged = function() {
console.log('Yep, definitely changed!');
}
self.selectedCategories.subscribe(self.categoryChanged);
Of course your sort function will have to be updated as well to check if an element exists in the selected array rather than checking the "state" of the object. that could look something like:
function(a, b) {
var indexA = self.selectedCategories().indexOf(a.id);
var indexB = self.selectedCategories().indexOf(b.id);
if(indexB == indexA){
return Number(a.id) - Number(b.id);
} else {
return indexB - indexA;
}
}
Example fiddle
It is a timing issue (as Jason noted), with the DOM being updated before the change event can fire. I modified your code to add an echoState member that gets the value of state after a brief delay. I based categoriesComputed on this, with the result being that the DOM isn't updated before the change event can fire.
I doubt this is useful for any real-world situations. :)
function viewModel() {
var self = this;
self.categories = ko.observableArray();
self.Category = function(id, name, state) {
this.id = id;
this.name = name;
this.state = ko.observable(state);
this.echoState = ko.observable(state);
this.state.subscribe((newValue) => {
setTimeout(() => {
this.echoState(newValue)
}, 0);
});
};
self.categoriesComputed = ko.computed(function() {
return self.categories
.sort(function(a, b) {
return a.echoState() < b.echoState() ? 1 : -1;
})
.slice(0, 4);
});
self.categoryChanged = function(data) {
console.log('Yep, definitely changed!', data);
}
}
var KO = new viewModel();
ko.applyBindings(KO);
setTimeout(function() {
var categories = [];
for (var i = 1; i <= 20; i++) {
//set state to true only for some elements
var state = i >= 5 && i <= 6;
categories.push(new KO.Category(i, 'category #' + i, state));
}
KO.categories(categories);
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
Uncheck any of first two checkboxes and the change event won't fire!
<ul>
<!-- ko foreach: categoriesComputed -->
<li>
<input data-bind="attr:{value: id}, checked: state,
event: {change: $root.categoryChanged}" type="checkbox">
<span data-bind="text: name"></span>
</li>
<!-- /ko -->
</ul>

Expand one element and collapse all other element in knockout js

In knockout is it possible to collapse all other opened row and expand only clicked row.
I am referring this Fiddle example for it.
view -
<ul data-bind="foreach: items">
<button data-bind="text:name"></button>
<div data-bind="visible:expanded">
<input data-bind="value:name"></input>
</div>
</ul>
viewModel -
function Sample(item) {
var self = this;
self.name = ko.observable(item.name);
self.id = ko.observable(item.id);
self.expanded = ko.observable(false);
self.toggle = function (item) {
self.expanded(!self.expanded());
};
self.linkLabel = ko.computed(function () {
return self.expanded() ? "collapse" : "expand";
}, self);
}
var viewModel = function () {
var self = this;
var json = [{
"name": "bruce",
"id": 1
}, {
"name": "greg",
"id": 2
}]
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item); // making things independent here
});
self.items = ko.observableArray(data);
};
ko.applyBindings(new viewModel());
Here its not collapsing already opened row. I tried to fetch complete items in toggle function but it did not work.
I am new to knock out. please suggest.
Update -
I tried this code to make first one extended by default -
var index=0;
var data = ko.utils.arrayMap(json, function(item) {
if(index++===0){
return new Sample(item,true);
}else{
return new Sample(item,false);
}
});
But above given code is not working as expected.
This is very common "problem" when you're working with knockout. You want to keep your Sample instances independent, while their behavior might still influence the behavior of any siblings... I usually pick one of three options:
Move the functionality that influences siblings to the parent viewmodel. For example:
var viewModel = function() {
/* ... */
self.toggle = function(sample) {
self.items().forEach(function(candidateSample) {
candidateSample.expanded(sample === candidateSample);
});
}
};
With data-bind:
<a data-bind="click: $parent.toggle"></a>
Personally, I'd go with this option. Here's it implemented in your fiddle: http://jsfiddle.net/cxzLsz56/
Pass siblings to each item:
self.items = ko.observableArray();
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item, self.items);
});
self.items(data);
And in Sample:
function Sample(item, siblings) {
self.toggle = function() {
siblings().forEach(/* collapse */);
self.expanded(true); // Expand
};
};
Create some sort of postbox/eventhub/mediator mechanism and make a Sample trigger an event. Each Sample listens to this event and collapses when another Sample triggers it.

how to render child component of only one generated child in react

In a React app, I have a component to display the results of a search called ResultsContainer. I am generating children by mapping a prop that has been passed down from a parent and creating a list element from each item in the array. Each of these children has a component that I (DetailsContainer) want to render when a button is clicked, and ONLY for the child that had the button clicked. As of now, the component renders for all of these children. I am not sure how to target the individual child so that the component only renders for that specific one.
ResultsContainer:
var React = require('react');
var DetailsContainer = require('./DetailsContainer.jsx');
var ResultsContainer = React.createClass ({
render: function () {
var queryType = this.props.queryType;
var details = this.props.details;
var that = this;
if (this.props.results == null) {
var searchResult = "Enter search terms"
} else {
var searchResult = this.props.results.map(function(result, index){
//if artist search
if (queryType == "artist") {
if (result.type == "artist") {
return <li className="collection-item" key={index}> {result.title} <button onClick={that.props.handleDetailClick}>Get details</button> <DetailsContainer details={details} /> </li> ;
}
}
});
};
return (
<div className="collection">
{searchResult}
</div>
);
},
});
module.exports = ResultsContainer;
When the button is clicked, I want to render the DetailsContainer component for that child ONLY, not all of them. How could I do this?
DetailsContainer:
var React = require('react');
var DetailsContainer = React.createClass({
render: function(){
if (this.props.details !== null) {
var detailsDisplay = "deets"
}
return (
<div className="detailsDisplay">
{detailsDisplay}
</div>
);
},
});
module.exports = DetailsContainer;
Here is handleDetailClick a function being passed down from the parent as a prop:
handleDetailClick: function() {
this.setState({details: "details"});
},
Also, in ResultsContainer, I could not figure out how to use .bind() to be able to use this.props.handleDetailClick from inside the callback of the .map, so i just created a variable "that" and set it equal to "this" outside the function. Is this bad practice? What is the proper way to do it using .bind()? Thanks so much.
First off map has an option to pass this. map(function(item){}, this); You have at least two options to limit the list. One is to use state like so;
render() {
let useList;
if (this.state.oneSelected)
useList = this.props.list[this.state.oneSelected];
else
useList = this.props.list;
let useListing = useList.map(function(item){}, this);
return ({useListing})
}
The other option is to pass the selected item to the parent this.props.selectOne(item.id) and have the parent return just the one item as a list.

Can't bind or render Rich Text from REST with Knockout

I'm getting data back and it does render Title, but can't seem to render rich text.
markup:
<div id="bodyarea">
<div data-bind=foreach:list>
<span data-bind="text:Title" />
<div data-bind="html: RichData"></div>
</div>
</div>
<p id="myarea"></p>
ko:
function LoadLists() {
var listItems = [];
var count = 0;
$.getJSON("https://myserver.com/sites/knockout/_api/lists/getbytitle('List%20One')/items?$filter=Title eq
'zzzz'",
function (data, textstatus, jqXHR) {
$(data.value).each(function (index, item) {
count++;
var koItem = {};
koItem.Title = item.Title;
koItem.RichData = item.Rich;
listItems.push(koItem);
if (data.value.length == count) {
var vm =
{
list: ko.observableArray(listItems)
};
ko.applyBindings(vm, document.getElementById("bodyarea"));
}
})
});
}
$(document).ready(function () { LoadLists(); });
In general, with knockout, you should not:
Call applyBindings more than once
Call applyBindings inside an ajax call
Create a viewModel as a plain object (usually, sometimes it's alright)
Do knockout databinding inside jQuery event handlers.
Code:
// this is a reusable view model for each item. it takes a raw item from the ajax return, and creates observables for each property.
var ListItem = function (item) {
var self = this;
self.Title = ko.observable(item.Title);
self.RichData = ko.observable(item.Rich);
}
// Your viewModel. Constructor-esque syntax is pretty standard.
var ViewModel = function () {
var self = this;
// this is your list array.
self.list = ko.observableArray();
// This is a your reusable function to load lists, when it returns, it maps each item
// in data.value to a ListItem viewModel and puts them all in the lists observableArray
self.loadList = function() {
$.getJSON('yourUrl', function(data) {
var items = data.value.map(function(item) { return new ListItem(item); });
self.list(items);
}
};
};
// When the document is ready, create a view model and apply bindings once. Then call loaLists to initialize
$(document).ready(function () {
var vm = new ViewModel();
ko.applyBindings(vm);
vm.loadList();
});

Categories

Resources