Skip to content

Instantly share code, notes, and snippets.

@romellem
Created November 26, 2019 17:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save romellem/a49a09a6775726eb48e160d95ff77674 to your computer and use it in GitHub Desktop.
Save romellem/a49a09a6775726eb48e160d95ff77674 to your computer and use it in GitHub Desktop.
expandAttributes Handlebars (HBS) Helper
/**
* List of valid attributes that can live on HTML elements. This can be trimmed down however you like.
* @note I've remove `class` and `data-*` since class is usually defined elsewhere, and `data-*` uses different logic to filter them in.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#Attribute_list
*/
const attribute_white_list = ['accept', 'accept-charset', 'accesskey', 'action', 'align', 'allow', 'alt', 'async', 'autocapitalize', 'autocomplete', 'autofocus', 'autoplay', 'background', 'bgcolor', 'border', 'buffered', 'challenge', 'charset', 'checked', 'cite', /* 'class', */ 'code', 'codebase', 'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'crossorigin', 'csp', 'data', /* 'data-*', */ 'datetime', 'decoding', 'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 'draggable', 'dropzone', 'enctype', 'enterkeyhint', 'for', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id', 'importance', 'integrity', 'intrinsicsize', 'inputmode', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang', 'language', 'loading', 'list', 'loop', 'low', 'manifest', 'max', 'maxlength', 'minlength', 'media', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'referrerpolicy', 'rel', 'required', 'reversed', 'rows', 'rowspan', 'sandbox', 'scope', 'scoped', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'spellcheck', 'src', 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'value', 'width', 'wrap'];
const attribute_lookup_white_list = attribute_white_list.reduce((obj, key) => {
obj[key] = true;
return obj;
}, {});
/**
* General function to allow partials and partial-blocks to take in any
* HTML attributes and then output them like we have in HTML.
*
* So say our template calls a partial block `linkPartial` like this:
*
* {{> linkPartial href="my/file.pdf" data-tracking="tag" data-js-attr=null icon="icon.png" title="<b>Download</b> the PDF" pruneAttributes="icon,title"}}
*
* And our `linkPartial`'s source looks like this:
*
* {{! linkPartial.hbs }}
* <a class="special-link" {{{expandAttributes this pruneAttributes}}}>
* <img src="{{icon}}">
* {{{title}}}
* </a>
*
* Then when we render the template, our resulting HTML will look like
*
* <a class="special-link" href="my/file.pdf" data-tracking="tag" data-js-attr>
* <img src="icon.png">
* <b>Download</b> the PDF
* </a>
*
* Note that through the use of `pruneAttributes`, we can select attributes
* that are getting passed in via context (or otherwise) that _should not_
* be rendered as an attribute, but will be used in some other way (in this
* case, `icon` and `title`).
*
* This does not have to be a hash param, but they can be passed inline. Using the
* variable here is for illustrative purposes.
*
* @note should be called with triple curlies, otherwise the content gets escaped.
*
* @param {Object} attributes - Object with `keys` being the attribute name, and its `value` being the value to render.
* @param {String} prune - String of **comma-separated** attributes that we _don't_ want to render.
* When calling `expandAttributes` within our partial, it not only
* takes in the hash params, but also the full context. So if we have a
* variable named `title`, then if we don't include in this `prune` list,
* it'll get rendered as an attribute.
* @returns {String}
*/
const expandAttributes = (attributes, prune = '') => {
// Verify that `attributes` is an object
if (!(attributes && attributes.constructor === Object)) {
return '';
}
let attribute_keys = Object.keys(attributes);
let prune_list = String(prune).split(',');
let prune_list_lookup = {};
prune_list.forEach(prune_attr => (prune_list_lookup[prune_attr] = true));
let attributes_output = [];
attribute_keys.forEach(attribute => {
let is_valid_attribute = attribute_lookup_white_list[attribute];
let is_data_attribute = attribute.indexOf('data-') === 0;
let should_be_pruned = prune_list_lookup[attribute];
if ((is_valid_attribute || is_data_attribute) && !should_be_pruned) {
/**
* If our value is `null` or `undefined`, then output the attribute without an equals sign.
* Otherwise, use the equals and a quoted attribute value.
*/
let output = attributes[attribute] == null ? attribute : `${attribute}="${attributes[attribute]}"`;
attributes_output.push(output);
}
});
return attributes_output.join(' ');
};
@romellem
Copy link
Author

Link to working Handlebars Example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment