Creating a clean Knockout binding without business logic (Popover - Radio Buttons) - javascript

I'm looking for a clean way to implement the following. Let's assume I have 5 buttons:
(Dog) - (Cat) - (Fish) - (Bird) - (Other)
In my ViewModel these buttons are represented as being a classification. So when I click the Dog button, the observable in my ViewModel should be set to Dog (this would be done through the click binding on each button).
Also, I want to show a specific style when one of the buttons is toggled (done by the css binding on the button):
(Dog) - (Cat) - (Fish) - (Bird) - (Other)
So for now this looks like it should turn my buttons into a radio button group. Now besides that, when I click the "Other" button I also want to show a little popover (bootstrap) in which the user can specifiy a custom value, like "Lion". Clicking an other button would close the popover.
Now on each button I could add a binding similar to this one:
{ css: { 'toggled': classficationMatches('Dog') },
popover: { action: 'close', id: 'mypopover' },
click: { click:setClassification.bind($data, 'Dog') }
But this feels dirty. I would prefer building a custom handler and use it like this:
{ toggleClassification: { type: 'Dog', popover: 'mypopover' } }
Now it would be up to the ViewModel to decided if the popover should be visible or not and the binding would contain all the logic of adding the css binding, the click and the popover bindings to the buttons.
I started trying some things with custom bindings but this code looks even worse:
ko.bindingHandlers["toggleClassification"] = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var classification = $(element).attr('data-type');
var selector = ko.utils.unwrapObservable(valueAccessor());
var pickerViewModel = new SimpleFilterSpecializationPickerViewModel(element, selector);
// Whenever we click the button, show the popover.
ko.utils.registerEventHandler(element, "click", function () {
// Hide the popup if it was visible.
if (pickerViewModel.isVisible()) {
pickerViewModel.hide();
}
else {
var requireInit = !pickerViewModel.loaded();
// Show the popover.
pickerViewModel.show();
// The first time we need to bind the popover to the view model.
if (requireInit) {
ko.applyBindings(viewModel, pickerViewModel.getPopoverElement());
// If the classification changes, we might not want to show the popover.
viewModel.isClassification(classification).subscribe(function () {
if (!viewModel.isClassification(classification)()) {
pickerViewModel.hide();
}
});
}
}
$('button[data-popoverclose]').click(function () {
$(element).popover('hide');
});
});
}
};
ko.bindingHandlers["toggleClassification"] = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Store the current value on click.
$(element).click(function () {
var observable = valueAccessor();
viewModel.setClassification(observable);
});
// Update the css of the button.
ko.applyBindingsToNode(element, { css: { 'active': viewModel.isClassification(valueAccessor()) } }, viewModel);
}
};
Anyone have some tips on how I could clean up my bindings so most of the 'logic' can be done in the ViewModel?

You can add this "dirty" binding programmatically with
ko.applyBindingsToNode(Element element, Object bindings, Object viewModel)
e.g.
HTML
data-bind="toggleClassification: { type: 'Dog', popover: 'mypopover' }"
JS
ko.bindingHandlers["toggleClassification"] =
{
init: function (element, valueAccessor, allBindings, viewModel, bindingContext)
{
var type = valueAccessor().type; // 'Dog'
var popover = valueAccessor().popover; // 'mypopover'
var binding = {
css: { 'toggled': classficationMatches(type) },
popover: { action: 'close', id: popover },
click: { click:setClassification.bind(bindingContext.$data, type) }
};
ko.applyBindingsToNode(element, binding , viewModel);
}
};

Related

disable jquery datepicker via knckoutjs radio button

The radio option in my application return three values allday, morning and halfday. How can I tie my jquery datepicker as such that it is enabled only when allday is selected.
var viewModel = function () {
this.holidayType = ko.observable();
this.allday = ko.computed(
{
read: function () {
return this.holidayType() == "allday";
},
write: function (value) {
if (value)
this.holidayType("allday");
}
}, this);
this.morning = ko.computed(
{
read: function () {
return this.holidayType() == "morning";
},
write: function (value) {
if (value)
this.holidayType("morning");
}
}, this);
this.afternoon = ko.computed(
{
read: function () {
return this.holidayType() == "afternoon";
},
write: function (value) {
if (value)
this.holidayType("afternoon");
}
}, this);
};
ko.applyBindings(new viewModel());
$(function () {
$("#e1").daterangepicker({
datepickerOptions: {
minDate: 0
}
});
});
First, I added an observable named dateRangeButton to your viewmodel. This observable will hold a jQuery selector to the dynamically generated <button> that is created by the date range picker plug-in:
this.dateRangeButton = ko.observable();
Then I added a function to the viewmodel called enableDisableDateRange. When called, this function will add or remove the disabled attribute to the date range picker button based on whether or not the holidayType equals "allday":
this.enableDisableDateRange = function(context) {
context.dateRangeButton().prop('disabled', context.holidayType() != 'allday');
}
How do we get the selector for the date range picker button? I created a custom binding which will apply the date range picker to the element and populate the dateRangeButton observable in the viewmodel:
ko.bindingHandlers.datePicker = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// apply the date range picker
$(element).daterangepicker({
datepickerOptions: {
minDate: 0
}
});
// tell the viewmodel where the date range picker button is
bindingContext.$data.dateRangeButton($(element).next('button'));
// enable or disable the button
bindingContext.$data.enableDisableDateRange(bindingContext.$data);
}
};
I added this binding to the <input> element that will contain the date range picker:
<input id="e1" name="e1" data-bind="datePicker: ''">
Finally, I added a subscribe to the holidayType observable which will call the enableDisableDateRange function any time holidayType changes:
this.holidayType.subscribe(function() {
this.enableDisableDateRange(this);
}, this);
Here is a fiddle: http://jsfiddle.net/6cfb1dLg/
NOTE: This all relies on how the date range picker arranges it's UI. Currently, it creates a <button> element directly after the <input> element it is applied to. If this changes, then the datePicker custom binding I wrote would have to be modified as well. The custom binding also assumes the presence of an observable named dateRangeButton and a function named enableDisableDateRange exist within the viewmodel.

Knockout register inserted element with other function

I am using Knockout to display some tags for an array of images. each tag will have a popup that gives more information about the tag. The elements is registered with popup class the following way:
function RegisterCharacterPopups() {
$('[data-characterid]').each(function() {
var cId = $(this).data('characterid');
var placement = $(this).data('position');
if (placement == null || placement == undefined) {
placement = "top-center";
}
$(this).PopUp({
url: "/Ajax/CharacterPop/" + cId,
position: placement,
});
});
}
And i have added this to my constructor of the view model that contains the tags:
// Hook on to update of Tags
ko.computed(() => {
var test = this.Tags();
RegisterCharacterPopups();
console.log("Tags updated");
});
I can see the methods is executed, but the tags do not register with the popup. if it force the Tags to update again, will it work though!
I think the problem is that this method in executed the first time, before the elements are in the html.
How can I fix this, so it will wait for the elements to be inserted before it executes it?
Solution: a custom binding
ko.bindingHandlers['tagpop'] = {
init: function (element, valueAccessor, allBindings, vm, context) {
var data = valueAccessor();
$(element).PopUp({
url: "/Ajax/CharacterPop/" + data.id,
position: data.placement,
});
},
update: function (element, valueAccessor) {
}
};

KnockoutJS/Bootstrap - Clearing modal form when closing modal using javascript

I have a Bootstrap Modal that contains a form for updating or creating an entity (Company in my example). Right now my issue is that if I view an entity using the modal, it doesn't clear out the fields when I close the modal by any means. Causing the form to still be populated if I then click a "Create" button, which should bring me up a blank modal.
How can I execute one of my ViewModels methods from just regular javascript? Here is some of my code:
function ViewModel() {
var self = this;
function CompanyViewModel(company) {
var self = this;
self.Id = company.CompanyId;
self.Name = company.Name;
}
function BlankCompanyViewModel() {
var self = this;
self.Id = 0;
self.Name = "";
}
self.company = ko.observable();
self.companies = ko.observableArray();
self.clearCurrentCompany = function() {
self.company(new BlankCompanyViewModel());
};
// Initialize the view-model
$.getJSON("/api/company", function(companies) {
$.each(companies, function(index, company) {
self.companies.push(new CompanyViewModel(company));
});
self.clearCurrentCompany();
});
}
Ideally I'd like to run ViewModel.clearCurrentCompany on the "Hidden" event of the modal like so:
$('#myModal').on('hidden', function() {
//Do something here, not sure what
});
I like to use a custom binding around a modal to make it open/close/display based on populating/clearing an observable.
Something like:
ko.bindingHandlers.modal = {
init: function(element, valueAccessor, allBindings, vm, context) {
var modal = valueAccessor();
//init the modal and make sure that we clear the observable no matter how the modal is closed
$(element).modal({ show: false, backdrop: 'static' }).on("hidden.bs.modal", function() {
if (ko.isWriteableObservable(modal)) {
modal(null);
}
});
//apply the template binding to this element
ko.applyBindingsToNode(element, { with: modal }, context);
return { controlsDescendantBindings: true };
},
update: function(element, valueAccessor) {
var data = ko.utils.unwrapObservable(valueAccessor());
//show or hide the modal depending on whether the associated data is populated
$(element).modal(data ? "show" : "hide");
}
};
You then use this against an observable. It acts like a with binding against that observable and shows/hides the modal based on whether the observable is populated.
Here is a sample that shows this in use and sets up a subscription where you could run custom code when the modal is closed. http://jsfiddle.net/rniemeyer/uf3DF/
function ViewModel() {
var self = this;
// your previous code
$('#myModal').on('hide', function() {
self.clearCurrentCompany();
});
}
Just like that. Note that you want hide, not hidden, because hidden fires only after the modal completely disappears. If a user opens a create before the previous view closes, it will still be populated.

Error with latest knockout and jquery ui dialog: cannot call prior to initialization

I try to use with knockout 2.2 custom binding new jquery 1.9 and jquery ui 1.9.2.
Code is from here: integrating jquery ui dialog with knockoutjs
With updated libraries: http://jsfiddle.net/SnPdE/323/
ko.bindingHandlers.dialog = {
init: function(element, valueAccessor, allBindingsAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor()) || {};
//do in a setTimeout, so the applyBindings doesn't bind twice from element being copied and moved to bottom
setTimeout(function() {
options.close = function() {
allBindingsAccessor().dialogVisible(false);
};
$(element).dialog(options);
}, 0);
//handle disposal (not strictly necessary in this scenario)
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).dialog("destroy");
});
},
update: function(element, valueAccessor, allBindingsAccessor) {
var shouldBeOpen = ko.utils.unwrapObservable(allBindingsAccessor().dialogVisible);
$(element).dialog(shouldBeOpen ? "open" : "close");
}
};
var viewModel = {
label: ko.observable('dialog test'),
isOpen: ko.observable(false),
open: function() {
this.isOpen(true);
},
close: function() {
this.isOpen(false);
}
};
ko.applyBindings(viewModel);
Problem is error: Error: cannot call methods on dialog prior to initialization; attempted to call method 'close'
If I remove setTimeout - applyBindings is applyed to dialog two times.
Checking if the dialog is initialized before calling open will fix it.
if ($(element).data('dialog')) {
$(element).dialog(shouldBeOpen ? "open" : "close");
}
The initial update is not required at all, as the dialog will be opened during initialization if autoOpen is true, which is the default.
Edit:
To be correct when the dialogVisible is initially false a change should be made to set the autoOpen option.
...
options.autoOpen = ko.utils.unwrapObservable(allBindingsAccessor().dialogVisible);
$(element).dialog(options);

knockout data-bind on dynamically generated elements

How is it possible to make knockout data-bind work on dynamically generated elements? For example, I insert a simple html select menu inside a div and want to populate options using the knockout options binding. This is what my code looks like:
$('#menu').html('<select name="list" data-bind="options: listItems"></select>');
but this method doesn't work. Any ideas?
If you add this element on the fly after you have bound your viewmodel it will not be in the viewmodel and won't update. You can do one of two things.
Add the element to the DOM and re-bind it by calling ko.applyBindings(); again
OR add the list to the DOM from the beginning and leave the options collection in your viewmodel empty. Knockout won't render it until you add elements to options on the fly later.
Knockout 3.3
ko.bindingHandlers.htmlWithBinding = {
'init': function() {
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
element.innerHTML = valueAccessor();
ko.applyBindingsToDescendants(bindingContext, element);
}
};
Above code snippet allows you to inject html elements dynamically with the "htmlWithBinding" property. The child elements which are added are then also evaluated... i.e. their data-bind attributes.
rewrite html binding code or create a new. Because html binding prevents "injected bindings" in dynamical html:
ko.bindingHandlers['html'] = {
//'init': function() {
// return { 'controlsDescendantBindings': true }; // this line prevents parse "injected binding"
//},
'update': function (element, valueAccessor) {
// setHtml will unwrap the value if needed
ko.utils.setHtml(element, valueAccessor());
}
};
For v3.4.0 use the custom binding below:
ko.bindingHandlers['dynamicHtml'] = {
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// setHtml will unwrap the value if needed
ko.utils.setHtml(element, valueAccessor());
ko.applyBindingsToDescendants(bindingContext, element);
}
};
EDIT: It seems that this doesn't work since version 2.3 IIRC as pointed by LosManos
You can add another observable to your view model using myViewModel[newObservable] = ko.observable('')
After that, call again to ko.applyBindings.
Here is a simple page where I add paragraphs dynamically and the new view model and the bindings work flawlessly.
// myViewModel starts only with one observable
var myViewModel = {
paragraph0: ko.observable('First')
};
var count = 0;
$(document).ready(function() {
ko.applyBindings(myViewModel);
$('#add').click(function() {
// Add a new paragraph and make the binding
addParagraph();
// Re-apply!
ko.applyBindings(myViewModel);
return false;
});
});
function addParagraph() {
count++;
var newObservableName = 'paragraph' + count;
$('<p data-bind="text: ' + newObservableName + '"></p>').appendTo('#placeholder');
// Here is where the magic happens
myViewModel[newObservableName] = ko.observable('');
myViewModel[newObservableName](Math.random());
// You can also test it in the console typing
// myViewModel.paragraphXXX('a random text')
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<div id="placeholder">
<p data-bind="text: paragraph0"></p>
</div>
<a id="add" href="#">Add paragraph</a>
It's an old question but here's my hopefully up-to-date answer (knockout 3.3.0):
When using knockout templates or custom components to add elements to prebound observable collections, knockout will bind everything automatically. Your example looks like an observable collection of menu items would do the job out of the box.
Based on this existing answer, I've achived something similar to your initial intentions:
function extendBinding(ko, container, viewModel) {
ko.applyBindings(viewModel, container.children()[container.children().length - 1]);
}
function yourBindingFunction() {
var container = $("#menu");
var inner = $("<select name='list' data-bind='options: listItems'></select>");
container.empty().append(inner);
extendBinding(ko, container, {
listItems: ["item1", "item2", "item3"]
});
}
Here is a JSFiddle to play with.
Be warned, once the new element is part of the dom, you cannot re-bind it with a call to ko.applyBindings- that is why I use container.empty(). If you need to preserve the new element and make it change as the view model changes, pass an observable to the viewModel parameter of the extendBinding method.
Checkout this answer: How do define a custom knockout 'options binding' with predefined Text and Value options
ko.applyBindingsToNode is particularly useful.

Categories

Resources