subscribing an observableArray to a computed - javascript

I have an object that is constructed upon a table row from the database. It has all the properties that are found in that entry plus several ko.computed that are the middle layer between the entry fields and what is displayed. I need them to be able translate foreign keys for some field values.
The problem is the following: One of the properties is an ID for a string. I retrieve that ID with the computed. Now in the computed will have a value that looks like this: 'option1|option2|option3|option4'
I want the user to be able to change the options, add new ones or swap them around, but I also need to monitor what the user is doing(at least when he adds, removes or moves one property around). Hence, I have created an observable array that I will bind in a way that would allow me to monitor user's actions. Then the array will subscribe to the computed so it would update the value in the database as well.
Some of the code:
function Control(field) {
var self = this;
self.entry = field; // database entry
self.choices = ko.observableArray();
self.ctrlType = ko.computed({
read: function () {
...
},
write: function (value) {
if (value) {
...
}
},
owner: self
});
self.resolvedPropriety = ko.computed({
read: function () {
if (self.ctrlType()) {
var options = str.split('|');
self.choices(createObservablesFromArrayElements(options));
return str;
}
else {
return '';
}
},
write: function (value) {
if (value === '') {
//delete entry
}
else {
//modify entry
}
},
deferEvaluation: true,
owner: self
});
self.choices.subscribe(function (newValue) {
if (newValue.length !== 0) {
var newStr = '';
$.each(newValue, function (id, el) {
newStr += el.name() + '|';
});
newStr = newStr.substring(0, newStr.lastIndexOf("|"));
if (self.resolvedPropriety.peek() !== newStr) {
self.resolvedPropriety(newStr);
}
}
});
self.addChoice = function () {
//user added an option
self.choices.push({ name: ko.observable('new choice') });
};
self.removeChoice = function (data) {
//user removed an option
if (data) {
self.choices.remove(data);
}
};
...
}
This combination works, but not as I want to. It is a cyclic behavior and it triggers too many times. This is giving some overload on the user's actions because there are a lot of requests to the database.
What am I missing? Or is there a better way of doing it?

Quote from knockout computed observable documentation
... it doesn’t make sense to include cycles in your dependency chains.
The basic functionality I interpreted from the post:
Based on a field selection, display a list of properties/options
Have the ability to edit said property/option
Have the ability to add property/option
Have the ability to delete property/option
Have the ability to sort properties/options (its there, you have to click on the end/edge of the text field)
Have the ability to save changes
As such, I have provided a skeleton example of the functionality, except the last one, you described #JSfiddle The ability to apply the changes to the database can be addressed in several ways; None of which, unless you are willing to sacrifice the connection overhead, should include a computed or subscription on any changing data. By formatting the data (all of which I assumed could be collected in one service call) into a nice nested observable view model and passing the appropriate observables around, you can exclude the need for any ko.computed.
JS:
var viewModel = {
availableFields : ko.observableArray([
ko.observable({fieldId: 'Field1',
properties: ko.observableArray([{propertyName: "Property 1.1"}])}),
ko.observable({fieldId: 'Field2',
properties: ko.observableArray([{propertyName:"Property 2.1"},
{propertyName:"Property 2.2"}])})]),
selectedField: ko.observable(),
addProperty: function() {
var propertyCount = this.selectedField().properties().length;
this.selectedField().properties.push({propertyName: "Property " + propertyCount})
},
};
ko.applyBindings(viewModel);
$("#field-properties-list").sortable({
update: function (event, ui) {
//jquery sort doesnt affect underlying array so we have to do it manually
var children = ui.item.parent().children();
var propertiesOrderChanges = [];
for (var i = 0; i < children.length; ++i) {
var child = children[i];
var item = ko.dataFor(child);
propertiesOrderChanges.push(item)
}
viewModel.selectedField().properties(propertiesOrderChanges);
}
});
HTML:
<span>Select a field</span>
<select data-bind='foreach: availableFields, value: selectedField'>
<option data-bind='text: $data.fieldId, value: $data'></option>
</select>
<div style="padding: 10px">
<label data-bind='text: "Properties for " + selectedField().fieldId'></label>
<button data-bind='click: $root.addProperty'>Add</button>
<ul id='field-properties-list' data-bind='foreach: selectedField().properties'>
<li style = "list-style: none;">
<button data-bind="click: function() { $root.selectedField().properties.remove($data) }">Delete</button>
<input data-bind="value: $data.propertyName"></input>
</li>
</ul>
</div>

Related

Knockout.js: how to dynamically assign id with foreach data-bind?

I have the following code in html:
<ul data-bind="template: {name:'location', foreach:locations}">
</ul>
<script type="text/html" id="location">
<li>
<a href='#' id="search_results" data-bind='text: title' class='w3-bar-item'></a>
</li>
</script>
and the following code in viewModel:
var locations = [ (location lists)
];
var viewModel = {
title: ko.observable("Attractions in Seattle, Washington"),
query: ko.observable(""),
};
viewModel.locations = ko.dependentObservable(function(){
var search = this.query().toLowerCase();
return ko.utils.arrayFilter(locations, function(location) {
return location.title.toLowerCase().indexOf(search) >= 0;
});
}, viewModel);
ko.applyBindings(viewModel);
as shown below:
demo
and there is the following code in one of my regular javascript functions
$("#search_results").on('click', function() {
var context = ko.contextFor(this);
for (var i = 0; i < placeMarkers.length; i++) {
temp = placeMarkers[i].title + ", Seattle";
if (temp == context.$data.title) {
getPlacesDetails(placeMarkers[i], placeInfoWindow);
}
}
});
I am trying to dynamically show the result based on what context the user clicks, but my function works only for the first item in the list (only Space Needle, in this case). How can I fix it? what would be knockout.js-ic way?
+
I wrote like this inside of viewModel:
show_infowindow: function() {
var context = ko.contextFor(this);
for (var i = 0; i < placeMarkers.length; i++) {
temp = placeMarkers[i].title + ", Seattle";
if (temp == context.$data.title) {
getPlacesDetails(placeMarkers[i], placeInfoWindow);
}
}
}
where
<a href='#' data-bind='text: title, click: show_infowindow' class='search_results w3-bar-item'></a>
and now nothing is working, how can I fix this?
I suggest you create viewModel function and use the new operator whenever you have a click function or a computed property (or dependentObservable prior to ko 2.0). This will reduce the pain of debugging and understanding what this means in callbacks.
So remove the jquery click event handler and change your viewmodel to:
var viewModel = function() {
var self = this;
self.title = ko.observable("Attractions in Seattle, Washington");
self.query = ko.observable("");
self.locations = ko.computed(function(){
var search = self.query().toLowerCase();
return ko.utils.arrayFilter(locations, function(location) {
return location.title.toLowerCase().indexOf(search) >= 0;
});
}
self.show_infowindow = function(location){
// "location" parameter has the current location object being clicked
// you can use it directly instead of ko.contextFor(this);
}
};
// don't forget the "new" keyword
ko.applyBindings(new viewModel());
Change your template to add a click binding like this:
<script type="text/html" id="location">
<li>
<a href='#' id="search_results" data-bind='text: title, click:$parent.show_infowindow' class='w3-bar-item'></a>
</li>
</script>
Since you are using the click binding inside a foreach, you need to prefix the click function with $parent keyword to get the proper binding context. Without $parent, knockout will look for show_infowindow in each location object instead of your viewModel.
Here's another useful answer on the differences between viewModel as an object literal vs a function

Knockout multiple dynamic text boxes with one observable array

I am trying to save the data from the text-boxes to the localStorage using knockout JS! However I am new and not able to figure out this particular scenario. The field has same observable name! Please find my code below.
HTML Code:
<form data-bind="foreach: trialData">
<input type="text" name="name" data-bind="textInput: myData"><br>
</form>
JS Code:
var dataModel = {
myData: ko.observable('new'),
dataTemplate: function (myData) {
var self = this;
self.myData = ko.observable(myData);
}
};
dataModel.collectedNotes = function () {
var self = this;
self.trialData = ko.observableArray([]);
for (var i=0; i<5; i++) {
self.trialData.push (new dataModel.dataTemplate());
}
};
dataModel.collectedNotes();
ko.applyBindings(dataModel);
Traget: The data entered inside the text-boxes should be available in localStorage.
You need to define a Handler function to read the data from the Textboxes and save it to the localstorage. You need to reference the Data which is bound to the click event, which can be accessed using the first parameter. Knockout passes the data and event information as 2 arguments to the click handler function. So, you can add the event handler to your viewModel using the click binding and then unwrap the value and save it to localStorage.
saveToLocalStorage : function(data){
var datatoStore = JSON.stringify(data.trialData().map(x=>x.myData()));
console.log(datatoStore);
localStorage.setItem("TextBoxValue", datatoStore);
}
Complete Code: Please note since this is a sandboxed environment (Running this js Snippet on StackOverflow), localStorage wouldn't work, but it should work in your code. I have added a line in console to get the value to Store.
var dataModel = {
myData: ko.observable('new'),
dataTemplate: function (myData) {
var self = this;
self.myData = ko.observable(myData);
},
saveToLocalStorage : function(data){
var datatoStore = JSON.stringify(data.trialData().map(x=>x.myData()));
console.log(datatoStore);
localStorage.setItem("TextBoxValue", datatoStore);
}
};
dataModel.collectedNotes = function () {
var self = this;
self.trialData = ko.observableArray([]);
for (var i=0; i<5; i++) {
self.trialData.push (new dataModel.dataTemplate());
}
};
dataModel.collectedNotes();
ko.applyBindings(dataModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<form data-bind="foreach: trialData">
<input type="text" name="name" data-bind="textInput: myData"><br>
</form>
<button data-bind="click:saveToLocalStorage">Save To local storage</button>

How to get all data-* attributes by Prefix

I have a tag like this:
Link
When I click this link, I have a function like this
$('#ssd').click(function (event) {
var customData;
// Code to get all the custom data in format like data-info*
});
Note, the data-info* like attributes could be any number, that means you could see 1 one of them, named data-info1, or there of them, named data-info1, data-info2, data-info3.
How would I do that, I looked up the JQuery selectors, something like Attribute Starts With Selector [name^="value"] won't work because the variation here is on name...
If I console.log($('#ssd').data()); I will get an object with extra attributes that I don't need, toggle: "popover", bs.popover: Popover
Any suggestions?
This is what I did:
dataFullList = $(this).data();
$.each(dataFullList, function (index, value) {
if (index !== "toggle" && index !== "bs.popover") {
item.name = value.split(":")[0];
item.number = value.split(":")[1];
dataIWant.push(item);
}
});
So I will get a dataIWant array without stuff I don't need.
Target all elements which data-* starts with
Custom jQuery selector selector:dataStartsWith()
Here's a custom jQuery selector that will help you to:
Given the data-foo-bar prefix , target the following elements:
data-foo-bar
data-foo-bar-baz
but not:
data-foo-someting
data-something
jQuery.extend(jQuery.expr[':'], {
"dataStartsWith" : function(el, i, p, n) {
var pCamel = p[3].replace(/-([a-z])/ig, function(m,$1) { return $1.toUpperCase(); });
return Object.keys(el.dataset).some(function(i, v){
return i.indexOf(pCamel) > -1;
});
}
});
// Use like:
$('p:dataStartsWith(foo-bar)').css({color:"red"});
// To get a list of data attributes:
$('p:dataStartsWith(foo-bar)').each(function(i, el){
console.log( el.dataset );
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p data-foo-bar="a">I have data-foo-bar</p>
<p data-foo-bar-baz="b" data-extra="bbb">I have data-foo-bar-baz</p>
<p data-bar="a">I have data-bar DON'T SELECT ME</p>
<p data-something="b">I have data-something DON'T SELECT ME</p>
Custom jQuery Method $().dataStartsWith()
$.fn.dataStartsWith = function(p) {
var pCamel = p.replace(/-([a-z])/ig, function(m,$1) { return $1.toUpperCase(); });
return this.filter(function(i, el){
return Object.keys(el.dataset).some(function(v){
return v.indexOf(pCamel) > -1;
});
});
};
$('p').dataStartsWith("foo-bar").css({color:"red"});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p data-foo-bar="a">I have data-foo-bar</p>
<p data-foo-bar-baz="b" data-extra="bbb">I have data-foo-bar-baz</p>
<p data-bar="a">I have data-bar DON'T SELECT ME</p>
<p data-something="b">I have data-something DON'T SELECT ME</p>
This function will get the data-info attributes and put them into an array:
function getDataInfo($element, i, a) {
var index = i || 1, array = a || [],
info = $element.data('info' + index);
if(info === undefined) {
return array;
}
array['info' + index] = info;
return getDataInfo($element, index + 1, array);
}
$(function() {
console.log(getDataInfo($('#ssd')));
});
Here's an if condition to isolate the invalid keys while you loop the data. Used as a filter, you can choose to delete the keys you do not want - like this:
$('#ssd').click(function(e){
var data = $(this).data();
for(var key in data) {
//here is a condition to use only those data-info items
if(data.hasOwnProperty(key) && key.indexOf('info') === -1) {
console.log(key); //just to see which key it is
delete data[key]; //if you need to build a collection of only data-info keys
}
}
});
Alternatively, negate the if condition to include only those keys you want.
You can use Prefix Data. It is jQuery plugin. Return the value at the prefixed data store for the first element in the set of matched elements. Returned value can be an object based on the attribute values and attributes name structure.
Usage
Take any HTML tag with multi data-* attributes with the same prefix. In the example we focus on myprefix prefix.
<div id="example-tag"
data-myprefix='{"property1": "value1", "property2": {"property21": "value21"}, "property3": "value2"}'
data-myprefix-property2='{"property22": "value22"}'
data-myprefix-property2-property23="value23"
data-myprefix-property3="overwite-value3"
data-myprefix-property4='{"property41": "value41"}'
data-other="We do not read it"></div>
If you want to read data from data-myprefix and every data-myprefix-* attribute you can use .prefixData() with given prefix.
$('#example-tag').prefixData('myprefix');
The previous example returns the object:
{
property1: "value1",
property2: {
property21: "value21",
property22: "value22",
property23: "value23"
},
property3: "overwite-value3",
property4: {
property41: "value41"
}
}

How can I toggle the display of a textarea via a button using knockout with the foreach binding?

I am new to knockout. For my problem, I am trying to make it so that for each project, there is a button and textarea. The textarea will be hidden upon page load. If I click the button, it will show the textarea (toggle). Currently, if I click the button, ALL textareas on the page will show, rather than just the corresponding textarea.
I'm hoping the fix for this isn't too dramatic and involving a complete reworking of my code as by some magic, every other functionality has been working thus far. I added the {attr id: guid} (guid is a unique identifier of a project retrieved from the database) statement in an attempt to establish a unique ID so that the right controls were triggered...although that did not work.
Sorry I do not have a working jfiddle to show the issue... I tried to create one but it does not demonstrate the issue.
JS:
//if a cookie exists, extract the data and bind the page with cookie data
if (getCookie('filterCookie')) {
filterCookie = getCookie('filterCookie');
var cookieArray = filterCookie.split(",");
console.log(cookieArray);
$(function () {
var checkboxes = new Array();
for (var i = 0; i < cookieArray.length; i++) {
console.log(i + cookieArray[i]);
checkboxes.push(getCheckboxByValue(cookieArray[i]));
//checkboxes.push(document.querySelectorAll('input[value="' + cookieArray[i] + '"]'));
console.log(checkboxes);
checkboxes[i].checked = true;
}
})
filterCookie = getCookie('filterResultsCookie');
cookieArray = filterCookie.split(",");
filterCookieObj = {};
filterCookieObj.action = "updateProjects";
filterCookieObj.list = cookieArray;
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify(filterCookieObj)
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
//if the cookie doesn't exist, just bind the page
else {
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify({
action: "getProjects"
})
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
View Model:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
};
};
HTML:
<!-- ko foreach: projects -->
<div id="eachOppyProject" style="border-bottom: 1px solid #eee;">
<table>
<tbody>
<tr>
<td><a data-bind="attr: { href: '/tools/oppy/' + guid }" style="font-size: 25px;"><span class="link" data-bind=" value: guid, text: name"></span></a></td>
</tr>
<tr data-bind="text: projectDescription"></tr>
<%-- <tr data-bind="text: guid"></tr>--%>
</tbody>
</table>
<span class="forminputtitle">Have you done project this before?</span> <input type="button" value="Yes" data-bind="click: $parent.toggleTextArea" class="btnOppy"/>
<textarea placeholder="Tell us a little of what you've done." data-bind="visible: $parent.show, attr: {'id': guid }" class="form-control newSessionAnalyst" style="height:75px; " /><br />
<span> <input type="checkbox" name="oppyDoProjectAgain" style="padding-top:10px; padding-right:20px;">I'm thinking about doing this again. </span>
<br />
</div><br />
<!-- /ko -->
Spencer:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.projects().forEach(function() { //also tried proj.forEach(function())
self.projects().showComments = ko.observable(false);
self.projects().toggleComments = function () {
self.showComments(!self.showComments());
};
})
};
It's weird that
data-bind="visible: show"
doesn't provide any binding error because context of binding inside ko foreach: project is project not the ProjectViewModel.
Anyway, this solution should solve your problem:
function ViewModel() {
var self = this;
var wrappedProjects = proj.map(function(p) {
return new Project(p);
});
self.projects = ko.observableArray(wrappedProjects);
}
function Project(proj) {
var self = proj;
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
}
return self;
}
The problem is that the show observable needs to be defined in the projects array. Currently all the textareas are looking at the same observable. This means you'll have to move the function showTextArea into the projects array as well.
Also you may want to consider renaming your function or getting rid of it entirely. Function names which imply they drive a change directly to the view fly in the face of the MVVM pattern. I'd recommend a name like "toggleComments" as it doesn't reference a view control.
EDIT:
As an example:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
foreach(var project in self.projects()) {
project.showComments = ko.observable(false);
project.toggleComments = function () {
self.showComments(!self.showComments());
};
}
};
There is probably a much cleaner way to implement this in your project I just wanted to demonstrate my meaning without making a ton of changes to the code you provided.

x-editable Select in Meteor does not update value on clients

I want to use a select x-editable in my Meteor application. My goal is to assign users to groups. This should be reactive, so when you assign a user, other clients should see the changes. The current problem is that the assignment works (data-value changes), but only the user who made the change is able to see the new value.
Here is my code:
Template.userGroup.rendered = function() {
var groupId = this.data._id;
var sourceUsers = [];
Users.find().forEach(function(user) {
sourceUsers.push({value: user._id, text: user.username});
});
Tracker.autorun(function() {
$('.assign-user').editable("destroy").editable({
emptytext: "Empty",
source: sourceUsers,
success: function(response, result) {
if (result) {
Groups.update({_id: groupId}, {$set: {adminId: result}});
}
}
});
});
};
<template name="userGroup">
</template>
I already tried to "destroy" the stale x-editable and put it inside the Tracker.autorun function, but unfortunately, this does not work.
Any help would be greatly appreciated.
I don't use Tracker.autorun but I use x-editable for inline editing like this:
(also used it for group assigments - just like your case, but found it too clumsy on the UI side). Anyway, here's my code:
Template
<template name="profileName">
<td valign='top'>
<div id="profileNameID" class="editable" data-type="text" data-rows="1">{{profile.name}}</div>
</td>
</template>
And on the JS side
Template.profileName.rendered = function () {
var Users = Meteor.users;
var container, grabValue, editableColumns, mongoID,
_this = this;
var container = this.$('#profileNameID');
var editableColumns = container.size();
grabValue = function () {
var gValue = $.trim(container.html());
return gValue;
};
$.fn.editable.defaults.mode = 'inline';
return container.editable({
emptytext: 'Your name goes here',
success: function (response, newValue) {
var mongoID = removeInvisibleChars($(this).closest("tr").find(".mongoid").text());
var editedUser = _users.findOne({
_id: mongoID
});
Meteor.users.update(mongoID, {
$set: {
"profile.name": newValue
}
});
return container.data('editableContainer').formOptions.value = grabValue;
}
});
Update happens immediately on all subscribed authorized clients.

Categories

Resources