Skip to content

« all projects

skeletor

2013-2017 · archived · delphic digital · ~40 repos

Skeletor was the front-end framework we built at Delphic Digital (Philadelphia, 2013–2017). It started as a Gulp + Sass boilerplate and grew through three distinct experimentation eras, pre-component utilities, a jQuery-based component framework, and finally native Web Components in late 2016. The same architectural instincts are what AXS scales today.


act i, before components (2013-2014)

The earliest pattern that survived the next decade: CSS owns the breakpoint truth; JavaScript subscribes to it. Sounds obvious now; in 2013 the web hadn’t settled. matchMedia was inconsistent. Most responsive JS was running its own arithmetic on window.innerWidth and drifting from the stylesheet.

on-media-query threaded the breakpoint names through html { font-family }, CSS as the source of truth, JS as the listener. Components could subscribe to layout contexts by name (large, xlarge) instead of magic numbers.

// onMediaQuery, threaded through Skeletor in 2013
//
// CSS owns the breakpoints (in html { font-family }), JS just
// listens for context changes and fires registered callbacks.
// A pre-matchMedia way to keep responsive logic synchronized
// with the stylesheet's source of truth.

mq.addQuery({
  context: ['large', 'xlarge'],
  match: function() { console.log('desktop layout'); },
  unmatch: function() { console.log('leaving desktop'); }
});

mq.listenForChange = function() {
  var query_string = window
    .getComputedStyle(document.documentElement, null)
    .getPropertyValue('font-family');

  // Android browsers append a "," to the family list;
  // most browsers wrap in quotes. Strip both.
  query_string = query_string.replace(/['",]/g, '');

  if (query_string !== this.context) {
    this.new_context = query_string;
    this.triggerCallbacks(this.context, 'unmatch');
    this.triggerCallbacks(this.new_context, 'match');
  }
  this.context = this.new_context;
};

on-media-query/js/onmediaquery.js · 2013


act ii, a framework, on jquery (2016)

By 2016 Delphic was running ~50 client projects on the same component runtime. The problem had shifted from “how do we write components consistently” to “how do we register, load, and tear them down across many pages, breakpoints, and contexts.”

Skeletor.core defined the Plugin.create() pattern. Every component subclassed a shared base, got a UUID, registered itself with the global, and surfaced via a kebab-case name derived from its class. The shape AXS would later codify, here in proto form.

// Skeletor.core, 2016 - the framework runtime.
//
// Components register themselves with the global object via
// Plugin.create(). Each gets a generated UUID, a kebab-case
// name derived from its class name, and a slot in the
// shared registry. Pre-custom-elements - this was the
// pattern we leaned on while waiting for the spec.

function Plugin(element, options, defaultOptions) {
  this.options = $.extend(true, {}, defaultOptions, options);
  this.$element = element || $(document);

  if (typeof this._init !== 'function') {
    throw this.name + ' needs an _init method';
  }

  var pluginName = functionName(this.constructor).toLowerCase();
  this.uuid = GetYoDigits(6, pluginName);
  Skeletor._uuids.push(this.uuid);
  if (!this.$element.data('skeletorPlugin')) {
    this.$element.data('skeletorPlugin', this);
  }

  this._init(element);
}

Plugin.create = function(SubConstructor, prototype) {
  SubConstructor.__super__ = Plugin;

  for (var key in Plugin.prototype) {
    if (!SubConstructor.prototype[key]) {
      SubConstructor.prototype[key] = Plugin.prototype[key];
    }
  }

  SubConstructor.prototype = $.extend(true, SubConstructor.prototype, prototype);
  SubConstructor.prototype.constructor = SubConstructor;
  SubConstructor.prototype.name = functionName(SubConstructor);

  var className = SubConstructor.prototype.name;
  var attrName  = hyphenate(className);

  Skeletor._plugins[attrName] = Skeletor[className] = SubConstructor;
};

Skeletor.core/skeletor.core.js · 2016

Skeletor.util.componentLoader scanned the DOM for [data-component] markers, grouped them by media-query context, and used on-media-query to lazy-load + initialize the right components at the right breakpoint, including destroy() on the way out. AMD-based code splitting before tree-shaking was a default.

// Skeletor.util.componentLoader, 2016 - dynamic loading.
//
// Scans the DOM for [data-component] elements, groups them by
// [data-component-context] (a media-query name), and uses
// onMediaQuery to load + init the right components at the
// right breakpoint. AMD-based code splitting before tree-
// shaking was a thing every bundler had.

_init: function(element) {
  var _this = this;
  var components = $.map($('[data-component]'), function(el){
    return {
      context: $(el).data('component-context') || null,
      name: 'components/' + $(el).data('component')
    };
  });
  var contexts = this._mergeByContext(components);

  if (!MQ.callbacks) MQ.init();

  $.each(contexts, function(i, component) {
    if (component.context != 'null') {
      _this._addContextQuery(component.context, component.name);
    } else {
      _this._loadComponent(component.name);
    }
  });
},

_addContextQuery: function(context, components) {
  MQ.addQuery({
    context: context.split(','),
    call_for_each_context: false,
    match: function() {
      require(components.split(','), function() {
        $.each(arguments, function(i, component) {
          component.init();
        });
      });
    },
    unmatch: function() {
      $.each(components.split(','), function(i, name) {
        var c = require(name);
        if (typeof c.destroy === 'function') c.destroy();
      });
    }
  });
}

Skeletor.util.componentLoader/skeletor.util.componentLoader.js · 2016


act iii, custom elements (late 2016)

The Web Components v1 spec stabilized in 2018. Lit didn’t exist yet. Stencil was over a year out. Skeletor picked standards anyway. The base class was rewritten to extend HTMLElement directly, with a connectedCallback instead of an init pattern. From here on, components were custom elements.

// skeletor-plugin-base, December 2016 - the moment we
// stopped emulating components and started writing them.
// extends HTMLElement, connectedCallback - the v1 spec
// before v1 stabilized.

import $ from 'jquery';
import skeletor from 'skeletor-core';

class SkeletorPlugin extends HTMLElement {

  constructor(element, options) {
    super();
    this.NAME = this.constructor.ELEMENT_NAME;
    this.UUID = skeletor.getYoDigits(6, this.NAME);
    this.$element = $(this) || $(document);
    this.options = options;

    if (!this.$element.data('skeletorComponent')) {
      this.$element.data('skeletorComponent', this);
    }

    skeletor.registerComponentInstance(this.UUID);
  }

  // web component v1 spec: Fired when custom element is added to DOM.
  connectedCallback() {
    this.init();
  }

  init() {
    console.log('connected:', this);
  }

  static register() {
    let elementName = this.ELEMENT_NAME;
    if (!elementName) {
      throw new TypeError('You need to define an element name for your component');
    }
    skeletor.registerComponent(this, this.ELEMENT_NAME);
  }

  set defaults(value) {
    this.options = $.extend(true, {}, value, this.options);
  }
}

export default SkeletorPlugin;

skeletor-plugin-base/src/skeletor-plugin-base.js · December 2016

A real component built on the base, the accordion plugin. The same shape AXS still uses in 2026: subclass the base, declare defaults, call register() to wire the custom element into the registry. The jQuery dependency dates it; the architectural choice doesn’t.

// skeletor-plugin-accordion, 2017 - a real component
// built on the base. extends SkeletorPlugin (which
// extends HTMLElement). Registers itself via the
// static .register() call. The whole pattern AXS
// rebuilt in TypeScript nine years later, already
// running in 2017.

import { skeletor, SkeletorPlugin } from 'skeletor';

class Accordion extends SkeletorPlugin {
  constructor(element, options) {
    super(element, options);
    this.VERSION = '0.1.0';

    this.defaults = {
      optionOne: true,
      optionTwo: false
    };

    console.log('created a new accordion');
  }
}

Accordion.register();

skeletor-plugin-accordion/src/accordion.js · 2017


act iv, accessibility as a baseline (2016)

A sister repo, accessible-components, ran in parallel. It bound WAI-ARIA Authoring-Practices attributes to the same DOM elements the framework owned, not as an audit pass after the fact, but as part of each component’s init. Roles, labels, hidden state, expanded state, all wired together.

// accessible-components/aria.js, 2016 - WAI-ARIA
// attributes bound to the same DOM elements the
// accordion component owned. Accessibility wasn't a
// separate pass; the binding ran as part of init.

bindAccordion: function($accordion, $heading, $tab, $panel) {
  var _ = this;

  $($accordion).each(function(i) {
    var $this = $(this);
    $this.attr('id', 'accordion_' + i);
    _.setAccordionAttributes($this, $heading, $tab, $panel, i);
  });
},

setAccordionAttributes: function($this, $heading, $tab, $panel, i) {
  $this.find($heading).each(function() {
    $(this).attr({ 'role': 'heading', 'aria-level': '3' });
  });

  $this.find($tab).each(function(j) {
    $(this).attr({
      'id':            'a' + i + '_tab'   + j,
      'role':          'button',
      'aria-expanded': 'false',
      'aria-controls': 'a' + i + '_panel' + j
    });
  });

  $this.find($panel).each(function(j) {
    $(this).attr({
      'id':              'a' + i + '_panel' + j,
      'role':            'region',
      'aria-hidden':     'true',
      'aria-labelledby': 'a' + i + '_tab'   + j
    });
  });
}

accessible-components/Static/src/js/components/common/aria.js · 2016


companion artifacts

A few more sister repos rounded out the story:


what this proved (and what came next)

Skeletor never needed to be a public open-source brand. It was infrastructure for one agency’s client work and did its job well for a few years. What matters now is what it shows:


stack

Components
Native Web Components (v1 spec), custom elements, jQuery-augmented base class
Build
Gulp 3, JSPM, Bower (yes, Bower), Babel, Sass, AMD modules
Design tokens
Sass utilities + helpers + flexgrid
Responsive
on-media-query, CSS-defined breakpoints, JS subscribers
Accessibility
WAI-ARIA Authoring Practices bound at component init
Distribution
npm + Delphic’s internal Bower registry
Documentation
Skeletor.docs (custom site) + the “Fednet” agency knowledge base

Archived org: github.com/delphic-digital · Related: Building AXS · Framework-Agnostic Architecture