Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Writing a jQuery Plugin That Doesn't Fight the Library

C
Carlo Fontanos
· 2 min read

Legacy codebases don't just use jQuery plugins - they accumulate half-written ones: functions bolted onto $.fn that break chaining, double-initialize, and leak handlers on removal. The proper pattern is thirty lines, and knowing it makes you the person who can maintain (or mercy-kill) any plugin in the codebase.

(function ($) {
    const NAME = 'tagInput';

    const defaults = {
        maxTags: 10,
        delimiter: ',',
        onAdd: null,
    };

    function Plugin(el, options) {
        this.$el = $(el);
        this.opts = $.extend({}, defaults, options, this.$el.data());   // data-* overrides!
        this._bind();
    }

    Plugin.prototype = {
        _bind() {
            this.$el.on('keydown.' + NAME, (e) => this._onKey(e));
        },
        addTag(text) { /* ... */ this.opts.onAdd?.(text); },
        destroy() {
            this.$el.off('.' + NAME).removeData(NAME);
        },
    };

    $.fn[NAME] = function (optionsOrMethod, ...args) {
        return this.each(function () {                    // 1. chainable, multi-element
            let inst = $.data(this, NAME);
            if (!inst) {
                $.data(this, NAME, inst = new Plugin(this, optionsOrMethod));  // 2. one instance per element
            } else if (typeof optionsOrMethod === 'string') {
                inst[optionsOrMethod]?.(...args);          // 3. method calls: $el.tagInput('addTag', 'css')
            }
        });
    };
})(jQuery);

Why each piece is there

return this.each(...) - chainability and multi-element support in one move. $('.tags').tagInput().addClass('ready') must work; plugins that return something else break the contract users expect from every jQuery method.

$.data guard - calling the plugin twice on an element returns the existing instance instead of double-binding (the bug that makes widgets fire twice after an AJAX re-init - namespaces being the other half of that defense, used in _bind and destroy).

Options merge order - defaults, then the options object, then this.$el.data(). That last one lets markup configure instances - <input data-max-tags="5"> - which is how one init line serves twenty differently-configured elements. (jQuery's .data() auto-reads data-* attributes, camelCased.)

String dispatch - $el.tagInput('destroy') is the jQuery-idiomatic method call. Prefix private methods with _ and skip dispatching them if you want an actual API boundary.

destroy() - unbind namespaced events, removeData. Plugins without teardown are why single-page-ified legacy apps leak; this is four lines.

The honest framing for 2026

Would I start a green-field widget as a jQuery plugin? No - a plain class taking an element covers it. But the pattern still pays rent three ways: maintaining the plugins you have, wrapping a modern component so thousand-line legacy files can consume it idiomatically, and reading third-party plugin source to debug it - which, with this pattern in your head, goes from archaeology to skimming.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading