diff --git a/README.md b/README.md index 4ef8818..626b308 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,6 @@ _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'), @@ -133,6 +131,17 @@ let element = crel.div( ); ``` +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. diff --git a/crel.js b/crel.js index 9073057..14e6cd6 100644 --- a/crel.js +++ b/crel.js @@ -9,14 +9,15 @@ 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 (Array.isArray(child)) { // Support (deeply) nested child elements @@ -28,56 +29,59 @@ 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) => { + // Define all used variables / shortcuts here, to make things smaller once compiled + let settings = children[0], + 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 + 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); + } } } - } - // 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 + // 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) => { diff --git a/crel.min.js b/crel.min.js index d17ea8c..ade5193 100644 --- a/crel.min.js +++ b/crel.min.js @@ -1 +1 @@ -(e=>{const t="function",n="isNode",r=document,o=(e,t)=>typeof e===t,i=(e,t)=>{null!==t&&(Array.isArray(t)?t.map(t=>i(e,t)):(a[n](t)||(t=r.createTextNode(t)),e.appendChild(t)))};function a(e,f){let l,d,s=arguments,c=1;if(e=a.isElement(e)?e:r.createElement(e),o(f,"object")&&!a[n](f)&&!Array.isArray(f))for(l in c++,f)d=f[l],l=a.attrMap[l]||l,o(l,t)?l(e,d):o(d,t)?e[l]=d:e.setAttribute(l,d);for(;ce instanceof Element),a[n]=(e=>e instanceof Node),a.proxy=new Proxy(a,{get:(e,t)=>(!(t in a)&&(a[t]=a.bind(null,t)),a[t])}),e(a,t)})((e,t)=>{"object"==typeof exports?module.exports=e:typeof define===t&&define.amd?define(e):this.crel=e}); \ No newline at end of file +(e=>{const t="function",n="isNode",r="proxy",o="tagTransform",i=document,a=(e,t)=>typeof e===t,s=(e,t)=>{null!==t&&(Array.isArray(t)?t.map(t=>s(e,t)):(f[n](t)||(t=i.createTextNode(t)),e.appendChild(t)))},f=new Proxy((e,...r)=>{let o,d,l=r[0];if(e=f.isElement(e)?e:i.createElement(e),a(l,"object")&&!f[n](l)&&!Array.isArray(l))for(o in r.shift(),l)d=l[o],a(o=f.attrMap[o]||o,t)?o(e,d):a(d,t)?e[o]=d:e.setAttribute(o,d);return s(e,r),e},{get:(e,t)=>t in e?e[t]:((t=e[o](t))in e[r]||(e[r][t]=e.bind(null,t)),e[r][t])});f.attrMap={},f.isElement=(e=>e instanceof Element),f[n]=(e=>e instanceof Node),f[r]=new Proxy({},{get:(e,t)=>e[t]||f[t]}),f[o]=(e=>e),e(f,t)})((e,t)=>{"object"==typeof exports?module.exports=e:typeof define===t&&define.amd?define(e):this.crel=e}); \ No newline at end of file diff --git a/package.json b/package.json index fb36550..935ff68 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ }, "scripts": { "test": "npm run testBuild && opn ./test/test.html", - "testBuild": "browserify ./test/index.js > ./test/index.browser.js", + "buildTest": "browserify ./test/test_logic.js > ./test/test_logic.browser.js", + "buildTestData": "browserify ./test/test_list.js > ./test/test_list.browser.js", "build": "terser crel.js -o crel.min.js --warn --ecma 6 -c evaluate=false,pure_getters -m" }, "repository": { diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 5671352..0000000 --- a/test/index.js +++ /dev/null @@ -1,253 +0,0 @@ -var crel = require('../'), - test = require('tape'); - -// -- Test element creation -- -test('Create an element with no arguments', function (t) { - t.plan(2); - - var testElement = crel('div'); - - t.ok(testElement instanceof HTMLElement, - 'element is an instance of `HTMLElement`'); - t.equal(testElement.tagName, 'DIV', - 'element is an instance of `div`'); -}); - -test('Create an element with no arguments, using an invalid tag name', function (t) { - t.plan(2); - - var testElement = crel('invalidtagname'); - - t.ok(testElement instanceof HTMLUnknownElement, - 'element is an instance of `HTMLUnknownElement `'); - t.equal(testElement.tagName, 'INVALIDTAGNAME', - 'element is an instance of `invalidtagname`'); -}); - -test('Crel doesn\'t modify existing elements if not instructed', function (t) { - t.plan(1); - - var testElement = document.createElement('div'); - var testedElement = crel(testElement); - - t.ok(testElement.isSameNode(testedElement), - 'element is still the same'); -}); - -// -- Test attribute handling -- -test('Create an element with simple attributes', function (t) { - t.plan(2); - - var testElement = crel('div', {'class': 'test', id: 'test'}); - - t.equal(testElement.className, 'test', - 'element has a `test` class'); - t.equal(testElement.getAttribute('id'), 'test', - 'element has a `test` id'); -}); - -test('Add attributes to an already existing element', function (t) { - t.plan(2); - - var testElement = document.createElement('div'); - crel(testElement, {'class': 'test', id: 'test'}); - - t.equal(testElement.className, 'test', - 'element has a `test` class'); - t.equal(testElement.getAttribute('id'), 'test', - 'element has a `test` id'); -}); - -test('Modify attributes of an already existing element', function (t) { - t.plan(2); - - var testElement = document.createElement('div'); - testElement.setAttribute('class', 'test'); - testElement.setAttribute('id', 'test'); - crel(testElement, {'class': 'testier', id: 'testier'}); - - t.equal(testElement.getAttribute('class'), 'testier', - 'elements class was changed'); - t.equal(testElement.getAttribute('id'), 'testier', - 'elements id was changed'); -}); - -test('Add an `onEvent` property to an element', function (t) { - t.plan(1); - - var testElement = crel('button', { - onclick: function () { - t.pass('onClick event triggered'); - } - }); - - testElement.click(); -}); - -test('Add an `onEvent` property to an element through attribute mapping', function (t) { - t.plan(1); - - crel.attrMap.on = function (element, value) { - for (var eventName in value) { - element.addEventListener(eventName, value[eventName]); - } - }; - - var testElement = crel('img', { on: { - click: function () { - t.pass('onClick event triggered'); - } - }}); - - testElement.click(); -}); - -// -- Test child node handling -- -test('Create an element with a child element', function (t) { - t.plan(2); - - var testElement = crel('div', document.createElement('p')); - - t.equal(testElement.childNodes.length, 1, - 'element has a child element'); - t.equal(testElement.childNodes[0].tagName, 'P', - 'child element is an instance of `p`'); -}); - -test('Create an element with a child text node', function (t) { - t.plan(2); - - var testElement = crel('div', document.createTextNode('test')); - - t.equal(testElement.childNodes.length, 1, - 'element has a child element'); - t.equal(testElement.childNodes[0].nodeType, 3, - 'child element is a text node'); -}); - -test('Create an element with an array of children', function (t) { - t.plan(6); - // TODO: make these more compact / robust - var testArray = [document.createElement('p'), document.createTextNode('I\'m a text node!'), 'I will be a text node!']; - var testElement = crel('div', testArray); - - t.equal(testElement.childNodes.length, 3, - 'element has three children'); - t.equal(testElement.childNodes[0].tagName, 'P'); - t.equal(testElement.childNodes[1].nodeType, 3); - t.equal(testElement.childNodes[1].textContent, 'I\'m a text node!'); - t.equal(testElement.childNodes[2].nodeType, 3); - t.equal(testElement.childNodes[2].textContent, 'I will be a text node!'); -}); - -test('Create an element with a deep array of children', function (t) { - t.plan(6); - // TODO: make these more compact / robust - var testArray = [document.createElement('p'), document.createTextNode('I\'m a text node!'), 'I will be a text node!']; - var testElement = crel('div', [[testArray]]); - - t.equal(testElement.childNodes.length, 3, - 'element has three children'); - t.equal(testElement.childNodes[0].tagName, 'P'); - t.equal(testElement.childNodes[1].nodeType, 3); - t.equal(testElement.childNodes[1].textContent, 'I\'m a text node!'); - t.equal(testElement.childNodes[2].nodeType, 3); - t.equal(testElement.childNodes[2].textContent, 'I will be a text node!'); -}); - -// -- Test exposed methods -- -test('Test that `isNode` is defined', function (t) { - // Assign into a variable to help readability - var isDefined = crel.isNode; - - t.plan(isDefined ? 2 : 1); - - t.ok(isDefined, '`isNode` is defined'); - - if (isDefined) { - // Do further tests - t.test('Test `isNode` against various arguments', function (ts) { - var testInputInvalid = ['', null, undefined, 'non-empty', - 42, 4.2, {'key': 'value'}, function () { return 'hi'; }]; - - var testInputValid = [document, - document.createElement('div'), - document.createTextNode('test'), - document.createElement('invalidtagname'), - document.createElementNS('http://www.w3.org/2000/svg', 'svg'), - document.createElementNS('http://www.w3.org/1998/mathml', 'element')]; - - ts.plan(testInputInvalid.lenght + testInputValid.lenght); // Test all values in arrays - - testInputInvalid.map(function (value) { - ts.notOk(crel.isNode(value), '`' + value + '` is not a Node'); - }); - testInputValid.map(function (value) { - ts.ok(crel.isNode(value), '`' + value + '` is a Node'); - }); - }); - } -}); - -test('Test that `isElement` is defined', function (t) { - // Assign into a variable to help readability - var isDefined = crel.isElement; - - t.plan(isDefined ? 2 : 1); - - t.ok(isDefined, '`isElement` is defined'); - - if (isDefined) { - // Do further tests - t.test('Test `isElement` against various arguments', function (ts) { - var testInputInvalid = ['', null, undefined, 'non-empty', - 42, 4.2, {'key': 'value'}, function () {}, - document.createTextNode('test'), document]; - - var testInputValid = [document.createElement('div'), - document.createElement('invalidtagname'), - document.createElementNS('http://www.w3.org/2000/svg', 'svg'), - document.createElementNS('http://www.w3.org/1998/mathml', 'element')]; - - ts.plan(testInputInvalid.lenght + testInputValid.lenght); // Test all values in arrays - - testInputInvalid.map(function (value) { - ts.notOk(crel.isElement(value), '`' + value + '` is not an Element'); - }); - testInputValid.map(function (value) { - ts.ok(crel.isElement(value), '`' + value + '` is an Element'); - }); - }); - } -}); - -// -- Test the Proxy API -- -test('Test that the Proxy API is defined', function (t) { - if (typeof Proxy === 'undefined') { - t.plan(1) - t.pass('Proxies are not supported in the current environment'); - } else { - var proxyCrel = crel.proxy; - - t.plan(proxyCrel ? 2 : 1); - - t.ok(proxyCrel, 'The Proxy API is defined'); - - 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); - - var testElement = proxyCrel.div({'class': 'test'}, - proxyCrel.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'); - }); - } - } -}); diff --git a/test/test.html b/test/test.html index d6b99f3..868170e 100644 --- a/test/test.html +++ b/test/test.html @@ -11,7 +11,9 @@ document.body.innerHTML += '
'; }; - + + + diff --git a/test/test_list.js b/test/test_list.js new file mode 100644 index 0000000..438cf7d --- /dev/null +++ b/test/test_list.js @@ -0,0 +1,263 @@ +// Contains all tests in an object list +// All Object should have the properties: +// - message : The message, or title, that Tape shows when running a test +// - test : The test function, takes as arguments: +// - t : the Tape test Object +// - crel : the function to be ran as crel, replaced by a wrapper when running proxyable tests +// - checks : the number of test checks ran inside the test function +// - proxyable : true / false, can the tests crel calls be converted to a proxy format +// aka. is the crel calls first argument a tag name (and there's no need to access crels internal properties) +window.tests = [ + // -- Test element creation -- + { + message: 'Create an element with no arguments', + test: (t, crel) => { + let testElement = crel('div'); + + t.ok(testElement instanceof HTMLElement, + 'element is an instance of `HTMLElement`'); + t.equal(testElement.tagName, 'DIV', + 'element is an instance of `div`'); + }, + checks: 2, + proxyable: true + }, + { + message: 'Create an element with no arguments, using an invalid tag name', + test: (t, crel) => { + let testElement = crel('invalidtagname'); + + t.ok(testElement instanceof HTMLUnknownElement, + 'element is an instance of `HTMLUnknownElement `'); + t.equal(testElement.tagName, 'INVALIDTAGNAME', + 'element is an instance of `invalidtagname`'); + }, + checks: 2, + proxyable: true + }, + { + message: 'Crel doesn\'t modify existing elements if not instructed', + test: (t, crel) => { + let testElement = document.createElement('div'); + let testedElement = crel(testElement); + + t.ok(testElement.isSameNode(testedElement), + 'element is still the same'); + }, + checks: 1, + proxyable: false + }, + // -- Test attribute handling -- + { + message: 'Create an element with simple attributes', + test: (t, crel) => { + let testElement = crel('div', {class: 'test', id: 'test'}); + + t.equal(testElement.className, 'test', + 'element has a `test` class'); + t.equal(testElement.getAttribute('id'), 'test', + 'element has a `test` id'); + }, + checks: 2, + proxyable: true + }, + { + message: 'Add attributes to an already existing element', + test: (t, crel) => { + let testElement = document.createElement('div'); + crel(testElement, {class: 'test', id: 'test'}); + + t.equal(testElement.className, 'test', + 'element has a `test` class'); + t.equal(testElement.getAttribute('id'), 'test', + 'element has a `test` id'); + }, + checks: 2, + proxyable: false + }, + { + message: 'Modify attributes of an already existing element', + test: (t, crel) => { + let testElement = document.createElement('div'); + testElement.setAttribute('class', 'test'); + testElement.setAttribute('id', 'test'); + crel(testElement, {class: 'testier', id: 'testier'}); + + t.equal(testElement.getAttribute('class'), 'testier', + 'elements class was changed'); + t.equal(testElement.getAttribute('id'), 'testier', + 'elements id was changed'); + }, + checks: 2, + proxyable: false + }, + { + message: 'Add an `onEvent` property to an element', + test: (t, crel) => { + let testElement = crel('button', { + onclick: () => { + t.pass('onClick event triggered'); + } + }); + + testElement.click(); + }, + checks: 1, + proxyable: true + }, + { + message: 'Add an `onEvent` property to an element through attribute mapping', + test: (t, crel) => { + crel.attrMap.on = (element, value) => { + for (const eventName in value) { + element.addEventListener(eventName, value[eventName]); + } + }; + + let testElement = crel('img', { on: { + click: () => { + t.pass('onClick event triggered'); + } + }}); + + testElement.click(); + }, + checks: 1, + proxyable: false + }, + // -- Test child node handling -- + { + message: 'Create an element with a child element', + test: (t, crel) => { + let testElement = crel('div', document.createElement('p')); + + t.equal(testElement.childNodes.length, 1, + 'element has a child element'); + t.equal(testElement.childNodes[0].tagName, 'P', + 'child element is an instance of `p`'); + }, + checks: 2, + proxyable: true + }, + { + message: 'Create an element with a child text node', + test: (t, crel) => { + let testElement = crel('div', document.createTextNode('test')); + + t.equal(testElement.childNodes.length, 1, + 'element has a child element'); + t.equal(testElement.childNodes[0].nodeType, 3, + 'child element is a text node'); + }, + checks: 2, + proxyable: true + }, + { + message: 'Create an element with an array of children', + test: (t, crel) => { + // TODO: make these more compact / robust + const testArray = [document.createElement('p'), document.createTextNode('I\'m a text node!'), 'I will be a text node!']; + let testElement = crel('div', testArray); + + t.equal(testElement.childNodes.length, 3, + 'element has three children'); + t.equal(testElement.childNodes[0].tagName, 'P'); + t.equal(testElement.childNodes[1].nodeType, 3); + t.equal(testElement.childNodes[1].textContent, 'I\'m a text node!'); + t.equal(testElement.childNodes[2].nodeType, 3); + t.equal(testElement.childNodes[2].textContent, 'I will be a text node!'); + }, + checks: 6, + proxyable: true + }, + { + message: 'Create an element with a deep array of children', + test: (t, crel) => { + // TODO: make these more compact / robust + const testArray = [document.createElement('p'), document.createTextNode('I\'m a text node!'), 'I will be a text node!']; + let testElement = crel('div', [[testArray]]); + + t.equal(testElement.childNodes.length, 3, + 'element has three children'); + t.equal(testElement.childNodes[0].tagName, 'P'); + t.equal(testElement.childNodes[1].nodeType, 3); + t.equal(testElement.childNodes[1].textContent, 'I\'m a text node!'); + t.equal(testElement.childNodes[2].nodeType, 3); + t.equal(testElement.childNodes[2].textContent, 'I will be a text node!'); + }, + checks: 6, + proxyable: true + }, + // -- Test the Proxy APIs features -- + { + message: 'Test the proxy APIs tag transformations', + test: (t, crel) => { + crel.tagTransform = (key) => key.replace(/([0-9a-z])([A-Z])/g, '$1-$2').toLowerCase(); + let testElement = crel.myTable(crel.span('test')); + + t.ok(testElement.isEqualNode(crel.proxy.myTable(crel.proxy.span('test'))), + 'proxies produce the same results'); + 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'); + }, + checks: 5, + proxyable: false + }, + // -- Test exposed methods -- + { + message: 'Test `isNode` against various arguments', + test: (t, crel) => { + if (!crel.isNode) { + t.end('`isNode` is undefined'); + } + + const testInputInvalid = ['', null, undefined, 'non-empty', + 42, 4.2, {'key': 'value'}, () => { return 'hi'; }]; + + const testInputValid = [document, + document.createElement('div'), + document.createTextNode('test'), + document.createElement('invalidtagname'), + document.createElementNS('http://www.w3.org/2000/svg', 'svg'), + document.createElementNS('http://www.w3.org/1998/mathml', 'element')]; + + testInputInvalid.map(value => { + t.notOk(crel.isNode(value), '`' + value + '` is not a Node'); + }); + testInputValid.map(value => { + t.ok(crel.isNode(value), '`' + value + '` is a Node'); + }); + }, + checks: 8 + 6, // Test all values in arrays + proxyable: false + }, + { + message: 'Test `isElement` against various arguments', + test: (t, crel) => { + if (!crel.isElement) { + t.end('`isElement` is undefined'); + } + + const testInputInvalid = ['', null, undefined, 'non-empty', + 42, 4.2, {'key': 'value'}, () => { return 'hi'; }, + document.createTextNode('test'), document]; + + const testInputValid = [document.createElement('div'), + document.createElement('invalidtagname'), + document.createElementNS('http://www.w3.org/2000/svg', 'svg'), + document.createElementNS('http://www.w3.org/1998/mathml', 'element')]; + + testInputInvalid.map(value => { + t.notOk(crel.isElement(value), '`' + value + '` is not an Element'); + }); + testInputValid.map(value => { + t.ok(crel.isElement(value), '`' + value + '` is an Element'); + }); + }, + checks: 10 + 4, // Test all values in arrays + proxyable: false + } +]; diff --git a/test/test_logic.js b/test/test_logic.js new file mode 100644 index 0000000..988d307 --- /dev/null +++ b/test/test_logic.js @@ -0,0 +1,28 @@ +const test = require('tape'); +const crel = require('../crel.js'); + +let proxyableChecks = 0; +// Tests are sourced in 'test.html' before this file is +for (const value of tests) { + test(value.message, (t) => { + t.plan(value.checks); + value.test(t, crel); + }); + + if (value.proxyable) { + proxyableChecks += value.checks; + } +} + +test('Rerun all "proxy-able" tests through the Proxy API', (t) => { + t.plan(proxyableChecks); + for (const value of tests) { + if (value.proxyable) { + value.test(t, (...args) => { + let tag = args[0]; + args.shift(); + return crel.proxy[tag].apply(this, args); + }); + } + } +});