bind Knockoutjs arrays to Bootstrap slider - javascript

I'm trying to implement a bootstrap slider in a webapplication and use knockoutjs for the data handling.
Javascript
$(document).ready(() => {
function ViewModel() {
singleValue = ko.observable(20)
arrayValues = ko.observableArray([20, 50])
}
ko.applyBindings(ViewModel)
})
HTML
<input
type ='text'
data-slider-min = 0
data-slider-max = 100
data-slider-step = 1
data-slider-tooltip ='hide'
data-provide ='slider'
data-slider-value = 20
data-bind = 'value: singleValue'
/>
<p data-bind='text: singleValue'></p>
That works perfectly fine. If I change the value of the slider, the value in p is changing accordingly.
<input
type ='text'
data-slider-min = 0
data-slider-max = 100
data-slider-step = 1
data-slider-tooltip ='hide'
data-provide ='slider'
data-slider-value = [20, 40]
data-bind = 'value: arrayValues'
/>
<p data-bind='text: arrayValues()'></p>
// output is 20,50, so knockout is taking precedence,
// because I set the initial value to [20, 40] in html
If I change the value of the slider, it seems the values are changing accordingly. But it's not an array anymore, its a string value. But the minimum and maximun values are changing correctly with the slider.
<p data-bind='text: arrayValues()[0]'></p>
Here the output is 20 when site is rendered. Which is what I expected.
But as soon as I start to move the slider, the array is becoming a string and the output changes to 2, which is the first char of 20,40.
Does anyone knows what I'm doing wrong?

I believe the value binding for <input>, <textarea> etc. really just understands string values. This is not true for <select>, which accepts arbitrary data types, even objects.
My suggestion would be to work-around the problem. In the example below, I use a writable computed to parse '20,40' into an array of the form you described. You might just provide such a computed using data-bind = 'value: val'.
let _val = ko.observable([20, 50]);
let val = ko.computed({
read: () => _val(),
write: v => {
_val(v.split(',').map(n => parseInt(n)));
},
});
console.log(val());
val('20,40');
console.log(val());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

Related

How can you select code nodes that were added through templates?

So I am using the template method to dynamically add HTML content in a page, and I want to change the value of an input through an event listener.
Here's a completely random snippet of code as an example (it's nonsensical on purpose):
favoriteElement += `<div class="favorite__page JS-favoritePage">
<p id="JS-amountOfFavorites">Quantity of saved pages : ${amount}</p>
<input type="number" class="favoritesQuantity" name="amountOfFavorites" min="1" max="100" value="${value}">
</div>`
So let's say that I want to have access to the value of the input, I'll declare a variable and get it through their query selector :
let inputFavoritesQuantity = document.querySelector('input [class="favoritesQuantity"]');
Now I'll add an event listener:
inputFavoritesQuantity.addEventListener("input", function(e){
let valueOfInput = e.target.value;
//Other code
}
Though the problem is that I do not have access to the input because it's added with a template, so it gives an error Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
I could add everything by hand using the properties createElement,setAttribute,appendChild...
But it would make the code VERY long and difficult to maintain! (without even considering the fact on my code project I'd have to add 5 nested elements which have 5 attributes each!)
Is there another efficient method to have access to an element with templates?
The DOMParser compiles strings into a document. You need to access the
documentElement in order to add to the existing dom. Here's an example of use
let amount = 100
let value = 50
favoriteElement = `<div class="favorite__page JS-favoritePage">
<p id="JS-amountOfFavorites">Quantity of saved pages : ${amount}</p>
<input type="number" name="amountOfFavorites" min="1" max="100" value="${value}" />
</div>`
// This converts the string and gets the documentElement.
var node = new DOMParser().parseFromString(favoriteElement, "text/html").documentElement
//Now we are working with an actual element and not a string of text.
let inputFavoritesQuantity = node.querySelector('input [class="favoritesQuantity"]');
node.addEventListener("input", function(e){
let valueOfInput = e.target.value;
console.log('value changed', valueOfInput);
})
var outputDiv = document.getElementById('content')
outputDiv.appendChild(node);
<div id="content">
</div>

Why does setting an optionsValue break Knockout updating?

I've been going through the Knockout tutorials, and I was playing around with one tutorial when something puzzled me. Here is my HTML:
<h2>Your seat reservations</h2>
<table>
<thead><tr>
<th>Passenger name</th><th>Meal</th><th>Surcharge</th>
</tr></thead>
<tbody data-bind="foreach: seats">
<tr>
<td><input data-bind="value: name" /></td>
<td><select data-bind="options: $root.availableMeals, optionsValue: 'mealVal', optionsText: 'mealName', value: meal"></select></td>
<td data-bind="text: formattedPrice"></td>
</tr>
</tbody>
</table>
<button data-bind="click: addSeat">Reserve another seat</button>
... and here is my JavaScript:
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
var self = this;
self.name = name;
self.meal = ko.observable(initialMeal);
self.formattedPrice = ko.computed(function() {
var price = self.meal().price;
return price ? "$" + price.toFixed(2) : "None";
});
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
self.availableMeals = [
{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 },
{ mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 },
{ mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }
];
// Editable data
self.seats = ko.observableArray([
new SeatReservation("Steve", self.availableMeals[0]),
new SeatReservation("Bert", self.availableMeals[0])
]);
// Operations
self.addSeat = function() {
self.seats.push(new SeatReservation("", self.availableMeals[0]));
}
}
ko.applyBindings(new ReservationsViewModel());
When I run this example and select a different "Meal" from the dropdown menu for a passenger, the "Surcharge" value is not updated. The reason for this seems to be that I added optionsValue: 'mealVal' into the data-bind attribute for the select, and when I remove that, the "Surcharge" does indeed update when a new dropdown option is selected. But why does adding optionsValue break the updating? All that does is set the select list's option value attributes, which is quite useful for form submission - I don't see why it should prevent Knockout from auto-updating.
UPDATE: Upon further investigation, I've discovered that the formattedPrice fn is still getting called, but self.meal() is now resolving to the value string such as PRM instead of the whole meal object. But why is this? The documentation says that optionsValue sets the value attribute in the HTML, but doesn't say anything about changing the view model behaviour.
I think what's going on is that when you specify options: $root.availableMeals, but don't specify an optionsValue, Knockout magically determines which selection in the list you've made when the selection is changed and gives you access to the object from availableMeals instead of just the string value that was put into the value attribute. This does not appear to be well-documented.
I think you understand what's happening and why it breaks your code, but are still looking for an explanation on when you actually need to use optionsValue, and when not.
When to use the optionsValue binding
Let's say your meals can be sold out and you want to check with the server for updates in availableMeals:
const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);
loadMeals();
ko.applyBindings({ loadMeals, availableMeals, selectedMeal });
function getMeals() {
return {
then: function(cb) {
setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: availableMeals,
value: selectedMeal,
optionsText: 'mealName'"></select>
<button data-bind="click: loadMeals">refresh meals</button>
<div data-bind="with: selectedMeal">
You've selected <em data-bind="text: mealName"></em>
</div>
<div data-bind="ifnot: selectedMeal">No selection</div>
<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>
What happens when you replace the objects in availableMeals:
Knockout re-renders the select box's options
Knockout checks the new values for selectedMeal() === mealObject
Knockout does not find the object in selectedMeal and defaults to the first option
Knockout writes the new object's reference to selectedMeal
Problem: you loose your UI selection because the object it points to is no longer in the available options.
optionsValue to the rescue!
The optionsValue allows us to solve this issue. Instead of storing a reference to an object that might be replaced at any time, we store a primitive value, the string inside mealVal, that allows us to check for equality in between different API calls! Knockout now does something like:
selection = newObjects.find(o => o["mealVal"] === selectedMeal());
Let's see this in action:
const availableMeals = ko.observableArray([]);
const loadMeals = () => getMeals().then(availableMeals);
const selectedMeal = ko.observable(null);
loadMeals();
ko.applyBindings({ loadMeals, availableMeals, selectedMeal });
function getMeals() {
return {
then: function(cb) {
setTimeout(cb.bind(null, [{ mealVal: "STD", mealName: "Standard (sandwich)", price: 0 }, { mealVal: "PRM", mealName: "Premium (lobster)", price: 34.95 }, { mealVal: "ULT", mealName: "Ultimate (whole zebra)", price: 290 }]), 500);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: availableMeals,
value: selectedMeal,
optionsText: 'mealName',
optionsValue: 'mealVal'"></select>
<button data-bind="click: loadMeals">refresh meals</button>
<div data-bind="if: selectedMeal">
You've selected <em data-bind="text: selectedMeal"></em>
</div>
<div data-bind="ifnot: selectedMeal">No selection</div>
<p>Make a selection, click on refresh and notice the selection is lost when new data arrives.</p>
The downsides of optionsValue
Notice how I had to rewrite the with binding? Suddenly, we only have one of meal's properties available in our viewmodel, which is quite limiting. Here's where you'll have to do some additional work if you want your app to be able to update its data. Your two options:
Store the string (hash) of your selection and the actual object independently, or
Have a repository of view models, when new server data arrives, map to the existing instances to ensure you keep selection states.
If it helps, I could add code snippets to explain those two approaches a bit better
OK, after looking through the Knockout code, I've figured out what's happening - and as of the time of writing this is not documented.
The value binding, when it reads the value of a select element, doesn't just look at the DOM value for the element; it calls var elementValue = ko.selectExtensions.readValue(element);
Now, what selectExtensions does, unsurprisingly, is implement special behaviour for select (and their child object) elements. This is where the magic happens, because as the comment in the code says:
// Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
// are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
// that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.
So, when the value binding tries to read the select element via selectExtensions.readValue(...), it will come to this code:
case 'select':
return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
This basically says "OK, find the selected index and use this function again to read the option element at that index. So then it reads the option element and comes to this:
case 'option':
if (element[hasDomDataExpandoProperty] === true)
return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
return ko.utils.ieVersion <= 7
? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text)
: element.value;
Aha! So it stores its own "has DOM data expando property" flag and if that is set it DOESN'T get the simple element.value, but it goes to its own JavaScript memory and gets the value. This is how it can return a complex JS object (like the meal object in my question's example) instead of just the value attribute string. However, if that flag is not set, it does indeed just return the value attribute string.
The writeValue extension, predictably, has the other side of this where it will write the complex data to JS memory if it's not a string, but otherwise it will just store it in the value attribute string for the option:
switch (ko.utils.tagNameLower(element)) {
case 'option':
if (typeof value === "string") {
ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
delete element[hasDomDataExpandoProperty];
}
element.value = value;
}
else {
// Store arbitrary object using DomData
ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
element[hasDomDataExpandoProperty] = true;
// Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
element.value = typeof value === "number" ? value : "";
}
break;
So yeah, as I suspected, Knockout is storing complex data behind-the-scenes but only when you ask it to store a complex JS object. This explains why, when you don't specify optionsValue: [someStringValue], your computed function received the complex meal object, whereas when you do specify it, you just get the basic string passed in - Knockout is just giving you the string from the option's value attribute.
Personally I think this should be CLEARLY documented because it is a bit unexpected and special behaviour that is potentially confusing, even if it's convenient. I'll be asking them to add it to the documentation.

Javascript / Vue : set background-color on specific parts of string, based on matched values in array

I'm struggling with making a script which sets background-color on specific parts of a string (or input in this case, but doesn't have to be...).
The solution is made with VUE, but i assume this has to be sorted out with core javascripting.
HTML :
<div class="col-sm-7">
<input v-model.lazy="item.text" class="form-control" style="width:98%">
</div>
VUE Computed :
nluData() {
return orderby(this.$store.getters.nlujson.filter(item => {
return item.intent.toLowerCase() === this.selectedIntent
}), ['intent', 'text'], ['asc', 'asc'])
},
Screenshot of JSON structure :
Screenshot of desired result (where arrows point, the word-blocks should be background-colored :
You can use v-html and style your text as desired.
<span v-html="text"></span>
And in you component you can use the computed value as you want:
computed: {
text () {
// Code logic of your convenience
// Be careful of XSS attack.
return '<b>' + this.someText + '</b>';
}
}

How should i display number in HTML for later calculation in javascript

I am trying to figure out how to display number a right and efficient way for later calculation in HTML. This is what i can think of right now but doesn't seems right.
<p class = "price"> <span class ="sign">$</span> 10 </p>
Later implementation includes
$("p.price") * (the desire currency rate being called)
It then updates the whole page with the p.price
Consider using data attributes:
<p class="price" data-usd-price="10"> any markup you want </p>
You can then format it however you like and access the raw value later with:
$("p.price").data("usd-price")
Here a bit more complicated example:
<p class="price" data-usd-price="10">foo<span class="converted"></span></p>
<p class="price" data-usd-price="30">bar<span class="converted"></span></p>
<p class="price" data-usd-price="49.99">buzz<span class="converted"></span></p>
<p class="price" data-usd-price="99.99"><span class="converted"></span></p>
$('p.price').each(function () {
$(this)
.children('span.converted')
.html(
$(this).data('usd-price') * 22
)
})
The selector $("p.price") will give you an array of all paragraph elements with the class price. So your first issue is that you need to be aware of that, and your current multiplication code is not.
Second, you're trying to multiply the elements rather than the value of the one element.
Third, the value will be a string and you need a number.
I'd try something like:
<p class="price"><span>$</span><span class="amount">10</span>
Then your JS could look like this (minus smart error checking and optimization and such)
var amount = parseFloat($("span.amount:first").text(), 10);
$("span.amount:first").text(amount * exchangeRate);
Try to loop through paragraph children and check, if nodeName of the children is text then parse it's wholeText
var pContent = $('.price')[0].childNodes,
elem, num;
$.each(pContent, function (i, e) {
elem = $(e)[0];
if (elem && elem.nodeName == "#text") {
num = parseInt(elem.wholeText);
}
})
console.log(num)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<p class = "price"> <span class ="sign">$</span> 10</p>
when the page load the span is left empty but i want it to be shown (GBP as the base)
Simply change the spans text on window load instead of onchange event
var selectedIndex = select.selectedIndex;
$('.sign').text(prefix[selectedIndex ]);
$('.converted').text(currency[selectedIndex ] * $(price).data('price'));
Also i have some notes, if you have just one element you don't need to implement each function , and you don't need to make loop on each change as selectedIndex will filter the option which has selected attribute. http://jsfiddle.net/whoq9zd0/2/

why is my list not rendering?

by default it shows 10 elements, but when i change the input it does not update, below is the code and fiddle.
JS Code:
var app = angular.module('myapp',[]);
app.controller('ctrlParent',function($scope){
$scope.listItems = 10;
$scope.newTotal = function(){
$scope.$apply(function(){$scope.lisItemsTotal})
}
$scope.lisItemsTotal = function(num) {
return new Array($scope.listItems);
}
});
http://jsfiddle.net/0dwmqn8y/1/
The reason is that after changing the input $scope.listItems is a string, as the input type is text. Change it to number and all will work. Working plunker: http://jsfiddle.net/yv0z9q8L/
<input type="number" name="red" ng-model="listItems" onchange="angular.element(this).scope().newTotal()">
Also note, that you don't need onchange attribute at all here, however I am unsure whether this is a good thing. Using a function in for ng-repeat is quite dangerous unless you know what you are doing, as this function will be called in each digest cycle. If you are planning to loop over large set of data, it will pretty much kill your performance.
Personally, I would rather go with:
<div ng-app="myapp">
<div ng-controller="ctrlParent">
<input type="number" name="red" ng-model="itemCount">
<ul>
<li ng-repeat="i in items track by $index"><span>{{$index+1}}</span></li>
</ul>
</div>
</div>
var app = angular.module('myapp',[]);
app.controller('ctrlParent',function($scope){
$scope.$watch('itemCount', function(val) {
$scope.items = Array.new(val)
});
$scope.itemCount = 10;
});
plunker: http://jsfiddle.net/m1zq3zqv/1/
The data, that you get from the input field is a string, so you can either change the input type to number or apply a bit of parsing in your code by changing the following line
return new Array($scope.listItems);
to this:
return new Array(parseInt($scope.listItems));

Categories