I am using Knockout.js to populate a set of HTML5 <details> elements. Here is the structure:
<div class="items" data-bind="foreach: Playlists">
<details class="playlist-details" data-bind="attr: {id: 'playlist-details-' + $index(), open: isOpen}">
<summary>
<span data-bind="text: name"></span> - <span data-bind="text: count"></span> item(s)
<div class="pull-right">
<button data-bind="click: $parent.play, css: {disabled: count() == 0}, attr: {title: playbtn_title}" class="btn"><i class="icon-play"></i> Play</button>
<button data-bind="click: $parent.deleteList" class="btn btn-danger"><i class="icon-trash"></i> Delete</button>
</div>
</summary>
<div class="list" data-bind="with: items" style="padding-top: 2px;">
...
</div>
</details>
</div>
The data in the ViewModel looks something like this:
var VM = {
Playlists: [
{
name: "My Playlist1",
count: 3,
items: [<LIST OF SONG ID'S>],
playbtn_title: "Play this playlist",
isOpen: true
},
{
name: "My Playlist2",
count: 5,
items: [<LIST OF SONG ID'S>],
playbtn_title: "Play this playlist",
isOpen: null
},
{
name: "My Playlist3",
count: 0,
items: [],
playbtn_title: "You need to add items to this list before you can play it!",
isOpen: null
}
]
};
I have added the ability to remember the open or closed state of the details view using the isOpen property of the ViewModel and an attr binding (As originally described here).
However, when I click the <summary> to expand the details, the ViewModel does not get updated - unlike value bindings, attr bindings aren't two-way.
How can I get this binding to update when the attribute value changes?
I know that the browser triggers a DOMSubtreeModified event when the element is opened or closed, but I;m not sure what I would put there - several things I have tried (including .notifySubscribers(), if (list.open()) ..., etc.) cause looping, where the property being changed makes the event trigger again, which changes the property again, which triggers the event again, etc.
Using $ to play DOM directly is not ko way :-)
Just create a two-way binding for HTML5 details tag, it's cheap in ko.
http://jsfiddle.net/gznf3/
ko.bindingHandlers.disclose = {
init: function(element, valueAccessor) {
if (element.tagName.toLowerCase() !== 'details') {
throw "\"disclose\" binding only works on <details> tag!";
}
var value = valueAccessor();
if (ko.isObservable(value)) {
$(element).on("DOMSubtreeModified", function() {
value($(element).prop('open'));
});
}
},
update: function(element, valueAccessor) {
$(element).prop('open', ko.unwrap(valueAccessor()));
}
};
The way that I found in the end that works is simply to have the DOMSubtreeModified "manually" update the value:
$(document).on('DOMSubtreeModified', 'details.playlist-details', function(e) {
var list = ko.dataFor(this);
list.open(this.getAttribute('open'));
});
(Somehow, this does not cause the looping that more-complex constructs I tried had caused.)
Related
I have a problem how to call onchanges knock js to my select option, i already have a function and html, but when i choose the select option, nothing changes
<select data-bind="event:{change:setSelectedStation() },
options: seedData,
optionsText: 'text',
optionsValue: 'value'">
</select>
here is my function
setSelectedStation: function(element, KioskId){
this.getPopUp().closeModal();
$('.selected-station').html(element);
$('[name="popstation_detail"]').val(element);
$('[name="popstation_address"]').val(KioskId);
$('[name="popstation_text"]').val(element);
// console.log($('[name="popstation_text"]').val());
this.isSelectedStationVisible(true);
},
Use knockout's two-way data-binds instead of manually subscribing to UI events.
Knockout's value data-bind listens to UI changes and automatically keeps track of the latest value for you.
Instead of replacing HTML via jQuery methods, you use text, attr and other value bindings to update the UI whenever your selection changes.
If you want to perform additional work when a selection changes (e.g. closing a pop up), you subscribe to the selected value.
var VM = function() {
this.seedData = [
{
text: "Item 1",
data: "Some other stuff"
},
{
text: "Item 2",
data: "Something else"
},
{
text: "Item 3",
data: "Another thing"
}
];
this.selectedItem = ko.observable();
this.selectedItem.subscribe(function(latest) {
console.log("Input changed");
}, this);
};
ko.applyBindings(new VM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="
value: selectedItem,
options: seedData,
optionsText: 'text'">
</select>
<!-- ko with: selectedItem -->
<p>
Your selection: <span data-bind="text: data"></span>
</p>
<!-- /ko -->
I have a problem when implementing a nested list in Angular: the view gets updated properly but, on the other side, the code is not updated on change.
I think it will be much clearer with the code:
_this.categories = injections.map(function (category) {
return {
title: category.get('title'),
object: category,
criteria: category._criteria.map(function (oneCriteria) {
return {
object: oneCriteria,
type: oneCriteria.get("type"),
min: _this.range(oneCriteria.get("range")).min,
max: _this.range(oneCriteria.get("range")).max,
key: oneCriteria.get("key"),
value: _this.range(oneCriteria.get("range")).min,
defaultValue: _this.range(oneCriteria.get("range")).min,
selected: false
}
})
}
});
_this.category = _this.categories[0];
_this.job = {
title: '',
description: '',
salaryAmount: 0,
salaryTimeUnit: _this.salaryTimeUnits[0],
category: _this.category.object,
criteria: _this.category.criteria,
location: {latitude: 48.137004, longitude: 11.575928}
};
So and, very quick here is my HTML:
<div ng-repeat="category in controller.categories">
<input type="radio" name="group" ng-value="category.object.get('title')" id="{{category.object.get('title')}}"
ng-checked="controller.category == category" ng-click="controller.category = category">
{{category.title}}
</div>
<br>
Criteria:
<div ng-repeat="criterium in controller.category.criteria">
<div class="row vertical-align">
<div class="col-xs-9">
<span ng-click="criterium.selected = !criterium.selected"
ng-class="['list-group-item', {active:criterium.selected == true}]">{{criterium.key}}</span>
</div>
</div>
</div>
The problem is the following: the value are getting updated in the view (when you click on a radio button on the category, you see the corresponding criteria(s)). But the job is for one reason that I ignore not updated although it has the same reference as the HTML (a reference to this category.criteria).
Did I miss something?
controller.job.criteria is still just a reference to controller.categories[0]. Your code should successfully update controller.category to point at whichever category you clicked on, but that does not also update the reference in your job data structure.
What you want to do is make your ngClick event a bit more robust, i.e.:
<input type="radio" ng-click="controller.updateCategory(category)" />
and then in your js:
_this.updateCategory = function (category) {
_this.category = category;
_this.updateJob(category);
};
_this.updateJob = function (category) {
_this.job.category = category.object;
_this.job.criteria = category.criteria;
};
This will update the references in your job to match the new jazz.
I would, however, recommend leveraging ngModel and ngChange in your radios instead. Like:
<input type="radio" ng-model="controller.category" ng-value="category" ng-change="updateJob(category)" /> {{category.title}}
So what exactly is the ko.observable() doing? Here's the situation. I have a boolean ko.observable(), as you can see. I have click set to that value, so it SHOULD toggle the value of the true false contained within it's method call.
When I watch the array get populated in the developer tools, I see that selected does not = true or false, it instead = a pretty extensive function, and I can't find the true or false value anywhere inside of that, so I have no idea what exactly is happening when ko.observable() is used
What I expected is for tab.selected to be the value of tabArray[tab].selected, and when the page loads, that is correct. However, after clicking, tabArray[tab].selected = [Object object] when the text value is written out. I attempt to use:
<pre data-bind="text: JSON.stringify(ko.toJS(tab.selected)"></pre>
(found here: http://www.knockmeout.net/2013/06/knockout-debugging-strategies-plugin.html) and that prints out either true or false, do I need to do this for the other places where i need that value? Because I'm not sure exactly what ko.observable is doing.
define(['knockout', 'text!../Content/SSB/PartialViews/MainContent.html'], function (ko, MCTemplate) {
ko.components.register('MainContent', {
template: MCTemplate
});
var MainViewModel = {
tabArray: [
{ name: 'bob', selected: ko.observable(true) },
{ name: 'bib', selected: ko.observable(false) },
{ name: 'bab', selected: ko.observable(false) },
{ name: 'bub', selected: ko.observable(false) },
{ name: 'beb', selected: ko.observable(false) },
]
};
ko.applyBindings(MainViewModel);
return {
viewModel: MainViewModel
}
});
the HTML
<div id="tab">
<ul class="nav nav-tabs" role="tablist">
<!--ko foreach: {data: $parent.tabArray, as: 'tab'}-->
<li data-bind="click: tab.selected, css: { 'active': tab.selected}">
<a data-bind="attr: {href: '#' + tab.name}, text: name"></a>
<div data-bind="text: tab.name"></div>
<div data-bind="text: tab.selected"></div>
</li>
<!--/ko-->
</ul>
<!--ko foreach: {data: $parent.tabArray, as: 'tab'}-->
<div class="ui-tabpanel" role="tabpanel" data-bind="visible: tab.selected">
<p data-bind="text: name"></p>
</div>
<!--/ko-->
</div>
The click binding calls the provided function, passing it the current view model (also called $data). That's why you see [Object object] as the observable's value after the click. Since you want the click to toggle the observable, you need to create a function to do that. A nice, clean way to do this is through a custom binding, which I'll call toggle:
ko.bindingHandlers.toggle = {
init: function(element, valueAccessor) {
ko.utils.registerEventHandler(element, 'click', function () {
var obs = valueAccessor();
obs(!obs());
});
}
};
Now you bind using toggle instead of click: <li data-bind="toggle: tab.selected...
I am using Knockout.js to populate a set of HTML5 <details> elements. Here is the structure:
<div class="items" data-bind="foreach: Playlists">
<details class="playlist-details" data-bind="attr: {id: 'playlist-details-' + $index()}">
<summary>
<span data-bind="text: name"></span> - <span data-bind="text: count"></span> item(s)
<div class="pull-right">
<button data-bind="click: $parent.play, css: {disabled: count() == 0}, attr: {title: playbtn_title}" class="btn"><i class="icon-play"></i> Play</button>
<button data-bind="click: $parent.deleteList" class="btn btn-danger"><i class="icon-trash"></i> Delete</button>
</div>
</summary>
<div class="list" data-bind="with: items" style="padding-top: 2px;">
...
</div>
</details>
</div>
The data in the ViewModel looks something like this:
var VM = {
Playlists: [
{
name: "My Playlist1",
count: 3,
items: [<LIST OF SONG ID'S>],
playbtn_title: "Play this playlist"
},
{
name: "My Playlist2",
count: 5,
items: [<LIST OF SONG ID'S>],
playbtn_title: "Play this playlist"
},
{
name: "My Playlist3",
count: 0,
items: [],
playbtn_title: "You need to add items to this list before you can play it!"
}
]
};
I want to add the ability to remember the open or closed state of the details view. I have implemented this behavior previously using jQuery and localStorage1, but for this project I want to use Knockout natively instead of using jQuery.
I have added an isOpen property to the playlists in the ViewModel which is retrieved from localStorage when the page loads. However, it seems that I can't use the attr binding in Knockout because the HTML5 spec says to look only for the presence or absence of the open attribute, not for a value.
How would I get Knockout to add and remove the open property of the <details> element as the isOpen property of the ViewModel changes?
1: Like this:
// On the initial page load.
contents += '<details ' + ((localStorage['tl_open_playlist-details-' + counter] == 1) ? 'open' : '') ' class="playlist-details" id="playlist-details-' + counter + '" data-name="' + escape(listname) + '">'
...
// Update storage when things are clicked.
$(document).on('DOMSubtreeModified', 'details.playlist-details', function() {
if ($(this).prop('open')) {
localStorage['tl_open_' + this.id] = 1;
} else {
delete localStorage['tl_open_' + this.id];
}
});
You can modify the attr binding to take into account another binding option (named attrRemoveWhenFalse here) and remove the attribute for you:
<input class='testInput' type="text"
data-bind="attr: { disabled: isDisabled }, attrRemoveWhenFalse: true" />
var originalAttr = { init: ko.bindingHandlers.attr.init,
update: ko.bindingHandlers.attr.update }
ko.bindingHandlers.attr.update = function (element, valueAccessor,
allBindingAccessor, viewModel,
bindingContext) {
if (typeof originalAttr.update === 'function')
originalAttr.update(element, valueAccessor, allBindingAccessor,
viewModel, bindingContext);
if (allBindingAccessor().attrRemoveWhenFalse) {
for (var prop in valueAccessor()) {
if (!ko.utils.unwrapObservable(valueAccessor()[prop])) {
element.removeAttribute(prop);
}
}
}
}
Demo
The menu is what I want, when mouse over the left, the right should changes but doesn't.
Here is my simplified viewmodel:
var currentSelectIndex = 0;
var AppModel = {
CurrentIndex: ko.observable(currentSelectedIndex),
OnMouseOver: function (data, event) {
// change currentIndex or currentSelectedIndex here
// CurrentSubCategory didn't updated
},
CurrentSubCategory: ko.computed({
read: function() {
return AppModel.Menu[AppModel.CurrentIndex()].subcategory;
},
deferEvaluation: true
}),
Menu: [
{
subcategory: [
{ name: '1', id: 50000436 },
{ name: '2', id: 50010402 },
{ name: '3', id: 50010159 }
],
}
};
And my html:
<div class="categories" id="categories">
<div class="first-category" id="first-category">
<ul data-bind="foreach:Menu">
<li data-bind="text:name,attr:{id:id,class:className},event{ mouseover: $root.myfunction}"></li>
</ul>
</div>
<div class="sub-category" id="sub-category">
<ul data-bind="foreach:CurrentSubCategory()">
<li><a data-bind="text:name,attr:{href:getListUrl(id)}"></a></li>
</ul>
<div class="clear">
</div>
</div>
<div class="clear">
</div>
</div>
Sorry, can't post images due to less than 10 reputation.
Thanks for any help!
There were several syntax errors in your code which I imagine are a result of your making it simpler to post.
I have posted a working jsFiddle here: http://jsfiddle.net/Gy6Gv/2/
I changed Menu to be an observable array only because knockout provides the helper method .indexOf to make it easier to get the index of the menu from the mouseover. Other than that there was no problem with the computed. I imagine there is some other syntactical error in your actual code.