Search code examples
knockout.jscontenteditableko.observablearray

Knockout.js contenteditable observablearray


I'm trying to allow editing items in an array of strings like so:

http://jsfiddle.net/adamfraser/sr4Fg/44/

html

<h4> edit </h4>
<ul class="list" data-bind="foreach:titles">
  <li class="title" data-bind="editableHTML:$data" contenteditable="true"></li>
</ul>

<h4> view </h4>
<ul class="" data-bind="foreach: titles">
  <li class="title" data-bind="text:$data"></li>
</ul>

js

ko.bindingHandlers.editableHTML = {
  init: function(element, valueAccessor) {
    var $element = $(element);
    var initialValue = ko.utils.unwrapObservable(valueAccessor());
    $element.html(initialValue);
    $element.on('keyup', function() {
      observable = valueAccessor();
      observable($element.html());
    });
  }
};

viewModel= {
  titles : ko.observableArray([
    "one", "two"
  ])
};

ko.applyBindings(viewModel);

While the custom editableHTML handler works for regular (non-array) observables, it's not cutting it for observableArrays. Anyone know why? I'm still new to KO.


Solution

  • Option 1:

    You can explicitly point the editableHTML handler to the parent array (titles in your case) using custom direction (context in my code).

    Once you have the reference to the parent array you can use the replace method that is available for each observable array to update the value.

    Something like:

    ko.bindingHandlers.editableHTML = {
      init: function(element, valueAccessor, allBindings) {
        var $element = $(element);
        var initialValue = ko.utils.unwrapObservable(valueAccessor());
        var parentContext = allBindings.get('context');
    
        $element.html(initialValue);
    
        $element.on('keyup', function() {
          var curVal = valueAccessor();
          var newVal = $element.html();
    
          if (parentContext)
              parentContext.replace(curVal, newVal);
          else if (ko.isObservable(valueAccessor()))
              valueAccessor()(newVal);
        });
      }
    };
    

    Then:

    <li class="title"
        contenteditable="true"
        data-bind="editableHTML: $data, context: $parent.titles"></li>
    

    See Fiddle

    Option 2:

    Change the titles array to contain observables instead of plain values, then call the observable upon value change.

    For example:

    ko.bindingHandlers.editableHTML = {
      init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var $element = $(element);
        var initialValue = ko.utils.unwrapObservable(valueAccessor());
        var origObservable = bindingContext.$rawData;
    
        $element.html(initialValue);
    
        $element.on('keyup', function() {
          var curVal = valueAccessor();
          var newVal = $element.html();
    
          if (ko.isObservable($origObservable))
              origObservable(newVal);
        });
      }
    };
    

    Your updated view-model:

    var viewModel = {
       titles: ko.observableArray([ko.observable("one"), ko.observable("two")])
    };
    

    And the HTML remains the same:

    <li class="title"
        contenteditable="true"
        data-bind="editableHTML: $data"></li>
    

    See Fiddle