4

I went looking for a knockout inline edit binding, but the only ones I found had external dependencies other than jQuery, or used more than just a binding.

So I figured I would share the simple one I came up with (other answer's of course welcome, especially those that only use knockout).

Kyeotic
  • 19,697
  • 10
  • 71
  • 128

3 Answers3

5

Just as an alternative: the code that I have used for inline editing looks like:

ko.bindingHandlers.hidden = {
    update: function(element, valueAccessor) {
        ko.bindingHandlers.visible.update(element, function() { return !ko.utils.unwrapObservable(valueAccessor()); });
    }        
};

ko.bindingHandlers.clickToEdit = {
    init: function(element, valueAccessor) {
        var observable = valueAccessor(),
            link = document.createElement("a"),
            input = document.createElement("input");

        element.appendChild(link);
        element.appendChild(input);

        observable.editing = ko.observable(false);

        ko.applyBindingsToNode(link, {
            text: observable,
            hidden: observable.editing,
            click: observable.editing.bind(null, true)
        });

        ko.applyBindingsToNode(input, {
            value: observable,
            visible: observable.editing,
            hasfocus: observable.editing
        });
    }
};

http://jsfiddle.net/rniemeyer/Rg8DM/

RP Niemeyer
  • 114,592
  • 18
  • 291
  • 211
  • That is quite a bit cleaner, thanks! The only downside is that inside a paragraph

    , since you aren't setting the width, it won't line up well. One question: why create a hidden binding, instead of just `visible: !observable.editing`?

    – Kyeotic Nov 13 '12 at 16:14
  • 1
    When calling `applyBindingsToNode` we need to pass it an observable/computed that will be unwrapped when the binding runs. If you simply pass it `!observable.editing()`, then it will be a static value that will never trigger changes. Another option would be to define a computed in the handler that returns the opposite of the `editing` sub-observable like: http://jsfiddle.net/rniemeyer/Rg8DM/2/. And yes it does not do anything with the width. – RP Niemeyer Nov 13 '12 at 18:14
  • @RPNiemeyer, Is there a way to incorporate a keyup listener to this? I'd like to have the Escape key also cancel/end the current edit. – Kal_Torak Apr 22 '13 at 06:36
  • @Kal_Torak - I would probably go with something like: http://jsfiddle.net/rniemeyer/8D5aj/ – RP Niemeyer Apr 22 '13 at 13:49
  • Can you give some hints how to improve the code to send updated data to database? – renathy Nov 23 '13 at 15:30
4

Here is my inline edit binding (fiddle), it relies on jQuery for some DOM manipulation though.

HTML:

<p>
    Set an alarm for <span data-bind="inline: startTime"></span>
    using <span data-bind="inline: snoozeCount"></span> Snooze(s).
</p>

JS:

ko.bindingHandlers.inline= {
    init: function(element, valueAccessor) {
        var span = $(element);
        var input = $('<input />',{'type': 'text', 'style' : 'display:none'});
        span.after(input);

        ko.applyBindingsToNode(input.get(0), { value: valueAccessor()});
        ko.applyBindingsToNode(span.get(0), { text: valueAccessor()});

        span.click(function(){
            input.width(span.width());
            span.hide();
            input.show();
            input.focus();
        });

        input.blur(function() { 
            span.show();
            input.hide();
        });

        input.keypress(function(e){
            if(e.keyCode == 13){
               span.show();
               input.hide();
           }; 
        });
    }
};

The width is set in the click function because of the unreliability of the width on Dom Ready: it was coming out as 0 half the time.

I also made one for toggles (boolean) that you just click to switch:

ko.bindingHandlers.inlineToggle = {
    init: function(element, valueAccessor, allBindingsAccessor) {

        var displayType = allBindingsAccessor().type || "bool";
        var displayArray = [];

        if (displayType == "bool") {
            displayArray = ["True", "False"];
        } else if (displayType == "on") {
            displayArray = ["On", "Off"];
        } else {
           displayArray = displayType.split("/");
        } 

        var target = valueAccessor();  
        var observable = valueAccessor()
        var interceptor = ko.computed(function() {
            return observable() ? displayArray[0] : displayArray[1];
        });

        ko.applyBindingsToNode(element, { text: interceptor });
        ko.applyBindingsToNode(element, { click: function(){ target (!target())} });
    }
};

Apply like so (second param is optional):

<span data-bind="inlineToggle: alert, type: 'on'"></span>

The fiddle also contains one for select and multi-select, but right now the multi-select causes the display to jump. I'll need to fix that.

Kyeotic
  • 19,697
  • 10
  • 71
  • 128
1

Even though the answer is already accepted I believe I found a better solution so I would like to share it.

I ended up using what was in the documentation here http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html#example-supplying-additional-values-to-descendant-bindings

and came up with this rough draft of the binding:

ko.bindingHandlers['textinlineeditor'] = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

        var observable = valueAccessor();
        var value = ko.utils.unwrapObservable(observable);

        var saveHandler = allBindingsAccessor().editorsavehandler || function (newValue) { observable(newValue); return true; };
        var inputType = allBindingsAccessor().editorinputtype || "text";            

        var vm = new inlineEditorViewModel({ val: value, saveHandler: saveHandler });

        $(element).append("<span data-bind=\"text: editableValue, hidden: editing\"></span>");
        $(element).append("<input type=\"" + inputType + "\" data-bind=\"value: editableValue, visible:editing\"/>");
        $(element).append("<div class=\"pull-right\"></div>");
        $(element).find("div.pull-right").append("<span class=\"glyphicon glyphicon-edit\" aria-hidden=\"true\" data-bind=\"click: edit, hidden: editing\"></span>");
        $(element).find("div.pull-right").append("<span class=\"glyphicon glyphicon-check\" aria-hidden=\"true\" data-bind=\"click: save, visible:editing\"></span>");
        $(element).find("div.pull-right").append("<span class=\"glyphicon glyphicon-remove-circle\" aria-hidden=\"true\" data-bind=\"click: cancelSave, visible:editing\"></span>");

        var innerBindingContext = bindingContext.extend(vm);
        ko.applyBindingsToDescendants(innerBindingContext, element);

        return { controlsDescendantBindings: true };
    }
};

the model I used in the code is

var inlineEditorViewModel = function (init) {
    var self = this;

    self.editableValue = ko.observable(init.val);
    self.lastEditableValue = ko.observable(init.val);

    self.editing = ko.observable(false);

    self.edit = function () {
        self.editing(true);
    };

    self.save = function () {
        if (init.saveHandler(self.editableValue())) {
            self.lastEditableValue(self.editableValue());
            self.editing(false);
        }
    };

    self.cancelSave = function () {
        self.editableValue(self.lastEditableValue());
        self.editing(false);
    };
};

in your html you would use it like

<div data-bind="textinlineeditor: name, editorsavehandler: saveName"></div>

Note: I am using Bootstrap so that is where the icons in the binding html are from.

Tony
  • 1,366
  • 1
  • 10
  • 9
  • What about this do you prefer to the other solutions? I think having to specify the save handler makes this more coupled, and I don't see what it buys you. – Kyeotic May 28 '15 at 21:46
  • I see what you are saying. I guess for my problem this solution makes the most sense since users will be editing and saving fields individually and not making changes and then saving the form all at once. The part that I meant that I preferred was using knockout instead of jQuery for the DOM manipulation with respect to showing and hiding elements. – Tony May 28 '15 at 21:52
  • You could also very easily set the save handler as a function to just update the observable. – Tony May 28 '15 at 21:54
  • The accepted answer does not use jQuery. A subscription on each observable would allow you to respond to them being changed without requiring a save handler; which the common use case (just updating an observable) doesn't require. I see the save handler as a definite downside. I see no upsides. – Kyeotic May 28 '15 at 22:48
  • I updated the example so the default behavior is to update the observable. Yes, like you said you can now add a subscription to watch for change on the observable. I'm not sure I understand why having the option of a function to handle saving is a downside? In the way that I am using it I am doing validation when a user tries to save and I want to keep the field editable if it is not valid. Do you have a better suggestion for doing that using a subscription instead? – Tony May 29 '15 at 03:29
  • the *option* to have a save handler is nice, the *requirement* to have one is bad since the normal use case is binding the observable. Your change fixes this entirely, its a solid improvement. +1 – Kyeotic May 29 '15 at 16:19