Observable is changed only for one viewModel - javascript

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 -->

Related

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.

knockout.js 3.3 - rerendering component in foreach binding

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.

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();
});

Change Views Content based on different modules event

My content box module enumerates a collection and creates a container view for each item ( passing the model to the view). It sets the initial content to the content property of its model. Base on a layout property in the model the container view is attached to the DOM. This is kicked off by the “_contentBoxCreate” method.
The content box module responds to clicks to sub items in a sidemenu. The sidemenu is implemented in a different module. The sidemenu sub click event passes an object along as well that contains a sub_id and some text content. I want to take the content from this object and use it to update container view(s).
Currently I’m doing this via the “_sideMenuClick” method. In backbonejs is there a best practice for updating a views content, given that no data was changed on its model?
thanks,
W.L.
APP.module("contentbox", function(contentbox) {
//Model
var Contentbox = Backbone.Model.extend({});
//Collection
var Contentboxes = Backbone.Collection.extend({
model: Contentbox,
url: 'ajax/contentboxResponse/tojson'
});
/*
* View:
*/
var Container = Backbone.View.extend({
initialize: function() {
contentbox.on('update', jQuery.proxy(this.update, this));
contentbox.on('refresh', jQuery.proxy(this.render, this));
var TemplateCache = Backbone.Marionette.TemplateCache;
this.template = TemplateCache.get("#contentbox-container");
},
render: function() {
var content = this.model.get('content').toString();
var html = this.template({content: content});
this.$el.html(html);//backbone element
return this;
},
update: function(fn) {
var content = fn.apply(this);
if (content !== null) {
var html = this.template({content: content});
this.$el.html(html);
}
}
});
//collection
var contentboxes = new Contentboxes();
var _sideMenuToggle = function() {
contentbox.trigger('refresh');
};
var _sideMenuClick = function(sideMenu) { //view contex
var fn = function() {
// this fn will have the context of the view!!
var linksub = this.model.get('linksub').toString();
if (linksub === sideMenu.id.toString()) {
return sideMenu.content.toString();
}
return null;
};
contentbox.trigger('update', fn);
};
var _contentBoxCreate = function() {
var create = function(cboxes) {
cboxes.each(function(model) {
var layout = "#body-" + model.get('layout');
var $el = jQuery(layout);
var container = new Container({model: model});
$el.append(container.render().$el);
});
};
contentboxes.fetch({
success: create
});
};
this.on("start", function() {
_contentBoxCreate();
});
this.addInitializer(function() {
APP.vent.on('sidemenu:toggle', _sideMenuToggle);
APP.reqres.setHandler('sidemenu:submenu', _sideMenuClick);//event and content...
//from another module
});
});
UPDATE:
Changed the view...
/*
* View
*/
var Container = Backbone.View.extend({
initialize: function() {
this.renderableModel = this.model; // Define renderableModel & set its initial value
contentbox.on('update', this.update, this);
contentbox.on('refresh', this.reset, this); // 3rd param gives context of view
var TemplateCache = Backbone.Marionette.TemplateCache;
this.template = TemplateCache.get("#contentbox-container");
},
render: function() {
var content = this.renderableModel.get('content').toString();
var html = this.template({content: content});
this.$el.html(html);//backbone element
return this;
},
update: function(fn) {
/**
* The "update" event is broadcasted to all Container views on the page.
* We need a way to determine if this is the container we want to update.
* Our criteria is in the fn
*/
var content = fn.apply(this); //criteria match return content, else null.
/*
* The render method knows how to render a contentbox model
*/
if (content !== null) {
this.renderableModel = new Contentbox();
this.renderableModel.set({content: content}); //add content to new contentbox model
this.render(); //Rerender the view
}
},
reset: function() {
this.renderableModel = this.model;
this.render(); // restore view to reflect original model
}
});

Categories

Resources