Data-bound Tags with At.js and Knockout.js

I wanted to add an editor that supported tags to an existing web application that used knockout.js (I know, not what the cool kids are using but a full re-write just to add tags seemed ill-advised). After persevering with ProseMirror I decided it was too rich for what i wanted. All I needed was plain text + tags, not the full richness of ProseMirror.

After doing a bit of investigation I found a nice little library called At.js. I put together the following binding extension to allow At.js to be ‘data-bound’.

ko.bindingHandlers.tagValue = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var replaceTags = function(taggedString, targetUrl, linkClass) {
if (!taggedString) {
return null;
}
var exp = /(^#|\W#)[\w-_]+/g;
var match = taggedString.match(exp);
if (match && match.length > 0) {
for (var i = 0; i < match.length; i++) {
var tag = match[i].trim();
if (tag != "#") {
var uri = " <span class='atwho-inserted' data-atwho-at-query='" + tag + "'><a href='" + targetUrl + tag.replace("#", "") + "' class='" + linkClass + "'>" + tag + "</a></span>";
taggedString = taggedString.replace(match[i], uri);
}
}
}
return taggedString;
};
var tags = allBindings.get('tags');
var tagUri = allBindings.get('tagUri');
// set current value
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
element.innerHTML = replaceTags(valueUnwrapped, tagUri + "?tag=", "tag");
$(element).atwho({
at: "#",
data: tags(),
insertTpl: '<a class="tag" href="' + tagUri + '?tag=${name}">#${name}</a>'
});
$(element).on('paste', function (e) {
e.preventDefault();
var text = (e.originalEvent || e).clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
});
$(element).on('keydown', function (e) {
if (e.ctrlKey && (String.fromCharCode(e.which).toLowerCase() === 'b' || String.fromCharCode(e.which).toLowerCase() === 'i' || String.fromCharCode(e.which).toLowerCase() === 'u')) {
e.preventDefault();
}
if (e.which == 13) {
e.preventDefault(); // suppress enter key
}
});
$(element).blur(function () {
var value = valueAccessor();
value(element.textContent);
});
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).atwho('destroy');
});
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// This will be called once when the binding is first applied to an element,
// and again whenever any observables/computeds that are accessed change
// Update the DOM element based on the supplied values here.
var tags = allBindings.get('tags');
$(element).atwho('load', '#', tags());
var tagUri = allBindings.get('tagUri');
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
element.innerHTML = util.replaceTags(valueUnwrapped, tagUri + "?tag=", "tag");
}
};
view raw tagedit.js hosted with ❤ by GitHub

Usage is something like this: <div contenteditable="true" data-bind="tagValue: Description, tags: $root.tags, tagUri:'/your-url-to-tags-page/'"></div>

And it will look like this (with here shown with bootstrap) screen-shot-of-tage