Skip to content

Instantly share code, notes, and snippets.

@james-jlo-long
Last active January 17, 2019 10:14
Show Gist options
  • Save james-jlo-long/2ad4bdf80ba6764f20034a87ab037eeb to your computer and use it in GitHub Desktop.
Save james-jlo-long/2ad4bdf80ba6764f20034a87ab037eeb to your computer and use it in GitHub Desktop.
A simple library for creating DOM elements.
/**
* A helper function for looping over all elements that match the given
* selector. This function returns the results of the function being called on
* all elements.
*
* @param {String} selector
* CSS selector to identify elements.
* @param {Function} handler
* Function to execute on all elements.
* @param {?} [context]
* Optional context for the handler.
* @return {Array}
* Result of executing the function.
*
* @example <caption>Looping over elements</caption>
* // Markup is:
* // <ul>
* // <li>One</li>
* // <li>Two</li>
* // <li>Three</li>
* // <li>Four</li>
* // </ul>
* domEach("li", function (li) {
* li.classList.add("hello");
* });
* // Now markup is:
* // <ul>
* // <li class="hello">One</li>
* // <li class="hello">Two</li>
* // <li class="hello">Three</li>
* // <li class="hello">Four</li>
* // </ul>
*
* @example <caption>Returning values</caption>
* // Markup is:
* // <ul>
* // <li>One</li>
* // <li>Two</li>
* // <li>Three</li>
* // <li>Four</li>
* // </ul>
* var texts = domEach("li", function (li) {
* return li.textContent;
* });
* texts; // -> ["One", "Two", "Three", "Four"]
*/
function domEach(selector, handler, context) {
return Array.from(document.querySelectorAll(selector), handler, context);
}
/**
* A function for looping over objects.
*
* @param {Object} object
* Object to loop over.
* @param {Function} handler
* Function to execute on each object entry.
* @param {?} [context]
* Optional context for the handler.
* @return {Array}
* Converted entries.
*
* @example <caption>Looping over an object</caption>
* var object = { one: 1, two: 2, three: 3 };
* forIn(object, function (key, value) {
* console.log("object[%s] = %o", key, value);
* });
* // Logs: "object[one] = 1"
* // Logs: "object[two] = 2"
* // Logs: "object[three] = 3"
* // Order is not guaranteed.
*
* @example <caption>Returning results</caption>
* var object = { one: 1, two: 2, three: 3 };
* var pairs = forIn(object, function (key, value) {
* return [key, value];
* });
* pairs; // => [["one", 1], ["two", 2], ["three", 3]]
* // Order is not guaranteed.
*/
function forIn(object, handler, context) {
return Object.keys(object).map(function (key) {
return handler.call(context, key, object[key]);
});
}
/**
* Creates an element.
*
* @param {Array.<String>|String} nodeName
* Name of the node. Optionally with a namespace.
* @param {Object} [settings]
* Optional settings.
* @param {Object} [settings.attributes]
* Optional attributes for the element.
* @param {Object} [settings.properties]
* Optional properties for the element.
* @param {Array.<Element>} [children]
* Optional children to add to the element.
* @return {Element}
* Created element.
*
* @example <caption>Creating an element</caption>
* createElement("div");
* // -> <div></div>
* createElement("div", {attributes: {"data-one": 1}});
* // -> <div data-one="1"></div>
* createElement("div", {attrs: {"data-one": 1}});
* // -> <div data-one="1"></div>
* createElement("input", {properties: {"checked": true}});
* // -> <input checked>
* createElement("input", {props: {"checked": true}});
* // -> <input checked>
*
* @example <caption>Createing an element with a namespace</caption>
* createElement(["http://www.w3.org/2000/svg", "defs"]);
* // -> <defs></defs>
*
* @example <caption>Creating an element with children</caption>
* createElement("div", {}, createElement("span"));
* // => <div><span></span></div>
*/
function createElement(nodeName, settings, children) {
var element = Array.isArray(nodeName)
? document.createElementNS(nodeName[0], nodeName[1])
: document.createElement(nodeName);
settings = createElement.readSettings(settings);
forIn(settings.properties, function (property, value) {
element[createElement.propertyMap[property] || property] = value;
});
forIn(settings.attributes, function (attribute, value) {
element.setAttribute(attribute, value);
});
(children || []).forEach(function (child) {
element.appendChild(child);
});
return element;
}
/**
* Helper function for reading the settings for {@link createElement}.
*
* @param {Object} [settings]
* Given settings.
* @return {Object}
* Settings that {@link createElement} can read.
*/
createElement.readSettings = function (settings) {
var copy = settings
? JSON.parse(JSON.stringify(settings))
: {};
if (!copy.properties) {
copy.properties = copy.props || {};
}
if (!copy.attributes) {
copy.attributes = copy.attrs || {};
}
return copy;
};
/**
* Map for properties. As properties are set, common mistakes may be made. This
* map will correct them.
*
* @type {Object}
*/
createElement.propertyMap = {
"for": "htmlFor",
"class": "className"
};
/**
* Property map for HTML attributes.
* @type {Object}
*/
export const propMap = {
"class": "className",
"for": "htmlFor"
};
/**
* Adds properties to the given element.
*
* @param {Element} element
* Element whose properties should be set.
* @param {Object} properties
* Properties to set.
*/
export const props = (element, properties) => {
Object
.entries(properties)
.forEach(([name, value]) => element[propMap[name] || name] = value);
};
/**
* Adds attribute to the given element. If an attribute name contains a space,
* everything before the space is treated as the attribute namespace and
* everything afterwards is the attribute name.
*
* @param {Element} element
* Element that should gain attributes.
* @param {Object} attributes
* Attributes to set.
*/
export const attr = (element, attributes) => {
Object
.entries(attributes)
.forEach(([name, value]) => {
let parts = name.split(" ");
if (parts.length > 1) {
element.setAttributeNS(parts[0], parts[1], value);
} else {
element.setAttribute(name, value);
}
});
};
/**
* Creates elements.
*
* @param {Array.<String>|String} nodeName
* Name of the element. This can take 3 forms: a string (creates an
* element), a string with a space (namespace and nodeName) or an array
* (namespace, nodeName).
* @param {Object} [settings={}]
* Optional settings for the element. It should have a "properties" (or
* "props") key for element properties and/or an "attributes" (or
* "attrs") key for element attributes. If neither of those keys are
* provided but an object is passed in, it is assumed to be attributes.
* @param {Array.<Element|String>|Element|String} [children=[]]
* Optional children for the element. Either an array of children or a
* single child to add to the new element. The children can be either
* elements or a string. Array-like structures (NodeList, for example)
* will still work.
* @return {Element}
* Newly created element with given settings and children.
*/
export const create = (nodeName, settings = {}, children = []) => {
if (typeof nodeName === "string" && nodeName.includes(" ")) {
nodeName = nodeName.split(" ");
}
let element = typeof nodeName === "string"
? document.createElement(nodeName)
: document.createElementNS(nodeName[0], nodeName[1]);
let properties = settings.properties || settings.props;
let attributes = settings.attributes || settings.attrs;
if (settings && !properties && !attributes) {
attributes = settings;
}
if (properties) {
props(element, properties);
}
if (attributes) {
attr(element, attributes);
}
if (typeof children === "string") {
children = [children];
} else if (children.length) {
children = [...children];
}
children.forEach((child) => element.append(child));
return element;
};
@james-jlo-long
Copy link
Author

Thinking aloud for a nicer version

var propMap = {
    class: "className",
    for: "htmlFor"
};

function setAttributes(element, attributes, value) {

    var name;

    if (typeof attributes === "string") {

        name = attributes;
        attributes = {};
        attributes[name] = value;
        
    }

    Object.entries(attributes).forEach(function ([name, value]) {

        var property;

        if (name.startsWith("!")) {

            name = name.slice(1);
            element[propMap[name] || name] = value;

        } else if (name.includes(":")) {

            property = name.split(":");
            element.setAttributeNS(property[0], property[1], value);

        } else {
            element.setAttribute(name, value);
        }
        
    });
    
}

var nsMap = {
    html: "http://www.w3.org/1999/xhtml",
    svg: "http://www.w3.org/2000/svg",
    mathml: "http://www.w3.org/1998/mathml"
};

function createNode(nodeString) {

    var node;

    if (nodeString.includes(":")) {

        nodeString = nodeString.split(":");
        node = document.createElementNS(
            nsMap[nodeString[0]] || nodeString[0],
            nodeString[1]
        );
        
    } else {
        node = document.createElement(nodeString);
    }

    return node;
    
}

function isNode(object) {
    return object instanceof Node;
}

function create(nodeString, attributes = {}, children = []) {

    var node = createNode(nodeString);

    if (
        typeof attributes === "string"
        || isNode(attributes)
        || Array.isArray(attributes)
    ) {

        children = attributes;
        attributes = {};
        
    }

    setAttributes(node, attributes);

    if (typeof children === "string" || isNode(children)) {
        children = [children];
    }

    Array.from(children, (child) => node.append(child));

    return node;

}

Usage:

createElement("div", {
    class: "lorem ipsum",
    "!hidden": true,
    "svg:image": "testing"
});

@james-jlo-long
Copy link
Author

More thoughts, check the type of attribute to see how it should work.

function setAttributes(element, attributes) {

    Object.entries(attributes).forEach(([key, value]) => {

        var property = propMap[key] || key;

        switch (typeof element[property]) {

        case "boolean":
            element[property] = Boolean(value);
            break;

        case "function":
            element[property](...arrayify(value));
            break;

        case "undefined":
            element.setAttribute(property, value);
            break;

        default:
            element[property] = value;
            
        }

    });

}

create("div", {
    class: "abc def",
    hidden: true,
    addEventListener: ["click", (e) => {}]
});

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