最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to integrate CodeMirror into KnockoutJS? - Stack Overflow

programmeradmin5浏览0评论

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 ?

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 ?

Share Improve this question edited Nov 14, 2012 at 9:09 Jalayn asked Nov 14, 2012 at 8:52 JalaynJalayn 9,2845 gold badges35 silver badges51 bronze badges 2
  • Could you please reproduce it in a jsfiddle? I do not see anything wrong with your binding handler. Only strange thing is that you wrap element with jquery and then take out the first element. YOu can skip that and just supply the element. – Anders Commented Nov 14, 2012 at 9:19
  • @Anders thanks, I removed the jquery wrap. The JSFiddle: jsfiddle/SKZSm – Jalayn Commented Nov 14, 2012 at 9:39
Add a ment  | 

6 Answers 6

Reset to default 5

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.

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();
        }
    };

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>

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.

I'm trying to use the binding handler above, but I'm having an issue with no text being visible in the TextArea until I enter something (a space or any other character).

It's blank when loading is done, and as soon as I enter something in the area, all the bound data bees visible.

One more question, if I bind a simple ko.observable() to the value of TextArea, everything works great, but if I replace this with one of the columns in an ko.observableArray, e.g. value: selectedProfile().QueryString, I'm getting the following error from the binding handler:

Line in question: editor.setValue(allBindingsAccessor().value()); Error: Uncaught TypeError: Property 'value' of object # is not a function

Thoughts?

/LM

I run into the same problem as Lars in one of the answers above i.e. codemirror didn't initially load data until first interaction. I resolved it by adding a flag to determine if the codemirror value changed by typing or programatcially and subscribing to the value.

ko.bindingHandlers.codemirror = {
        init: function (element, valueAccessor, allBindingsAccessor) {
            var typed = false;
            var options = $.extend(valueAccessor(), {
                onChange: function (cm) {
                    typed = true;
                    allBindingsAccessor().value(cm.getValue());
                    typed = false;
                }
            });
            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();
            });

            allBindingsAccessor().value.subscribe(function (newValue) {
                if (!typed) {
                    editor.setValue(newValue);
                    editor.refresh();
                }
            });
        }
    };

I believe there would be a 'cleaner' method to achieve the same, but this seems fine for the time being.

发布评论

评论列表(0)

  1. 暂无评论