ponyfoo.com

Baking Modularity into Tag Editing

For quite some time I’ve been wanting some sort of input that dealt with user-submitted tags in a reasonable way. I wanted this input to still be half-decent when JavaScript was out of the picture, so this meant starting with the basic building blocks of HTML.

Improve this article
Nicolás Bevacqua
| 7 minute read | 7

Turns out, all libraries out there (google for “tag input javascript” if you’re curious) that do this have at least one of these flaws.

  • Feature bloat
  • Human unfriendly
  • Addiction to dependencies

Instead of creating separate components for, say, tag input and autocompletion, most of these libraries try to do it all, and fail miserably at doing any one thing well. This is the defining characteristic of modular development, where we define a (narrow) purpose for what we’re building and we set out to build the best possible solution to meet that purpose.

Multi-purpose libraries end up doing a lot of things I do not need or want, while single-purpose components typically do exactly what you need.

Being user friendly is why we build these libraries, and yet I couldn’t find a single library that I would’ve used over a simple <input type='text' /> and telling my humans to enter tags separated by a single space. The vast majority of these presented inconveniences such as being unable to walk around the input like we do in an input tag, insert tags in any position, or not even allowing humans to edit tags once they receive the “this is a tag” treatment.

Human-facing interface elements must be sensitive to the needs of the human. Sometimes we have to wonder if a component is even worth the trouble. Wouldn’t a simple <input> be more amicable than an inflexible “tag input” solution? Is the component more useful to your humans?

Finally, most of these libraries tie themselves to a dependency. If I really liked one of them, I probably would’ve needed to commit to jQuery myself. Or Angular. Or React. It doesn’t matter what their “big dependency” is, the point is that it constraints the consumer from being able to easily port the component to different projects with other assortments of frameworks or libraries.

The fact that most of them count jQuery among their dependencies leaves much to be desired.

This article will explore the approach I took to build Insignia in just one day, as I think you may apply some of these concepts to any development you do on a daily basis.

Insignia

Insignia has the purpose to make tag editing simple. If you want to suggest tags to your humans, based on the results of an API endpoint or events over a WebSocket, you are certainly welcome to do that, but Insignia won’t take any part of it.

If I needed something to suggest tags for humans using Insignia, I wouldn’t create a plugin system. The DOM is the plugin system. I would create a separate component to do just that. It would also be a component that makes sense on its own, when paired with another input that deserves a suggestion mechanism. For instance, that theoretical component might work well enough to suggest dates on a field that’s already using Rome for date input. This way, the composable small components I create could serve me well for other projects, and hooking them to my framework of choice shouldn’t take a lot of effort.

The purpose then is to create a tag editor that’s human friendly. Humans should be able to enter values into the field even when JavaScript hasn’t finished loading. Once JavaScript loads, the tags that are already in the field should be extracted into a prettier visualization, but the human should still be able to remove them, edit them, and walk through them with the arrow keys.

A minimal API would also go a long way towards being able to translate the altered visual representation into a collection of tags, something the implementation shouldn’t have any problem putting together.

screenshot.png
screenshot.png

Once I’ve settled for a purpose and I roughly understand how the interface should look like, there’s one more pause we need to take before starting to smash our fingers on the keyboard.

What kind of browser support will we offer?

This is an important question because there’s nothing quite terrible as suddenly trying to support IE7+ and finding out that even things like Element.setAttribute or Array.prototype.indexOf aren’t even in the browser, let alone Element.selectionStart and Element.selectionEnd.

If you face module development with a “take no API for granted” mentality, you’ll be a significantly happier web developer than most people “dealing with IE8”. You won’t just think of everything in terms of “Is there a polyfill for this?” but rather you’ll be able to see further along and say “I’ll just use a for loop, then”.

Initial Commit

Think of the following as a recipe. Set up an index.html page with usage examples before you have any working code. That’s what you’ll use as a playground for your component. It doesn’t have to actually exist yet, but I find this works best when I also display code for how I want the API to look like.

This kind of preemptive design allows you to predict issues with the API and work your way from basic HTML elements (in my case, just an <input type='text' />), towards the component you want. Effectively, this is the same progression that will be followed by user agents, allowing you to experience the same “rollout” your humans will.

Once you’ve put together the HTML page, start adding the JavaScript and styling. Feel free to code up the examples in plain JavaScript and CSS (basically consuming the API and styling the page itself), but do use whatever module system and pre-processor you feel comfortable with for the library code. These always come with a simple CLI to compile the code into a JavaScript or CSS bundle.

In my case, it just takes the command below to compile any number of CommonJS modules into a single bundle.

browserify main.js -o bundle.js

I usually use watchify during development, which is equivalent to browserify, except it incrementally rebuilds the bundle. For styles I use Stylus, which also comes with a CLI that can be used to watch for changes.

Here’s the entire build and deployment process for insignia.

"scripts": {
  "start": "watchify -s insignia -do dist/insignia.js insignia.js & stylus -w insignia.styl -o dist",
  "scripts": "jshint . && browserify -s insignia -do dist/insignia.js insignia.js && uglifyjs -m -c -o dist/insignia.min.js dist/insignia.js",
  "styles": "stylus insignia.styl -o dist && cleancss dist/insignia.css -o dist/insignia.min.css",
  "build": "npm run scripts && npm run styles",
  "deployment": "npm version ${BUMP:-\"patch\"} --no-git-tag-version && git add package.json && git commit -m \"Autogenerated pre-deployment commit\" && bower version ${BUMP:-\"patch\"} && git reset HEAD~2 && git add . && git commit -am \"Release $(cat package.json | jq -r .version)\" && git push --tags && npm publish && git push",
  "sync": "git checkout gh-pages ; git merge master ; git push ; git checkout master",
  "deploy": "npm run build && npm run deployment && npm run sync"
}

Throw in any assortment of .gitignore, .editorconfig, license, changelog.markdown, readme.markdown, .jshintrc, and .jshintignore you feel comfortable with, and you’re ready to get started developing your module.

Make sure to abstract potentially troublesome code into separate modules, so that you can later brush off cross-browser inconsistencies. For instance, I created a './selection' module for getting and setting the selected text range, even when I was initially just working on Google Chrome (which has perfectly functional Element.selectionStart and Element.selectionEnd property getters and setters).

Conclusion

If you approach component design thoughtfully you should have very little issue making a functioning cross-browser component that doesn’t alienate your humans, does what you need, and has a focused API surface that allows you to compose several modules into a complex, scalable application.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (7)

Palmer wrote

** So awesome, so useful !!!** Thanks for sharing.

Luke wrote

Well this is great. Do you have any plans to integrate (or would you be opposed to integrating) with typeahead.js? I’ve been looking to replace jsTag with something more sane for a while but haven’t had the time to start something from scratch. I’d be happy to look into putting together a PR if you’d accept it.

Nicolas Bevacqua wrote

As I mention in the article I’d rather use a component that just does autocomplete and have them interoperate, but I wouldn’t tightly couple them together.

Enzo Strongoli wrote

I love it, so simple, much modular, such progressive, it’s great, you’ve done a great job with this.

it does what a tag inputs needs to do, nothing less and nothing more, 8.558kb minified, no dependencies needed, what more do you want?

thanks for doing this post!

Vincent Voyer wrote

Hi, great post.

I would have used the comma separator as it’s the default on many websites, like stackoverflow. Most of the time a space is considered as a possible character in a multi word tag. While it’s never the case with a comma.

I like to have a “dev” task in my frontend modules where I can just run:

npm run dev
# output..
# output..

You can now go to http://localhost:8080 to hack on things

# watch and rebuild is done automatically

What do you think? I also realy like you current npm run scripts, altought at some point like the sync scripts, I would make it a scripts/.sh and run it from npm package.json scripts.

Nicolas Bevacqua wrote

In SO both ’ ’ and ‘,’ are used as separators. Currently, Insignia lets you pick the separator you want to use, and in the demo you can see how to add separators like Enter.

I definitely end up moving build tasks into a build directory of some sort where my shell scripts live, as the build grows. That’s rarely the case with libraries, but often what ends up happening when I’m building an app.

Jay wrote

Hey, I have featured this awesome javascript library Insignia at http://jquer.in/