Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to integrate CodeMirror into KnockoutJS?

I would like to integrate the CodeMirror JavaScript editor into KnockoutJS. I know there is also Ace, but it seems to me it would be easier with CodeMirror.

I already integrated custom bindings for JQueryUI widgets and QTip but these were pieces of code I found on the Internet and I then only needed to modify very small parts.

Unfortunately, it seems I've reached my limits on Javascript so I'm turning to JavaScript Sith Masters here. I don't necessarily want the whole thing written for me, pointers, and advice on how to continue would be of great help.

The piece of code I have:

The HTML (I removed custom bindings I already have on the textarea, they don't matter here)

<body>
    <textarea id="code" cols="60" rows="8" 
              data-bind="value: condition, 
              tooltip: 'Enter the conditions', 
              codemirror: { 'lineNumbers': true, 'matchBrackets': true, 'mode': 'text/typescript' }"></textarea>
</body>

The start of my custom binding handler for CodeMirror:

ko.bindingHandlers.codemirror = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var options = valueAccessor() || {};
        var editor = CodeMirror.fromTextArea($(element)[0], options);
    }
};

At the moment, this does not produce JS errors but 2 text areas are displayed instead of one.

So what should I do next ?

like image 553
Jalayn Avatar asked Nov 14 '12 08:11

Jalayn


4 Answers

The solutions posted before seem a bit out of date and wouldn't work for me so I have rewritten them in a form that works:

// Example view model with observable.
var viewModel = {
    fileContent: ko.observable(''),
    options: {
        mode:  "markdown",
        lineNumbers: true
    }
};

// Knockout codemirror binding handler
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;
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var observedValue = ko.unwrap(valueAccessor());
        if (element.editor) {
            element.editor.setValue(observedValue);
            element.editor.refresh();
        }
    }
};

ko.applyBindings(viewModel);

With <div data-bind="codemirror:fileContent"></div> as the target for code mirror to attach to this will create a new codemirror instance and pass in options from the view model if they have been set.

[edit] I have amended the update method of the codemirror binding handler to unwrap the passed valueAccessor, without that line knockout would not fire the update method when the observable is updated - it now works as you would expect it to.

like image 89
carbontwelve Avatar answered Nov 10 '22 03:11

carbontwelve


The code listed by Jalayn (or one in jsfiddle) doesn't update the observing variable also the editor doesnt show the value on load.. here is my updated code

    ko.bindingHandlers.codemirror = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
            var options = valueAccessor();
            var editor = CodeMirror.fromTextArea(element, options);
            editor.on('change', function(cm) {
                allBindingsAccessor().value(cm.getValue());
            });
            element.editor = editor;
            if(allBindingsAccessor().value())
                editor.setValue(allBindingsAccessor().value());
            editor.refresh();
            var wrapperElement = $(editor.getWrapperElement());

            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                wrapperElement.remove();
            });
        },
        update: function (element, valueAccessor) {
            if(element.editor)
                element.editor.refresh();
        }
    };
like image 35
Dhana Krishnasamy Avatar answered Nov 10 '22 03:11

Dhana Krishnasamy


I was having an issue with the cursor being set to the first position. This fiddle fixes that and it also accepts CodeMirror options object as binding value, and the content is bound to an options.value observable (I find that less confusing, because that's actually the property name from where CM gets it's starting value)

ko.bindingHandlers.codemirror = {
    init: function(element, valueAccessor) {
        var options = ko.unwrap(valueAccessor());
        element.editor = CodeMirror(element, ko.toJS(options));
        element.editor.on('change', function(cm) {
            options.value(cm.getValue());
        });

        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            var wrapper = element.editor.getWrapperElement();
            wrapper.parentNode.removeChild(wrapper);
        });
    },
    update: function(element, valueAccessor) {
        var value = ko.toJS(valueAccessor()).value;
        if (element.editor) {
            var cur = element.editor.getCursor();
            element.editor.setValue(value);
            element.editor.setCursor(cur);
            element.editor.refresh();
        }
    }
};

Sample HTML:

<div data-bind="codemirror: {
    mode: 'javascript',
    value: text
}"></div>
like image 4
boris Avatar answered Nov 10 '22 03:11

boris


Well, I finally managed to do it (see the updated fiddle).

I quickly managed to set the initial value in the custom textarea. But after that, the bound element was not being updated.

However CodeMirror's API allows you to register a callback method to the onChange event to be called whenever the content of the textarea is modified. So it was just a matter of implementing the callback that updates the value of the bound element. This is done at the creation of the custom text area, in the options.

Here is the custom binding:

ko.bindingHandlers.codemirror = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
        var options = $.extend(valueAccessor(), {
            onChange: function(cm) {
                allBindingsAccessor().value(cm.getValue());
            }
        });
        var editor = CodeMirror.fromTextArea(element, options);
        element.editor = editor;
        editor.setValue(allBindingsAccessor().value());
        editor.refresh();
        var wrapperElement = $(editor.getWrapperElement()); 

        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            wrapperElement.remove();
        });
    }
};

It may lack some features maybe, but for what I need it works perfectly.

Edit: Following Anders' remark, I've added the addDisposeCallback part which effectively destroys the DOM element produced by CodeMirror when the template is re-rendered. Since everything CodeMirror produces is inside one node, it's just a matter of removing this node.

like image 3
Jalayn Avatar answered Nov 10 '22 04:11

Jalayn