I am trying to create knockoutjs bindings for jquery ui dialogs, and cannot get the dialog to open. The dialog element is created correctly, but seems to have display: none that calling dialog('open') doesn't remove. Also, the call to dialog('isOpen') returns the dialog object rather than a boolean.
I am using the latest knockoutjs and jquery 1.4.4 with jquery ui 1.8.7. I've also tried it with jQuery 1.7.1 with the same results. Here's my HTML:
<h1 class="header" data-bind="text: label"></h1>
<div id="dialog" data-bind="dialog: {autoOpen: false, title: 'Dialog test'}">foo dialog</div>
<div>
<button id="openbutton" data-bind="dialogcmd: {id: 'dialog'}" >Open</button>
<button id="openbutton" data-bind="dialogcmd: {id: 'dialog', cmd: 'close'}" >Close</button>
</div>
and this is the javascript:
var jQueryWidget = function(element, valueAccessor, name, constructor) {
var options = ko.utils.unwrapObservable(valueAccessor());
var $element = $(element);
var $widget = $element.data(name) || constructor($element, options);
$element.data(name, $widget);
};
ko.bindingHandlers.dialog = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
jQueryWidget(element, valueAccessor, 'dialog', function($element, options) {
console.log("Creating dialog on " + $element);
return $element.dialog(options);
});
}
};
ko.bindingHandlers.dialogcmd = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
$(element).button().click(function() {
var options = ko.utils.unwrapObservable(valueAccessor());
var $dialog = $('#' + options.id).data('dialog');
var isOpen = $dialog.dialog('isOpen');
console.log("Before command dialog is open: " + isOpen);
$dialog.dialog(options.cmd || 'open');
return false;
});
}
};
var viewModel = {
label: ko.observable('dialog test')
};
ko.applyBindings(viewModel);
I have set up a JSFiddle that reproduces the problem.
I am wondering if this has something to do with knockoutjs and event handling. I tried returning true from the click handler, but that did not appear to affect anything.
It looks like writing to the widget to .data("dialog") and then trying to operate on it is causing an issue. Here is a sample where .data is not used and the open/close is called based on the element: http://jsfiddle.net/rniemeyer/durKS/
Alternatively, I like to work with the dialog in a slightly different way. I like to control whether the dialog is open or closed by using an observable. So, you would use a single binding on the dialog itself. The init would initialize the dialog, while the update would check an observable to see if it should call open or close. Now, the open/close buttons just need to toggle a boolean observable rather than worry about ids or locating the actual dialog.
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),
$el = $(element),
dialog = $el.data("uiDialog") || $el.data("dialog");
//don't call open/close before initilization
if (dialog) {
$el.dialog(shouldBeOpen ? "open" : "close");
}
}
};
Used like:
<div id="dialog" data-bind="dialog: {autoOpen: false, title: 'Dialog test' }, dialogVisible: isOpen">foo dialog</div>
Here is a sample:
http://jsfiddle.net/rniemeyer/SnPdE/
I made a little change to RP Niemeyer's answer to allow the dialog's options to be observables
http://jsfiddle.net/YmQTW/1/
Get the observables values with ko.toJS to initialize the widget
setTimeout(function() {
options.close = function() {
allBindingsAccessor().dialogVisible(false);
};
$(element).dialog(ko.toJS(options));
}, 0);
and check for observables on update
//don't call dialog methods before initilization
if (dialog) {
$el.dialog(shouldBeOpen ? "open" : "close");
for (var key in options) {
if (ko.isObservable(options[key])) {
$el.dialog("option", key, options[key]());
}
}
}
Adding this here because this is what most people find when searching on issues with jQuery UI Dialog and Knockout JS.
Just another option to avoid the "double binding" issue explained in the above answer. For me, the setTimeout() was causing other bindings to fail that require the dialog to be initialized already. The simple solution that worked for me was making the following changes to the accepted answer:
Add class='dialog' to any elements using the custom dialog binding.
Call this after page load, but before calling ko.applyBindings():
$('.dialog').dialog({autoOpen: false});
Remove the setTimeout inside the init of the custom binding, and just call the code directly.
Step 2 makes sure that any jQuery UI Dialogs have been initialized prior to any KO bindings. This way jQuery UI has already moved the DOM elements, so that you don't have to worry about them moving in the middle of applyBindings. The init code still works as-is (other than removing setTimeout) because the dialog() function will just update an existing dialog if already initialized.
An example of why I needed this is due to a custom binding I use to update the title of the dialog:
ko.bindingHandlers.jqDialogTitle = {
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
$(element).dialog('option', 'title', value);
}
};
I use a separate binding for this instead of the update function for the main dialog binding, because I only want to update the title, not other properties such as height and width (don't want the dialog to resize just because I change the title). I suppose I could also use update and just remove height/width, but now I can do both/either and not worry about the setTimeout being completed or not.
This is a variation of the great RP Niemeyer binding handler, which is useful for a differente scenario.
To allow the edition of an entity, you can create a <div> with edition controls, and use a with binding, which depends on an observable made on purpose for the edition.
For example, to allow the edition of a person, you can create and observable like editedPerson, and create a div with edition controls, with a binding like this:
data-bind="with: editedPerson"
When you add a person to the observable lke so:
vm.editedPerson(personToEdit);
the binding makes the div visible. When you finish the edition, you can set the observable to null, like so
vm.editedPerson(null);
and the div will close.
My variation of RP Niemeyer's bindingHandler allows to automatically show this div inside a jQuery UI dialog. To use it you simply have to keep the original with binding, and specify the jQuery UI dialog options like so:
data-bind="with: editedPerson, withDialog: {/* jQuery UI dialog options*/}"
You can get the code of my binding handler, and see it in action here:
http://jsfiddle.net/jbustos/dBLeg/
You can modify this code easily to have different defaults for the dialog, and even to make these defaults configurable by enclosing the handler in a js module, and adding a public configuration function to modify it. (You can add this function to the binding handler, and it will keep working).
// Variation on Niemeyer's http://jsfiddle.net/rniemeyer/SnPdE/
/*
This binding works in a simple way:
1) bind an observable using "with" binding
2) set the dialog options for the ui dialog using "withDialog" binding (as you'd do with an standard jquery UI dialog) Note that you can specify a "close" function in the options of the dialog an it will be invoked when the dialog closes.
Once this is done:
- when the observable is set to null, the dialog closes
- when the observable is set to something not null, the dialog opens
- when the dialog is cancelled (closed with the upper right icon), the binded observable is closed
Please, note that you can define the defaults for your binder. I recommend setting here the modal state, and the autoOpen to false.
*/
ko.bindingHandlers.withDialog = {
init: function(element, valueAccessor, allBindingsAccessor) {
var defaults = {
modal: false,
autoOpen: false,
};
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
$.extend(defaults, options)
setTimeout(function() {
var oldClose = options.close;
defaults.close = function() {
if (options.close) options.close();
allBindingsAccessor().with(null);
};
$(element).dialog(defaults);
}, 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().with),
$el = $(element),
dialog = $el.data("uiDialog") || $el.data("dialog");
//don't call open/close before initilization
if (dialog) {
$el.dialog(shouldBeOpen ? "open" : "close");
}
}
};
var person = function() {
this.name = ko.observable(),
this.age = ko.observable()
}
var viewModel = function() {
label= ko.observable('dialog test');
editedPerson= ko.observable(null);
clearPerson= function() {
editedPerson(null);
};
newPerson= function() {
editedPerson(new person());
};
savePerson= function() {
alert('Person saved!');
clearPerson();
};
return {
label: label,
editedPerson: editedPerson,
clearPerson: clearPerson,
newPerson: newPerson,
savePerson: savePerson,
};
}
var vm = viewModel();
ko.applyBindings(vm);
.header {
font-size: 16px;
font-family: sans-serif;
font-weight: bold;
margin-bottom: 20px;
}
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/base/jquery-ui.css" rel="stylesheet"/>
<h1 class="header" data-bind="text: label"></h1>
<div id="dialog" data-bind="with: editedPerson, withDialog: {autoOpen: false, title: 'Dialog test', close: function() { alert('closing');} }">
Person editor<br/>
Name:<br/><input type="text" data-bind="value: $data.name"/><br/>
Age:<br/><input type="text" data-bind="value: $data.age"/><br/>
<button data-bind="click: $parent.savePerson">Ok</button>
<button data-bind="click: $parent.clearPerson">Cancel</button>
</div>
<div>
<button data-bind="click: clearPerson">Clear person</button>
<button data-bind="click: newPerson">New person</button>
</div>
<hr/>
<div data-bind="text: ko.toJSON($root)"></div>
There's now this library that has all the JQueryUI bindings for KnockoutJS including, of course, the dialog widget.
Related
Hi this is a continuation to this question here:
integrating jquery ui dialog with knockoutjs
I have implemented Niemeyers solution for a modal to handle login but would like it to handle a create-user page as well. This is the first time I'm writing a custom handler and don't really know how to continue from here..
Is this possible to implement in the same handler or do I need to create two separate handler for each page?
Anyways there are some comments in there on how I tried to approach this, but could use a helping hand on this..
Link to fiddle: http://jsfiddle.net/king_s/9m3678m2/6/
HTML
<div id="dialog" data-bind="dialog: {autoOpen: false, title: 'Sign in' }, signIn_Visible: isOpen">
<table>
<tr>
username
<input />
</tr>
<tr>
password
<input />
</tr>
<tr>
sign up
</tr>
</table>
<button data-bind="click: close">Close</button>
</div>
<!--Mark up for dialog page 2
<div id="dialog_two" data-bind="dialog: {autoOpen: false, title: create user }", create_visible>
MARK UP GOES HERE..
</div>
-->
<div>
<button data-bind="click: open">Open</button>
<button data-bind="click: close">Close</button>
</div>
<hr/>
<div data-bind="text: ko.toJSON($root)"></div>
JS
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().signIn_Visible(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().signIn_Visible),
$el = $(element),
dialog = $el.data("uiDialog") || $el.data("dialog");
// var openRegistration = ko.utils.unwrapObservable(allBindingsAccessor()
// .create_visible),
// $ele = $(element),
// dialog_reg = $el.data("uiDialog") || $el.data("dialog");
//don't call open/close before initilization
if (dialog) {
//open or close - functions on the jquery dialog object
$el.dialog(shouldBeOpen ? "open" : "close");
//Create conditional statement here to decide wheter to open/closes this page
//or navigate to page 2??
}
}
};
var viewModel = {
label: ko.observable('dialog test'),
isOpen: ko.observable(false),
open: function() {
this.isOpen(true);
},
close: function() {
this.isOpen(false);
}
};
ko.applyBindings(viewModel);
I want to add animation when working with observable not observableArray which is already available in knockout documents.
Consider a <div data-bind="text: fullname"/> which fullname is an observable. I want the content of <div> element animate and slides down when the value of fullname changes. Something like:
<div data-bind="text: fullname, transition: slideDown"/>
Also, I'm using Durandal if it helps.
Question: How can I assign transition animations to observables?
The beforeRemove and afterAdd are just wrappers around the subscribe function.
ko.bindingHandlers['transition'] = {
init: function(element, valueAccessor, allBindings) {
var transition = valueAccessor(),
target = allBindings.get('text'),
subscriptions = [
target.subscribe(target, onChange),
target.subscribe(target, onBeforeChange, 'beforeChange')
];
function onChange() {
// handle transition when target updates
}
function onBeforeChange() {
// handle transition BEFORE target updates
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
subscription[0].dispose();
subscription[1].dispose();
});
}
};
You can create a custom binding handler which does the animation for you:
ko.bindingHandlers.textSlide = {
init: function(element, valueAccessor) {
$(element).text(ko.unwrap(valueAccessor()));
},
update: function(element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
if (value != $(element).text()) {
$(element).slideUp(function() {
$(this).text(value).slideDown();
});
}
}
};
See http://jsfiddle.net/ro0u9Lmb/
I use select2 and knockoutJs with this simple binding:
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = ko.toJS(valueAccessor()) || {};
setTimeout(function () {
$(element).select2(options);
}, 0);
}
};
Markup:
<select class="select2" style="width:100%" data-bind="optionsCaption: '',options: $root.items,optionsText: 'description',optionsValue: 'id', value: $root.selectedItem,select2: { placeholder: 'Select an item...',allowClear: true }"></select>
it works! Now I enabled the allowClear option in Select2 to clear dropdown to a placeholder value like Select an item....
If I click the x icon dropdown properly sets the placeholder but knockout don't update observable binded value!
I think I've to change custombinding adding something like this:
setTimeout(function () {
$(element).select2(options).on("select2-removed", function (e) {
ko.bindingHandlers.value.update(element, function () { return ''; });
});
...
but it won't work!
There are couple of issues.
1) the update in bindinghandler is to update DOM based on value change, you should never define an update callback with the ability to mutate your model.
The right way is, when define a new bindinghandler, use init callback to hook up all change events with the model, the update callback is merely a DOM drawing routine.
If your init provided DOM drawing (such as provided by select2), you don't need to define an update callback.
Hence the line ko.bindingHandlers.value.update(element, function () { return ''; }); only redraw the DOM, it doesn't do what you want.
2) the select2 binding you created has some holes.
first, value binding doesn't know the existence of select2 binding, that's where you struggled.
second, your select2 binding has to wait for other binding (the options binding) to finish DOM creation, what's why you use setTimeout. But ko provided a way to define sequence of the bindings, just look the source code of ko value binding, it's defined as 'after': ['options', 'foreach']
third, your select2 doesn't respond to outside change. For instance, if you have another UI to update $root.selectedItem (a normal select list), the change raised by that UI would not sync back to your select2.
The solution
Define select2 binding based on existing value binding (just found out it's not needed), and hook up all change events.
we don't need "select2-removed" event, it's all about "change" event.
select2 provided all drawing, we don't need update callback.
use a flag shouldIgnore to break the loop between value subscriber and select2 change event handler.
http://jsfiddle.net/huocp/8N3zX/6/
http://jsfiddle.net/huocp/8N3zX/9/
ko.bindingHandlers.valueSelect2 = {
'after': ['options'],
'init': function(element, valueAccessor, allBindings) {
// kind of extend value binding
// ko.bindingHandlers.value.init(element, valueAccessor, allBindings);
var options = allBindings.get('select2Options') || {};
$(element).select2(options);
var value = valueAccessor();
// init val
$(element).val(ko.unwrap(value)).trigger("change");
var changeListener;
if (ko.isObservable(value)) {
var shouldIgnore = false;
changeListener = value.subscribe(function(newVal) {
if (! shouldIgnore) {
shouldIgnore = true;
// this is just select2 syntax
$(element).val(newVal).trigger("change");
shouldIgnore = false;
}
});
// this demo only works on single select.
$(element).on("change", function(e) {
if (! shouldIgnore) {
shouldIgnore = true;
if (e.val == '') {
// select2 use empty string for unselected value
// it could cause problem when you really want '' as a valid option
value(undefined);
} else {
value(e.val);
}
shouldIgnore = false;
}
});
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (changeListener) changeListener.dispose();
$(element).select2("destory");
});
}
};
I've found a couple other questions and resources here on using Bootstrap tooltips with a custom knockout binding handler. However, I haven't found is a cohesive solution that 1) involves using a dynamic knockout template 2) one where the tooltip can change when data it is bound to changes.
I'm also away of knockout-bootstrap on GitHub, but the tooltip title in that is rendered only once,
I've created a NEW JSFiddle with the following new dynamicTooltip that's based on the prior JSFiddle.
The new DynamicTooltip data binder looks like:
ko.renderTemplateHtml = function (templateId, data) {
var node = $("<div />")[0];
ko.renderTemplate(templateId, data, {}, node);
return $(node).html();
};
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var local = ko.utils.unwrapObservable(valueAccessor()),
options = {};
ko.utils.extend(options, ko.bindingHandlers.tooltip.options);
ko.utils.extend(options, local);
var tmplId = options.kotemplate;
ko.utils.extend(options, {
title: ko.renderTemplateHtml(tmplId, viewModel)
});
$(element).tooltip(options);
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).tooltip("destroy");
});
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var local = ko.utils.unwrapObservable(valueAccessor()),
options = {};
ko.utils.extend(options, ko.bindingHandlers.tooltip.options);
ko.utils.extend(options, local);
var tmplId = options.kotemplate;
var forceRefresh = options.forceRefresh;
var newdata = ko.renderTemplateHtml(tmplId, viewModel);
$(element).data('bs.tooltip').options.title = newdata
},
options: {
placement: "top",
trigger: "hover",
html: true
}};
It's not complete, as I'm manually triggering the update call manually by passing in a dummy databinding property on the view Model, in this case, it's called renderTooltip():
<a data-bind="tooltip: { title: firstName, placement: 'bottom', kotemplate: 'tile-tooltip-template', forceRefresh: renderTooltip() }">Hover on me</a>
I'd like to be able to trigger the tooltip to refresh itself when data changes.
I'm thinking I should be using createChildContext() and maybe controlsDescendantBindings, but I'm not really sure.
Any thoughts? I'll continue to update this, because it seems like dynamic bootstrap tooltips would be a common idea.
The root of the problem is that the update binding isn't firing, because it doesn't have a dependency on the properties that you are trying to update (i.e. firstName and address);
Normally you can delegate these properties to a new binding and let knockout automatically handle the dependency tracking. However, in this case, you're actually returning a string, so the element's automatic binding can't be used. A string is necessary, because that's how the tooltip works. If it could dynamically read from a DOM element, then the automatic bindings would work, but because it requires a HTML string, there's no way for the bindings to affect that.
Couple of options that I see:
1. Automatically create a dependency on the properties used by the template. This can be done by isolating the template view model (data) as seen in this fiddle: http://jsfiddle.net/tMbs5/13/
//create a dependency for each observable property in the data object
for(var prop in templateData)
if( templateData.hasOwnProperty(prop) && ko.isObservable(templateData[prop]))
templateData[prop]();
2. Instead of using an DOM-based template, use ko.computed to generate the template inside your view model. This will automatically create the dependencies as needed.
See this fiddle for an example of that: http://jsfiddle.net/tMbs5/12/
var vm = {
firstName: ko.observable('Initial Name'),
address: ko.observable('55 Walnut Street, #3'),
changeTooltip: function () {
this.firstName('New Name');
}
};
vm.tooltipHtml = ko.computed(function () {
return "<h2>" + vm.firstName() + "</h2>" +
"<span>" + vm.address() + "</span>";
});
ko.applyBindings(vm);
note: In both fiddles, I have refactored things a tiny bit - mostly for simplification
I want to query a element in a durandaljs widget, when it's ready.
If i use the selector directly in the data-binding, the element will not be found:
html (no attached view):
<button id="myButton"></button>
<div data-bind="widget: { kind: 'myWidget', options: { btn: $('#myButton') } }"></div>
controller.js:
define(function (require) {
var ctor = function (element, settings) {
var btn = settings.options.btn;
// btn = $('#myButton'); // this will work, but i'm not sure if the DOM is
// currently ready in the constructor
btn.on("click", function () {
console.log("I want to be fired");
});
};
return ctor;
});
Whats the best way to query a DOM element from a durandal widget at start?
I'm not sure where the html fragment belongs to so there are two slightly different answers.
First I'd suggest that you don't pass in the btnas jQuery object ({btn: $('myButton')}) , when you're not sure that it already exists. It's probably better to pass in a selector {btn: '#myButton'} and let the widget figure out how to deal with it.
Does your widget have its own view.html and the button is defined inside? If that's the case than you should take a look at the viewAttached callback.
var ctor = function (element, settings) {
this.btn = settings.options.btn;
};
ctor.prototype.viewAttached = function (view){
var btn = $(this.btn, view);
if ( btn.length > 0 ) {
btn.on("click", function () {
console.log("I want to be fired");
});
}
}
If your widget doesn't have its own view.html than you should let the widget know by adding a view property to the settings object with a value of false.
Here's the paragraph from http://durandaljs.com/documentation/Creating-A-Widget/ that explains that.
Note: In some cases, your widget may not actually need a view. Perhaps it's just adding some jQuery behavior or applying an existing jQuery plugin to a dom element. To tell Durandal that there is no view to load and bind, add a view property to the settings object with a value of false inside your widget's constructor.
In that instance however you can only access elements that are already in the DOM when the widget is instantiated e.g.
var ctor = function (element, settings) {
settings.view = false;
this.btn = $(settings.options.btn);
if ( this.btn.length > 0 ) {
this.btn.on("click", function () {
console.log("I want to be fired");
});
}
};