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 ?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With