npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

declarative

v1.3.8

Published

Mapper for custom user interface markup

Downloads

8

Readme

#declarative

Mapper for custom user interface markup.

###Why

When writing web user interfaces without using JavaScript we are restricted to the set of native interface elements defined in the HTML standard and thus implemented by the browser.

Every time we want to add custom elements to our interface we do this by implementing the desired functionality in JavaScript and linking the behavior to one or more DOM elements and their events. We find the elements by querying the DOM using some criteria such as IDs or classes.

We do this because we want to separate the behavior from the content and its structure. However in most cases we are not only pulling out the behavior but also the interface configuration. Consider the following search form with a custom character counter in combination with some JavaScript:

<form action="/" method="POST">
    <input id="search" name="search" type="text" maxlength="50" />
    <span id="counter"></span>
    <input type="submit">
</form>
var countCharacters = function(input, counter) {
    input.addEventListener('keyup', function() {
        counter.innerHTML = (50 - this.value.length) + ' characters left';
    });
};

var search = document.getElementById('search');
var counter = document.getElementById('counter');
countCharacters(search, counter);

The HTML holds nothing but the content and its structure. The script however contains the following interface configuration which should actually be placed in the markup:

  • The maximum count of characters for a specific input element
  • The displayed format text for a specific counter
  • The linking between a counter and its corresponding input field

One better way to implement this would be the following:

<form action="/" method="POST">
    <input id="search" name="search" type="text" maxlength="50" />
    <span id="counter" data-target="search" data-text="{0} characters left"></span>
    <input type="submit">
</form>
var countCharacters = function(input, counter, text) {
    var maxlength = input.getAttribute('maxlength');
    input.addEventListener('keyup', function() {
        counter.innerHTML = text.replace('{0}', maxlength - this.value.length);
    });
};

var counter = document.getElementById('counter');
var input = document.getElementById(counter.getAttribute('data-target'));
var text = counter.getAttribute('data-text');
countCharacters(input, counter, text);

The markup now also holds the configuration for custom interface elements but still does not contain any behavior or implementation details. Note that we are using custom data attributes in order to have valid HTML markup. The above example becomes even more obvious when you imagine the counter being a native HTML element:

<form action="/" method="POST">
    <input id="search" name="search" type="text" maxlength="50" />
    <counter for="search">{0} characters left</counter>
    <input type="submit">
</form>

###Features

declarative provides the possibility to declare custom interface elements in the markup and to easily map them to arbitrary JavaScript code. It also prevents from writing similar querying and mapping code over and over again.

Let´s assume we want to implement our search interface using declarative. We start off with the following HTML:

<form action="/" method="POST">
    <input id="search" name="search" type="text" maxlength="50" />
    <span data-widget-counter="target: 'search', text: '{0} characters left'"></span>
    <input type="submit">
</form>

When working with custom interface elements there are three important values to consider: the DOM element itself, the custom type and its options. In the above form there is one span element having the custom type "counter" and two options. One is the ID of the input element and the other one is the format text for displaying. Note the value of the "data-counter" attribute. The syntax used by declarative is equivalent to the object syntax in JavaScript without the most outer curly braces. The attribute value can also be omitted (interpreted as an empty options object).

The next step is to register the counter as a custom type and describe how it should be mapped to JavaScript code. This is done by adding a mapping to declarative:

declarative.mappings.add({
    id: 'counter',
    prefix: 'data-widget-',
    types: ['counter']
    callback: function(counter, type, options) {
        var input = document.querySelector(options.target);
        var maxlength = input.getAttribute('maxlength');
        countCharacters(input, counter, maxlength);
    }
});

The id of a mapping is used for later identification. The optional prefix defines the string that is put before the type when used as an attribute of an HTML element. While any string is valid in most cases it should start with "data-" to make use of HTML custom data attributes. The types array defines the types declarative searches for when applying the mapping. The callback function is called for every match of the mapping when applied. Parameters for the callback are the DOM element, the type without the prefix and the options as an object.

Applying the above mapping to the whole DOM is done by writing the following:

declarative.apply('counter').to(document);

###API

Adding mappings:

declarative.mappings.add({
    id: 'example mapping', // string identifier
    prefix: 'data-attribute-prefix-', // lowercase attribute prefix
    types: ['types', 'to', 'map'], // types that will be mapped when found
    callback: function(element, type, options) {
        // callback is called for every match in the current mapping when applied
    }
});

Applying mappings:

// mappings can be applied to any DOM element
declarative.apply('example mapping').to(document);
// multiple mappings for the same DOM element should be passed in a single call
declarative.apply(['example mapping', 'another mapping']).to(document);
// all available mappings can be applied at once
declarative.applyAll().to(document);
// DOM elements need to be unwrapped when using jquery
declarative.applyAll().to($('#someElement').get(0));

###Examples

While mapping one single custom type might not look too useful have a look at the following examples:

#####Mapping jQueryUI types

<div data-ui-draggable></div>
<div data-ui-progressbar="value: '100'"></div>
<div data-ui-dialog="closeText: 'hide'"></div>
<input type="text" data-ui-datepicker="minDate: '2012/03/01'" />
declarative.mappings.add({
    id: 'jQueryUI',
    prefix: 'data-ui-',
    types: ['draggable', 'progressbar', 'dialog', 'datepicker'],
    callback: function(element, type, options) {
        $(element)[type](options);
    }
});

declarative.apply('jQueryUI').to(document);

#####Simplified mapping of jQuery.validate

<form data-validate-form>
    <input type="text" name="required" data-validate-required="is: true, withMessage: 'Required'" />
    <input type="text" name="minlength" data-validate-minlength="is: 3, withMessage: 'Minimum of 3'" />
    <input type="text" name="maxlength" data-validate-maxlength="is: 6, withMessage: 'Maximum of 6'" />
    <input type="submit" />
</form>
declarative.mappings.add({
    id: 'jQuery.validate.form',
    prefix: 'data-validate-',
    types: ['form'],
    callback: function(element) {
        $(element).validate();
    },
});

declarative.mappings.add({
    id: 'jQuery.validate.input',
    prefix: 'data-validate-',
    types: ['required', 'minlength', 'maxlength'],
    callback: function(element, type, options) {
        var rule = {messages: {}};
        rule[type] = options.is;
        rule.messages[type] = options.withMessage;
        $(element).rules('add', rule);
    }
});

declarative.apply('jQuery.validate.form').to(document);
declarative.apply('jQuery.validate.input').to(document);

###Performance

declarative is optimized for performance. It uses query selectors where available and parses the minimum set of HTML elements and attributes. However in Internet Explorer 7 or below it could lead to performance issues when applying mappings to pages which contain more than 1500 DOM elements.

###Roll your own markup language

declarative can also map elements giving the possibility to use a custom markup language. This is done by changing the mappingMode of a mapping explicitely to "element" (otherwise it is "attribute" by default).

<form action="/" method="POST">
    <input id="search" name="search" type="text" maxlength="50" />
    <counter target="search" text="{0} characters left"></counter>
    <input type="submit">
</form>
declarative.mappings.add({
    id: 'counter',
    prefix: '',
    types: ['counter'],
    callback: function(counter, type, options) {
        var input = document.querySelector(options.target);
        countCharacters(input, counter, options.text);
    },
    mappingMode: 'element'
});

Notes:

  • Using custom elements that are not part of the HTML standard might cause rendering proglems and styling issues
  • This element mapping feature is not tested across different browsers as it is not really a best practice