jQuery Design Patterns: Practical Techniques for Modern Frontend Development

The Facade Pattern

The facade pattern is a structural design pattern that abstracts complex underlying systems to expose a streamlined, use-case-specific interface. By wrapping intricate implementation details, it reduces code duplication, improves readability, and simplifies testing.

In frontend development, facades often apear as standalone functions, objects, or modules. For applications with modular architectures, facades can be nested to layer abstractions, with top-level facades orchestrating sub-modules while hiding internal complexity.

Core Benefits

  • Reduces boilerplate code by encapsulating repeated low-level operations
  • Improves code readability by using method names aligned with business concepts
  • Simplifies testing by proviidng a single, stable entry point to complex systems
  • Decouples application code from third-party libraries, making replacements easier

A simple facade example wraps a third-party math library to focus on a specific use case:

function generateAdjustedRandom(base, range) {
  const normalizedBase = base - range / 3;
  const squaredRange = Math.pow(range, 2);
  const randomOffset = 2.5 * Math.random();
  const defaultPrecision = 2;
  return MathLib.computeScaledValue(normalizedBase, squaredRange, randomOffset, defaultPrecision);
}

jQuery and Facades

jQuery itself is a collection of facades. Its core APIs wrap raw DOM, XMLHttpRequest, and event handling, providing consistent, concise methods across browsers.

DOM Traversal

Before jQuery, developers relied on limited, inconsistent DOM methods like getElementById and getElementsByTagName. jQuery's $() function (backed by the Sizzle selector engine) provides a unified facade for selecting elements with CSS selectors, handling cross-browser inconsistencies internally. Even modern querySelectorAll benefits from jQuery's optimizations and edge-case handling.

Property Access

jQuery's $.fn.prop() and $.fn.attr() methods act as facades for standardizing property/attribute access across browsers. They handle differences in naming (e.g., class vs className) and provide getter/setter behavior in a single method:

jQuery.extend({
  propUtil: function(element, name, value) {
    if (element.nodeType !== 1 || jQuery.isXMLDoc(element)) return;
    const fixedName = jQuery.propMap[name] || name;
    const hook = jQuery.propHooks[fixedName];

    if (value !== undefined) {
      if (hook && hook.set) {
        const result = hook.set(element, value, fixedName);
        if (result !== undefined) return result;
      }
      return (element[fixedName] = value);
    }

    if (hook && hook.get) {
      const result = hook.get(element, fixedName);
      if (result !== null) return result;
    }
    return element[fixedName];
  },
  propMap: { "for": "htmlFor", "class": "className" },
  propHooks: {
    tabIndex: {
      get: function(el) {
        const attr = jQuery.find.attr(el, "tabindex");
        return attr ? parseInt(attr, 10) : -1;
      }
    }
  }
});

Practical Application: Random Item Picker

To demonstrate facades in action, we'll build a simple random item picker that uses unique IDs and decouples from third-party libraries.

First, the main module:

(function() {
  window.randomItemPicker = window.randomItemPicker || {};
  const itemIds = [];
  let $pickerContainer;
  const itemCount = 40;

  randomItemPicker.init = function() {
    itemIds.length = 0;
    $pickerContainer = $('#picker-container').empty();
    randomItemPicker.addItems(itemCount);
    $('#pick-btn').on('click', randomItemPicker.selectRandom);
  };

  randomItemPicker.addItems = function(count) {
    for (let i = 0; i < count; i++) {
      const id = randomItemPicker.uniqueIdGenerator.getNext();
      itemIds.push(id);
      $pickerContainer.append(randomItemPicker.itemRenderer.createHtml(id));
    }
  };

  randomItemPicker.selectRandom = function() {
    const index = Math.floor(Math.random() * itemIds.length);
    const selectedValue = $pickerContainer.find('#' + itemIds[index]).text();
    alert('Selected: ' + selectedValue);
    return selectedValue;
  };

  $(document).ready(randomItemPicker.init);
})();

Next, the item renderer facade:

(function() {
  randomItemPicker.itemRenderer = randomItemPicker.itemRenderer || {};

  randomItemPicker.itemRenderer.createHtml = function(id) {
    const itemValue = Math.floor(Math.random() * 2000);
    return '<div id="' + id + '" class="picker-item">' + itemValue + '</div>';
  };
})();

Then, the unique ID generator facade, which wraps a third-party library:

(function() {
  randomItemPicker.uniqueIdGenerator = randomItemPicker.uniqueIdGenerator || {};

  randomItemPicker.uniqueIdGenerator.getNext = function() {
    return 'pick-' + simpleGuid.generate();
  };
})();

If we later switch to a different ID library, only the uniqueIdGenerator module needs changes:

(function() {
  randomItemPicker.uniqueIdGenerator = randomItemPicker.uniqueIdGenerator || {};

  randomItemPicker.uniqueIdGenerator.getNext = function() {
    return uuid.v4();
  };
})();

Creational Patterns: Factory and Builder

Creational pattrens abstract object creation, decoupling application code from specific classes or initialization steps.

Factory Pattern

The factory pattern defines an interface for creating objects, letting subclasses or implementations decide which class to instantiate. It's ideal for cases where object creation depends on runtime conditions or requires cross-browser compatibility.

jQuery's AJAX Factory

jQuery uses the factory pattern internally for creating XMLHttpRequest (XHR) objects. The jQuery.ajaxSettings.xhr function acts as a factory, returning the appropriate XHR implementation for the current browser (e.g., XMLHttpRequest for modern browsers, ActiveXObject for older IE):

jQuery.ajaxSettings.xhr = window.XMLHttpRequest !== undefined ?
  function() {
    try { return new window.XMLHttpRequest(); } catch (e) {}
  } :
  function() {
    try { return new window.ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
  };

Practical Application: Data-Driven Form Factory

We'll build a factory that generates HTML form elements based on a configuration array. The factory accepts a type, name, and label, returning the appropriate HTML string with sensible defaults.

First, the HTML:

<h1>Data-Driven Form</h1>
<form id="dynamic-form"></form>
<script src="jquery.js"></script>
<script src="form-factory.js"></script>

Next, the factory implementation:

(function() {
  'use strict';
  window.formFactory = window.formFactory || {};

  formFactory.createElement = function(config) {
    const label = config.label || config.name;
    let html = '';

    switch (config.type) {
      case 'text':
        html = '<div><label><span>' + label + ':</span><br>' +
               '<input type="text" maxlength="250" name="' + config.name + '">' +
               '</label></div>';
        break;
      case 'email':
        html = '<div><label><span>' + label + ':</span><br>' +
               '<input type="email" required name="' + config.name + '">' +
               '</label></div>';
        break;
      case 'number':
        html = '<div><label><span>' + label + ':</span><br>' +
               '<input type="number" min="0" max="1000000" name="' + config.name + '">' +
               '</label></div>';
        break;
      case 'textarea':
        html = '<div><label><span>' + label + ':</span><br>' +
               '<textarea cols="35" rows="4" maxlength="1000" name="' + config.name + '"></textarea>' +
               '</label></div>';
        break;
      case 'checkbox':
        html = '<div><label><span>' + label + ':</span>' +
               '<input type="checkbox" name="' + config.name + '">' +
               '</label></div>';
        break;
      case 'notice':
        html = '<p>' + config.name + '</p>';
        break;
      case 'button':
        html = '<button name="' + config.name + '">' + label + '</button>';
        break;
    }
    return html;
  };

  formFactory.configs = [
    { type: 'text', name: 'firstName', label: 'First Name' },
    { type: 'text', name: 'lastName', label: 'Last Name' },
    { type: 'email', name: 'email', label: 'Email Address' },
    { type: 'number', name: 'experience', label: 'Years of Experience' },
    { type: 'textarea', name: 'bio', label: 'Short Bio' },
    { type: 'checkbox', name: 'newsletter', label: 'Subscribe to Newsletter' },
    { type: 'notice', name: 'By submitting, you agree to our terms.' },
    { type: 'button', name: 'submit', label: 'Submit' }
  ];

  formFactory.init = function() {
    const $form = $('#dynamic-form');
    formFactory.configs.forEach(function(config) {
      const elementHtml = formFactory.createElement(config);
      if (elementHtml) $form.append(elementHtml);
    });
  };

  $(document).ready(formFactory.init);
})();

Builder Pattern

The builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It's ideal for objects that require many optional parameters or step-by-step configuration.

jQuery's Element Builder

jQuery uses the builder pattern when creating new DOM elements with $('<div>'). The returned jQuery object acts as a builder, allowing chained method calls to configure the element before attaching it to the DOM:

$('<input>')
  .attr({ type: 'number', min: '0', max: '200' })
  .prop('required', true)
  .val(42)
  .css('width', '100px')
  .wrap('<div class="form-field">')
  .parent()
  .prepend('<label>Quantity: </label>')
  .appendTo('#dynamic-form');

Practical Application: Quiz Question Builder

We'll build a builder for creating quiz questions with optional multiple answers. The builder uses chained methods to configure the question before generating the final HTML.

First, the HTML:

<h1>Data-Driven Quiz</h1>
<form id="quiz-form"></form>
<script src="jquery.js"></script>
<script src="quiz-builder.js"></script>

Next, the CSS:

.quiz-question { margin-bottom: 20px; }
.quiz-options { list-style: none; padding: 0; }
.quiz-option { margin: 8px 0; }

Then, the builder implementation:

(function() {
  'use strict';
  window.quizBuilder = window.quizBuilder || {};

  function QuestionBuilder() {
    this.title = 'Untitled Question';
    this.options = [];
    this.allowMultiple = false;
  }

  quizBuilder.QuestionBuilder = QuestionBuilder;

  QuestionBuilder.prototype.setTitle = function(title) {
    this.title = title;
    return this;
  };

  QuestionBuilder.prototype.setAllowMultiple = function(allow) {
    this.allowMultiple = allow;
    return this;
  };

  QuestionBuilder.prototype.addOption = function(optionText) {
    this.options.push(optionText);
    return this;
  };

  QuestionBuilder.prototype.build = function() {
    const $question = $('<article class="quiz-question">');
    $('<header>').text(this.title).appendTo($question);

    const questionId = 'q_' + (jQuery.guid++);
    const $optionsList = $('<ul class="quiz-options">');

    this.options.forEach(function(option, index) {
      const $input = $('<input>')
        .attr({
          type: this.allowMultiple ? 'checkbox' : 'radio',
          value: index,
          name: questionId
        });
      const $optionItem = $('<li class="quiz-option">');
      $('<label>').append($input, $('<span>').text(option)).appendTo($optionItem);
      $optionsList.append($optionItem);
    }, this);

    $question.append($optionsList);
    return $question;
  };

  quizBuilder.questions = [
    {
      title: 'What is the primary goal of the facade pattern?',
      options: [
        'To add new functionality to existing classes',
        'To abstract complex systems into a simplified interface',
        'To enforce strict type checking',
        'To optimize database queries'
      ]
    },
    {
      title: 'Which jQuery method acts as a builder for new DOM elements?',
      options: [
        '$() with a CSS selector',
        '$() with an HTML string',
        '$.ajax()',
        '$.extend()'
      ]
    },
    {
      title: 'Which of these are creational patterns? (Select all that apply)',
      allowMultiple: true,
      options: ['Singleton', 'Observer', 'Factory', 'Decorator']
    }
  ];

  quizBuilder.init = function() {
    const $form = $('#quiz-form');
    quizBuilder.questions.forEach(function(config) {
      const builder = new quizBuilder.QuestionBuilder();
      builder
        .setTitle(config.title)
        .setAllowMultiple(config.allowMultiple);
      config.options.forEach(function(option) {
        builder.addOption(option);
      });
      $form.append(builder.build());
    });
  };

  $(document).ready(quizBuilder.init);
})();

Asynchronous Control Flow Patterns

Asynchronous programming is core to frontend development, enabling non-blocking operations like AJAX requests, animations, and user interactions.

Callbacks

Callbacks are functions passed as arguments to other functions, to be executed later when an operation completes. They're simple but can lead to "callback hell" with deeply nested code.

Simple Callback Example

function generateRandomAsync(max, callback) {
  const delay = 1500 + Math.random() * 1000;
  setTimeout(function() {
    const result = Math.random() * max;
    callback(result);
  }, delay);
}

generateRandomAsync(20, function(num) {
  console.log('Generated:', num);
});

Sequential Execution with Callbacks

function getThreeNumbers(callback, errorCallback) {
  const results = [];
  generateRandomAsync(20, function(num1) {
    results.push(num1);
    generateRandomAsync(20, function(num2) {
      results.push(num2);
      $.getJSON('random-number-api', function(data) {
        results.push(data.value);
        callback(results);
      }).fail(errorCallback);
    });
  });
}

Avoiding Callback Hell

To flatten nested callbacks, use named functions:

function getThreeNumbers(callback, errorCallback) {
  const results = [];

  generateRandomAsync(20, function(num1) {
    results.push(num1);
    fetchSecondNumber();
  });

  function fetchSecondNumber() {
    generateRandomAsync(20, function(num2) {
      results.push(num2);
      fetchThirdNumber();
    });
  }

  function fetchThirdNumber() {
    $.getJSON('random-number-api', function(data) {
      results.push(data.value);
      callback(results);
    }).fail(errorCallback);
  }
}

Parallel Execution with Callbacks

function getNumbersParallel(callback, errorCallback) {
  const results = [];
  let completed = 0;
  const total = 3;

  function collectResult(index) {
    return function(result) {
      results[index] = result;
      completed++;
      if (completed === total) callback(results);
    };
  }

  generateRandomAsync(20, collectResult(0));
  generateRandomAsync(20, collectResult(1));
  $.getJSON('random-number-api', collectResult(2)).fail(errorCallback);
}

Promises

Promises provide a more structured way to handle asynchronous operations, representing a value that may be available now, later, or never. They support chaining and parallel execution, eliminating callback hell.

jQuery Deferred and Promise

jQuery's $.Deferred() creates a deferred object, which can be resolved or rejected. The promise() method returns a read-only promise object to consumers:

function generateRandomPromise(max) {
  const deferred = $.Deferred();
  const delay = 1500 + Math.random() * 1000;
  setTimeout(function() {
    const result = Math.random() * max;
    deferred.resolve(result);
  }, delay);
  return deferred.promise();
}

generateRandomPromise(20).then(function(num) {
  console.log('Generated:', num);
});

ES6 Promises

ES6 native promises follow the Promises/A+ specification, with a simpler constructor:

function generateRandomES6(max) {
  return new Promise(function(resolve, reject) {
    const delay = 1500 + Math.random() * 1000;
    setTimeout(function() {
      const result = Math.random() * max;
      resolve(result);
    }, delay);
  });
}

generateRandomES6(20).then(function(num) {
  console.log('Generated:', num);
});

Chaining Promises

Promises can be chained to execute operations sequentially:

generateRandomPromise(20)
  .then(function(num) {
    console.log('First:', num);
    return num * 2;
  })
  .then(function(doubled) {
    console.log('Doubled:', doubled);
    return generateRandomPromise(doubled);
  })
  .then(function(secondNum) {
    console.log('Second:', secondNum);
  });

Parallel Execution with Promises

jQuery's $.when() and ES6's Promise.all() combine multiple promises into one:

// jQuery
$.when(
  generateRandomPromise(20),
  generateRandomPromise(30),
  $.getJSON('random-number-api')
).then(function(num1, num2, apiData) {
  console.log('All results:', num1, num2, apiData.value);
});

// ES6
Promise.all([
  generateRandomES6(20),
  generateRandomES6(30),
  fetch('random-number-api').then(res => res.json())
]).then(function(results) {
  console.log('All results:', results[0], results[1], results[2].value);
});

Mock Object Pattern

Mock objects simulate the behavior of real, complex objects, enabling parallel development, testing, and decoupling from external dependencies.

Key Use Cases

  • Developing modules that depend on unimplemented components
  • Testing edge cases (e.g., network errors, rare events)
  • Decoupling frontend development from backend APIs

Practical Application: Video Service Mock

We'll build a mock video service for a dashboard that displays YouTube videos, decoupling frontend development from the real YouTube API.

First, the HTML:

<section class="dashboard-categories">
  <select id="category-selector"></select>
  <div class="category-buttons"></div>
  <div class="clear"></div>
</section>
<div class="video-boxes"></div>
<script src="jquery.js"></script>
<script src="video-service-mock.js"></script>
<script src="dashboard.js"></script>

Next, the mock video service:

(function() {
  'use strict';
  window.videoService = window.videoService || {};
  window.videoService.mockData = window.videoService.mockData || {};

  // Mock data
  videoService.mockData.searches = [
    {
      keywords: 'jquery conference',
      data: {
        items: [
          {
            id: { videoId: 'abc123' },
            snippet: {
              title: 'jQuery Conference 2023: Modern Frontend Patterns',
              thumbnails: {
                default: { url: 'https://example.com/thumb1.jpg' },
                medium: { url: 'https://example.com/thumb1-m.jpg' }
              }
            }
          },
          {
            id: { videoId: 'def456' },
            snippet: {
              title: 'jQuery UI Deep Dive',
              thumbnails: {
                default: { url: 'https://example.com/thumb2.jpg' },
                medium: { url: 'https://example.com/thumb2-m.jpg' }
              }
            }
          }
        ]
      }
    },
    {
      keywords: 'javascript design patterns',
      data: {
        items: [
          {
            id: { videoId: 'ghi789' },
            snippet: {
              title: 'Design Patterns for JavaScript Developers',
              thumbnails: {
                default: { url: 'https://example.com/thumb3.jpg' },
                medium: { url: 'https://example.com/thumb3-m.jpg' }
              }
            }
          }
        ]
      }
    }
  ];

  // Build all videos array for single-video lookups
  const allVideos = [];
  videoService.mockData.searches.forEach(function(search) {
    allVideos.push.apply(allVideos, search.data.items);
  });
  videoService.mockData.allVideos = allVideos;

  // Mock service methods
  videoService.searchVideos = function(keywords) {
    return $.Deferred(function(deferred) {
      const searches = videoService.mockData.searches;
      for (let i = 0; i < searches.length; i++) {
        if (searches[i].keywords.toLowerCase() === keywords.toLowerCase()) {
          deferred.resolve(searches[i].data);
          return;
        }
      }
      deferred.reject('No results found');
    }).promise();
  };

  videoService.getVideo = function(title) {
    return $.Deferred(function(deferred) {
      const videos = videoService.mockData.allVideos;
      for (let i = 0; i < videos.length; i++) {
        if (videos[i].snippet.title === title) {
          deferred.resolve(videos[i]);
          return;
        }
      }
      deferred.reject('Video not found');
    }).promise();
  };

  const videoBaseUrl = 'https://www.youtube.com/watch?v=';
  videoService.getVideoUrl = function(videoId) {
    return videoBaseUrl + videoId;
  };
})();

Then, the dashboard module:

(function() {
  'use strict';
  window.videoDashboard = window.videoDashboard || {};

  videoDashboard.categories = [
    { title: 'jQuery Conferences', keywords: 'jquery conference' },
    { title: 'JS Design Patterns', keywords: 'javascript design patterns' }
  ];

  videoDashboard.initCategories = function() {
    const $selector = $('#category-selector');
    const $buttons = $('.category-buttons');

    videoDashboard.categories.forEach(function(category, index) {
      $('<option>').val(index).text(category.title).appendTo($selector);
    });

    $selector.on('change', function() {
      const index = $(this).val();
      const category = videoDashboard.categories[index];
      videoDashboard.loadVideos(category.keywords);
    });

    // Load initial videos
    videoDashboard.loadVideos(videoDashboard.categories[0].keywords);
  };

  videoDashboard.loadVideos = function(keywords) {
    const $boxes = $('.video-boxes').empty();
    videoService.searchVideos(keywords).then(function(data) {
      data.items.forEach(function(video) {
        videoDashboard.openVideoBox(video.snippet.title);
      });
    }).fail(function(error) {
      $boxes.text('Error loading videos: ' + error);
    });
  };

  videoDashboard.openVideoBox = function(title) {
    const $box = $('<div class="video-box">')
      .append($('<header>').text(title).append($('<button class="close-btn">×</button>')))
      .append($('<div class="content">Loading...</div>'))
      .appendTo('.video-boxes');

    videoService.getVideo(title).then(function(video) {
      const $link = $('<a>').attr('href', videoService.getVideoUrl(video.id.videoId));
      $link.append($('<img>').attr('src', video.snippet.thumbnails.medium.url));
      $box.find('.content').empty().append($link);
    }).fail(function() {
      $box.find('.content').text('Error loading video');
    });

    $box.find('.close-btn').on('click', function() {
      $box.remove();
    });
  };

  $(document).ready(videoDashboard.initCategories);
})();

Client-Side Templating

Client-side templating libraries generate HTML dynamically from data and templates, separating presentation from logic and reducing string concatenation.

Underscore.js Templates

Underscore.js provides _.template(), which uses <% %> for execution, <%= %> for interpolation, and <%- %> for HTML-escaped interpolation.

Basic Example

const templateFn = _.template('<h1><%- title %></h1>');
const html = templateFn({ title: 'Underscore.js Template Example' });

Iteration with Underscore

const templateSource = '<% _.each(categories, function(cat, i) { %>' +
                      '<option value="<%= i %>"><%- cat.title %></option>' +
                      '<% }); %>';
const optionsHtml = _.template(templateSource)({ categories: videoDashboard.categories });
$('#category-selector').append(optionsHtml);

Embedded Templates in HTML

Store templates in <script type="text/template"> tags to separate them from JavaScript:

<script id="video-box-template" type="text/template">
  <div class="video-box">
    <header>
      <button class="close-btn">×</button>
      <%- title %>
    </header>
    <div class="content">Loading...</div>
  </div>
</script>

Create a template provider with caching:

const templateCache = {};
function getTemplate(name) {
  if (templateCache[name]) return templateCache[name];
  const template = $('#' + name).html();
  templateCache[name] = _.template(template);
  return templateCache[name];
}

videoDashboard.openVideoBox = function(title) {
  const template = getTemplate('video-box-template');
  const $box = $(template({ title: title })).appendTo('.video-boxes');
  // Rest of implementation...
};

Async Template Loading

Load templates via AJAX for better scalability:

const templateCache = {};
function getTemplateAsync(name) {
  if (templateCache[name]) return $.Deferred().resolve(templateCache[name]).promise();
  return $.ajax({
    url: name + '.html',
    mimeType: 'text/html'
  }).then(function(template) {
    templateCache[name] = _.template(template);
    return templateCache[name];
  });
}

videoDashboard.openVideoBox = function(title) {
  getTemplateAsync('video-box').then(function(template) {
    const $box = $(template({ title: title })).appendTo('.video-boxes');
    // Rest of implementation...
  });
};

Handlebars.js Templates

Handlebars.js provides logic-less templates with {{ }} for interpolation (HTML-escaped by default), {{{ }}} for unescaped interpolation, and built-in helpers like {{#each}} and {{#if}}.

Basic Example

const templateFn = Handlebars.compile('<h1>{{ title }}</h1>');
const html = templateFn({ title: 'Handlebars.js Template Example' });

Iteration with Handlebars

const templateSource = '{{#each categories}}' +
                      '<option value="{{@index}}">{{ title }}</option>' +
                      '{{/each}}';
const optionsHtml = Handlebars.compile(templateSource)({ categories: videoDashboard.categories });
$('#category-selector').append(optionsHtml);

Embedded Templates in HTML

Store templates in <script type="text/x-handlebars-template"> tags:

<script id="video-box-template" type="text/x-handlebars-template">
  <div class="video-box">
    <header>
      <button class="close-btn">×</button>
      {{ title }}
    </header>
    <div class="content">Loading...</div>
  </div>
</script>

Create a template provider with caching:

const templateCache = {};
function getTemplate(name) {
  if (templateCache[name]) return templateCache[name];
  const template = $('#' + name).html();
  templateCache[name] = Handlebars.compile(template);
  return templateCache[name];
}

Precompiled Templates

Precompile Handlebars templates for better performance. Install Handlebars via npm, then run:

handlebars video-box-template.handlebars -f video-box-template.js

Use the precompiled template:

function getTemplate(name) {
  return Handlebars.templates[name];
}

Tags: jquery javascript Design Patterns Asynchronous Programming Client-Side Templating

Posted on Mon, 11 May 2026 01:58:07 +0000 by alexboyer