Skip to content

Instantly share code, notes, and snippets.

@jkreitzman
Last active January 3, 2020 01:22
Show Gist options
  • Save jkreitzman/09013da7560e11e71aac43432ec416df to your computer and use it in GitHub Desktop.
Save jkreitzman/09013da7560e11e71aac43432ec416df to your computer and use it in GitHub Desktop.
Knockout Brace/Ace Binding (TypeScript)
// A knockout binding for brace/ace.
// Inspired by https://github.com/probonogeek/knockout-ace, but updated to the latest version of knockout and rewritten in TypeScript.
// To consume in a webpacked view model:
// import(/* webpackMode: "eager" */ "./knockout-brace.ts"); // eager ensures the module is included.
import ace from "brace";
import "brace/ext/language_tools";
import "brace/mode/css";
import "brace/mode/javascript";
import "brace/theme/clouds";
import ko from "knockout";
interface IDictionary<T> {
[key: string]: T;
}
const instancesById = {} as IDictionary<ace.Editor>; // needed for referencing instances during updates.
let initId = 0;
ko.bindingHandlers.ace = {
init(element: HTMLElement, valueAccessor: () => ko.Observable | any, allBindings: any, viewModel: any, bindingContext: ko.BindingContext) {
const options = allBindings.get("aceOptions") || {};
const value = ko.unwrap(valueAccessor()) || "";
// Ace attaches to the element by DOM id, so we need to make one for the element if it doesn't have one already.
if (!element.id) {
element.id = "knockout-ace-" + initId;
initId += 1;
}
const editor = ace.edit(element.id);
editor.$blockScrolling = Infinity;
editor.setOptions({
// Could be added to the binding options to allow binding-based config.
enableBasicAutocompletion: false,
enableLiveAutocompletion: true,
enableSnippets: false,
highlightActiveLine: false,
showPrintMargin: false,
useSoftTabs: false
});
// Could require/import all themes/modes to allow dynamic theming.
if (options.theme) { editor.setTheme("ace/theme/" + options.theme); }
if (options.mode) { editor.getSession().setMode("ace/mode/" + options.mode); }
if (options.enabled) {
// Auto-update
if (ko.isObservable(options.enabled)) {
(options.enabled as ko.Observable<boolean>).subscribe(newVal => {
editor.setReadOnly(!newVal);
});
}
editor.setReadOnly(!ko.unwrap(options.enabled));
}
editor.session.setValue(value);
editor.gotoLine(0);
editor.getSession().on("change", _ => {
valueAccessor()(editor.getValue());
});
instancesById[element.id] = editor;
// destroy the editor instance when the element is removed
ko.utils.domNodeDisposal.addDisposeCallback(element, _ => {
editor.destroy();
delete instancesById[element.id];
});
},
update(element: HTMLElement, valueAccessor: () => ko.Observable | any, allBindings: any, viewModel: any, bindingContext: ko.BindingContext) {
const value = ko.unwrap(valueAccessor()) || "";
const id = element.id;
// handle programmatic updates to the observable
// also makes sure it doesn't update it if it's the same.
// otherwise, it will reload the instance, causing the cursor to jump.
if (id !== undefined && id !== "" && instancesById.hasOwnProperty(id)) {
const editor = instancesById[id];
const content = editor.getValue();
if (content !== value) {
editor.$blockScrolling = Infinity;
editor.session.setValue(value);
editor.gotoLine(0);
}
}
}
};
<!-- Sample of how to implement this binding -->
<!--
"css" should exist on your view model and be an observable.
In this example, "css" contains your CSS code.
"cssEnabled" is also observable and would enable/disable the control.
-->
<div data-bind="ace: css, aceOptions: { mode: 'css', theme: 'clouds', enabled: cssEnabled }"></div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment