integrating jquery ui dialog with knockoutjs
Asked Answered
E

5

37

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.

Emetine answered 23/12, 2011 at 1:20 Comment(0)
G
64

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/

Godesberg answered 23/12, 2011 at 3:10 Comment(21)
Thanks! I am wondering how to handle the fact that init gets called twice here. I'd rather not create the dialog twice.Emetine
Interesting. I never noticed that before. .dialog() wraps and moves the element to the bottom and applyBindings hits it again. Easy enough to handle by calling .dialog() in a setTimeout. I updated the fiddles and code above.Godesberg
I don't quite understand why the timeout is not being called twice. But it certainly works!Emetine
Also, should the disposal callback be in the timeout as well?Emetine
The issue was that applyBindings is going through the elements top-to-bottom and when .dialog() ran it actually moved (and wrapped) the element to the bottom of the page. Then applyBindings encountered it again and ran the init again. The setTimeout allows applyBindings to complete its pass through the page before calling .dialog(). The disposal callback is okay to not put in a timeout as it will only be called when Knockout cleans up elements (like as part of templating).Godesberg
I think I understand the process; I was just wondering about registering the dispose calback twice.Emetine
Yep. The init will only get called once, as applyBindings won't hit the element twice when the dialog call happens in the setTimeout.Godesberg
I know it can be handled, but I'd like to point out that, as it is, if you close the dialog with its X button, the dialog will not open again until you click the close button on the page.Yettie
nice, I like the toJSON divYettie
I recommend moving the dialog call with options to the update so changes to option observables update the dialog. For example, jsfiddle.net/4mUqb/1 Open the dialog and change the text in the input, making sure it loses focus so the edit updates.Berneta
@RP Niemeyer: one more note: in jquery UI 1.10 $(element).data('dialog') doesn't works, use $(element).data('uiDialog')... I'm afraid, that in version 1.11 or 1.12 we will get another change :-(Saurischian
OK- probably can go with $el.data("uiDialog") || $el.data("dialog");Godesberg
Would love to know if anyone has figured out a way to avoid double-binding without need for setTimeout. I have other bindings that also work with the dialog, and maybe in the future code outside of bindings as well.. would be nice to not have to add extra code everywhere to worry about cases where dialog hasn't been initialized yet due to the timer. I could use a jQuery promise for waiting, but still, would rather not have to. Working on an idea of setting a flag on first bind, but not sure how to stop KO from binding again.Brom
You can avoid double-binding by placing the dialog inside a template, but for me that isn't an option because my dialogs have 100s of elements in them which causes the dialog to open/close slowly when using a template (too much rebinding/DOM work).Brom
How would you fill the dialogue with some json data using that method? Say the button was in a KO foreach statement and wanted to send data to the dialogue.Gonyea
@RPNiemeyer How would you go about putting the close button inside the dialog? Let's say, we want a Cancel and a Save button, would you simply add them inside the dialog div with data-bindings (<button data-bind="click: close" value="Cancel" />), or is there a way to use the 'buttons' dialog property and still apply databindings (data-bind="dialog: { buttons : { Cancel: function() {...}, Save: function() {...} } }; )?Osset
I think that adding them yourself with data-binds is the easiest way to go, so that you can easily set the observable to false like: jsfiddle.net/rniemeyer/YmQTWGodesberg
I used this code but was having trouble with the update. The problem was that I updated dialogVisible via an ajax call and the results were coming back to quickly...before the dialog had been initialized. So the if (dialog) condition kept skipping over the code to open/close the dialog. I therefore wrapped the if (dialog) check in a setInterval, which waited for jQuery to create the dialog. I cleared the setInterval loop as soon as the $el.dialog(shouldBeOpen ? "open" : "close"); line executed (or x milliseconds...whichever came first).Tecu
@JotaBe - can you add your information as a separate answer or comment? Thanks!Godesberg
Of course. I'm sure I'm missing something, because I thought it was better to update your answer than to create a new one or add a comment. What is the reason?Aquanaut
jquery ui 1.11.3 - new change: $(element).data('ui-dialog'). This way of dialog detection is breaking too often, looks like it's unofficial dialog detection...Saurischian
C
5

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]());
        }
    }
}
Ciel answered 9/5, 2013 at 17:50 Comment(0)
B
4

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:

  1. Add class='dialog' to any elements using the custom dialog binding.

  2. 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.

Brom answered 19/2, 2013 at 17:41 Comment(0)
A
4

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>
Aquanaut answered 14/3, 2014 at 1:28 Comment(0)
E
3

There's now this library that has all the JQueryUI bindings for KnockoutJS including, of course, the dialog widget.

Edgerton answered 17/9, 2013 at 2:22 Comment(1)
That's the answer I was looking for! Thx.Hyperbole

© 2022 - 2024 — McMap. All rights reserved.