Knockoutjs if/shim binding - javascript

I want to create a custom binding that behaves like the if binding, but instead of removing the element entirely, it replaces it with another element of the same height whenever it should be removed.
I'm struggling to find a way of doing this that isn't hacky. I don't know enough about the internals of knockout to go about this in an educated way.
Any pointers greatly appreciated.
Thanks!

You could write your own binding:
ko.bindingHandlers.shim = {
update: function(element, valueAccessor, allBindings) {
// First get the latest data that we're bound to
var value = valueAccessor();
// Next, whether or not the supplied model property is observable, get its current value
var shim = ko.unwrap(value);
if (shim) {
var shimEl = $(element).data('shim');
// Create the shim element if not created yet
if (!shimEl) {
shimEl = $('<div />').addClass('shim').appendTo(element);
// Equal the height of the elements
shimEl.height($(element).height());
$(element).data('shim', shimEl);
}
shimEl.show();
} else {
var shimEl = $(element).data('shim');
if (shimEl) {
shimEl.hide();
}
}
// You can also trigger the if-binding at this point
// ko.bindingHandlers.if.update(element, valueAccessor, allBindings);
}
};
Then use it like this:
<div data-bind="shim: [condition]"></div>

Related

Knockout JS, live updating data from websocket

I am trying to load data from a websocket to show on a gauge using knockout and gaugeMeter.js.
I keep getting the "cannot apply bindings multiple times to the same element" error.
Here is the code
HTML
<div class="GaugeMeter gaugeMeter" id="PreviewGaugeMeter" data-bind="gaugeValue: Percent" data-size=350 data-theme="Orange" data-back="Grey"
data-text_size="0.4" data-width=38 data-style="Arch" data-animationstep="0" data-stripe="3"></div>
JS
// Bind new handler to init and update gauges.
ko.bindingHandlers.gaugeValue = {
init: function(element, valueAccessor) {
$(element).gaugeMeter({ percent: ko.unwrap(valueAccessor()) });
},
update: function(element, valueAccessor) {
$(element).gaugeMeter({ percent: activePlayerData.boost });
}
};
// Create view model with inital gauge value 15mph
// Use observable for easy update.
var myViewModel = {
Percent: ko.observable(15)
};
ko.applyBindings(myViewModel);
The activePlayerData.boost is the data I am getting from the websocket and need to update the value, it always shows the first value but everything after that is giving the error.
I am really lost with the knockout stuff as I am very new to coding.
A minimal, working sample for your use case is below. You can run it to see what it does:
// simple gaugeMeter jQuery plugin mockup
$.fn.gaugeMeter = function (params) {
return this.css({width: params.percent + '%'}).text(params.percent);
}
// binding handler for that plugin
ko.bindingHandlers.gaugeValue = {
update: function(element, valueAccessor) {
var boundValue = valueAccessor();
var val = ko.unwrap(boundValue); // or: val = boundValue();
$(element).gaugeMeter({ percent: val });
}
};
// set up viewmodel
var myViewModel = {
Percent: ko.observable(15)
};
ko.applyBindings(myViewModel);
// simulate regular value updates
setInterval(function () {
var newPercentValue = Math.ceil(Math.random() * 100);
myViewModel.Percent(newPercentValue);
}, 1500);
div.gaugeMeter {
background-color: green;
height: 1em;
color: white;
padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="gaugeMeter" data-bind="gaugeValue: Percent"></div>
Things to understand:
The update part of the binding handler is called whenever the bound observable (i.e. Percent) changes its value. You don't strictly need the init part if there is nothing to initialize.
The valueAccessor gives you the thing that holds the bound value. Typically, but not necessarily this is an observable - like in your case. Once you have called valueAccessor(), you still need to open (aka "unwrap") that observable to see what's inside. This is why my code above takes two steps, valueAccessor() and ko.unwrap().
When would it ever not be an observable? - You are free to bind to literal values in your view (data-bind="gaugeValue: 15"), or to non-observable viewmodel properties (data-bind="gaugeValue: basicEfficiency") when the viewmodel has {basicEfficiency: 15}. In all cases, valueAccessor() will give you that value, observable or not.
Why use ko.unwrap(boundValue) instead of boundValue()? - The former works for both literal values and for observables, the latter only works for observables. In a binding handler it makes sense to support both use cases.
Once an update arrives, e.g. via WebSocket, don't try to re-apply any bindings or re-initialize anything.
All you need to do is to change the value of the respective observable in your viewmodel. Changing an observable's value works by calling it: myViewModel.Percent(15); would set myViewModel.Percent to 15.
If 15 is different from the previous value, the observable will take care of informing the necessary parties (aka "subscribers"), so all required actions (such as view updates) can happen.

How to pass the binding context to custom handler call?

I'm using Knockout custom handlers to update my objects like this:
ko.bindingHandlers.Tile = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var $element = $(element);
var $parent = bindingContext.$parent || viewModel.$parent; // TODO: Clean up this mess
/*
Do lot of things using bindingContext.$parent, calculate elements dimension, populate fields etc.
*/
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
$(element).show();
}
};
ko.bindingHandlers.TileColumn = {
/* another custom handler, we got a lot of those things */
}
Now I implemented a Resize function and I can reuse all that weird logic in the custom handler to redraw/resize my elements.
var baseResize = this.Resize;
this.Resize = function () {
baseResize.call(this);
var tiles = this.Element.find('.tile');
var tilesVM = this.DataSource.Items;
for (var i = 0x0; i < tiles.length; i++) {
tilesVM[i].$parent = this.ViewModel; // Clean up this mess too
ko.applyBindingsToNode(tiles[i], null, tilesVM[i]);
}
}
This works but feels like it's not the right way to do it. I'm struggling to refactor that handlers and get out of it the resize logic: not sure if this is the right way to do it.
While we wait for that refactoring I want a better way to call the applyBindings passing the correct bindingContext.
Note I did a very ugly thing here:
var $parent = bindingContext.$parent || viewModel.$parent; // TODO: Clean up this mess
And here:
tilesVM[i].$parent = this.ViewModel; // Clean up this mess too
Because the bindingContext parameter does not contain the parent view model when I explicit call the custom handler.
Of course that handler works just fine without that ugly messy line when the component loads and knockout is doing all the magic behind the curtain.
Robert, thanks for your time.
That documentation link does not helped since I got 3 levels: TileParent, Tile and TileChilds and I want to only call the Tile level and I cannot see a way to do it without applyBindingsToNode.
But that pointed me out to the binding documentation. After reading this blog and digging in the ko source code I realized one wonderfull thing: I can pass a ViewModel OR a BindingContext as parameter.
At first I was sad I cannot pass both at the same time but I see the VM is inside the BC $data.
Finally I found this usefull ko.contextFor(element)
for (var i = 0x0; i < tiles.length; i++) {
var bindingContext = ko.contextFor(tiles[i]);
ko.applyBindingsToNode(tiles[i], null, bindingContext);
}
and I also cleaned my handler callback
var $parent = bindingContext.$parent;
Everything still working, but this time a bit more cleaner!

Knockout append observables w/ characters

I have observables in my KnockoutJS app and their values are being fetched from an array that's contained globally in the app, eg:
self.observable = ko.observable(App[0].Observable_Value);
For ease, let's say Observable_Value = 10.
This is working as you'd expect it to and the <input> that self.observable is binded to has the correct value in it by default; 10.
What I want to do now is to append the observable with a % in the <input> so the displayed output in the input field will be 10%.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string to prevent NaN errors later on in the app. The app relies heavily on numbers.
I've tried doing this as a computed function with read/write & parseFloat but to no success.
Any ideas?
I think that the best option in this case would be a custom binding handler.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string
That is precisely what a binding handler could do - preserve the original value of the observable, but change the way that it is displayed.
I've tried doing this as a computed function
Although a computed function could do the trick, it's usually unecessary unless you want to actually work with the return value from the computed. In this case, since you just want to change the visual display, you don't need a another variable representing the same value.
Here is a very basic one that just puts a % sign in front of the observable's value. See fiddle
ko.bindingHandlers.percent = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor());
$(element).text('%' + value);
}
};
You can add a read/write computable that will add the percent for display, then remove it when setting the underlying value (fiddle):
ko.observable.fn.formatAsPercent = function() {
var base = this;
return ko.computed({
read: function() {
return base() + "%";
},
write: function(newValue) {
base(parseInt(newValue.replace('%', '')));
}
});
};
function ViewModel() {
var self = this;
self.number = ko.observable(10); // actual number
self.percent= self.number.formatAsPercent(); // used for binding to show %
}
var my = { vm : new ViewModel() };
ko.applyBindings(my.vm);

KnockoutJs v3 - _ko_property_writers = undefined

I'm trying to get my custom binding to work with both observables and plain objects. I followed the answer in this question:
writeValueToProperty isn't available
However, if I look at the object returned if I execute the allBindingsAccessor, the property '_ko_property_writers' is undefined.
Does anyone know if this has changed at all in version 3 of knockout?
edit
Sorry I should have stated, I am trying to 'write' the value back to the model, in an observable agnostic way
This was helpful for me:
ko.expressionRewriting.twoWayBindings.numericValue = true;
ko.bindingHandlers.numericValue = {
...
}
It is defined after specifying binding as two-way.
So I can use something like that inside my custom binding:
ko.expressionRewriting.writeValueToProperty(underlying, allBindingsAccessor, 'numericValue', parseFloat(value));
writeValueToProperty is defined internally as:
writeValueToProperty: function(property, allBindings, key, value, checkIfDifferent) {
if (!property || !ko.isObservable(property)) {
var propWriters = allBindings.get('_ko_property_writers');
if (propWriters && propWriters[key])
propWriters[key](value);
} else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
property(value);
}
}
The standard way to do this is with ko.unwrap as described here: http://knockoutjs.com/documentation/custom-bindings.html
For example:
ko.bindingHandlers.slideVisible = {
update: function(element, valueAccessor, allBindings) {
// First get the latest data that we're bound to
var value = valueAccessor();
// Next, whether or not the supplied model property is observable, get its current value
var valueUnwrapped = ko.unwrap(value);
// Grab some more data from another binding property
var duration = allBindings.get('slideDuration') || 400; // 400ms is default duration unless otherwise specified
// Now manipulate the DOM element
if (valueUnwrapped == true)
$(element).slideDown(duration); // Make the element visible
else
$(element).slideUp(duration); // Make the element invisible
}
};
In that example valueUnwrapped is correct whether the user bound to an observable or a normal object.

KnockoutJS custom binding calling click event during bind, not on click

I`m attempting to bind an observable array of people two a two column responsive layout with click events using knockoutjs.
I have created a custom binding called TwoCol that loops through the array, and appends nodes to the DOM to create my suggested layout, but the click events are giving me trouble when I try to apply them in a custom binding nested in a loop.
I have played with it quite a bit, and encountered all types of results, but where I`m at now is calling my ~click~ event during binding, rather than on click.
http://jsfiddle.net/5SPVm/6/
HTML:
<div data-bind="TwoCol: Friends" id="" style="padding: 20px">
JAVASCRIPT:
function FriendsModel() {
var self = this;
this.Friends = ko.observableArray();
this.SelectedFriend = "";
this.SetSelected = function (person) {
alert(person);
self.SelectedFriend = person;
}
}
function isOdd(num) {
return num % 2;
}
ko.bindingHandlers.TwoCol = {
update: function (elem, valueAccessor) {
var i = 0;
var rowDiv;
var vFriends = ko.utils.unwrapObservable(valueAccessor());
$(elem).html('');
while (i < vFriends.length) {
//create row container every other iteration
if (!isOdd(i)) {
rowDiv = document.createElement("div");
$(rowDiv).addClass("row-fluid");
elem.appendChild(rowDiv);
}
//add column for every iteration
var colDiv = document.createElement("div");
$(colDiv).addClass("span6");
rowDiv.appendChild(colDiv);
//actual code has fairly complex button html here
var htmlDiv = document.createElement("div");
var htmlButton = vFriends[i]
htmlDiv.innerHTML = htmlButton;
colDiv.appendChild(htmlDiv);
//i think i need to add the event to the template too?
//$(htmlDiv).attr("data-bind", "click: { alert: $data }")
//it seems that the SetSelected Method is called while looping
ko.applyBindingsToDescendants(htmlDiv, { click: friends.SetSelected(vFriends[i]) });
i++;
}
return { controlsDescendantBindings: true };
}
}
var friends = new FriendsModel();
friends.Friends.push('bob');
friends.Friends.push('rob');
friends.Friends.push('mob');
friends.Friends.push('lob');
ko.applyBindings(friends);
I don't think you're using ko.applyBindingsToDescendants correctly. I admit I'm a little confused as to the meaning of some of the values in your code, so I may have interpreted something incorrectly.
Here's a fiddle where I think it's working the way you intended:
http://jsfiddle.net/5SPVm/7/
http://jsfiddle.net/5SPVm/8/
Notice if manually control descendant bindings (return { controlsDescendantBindings: true };), you need to set that up in the init callback, instead of update. The update callback is too late for that.
Quick rundown of the changes (edited):
Moved the controlsDescendantBindings into the init binding callback
Added the necessary parameter names to the binding param list to access additional values.
I re-enabled the html.attr call. Notice that now, because the binding context is set to the actual item, the SetSelected method doesn't exist at that level anymore, so it is necessary to use $parent.SetSelected.
$(htmlDiv).attr("data-bind", "click: $parent.SetSelected")
Fixed the ko.applyBindingsToDescendants call. This method takes a binding context, which is created from the current binding context, and also takes the element to apply the binding to. You don't want to reapply the binding, which is why this whole thing needs to be in the init handler.
var childBindingContext = bindingContext.createChildContext(vFriends[i]);
ko.applyBindingsToDescendants(childBindingContext, colDiv);

Categories

Resources