Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for invalid input #43

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ where `childN` may be:

- a DOM element,
- a string, which will be inserted as a `textNode`,
- `null`, which will be ignored, or
- `null` or `undefined` which will be ignored, or
- an `Array` containing any of the above

## Examples
Expand All @@ -79,8 +79,7 @@ You can add attributes that have dashes or reserved keywords in the name, by usi
crel('div', { 'class': 'thing', 'data-attribute': 'majigger' });
```

You can define custom functionality for certain keys seen in the attributes
object:
You can define custom functionality for certain keys seen in the attributes object:

```javascript
crel.attrMap['on'] = (element, value) => {
Expand Down Expand Up @@ -124,15 +123,24 @@ _But don't._
If you are using Crel in an environment that supports Proxies, you can also use the new API:

```javascript
let crel = require('crel').proxy;

let element = crel.div(
crel.h1('Crello World!'),
crel.p('This is crel'),
crel.input({ type: 'number' })
);
```

If you want to transform tags to for example get dashes in them, you can define a `tagTransform` function:

```javascript
// Adds dashes on camelCase, ex: `camelCase` -> `camel-case`
crel.tagTransform = key => key.replace(/([0-9a-z])([A-Z])/g, '$1-$2');

let element = crel.myTag('Crello World!');

console.log(element.tagName); // my-tag
```

# Browser support

Crel uses ES6 features, so it'll work in all evergreen browsers.
Expand Down
105 changes: 57 additions & 48 deletions crel.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ This might make it harder to read at times, but the code's intention should be t

// IIFE our function
((exporter) => {
// Define our function and its properties
// These strings are used multiple times, so this makes things smaller once compiled
const func = 'function',
isNodeString = 'isNode',
proxyString = 'proxy',
tagTransformString = 'tagTransform',
d = document,
// Helper functions used throughout the script
isType = (object, type) => typeof object === type,
// Recursively appends children to given element. As a text node if not already an element
// Recursively appends children to given element if they're not `null`. As a text node if not already an element
appendChild = (element, child) => {
if (child !== null) {
if (child != null) {
if (Array.isArray(child)) { // Support (deeply) nested child elements
child.map(subChild => appendChild(element, subChild));
} else {
Expand All @@ -28,56 +29,64 @@ This might make it harder to read at times, but the code's intention should be t
element.appendChild(child);
}
}
};
//
function crel (element, settings) {
// Define all used variables / shortcuts here, to make things smaller once compiled
let args = arguments, // Note: assigned to a variable to assist compilers.
index = 1,
key,
attribute;
// If first argument is an element, use it as is, otherwise treat it as a tagname
element = crel.isElement(element) ? element : d.createElement(element);
// Check if second argument is a settings object
if (isType(settings, 'object') && !crel[isNodeString](settings) && !Array.isArray(settings)) {
// Don't treat settings as a child
index++;
// Go through settings / attributes object, if it exists
for (key in settings) {
// Store the attribute into a variable, before we potentially modify the key
attribute = settings[key];
// Get mapped key / function, if one exists
key = crel.attrMap[key] || key;
// Note: We want to prioritise mapping over properties
if (isType(key, func)) {
key(element, attribute);
} else if (isType(attribute, func)) { // ex. onClick property
element[key] = attribute;
} else {
// Set the element attribute
element.setAttribute(key, attribute);
},
// Define our function as a proxy interface
crel = new Proxy((element, ...children) => {
// If first argument is an element, use it as is, otherwise treat it as a tagname
if (!crel.isElement(element)) {
if (!isType(element, 'string') || element.length < 1) {
return; // Do nothing on invalid input
}
element = d.createElement(element);
}
}
// Loop through all arguments, if any, and append them to our element if they're not `null`
for (; index < args.length; index++) {
appendChild(element, args[index]);
}

return element;
}

// Used for mapping attribute keys to supported versions in bad browsers, or to custom functionality
// Define all used variables / shortcuts here, to make things smaller once compiled
let settings = children[0],
key,
attribute;
// Check if second argument is a settings object
if (isType(settings, 'object') && !crel[isNodeString](settings) && !Array.isArray(settings)) {
// Don't treat settings as a child
children.shift();
// Go through settings / attributes object, if it exists
for (key in settings) {
// Store the attribute into a variable, before we potentially modify the key
attribute = settings[key];
// Get mapped key / function, if one exists
key = crel.attrMap[key] || key;
// Note: We want to prioritise mapping over properties
if (isType(key, func)) {
key(element, attribute);
} else if (isType(attribute, func)) { // ex. onClick property
element[key] = attribute;
} else {
// Set the element attribute
element.setAttribute(key, attribute);
}
}
}
// Append remaining children to element and return it
appendChild(element, children);
return element;
}, {// Binds specific tagnames to crel function calls with that tag as the first argument
get: (target, key) => {
if (key in target) {
return target[key];
}
key = target[tagTransformString](key);
if (!(key in target[proxyString])) {
target[proxyString][key] = target.bind(null, key);
}
return target[proxyString][key];
}
});
// Used for mapping attribute keys to custom functionality, or to supported versions in bad browsers
crel.attrMap = {};
crel.isElement = object => object instanceof Element;
crel[isNodeString] = node => node instanceof Node;
// Expose proxy interface
crel.proxy = new Proxy(crel, {
get: (target, key) => {
!(key in crel) && (crel[key] = crel.bind(null, key));
return crel[key];
}
});
// Bound functions are "cached" here for legacy support and to keep Crels internal structure clean
crel[proxyString] = new Proxy({}, { get: (target, key) => target[key] || crel[key] });
// Transforms tags on call, to for example allow dashes in tags
crel[tagTransformString] = key => key;
// Export crel
exporter(crel, func);
})((product, func) => {
Expand Down
2 changes: 1 addition & 1 deletion crel.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 65 additions & 19 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,49 @@ test('Create an element with a deep array of children', function (t) {
t.equal(testElement.childNodes[2].textContent, 'I will be a text node!');
});

// -- Test invalid input handling --
test('Pass invalid elements / tagnames ([nothing], \'\', null, undefined, {}, [numbers])', function (t) {
var testElement;
// ? what about functions as tagnames
// ? should we also add tests for valid input, like with isElement and isNode bellow
var testInput = ['', null, undefined, {}, 0, 4.2, -42];

t.plan((testInput.length + 1) * 2); // 2 tests for every value in array, + for no input

t.doesNotThrow(function () { testElement = crel(); }, 'Pass no input');
// Not returning an element with no input should be sane behaviour
t.equal(testElement, undefined,
'no element was created');

testInput.map(function (value) {
testElement = undefined;
t.doesNotThrow(function () { testElement = crel(value); },
'Pass invalid input: `' + value + '`');
// Not returning an element on invalid input should be sane behaviour
t.equal(testElement, undefined,
'no element was created with invalid input: `' + value + '`');
});
});

test('Pass invalid children (null, undefined)', function (t) {
var testElement = crel('div');
var testedElement;
// ? should empty strings be ignored? Adding empty text nodes only bloats childNodes
// ? what about functions and objects as children
var testInput = [null, undefined];

t.plan(testInput.length * 2); // 2 tests for every value in array

testInput.map(function (value) {
t.doesNotThrow(function () { testedElement = crel('div', value); },
'Pass invalid child argument: `' + value + '`');
// Ignoring invalid children should be sane behaviour
t.ok(testElement.isEqualNode(testedElement),
'Elements have the same child tree');
testedElement = undefined;
});
});

// -- Test exposed methods --
test('Test that `isNode` is defined', function (t) {
// Assign into a variable to help readability
Expand Down Expand Up @@ -222,32 +265,35 @@ test('Test that `isElement` is defined', function (t) {
});

// -- Test the Proxy API --
test('Test that the Proxy API is defined', function (t) {
test('Test that the Proxy API works', function (t) {
if (typeof Proxy === 'undefined') {
t.plan(1)
t.pass('Proxies are not supported in the current environment');
} else {
var proxyCrel = crel.proxy;
// I'm not proficient with proxies, so
// TODO: Add #moar-tests
t.plan(4);

t.plan(proxyCrel ? 2 : 1);
var testElement = crel.proxy.div({'class': 'test'},
crel.proxy.span('test'));

t.ok(proxyCrel, 'The Proxy API is defined');
t.equal(testElement.className, 'test');
t.equal(testElement.childNodes.length, 1);
t.equal(testElement.childNodes[0].tagName, 'SPAN');
t.equal(testElement.childNodes[0].textContent, 'test');
}
});

if (proxyCrel) {
// Do further tests
t.test('Test that the Proxy API works', function (ts) {
// I'm not proficient with proxies, so
// TODO: Add #moar-tests
ts.plan(4);
// -- Test the Proxy APIs features --
test('Test the proxy APIs tag transformations', (t) => {
t.plan(4);

var testElement = proxyCrel.div({'class': 'test'},
proxyCrel.span('test'));
crel.tagTransform = (key) => key.replace(/([0-9a-z])([A-Z])/g, '$1-$2').toLowerCase();
let testElement = crel.myTable(crel.span('test'));

ts.equal(testElement.className, 'test');
ts.equal(testElement.childNodes.length, 1);
ts.equal(testElement.childNodes[0].tagName, 'SPAN');
ts.equal(testElement.childNodes[0].textContent, 'test');
});
}
}
t.equal(testElement.tagName, 'MY-TABLE',
'tagname had dashes added to it');
t.equal(testElement.childNodes.length, 1);
t.equal(testElement.childNodes[0].tagName, 'SPAN');
t.equal(testElement.childNodes[0].textContent, 'test');
});
1 change: 1 addition & 0 deletions test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
};
</script>
<script src="index.browser.js"></script>
<script src="../crel.js"></script>
</head>
<body></body>
</html>