Knockout.js: access parent collection in data-bind event - javascript

Let's say I have a
<button type="button" data-bind="click: actions.remove">×</button>
and a handler
var actions = {
remove: function(item) {
?array?.remove(item); // ?array? is a containing array, accessed somehow
}
}
How do I find ?array? so I can use the same button in any foreach binding?
Clarification:
I know how to do that if I put remove into the view model. However the view model contains hierarchical arrays and I do not really want to go through it all just to get methods in the right places. View model is also updated from server occasionally with the help of ko.mapping, but that does not add any methods to the new data. That is why I implemented the handlers separately.

You try something like this.
<div data-bind="foreach: someArray">
<button type="button" data-bind="click: $parent.actions.remove">x</button>
</div>
//Inside your viewmodel.
var self = this;
self.someArray = ko.observableArray();
self.actions = {
remove: function() {
self.someArray.remove(this); // ?array? is a containing array, accessed somehow
}
}
edit: Sorry, I misread what you meant. You can try something like this to make it work for any foreach binding.
<div data-bind="foreach: someArray">
<button type="button" data-bind="click: function() {$parent.actions.remove($parent.someArray(), $data}">x</button>
</div>
//Inside your viewmodel.
var self = this;
self.someArray = ko.observableArray();
self.actions = {
remove: function(arr, item) {
arr.remove(item); // ?array? is a containing array, accessed somehow
}
}

It is not currently possible.
I raised a new knockout issue for that (currently open):
Allow access to the current array through the binding context.
Also relevant: Support for $last in foreach.

Related

Better way to displaying data from view model using knockoutjs?

I have a view model with a separate hierarchy and during, say a click event, I'd like to display a modal dialog of the data from the secondary hierarchy for the "clicked" data item. To make this a little easier to follow, I have mocked up an example in jsfiddle that achieves the desired result (without a modal for simplicity), but it's done by repeating the same markup instead of modifying the data from a single group of markup.
var FoundingFathersViewModel = function(data) {
var self = this;
self.foundingFathers = ko.observableArray([]);
//click
self.detail = function(father) {
//get the selected Founding Father's positions HTML and
//set the HTML of the detail div
var html = $('#'+father.id()).html();
$('#detail').html(html);
};
var mapping = $.map(data, function(item) { return new FoundingFather(item); });
self.foundingFathers(mapping);
};
var FoundingFather = function(item) {
this.id = ko.observable(item.id);
this.name = ko.observable(item.name);
this.positions = ko.observableArray(item.positions);
};
ko.applyBindings(new FoundingFathersViewModel(data));
The jsfiddle code simply modifies the CSS display property to display the correct detail. I'd like to think there would be a "cleaner" way to do this. Any help would be appreciated. And if there's not a more elegant solution, I'd like to know that too.
https://jsfiddle.net/jvz6gktm/2/
I'd suggest moving towards a selectedItem approach. Clicking on a president sets that one as the selected item, which is what populates the detail section.
see my updated fiddle:
https://jsfiddle.net/jvz6gktm/5/
detail area:
<div id="detail" data-bind="with: selectedPresident">
<div class="positions">
<div data-bind="foreach: positions" class="detail">
<h5 data-bind="html: position" />
<h6 data-bind="html: yearsActive" />
</div>
</div>
</div>
selection:
self.selectedPresident = ko.observable();
self.selectPresident = function(father) {
self.selectedPresident(father);
};
As a side note: if you're using knockout, don't use jQuery to go messing with the DOM, you're just asking for trouble.

How can I watch a filtered collection in Angular.JS?

I'm trying to make an event fire whenever a filtered collection is changed. The filtered list is attached to the non-filtered list in ng-repeat.
<tr ng-repeat="item in $scope.filtered = (vm.list | filter:vm.searchText) | limitTo:vm.limit:vm.begin">
And here's my event I want to fire:
$scope.$watchCollection('filtered', function () {
alert($scope.filtered.length);
}, true);
It fires once when the page first loads, before my ajax call populates vm.list, so the alert says 0, but then it should fire again after vm.list gets populated, and every time a change to vm.searchText causes a change to $scope.filtered, but it's not.
I also tried making the $watchCollection method like this:
$scope.$watchCollection('filtered', function (newList, oldList) {
alert(newList.length);
});
But that had the same result.
I also tried doing as is suggested here, and it ended up like this:
<tr ng-repeat="item in catchData((vm.list | filter:vm.searchText)) | limitTo:vm.limit:vm.begin">
$scope.catchData = function (filteredData) {
alert(filteredData.length);
return filteredData;
}
That seemed like it fixed it at first. It now fired when the API call populated the list, and fired again whenever the searchText caused the filtered list to change. Unfortunately it made it so changing the begin option on the limitTo filter no longer worked. Changing the limit option still worked, but not the begin. Changing the begin does still work with the $watchCollection method.
Does anyone have any ideas?
When you create some variables in view, it added as property to current scope. So, in your case you create $scope.filtered, and this added to current scope.
To get it in watch, you just need use same declaration
$scope.$watchCollection('$scope.filtered', function () {
console.log($scope.$scope.filtered.length)
}
But better not use variable name like $scope, so as not to confuse them with angular variables.
so, you can change it ro simple: filtered
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.$watchCollection('$scope.filtered', function(nval) {
if(!nval) return; //nval - new value for watched variable
console.log('as $scope.filtered in view', $scope.$scope.filtered.length);
}, true);
$scope.$watchCollection('filtered', function(nval) {
if(!nval) return; //nval - new value for watched variable
console.log('as filtered in view', $scope.filtered.length);
}, true);
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<input type="text" data-ng-model="search" />
<h3>as $scope.filtered</h3>
<div ng-repeat="item in $scope.filtered = ([11,12,23]| filter:search)">item_{{item}} from {{$scope.filtered}}</div>
<h3>as filtered</h3>
<div ng-repeat="item in filtered = ([11,12,23]| filter:search)">item_{{item}} from {{filtered}}</div>
</div>
you will want to use a function to return the filtered list and set object equality to true.
$scope.$watch(function () {
return $scope.filtered;
}, function (newList) {
alert(newList.length);
}, true);

Is it possible to defer a computed observable until certain UI elements become visible?

I have a computed observable in my model which looks like this:
this.TrainingPlanTemplates = ko.computed(function ()
{
var workgroups = model.WorkgroupsImpacted();
var areas = model.AreasImpacted();
var plans = model.PrescribedPlan();
$(plans).each(function (i, v)
{
// A bunch of stuff that really slows everything down
});
// ...
}
I then have a UI template:
<table>
<!-- ko foreach: TrainingPlanTemplates -->
<tr> ... Various columns bound to TrainingPlanTemplates properties ... </tr>
<!-- /ko -->
</table>
The issue is, the HTML template above contains various custom binding handlers and potentially has a large amount of data. Rendering this table is somewhat slow (like 5 seconds or so). This UI uses the jQuery UI tabs control, so I don't even show the data when the page loads. Most users will never even switch to that tab, meaning I'm usually wasting my time binding that data.
Question: Is there a way to defer the binding of a computed observable until I say so, for example, until a certain jQuery tab becomes active?
Ideas:
I got a few ideas from this page. There does exist a deferEvaluation property, however this will only defer the property until something accesses it, which will happen immediately as a hidden HTML table is still bound to the data.
One idea would be to create a new observable property called TrainingPlanTemplatesLoaded, and set that to true when the tab becomes active. Then, create a dependency between TrainingPlanTemplates and TrainingPlanTemplatesLoaded so that when TrainingPlanTemplatesLoaded changes, TrainingPlanTemplates actually loads in the real data.
Any other ideas on the best way to accomplish this?
Yes, just making another observable that you check before doing your computation:
// set to true when you want the computation to run
this.TrainingPlanTemplatesLoaded = ko.observable(false);
this.TrainingPlanTemplates = ko.computed(function ()
{
if (this.TrainingPlanTemplatesLoaded()) {
var workgroups = model.WorkgroupsImpacted();
var areas = model.AreasImpacted();
var plans = model.PrescribedPlan();
$(plans).each(function (i, v)
{
// A bunch of stuff that really slows everything down
});
// ...
}
}, this);
Sure its possible, see my example:
function VM(){
var self = this;
self.FirstName = ko.observable("John");
self.LastName = ko.observable("Smith");
self.canShow = ko.observable(false);
self.FullName = ko.computed(function(){
if (self.canShow()){
return self.FirstName() + " " + self.LastName();
}
});
}
myVm = new VM();
ko.applyBindings(myVm);
// Represents that at some point
// Some function make something happen
setTimeout(function(){
// Let's say we check if an element was visible
// or check anything that we want to know has happened, then:
myVm.canShow(true);
}, 4000);
<p data-bind="text: FirstName"></p>
<p data-bind="text: LastName"></p>
<p data-bind="text: FullName"></p>

Creating Bootstrap tabs using Knockout.js foreach

I'm trying to create some tabs, one per profile the user chooses to save. Each profile is a ViewModel. So I thought I'd just create another ViewModel that contains an observableArray of objects of type: {name: profile_name, model: model_converted_to_json}.
I followed this example to create my code - but I get nothing bound, for some reason.
Here's my code:
-ViewModel (I use Requirejs, that explains the external wrapper):
"use strict";
// profiles viewmodel class
define(["knockout"], function(ko) {
return function() {
var self = this;
this.profilesArray = ko.observableArray();
this.selected = ko.observable();
this.addProfile = function(profile) {
var found = -1;
for(var i = 0; i < self.profilesArray().length; i++) {
if(self.profilesArray()[i].name == profile.name) {
self.profilesArray()[i].model = profile.model;
found = i;
}
}
if(found == -1) {
self.profilesArray().push(profile);
}
};
};
});
-The JS code (excerpt of larger file):
var profiles = new profilesViewMode();
ko.applyBindings(profiles, $("#profileTabs")[0]);
$("#keepProfile").on("click", function() {
var profile = {
name: $("#firstName").text(),
model: ko.toJSON(model)
};
profiles.addProfile(profile);
profiles.selected(profile);
console.log(profile);
$("#profileTabs").show();
});
-The HTML (Thanks Sara for correcting my HTML markup)
<section id="profileTabs">
<div>
<ul class="nav nav-tabs" data-bind="foreach: profilesArray">
<li data-bind="css: { active: $root.selected() === $data }">
</li>
</ul>
</div>
</section>
I have verified that the observableArray does get new, correct value on button click - it just doesn't get rendered. I hope it's a small thing that I'm missing in my Knockout data-bind syntax.
Thanks for your time!
You will want to call push directly on the observableArray, which will both push to the underlying array and notify any subscribers. So:
self.profilesArray.push(profile);
You are setting name using name: $('#firstName').text(); you may need to change that to .val() if this is referencing an input field (which I assumed here).
You are using .push() on the underlying array which bypasses ko's subscribers (the binding in this case)
Here is a working jsfiddle based on your code. I took some liberties with model since that wasn't included.

How do I correctly reference an attribute of the current element passed into knockout.JS?

Working on a little feedback form and I'm new at the Knockout/jQuery game so I'm sure this is a syntax error.
Goal / Background
I have a feedback form, part of which includes a list with feedback types. The actual text of the feedback type I'd like to use is stored in the "Title" attribute of the LI tags.
I'd like to pass an onclick from each of a set of LI tags denoting the type of feedback.
I would like knockout to receive this onclick event with the calling element
I'd like the ViewModel function to update the ViewModel's feedback type based on the content of the LI's title attribute
I'd then like to remove a class from all the list and apply it to the selected element.
I already have jQuery that does this; just want to incorporate it into the model change.
What I Have So Far
The relevant part of the HTML Feedback Form (the UL list):
<ul class="thumbnails" id="feedbackList">
<li class="feedbackItem" id="feedbackItemPraise" title="Praise" data-bind="click: updateFeedbackType"><i class="icon-thumbs-up"></i>Praise</li>
<li class="feedbackItem" id="feedbackItemCriticism" title="Criticism" data-bind="click: updateFeedbackType"><i class="icon-thumbs-down"></i>Criticism</li>
<li class="feedbackItem" id="feedbackItemProblem" title="Problem" data-bind="click: updateFeedbackType"><i class="icon-warning-sign"></i>Problem</li>
<li class="feedbackItem" id="feedbackItemQuestion" title="Question" data-bind="click: updateFeedbackType"><i class="icon-question-sign"></i>Question</li>
</ul>
The ViewModel so far (including some irrelevant parts):
var FeedbackViewModel = function () {
var self = this;
self.manualEMailAddress = "MyEmail#MyProvider.com";
self.manualApplicationName = "MyApplication";
self.username = ko.observable($("#feedbackUsernameFromServer").val());
self.feedbackType = ko.observable("Praise");
self.wantsFollowUp = ko.observable(true);
self.enteredName = ko.observable("");
self.feedbackText = ko.observable("");
self.userNameCaptured = ko.computed(function () { return self.username().length > 3; }, self);
self.mailToLink = ko.computed(function () { return "mailto:" + self.manualEMailAddress + "?subject=" + encodeURIComponent(self.feedbackType()) + encodeURIComponent(" for ") + encodeURIComponent(self.manualApplicationName) + "&body=" + encodeURIComponent(self.feedbackText()) }, self);
};
var feedbackViewModel = new FeedbackViewModel();
ko.applyBindings(feedbackViewModel, document.getElementById("feedbackModal"));
The current jQuery to change the style (not linked to the model yet):
$("#feedbackList li").click(function () {
$("#feedbackList li.feedbackItem-Highlighted").removeClass("feedbackItem-Highlighted");
$(this).addClass("feedbackItem-Highlighted");
});
What I think I need to add to the ViewModel, but doesn't quite work:
self.updateFeedbackType = function (elementToChangeTo) {
self.feedbackType($(elementToChangeTo).attr("title"));
$("#feedbackList li.feedbackItem-Highlighted").removeClass("feedbackItem-Highlighted");
$(elementToChangeTo).addClass("feedbackItem-Highlighted");
};
This results in feedbackType being turned into an undefined and the visual change not happening.
Where am I going wrong? Thanks for any help!
I think you just needed that function in the definition of the vm.
Here's a jsfiddle that seems to work:
http://jsfiddle.net/gN3HV/
Update: Here's a fiddle which better leverages knockout and properly accomplishes the goal:
http://jsfiddle.net/gN3HV/7/
elementToChangeTo returns the FeedbackViewModel (same as this) and not the element clicked on--the behavior is a bit different than jQuery.
The second argument passed into updateFeedbackType will be an event, so you could use $(event.target) to get a reference to the clicked element.
self.updateFeedbackType = function (view, event) {
var $elementToChangeTo = $(event.target);
self.feedbackType($elementToChangeTo.attr("title"));
$("#feedbackList li.feedbackItem-Highlighted").removeClass("feedbackItem-Highlighted");
$elementToChangeTo.addClass("feedbackItem-Highlighted");
};
However, #daedalus28 has addressed the larger problem, which is that you're not utilizing knockout.js's strengths and are over-complicating the process. You don't really need both to solve this simplistic condition.

Categories

Resources