Search code examples
knockout.jscodemirror

CodeMirror custom binding doesn't work inside "foreach" binding


I have created a custom binding for CodeMirror. Custom binding works with a simple string, but inside foreach binding, it ceases to be initialized, although all the HTML and CSS is added.

Here's a working snippet:

var viewModel = {
  options: {
    mode: "text/x-csharp",
    lineNumbers: true
  },

  //IT WORKS
  fileContent: "public sealed class DictionaryAttribute : Attribute{}1",

  //IT DOESN'T WORK
  codes: ["public sealed class DictionaryAttribute : Attribute{}1"]
};

ko.bindingHandlers.codemirror = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    var options = viewModel.options || {};
    options.value = ko.unwrap(valueAccessor());
    var editor = CodeMirror(element, options);

    editor.on('change', function(cm) {
      var value = valueAccessor();
      value(cm.getValue());
    });

    element.editor = editor;
  }
};

ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<link href="https://codemirror.net/lib/codemirror.css" rel="stylesheet" />
<script src="https://codemirror.net/lib/codemirror.js"></script>
<script src="https://codemirror.net/mode/clike/clike.js"></script>

<!-- This works -->
<div data-bind="codemirror: fileContent" style="width: 700px; height: 100px"></div>

<!-- This doesn't work -->
<div data-bind="foreach: codes">
  <div data-bind="codemirror: $data" style="width: 700px; height: 100px"></div>
</div>


Solution

  • The problem is with var options = viewModel.options || {}; in your custom binding. viewModel parameter refers to the current $data in context, not the viewModel used in applyBindings. It works for a simple string because in that case, viewModel parameter is the main viewModel object you're passing to applyBidnigs. Inside foreach, viewModel will be each $data in your array.

    So, use the $root property of bindingContext parameter instead. Besides, viewModel parameter is deprecated in Knockout 3.x:

    Like this: var options = bindingContext.$root.options || {};

    Updated snippet:

    var viewModel = {
      options: {
        mode: "text/x-csharp",
        lineNumbers: true
      },
    
      //IT WORKS
      fileContent: "public sealed class DictionaryAttribute : Attribute{}1",
    
      //IT DOESN'T WORK
      codes: ["public sealed class DictionaryAttribute : Attribute{}1"]
    };
    
    ko.bindingHandlers.codemirror = {
      init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var options = bindingContext.$root.options || {};
        options.value = ko.unwrap(valueAccessor());
        var editor = CodeMirror(element, options);
    
        editor.on('change', function(cm) {
          var value = valueAccessor();
          value(cm.getValue());
        });
    
        element.editor = editor;
      }
    };
    
    ko.applyBindings(viewModel);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <link href="https://codemirror.net/lib/codemirror.css" rel="stylesheet" />
    <script src="https://codemirror.net/lib/codemirror.js"></script>
    <script src="https://codemirror.net/mode/clike/clike.js"></script>
    
    <div data-bind="codemirror: fileContent" style="width: 700px; height: 100px"></div>
    
    <div data-bind="foreach: codes">
      <div data-bind="codemirror: $data" style="width: 700px; height: 100px"></div>
    </div>


    The above code works in your case. But, the binding expects the top $root object to have the options property. Another way to do this would be to add a codeMirrorOptions parameter to the binding and remove that dependency altogether.

    var viewModel = {
      options: {
        mode: "text/x-csharp",
        lineNumbers: true
      },
    
      //IT WORKS
      fileContent: "public sealed class DictionaryAttribute : Attribute{}1",
    
      //IT DOESN'T WORK
      codes: ["public sealed class DictionaryAttribute : Attribute{}1"]
    };
    
    ko.bindingHandlers.codemirror = {
      init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // use allBindings
        var options = ko.unwrap(allBindings().codeMirrorOptions) || {};
        options.value = ko.unwrap(valueAccessor());
        var editor = CodeMirror(element, options);
    
        editor.on('change', function(cm) {
          var value = valueAccessor();
          value(cm.getValue());
        });
    
        element.editor = editor;
      }
    };
    
    ko.applyBindings(viewModel);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <link href="https://codemirror.net/lib/codemirror.css" rel="stylesheet" />
    <script src="https://codemirror.net/lib/codemirror.js"></script>
    <script src="https://codemirror.net/mode/clike/clike.js"></script>
    
    <div data-bind="codemirror: fileContent, codeMirrorOptions:options" style="width: 700px; height: 100px"></div>
    
    <div data-bind="foreach: codes">
      <div data-bind="codemirror: $data, codeMirrorOptions:$parent.options" style="width: 700px; height: 100px"></div>
    </div>

    In this case, the custom binding is independent of the viewModel. Even if your viewModel isn't the $root object, the custom binding will work.