25

I am trying to fully bind twitter bootstrap modal with knockout. By fully bind I mean that I want every close interaction with modal dialog to work with knockout. I have seen some of the questions, which partially bind them (for example this one does not allow esc).

I am using almost identical binding (which I actually found elsewhere)

ko.bindingHandlers.modal = {
    init: function (element, valueAccessor) {
        $(element).modal({
            show: false
        });
    },
    update: function (element, valueAccessor) {
        var value = valueAccessor();
        if (ko.utils.unwrapObservable(value)) {
            $(element).modal('show');
        } else {
            $(element).modal('hide');
        }
    }
}

But the problem is that not everything work in my Fiddle. As you see if you close Modal with Close button, you can fire this modal again. But if you close it with Esc key, or with clicking on background, or on the X button, you can not open Modal again.

I know that the problem is due to the fact that when I close modal with other means (it is not changing observable and therefore when I fire it for the second time - nothing changes). But I can not figure out how to do this properly.

Here is my hack :-), where everything works. I am giving new value each time. But is there a better way?

Community
  • 1
  • 1
Salvador Dali
  • 214,103
  • 147
  • 703
  • 753

2 Answers2

43

bootstrap modal provided events, you just need to hook up event 'hidden.bs.modal'.

BTW, do proper disposal too. http://jsfiddle.net/C8w8v/377/

ko.bindingHandlers.modal = {
    init: function (element, valueAccessor) {
        $(element).modal({
            show: false
        });

        var value = valueAccessor();
        if (ko.isObservable(value)) {
            // Update 28/02/2018
            // Thank @HeyJude for fixing a bug on
            // double "hide.bs.modal" event firing.
            // Use "hidden.bs.modal" event to avoid
            // bootstrap running internal modal.hide() twice.
            $(element).on('hidden.bs.modal', function() {
               value(false);
            });
        }

        // Update 13/07/2016
        // based on @Richard's finding,
        // don't need to destroy modal explicitly in latest bootstrap.
        // modal('destroy') doesn't exist in latest bootstrap.
        // ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
        //    $(element).modal("destroy");
        // });

    },
    update: function (element, valueAccessor) {
        var value = valueAccessor();
        if (ko.utils.unwrapObservable(value)) {
            $(element).modal('show');
        } else {
            $(element).modal('hide');
        }
    }
}
huocp
  • 3,898
  • 1
  • 17
  • 29
  • 1
    Cool, thank you. Will accept in few minutes. Can you please tell me why do I need disposal here? – Salvador Dali Mar 28 '14 at 07:57
  • 3
    in your case, it probably doesn't matter. But if you wrap your modal dom in another
    . ko will init your modal and dispose your modal when some_condition changes. The disposal prevents potential memory leak when ko removes the dom elements (your modal). It's used throughout ko standard bindings. Too shame ko website didn't document this.
    – huocp Mar 28 '14 at 09:21
  • 6
    They did document this ;) http://knockoutjs.com/documentation/custom-bindings-disposal.html – krzychu Jun 13 '14 at 21:53
  • Great answer! I didn't know the dispose callback existed, so hopefully adding it to my custom tooltip binding will fix an issue I was having with it still displaying after the element got removed. – zaparker Dec 12 '14 at 18:56
  • I had the problem that a nested datepicker emitting a 'hide' event interfered with the binding to the 'hide.bs.modal' event. I fixed it by checking the event namespace like this: `if(e.namespace != 'bs.modal') return;` – Christ A Jul 16 '15 at 12:45
  • @ChristA, could you put one example on jsfiddle? At least my jqueryui datepicker doesn't generate 'hide' event which confuses the modal. – huocp Jul 17 '15 at 02:53
  • 1
    It may have been different at the time, but the modal plugin does not have a destroy method (as of 3.3.6). The code as-is will generate an error when disposal is called. – Richard Jul 12 '16 at 14:45
  • 1
    @Richard thx. It looks like bootstrap will clean it up eventually. https://github.com/jschr/bootstrap-modal/blob/3a1aed346aa00a8aea81a9e167fc54a2ac8fb252/js/bootstrap-modalmanager.js#L112-L133 – huocp Jul 13 '16 at 04:43
  • 1
    I enhanced your fiddle to show how to send data into the modal dialog. Fantastic work, this saved me tons of time. Thanks. [http://jsfiddle.net/kahanu/df64zrhh/4/](http://jsfiddle.net/kahanu/df64zrhh/4/) – King Wilder Oct 23 '16 at 01:01
  • Just the answer I was looking for – Kent Aguilar Nov 15 '16 at 07:08
  • @huocp, note that when the modal is closed with a click outside the modal, `hide.bs.modal` event happens twice: First when the click is made, which fires the callback that calls `value(false)`, causing `update` to fire, which results on the second `hide.bs.modal`. If one would like to prevent the callback code from running the second time, he can simply wrap `value(false)` with an `if (value() != false)`. – OfirD Feb 27 '18 at 22:05
  • @HeyJude, bootstrap modal did not provide any API to check the state. It could be nice to do "if not showing, hide it". Fortunately, bootstrap does check (yeah, who would not) in its implementation of [`hide()`](https://github.com/twbs/bootstrap/blob/29f178d89129732d35ae812667709c64cdb28a6f/js/src/modal.js#L155) and `show()`. So the second `modal('hide')` is noop anyway. – huocp Feb 27 '18 at 22:49
  • @HeyJude, sorry I think I missed something. Listen event `hidden.bs.modal` instead of `hide.bs.modal` should resolve your issue. Please let me know whether it works, I will update the answer accordingly. Thanks very much! (fyi, I am not using KO for years, no local setup to test it now). – huocp Feb 27 '18 at 22:52
  • @HeyJude, I believe you don't need `if (value() != false)` wrapper, KO is smart enough to avoid updating with same value. That's why this issue didn't manifest as endless loop. – huocp Feb 27 '18 at 22:54
  • @huocp, listening to `hidden.bs.modal` solves it. If one still wants to listen to `hide.bs.modal` (without listening to `hidden.bs.modal`) and prevent the second call to `value(false)`, he'd have to use an `if`. – OfirD Feb 27 '18 at 23:17
  • @HeyJude, understand what you mean. My point was even you have 2nd call to `value(false)`, for efficiency, KO would not notify the value's subscribers since the value did not change. After all, thanks for the bug fix!! – huocp Feb 27 '18 at 23:30
2

slightly neater BS binding code - and classes are added when needed.:

ko.bindingHandlers.BSModal= {
    init: function (element, valueAccessor) {
        var value = valueAccessor();
        $(element).addClass('modal').addClass('fade').modal({ keyboard: false, show: ko.unwrap(value) });;
    },
    update: function (element, valueAccessor) {
         var value = valueAccessor();
         ko.unwrap(value) ? $(element).modal('show') : $(element).modal('hide');
    }
};

Then just data-bind="BSModal: true/false Observable" value.

Nerdroid
  • 13,398
  • 5
  • 58
  • 69
Piotr Stulinski
  • 9,241
  • 8
  • 31
  • 46