diff --git a/src/dom/attributes/BooleanAttributes.js b/src/dom/attributes/BooleanAttributes.js new file mode 100644 index 0000000..9083b6a --- /dev/null +++ b/src/dom/attributes/BooleanAttributes.js @@ -0,0 +1,33 @@ +/** + * List of boolean attributes + * These attributes should have their React attribute value set to be the same as their name + * E.g. = + * = + * = + * @type {Array} + */ +export default [ + 'allowfullScreen', + 'async', + 'autoplay', + 'capture', + 'checked', + 'controls', + 'default', + 'defer', + 'disabled', + 'formnovalidate', + 'hidden', + 'loop', + 'multiple', + 'muted', + 'novalidate', + 'open', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'itemscope' +]; diff --git a/src/dom/attributes/ReactAttributes.js b/src/dom/attributes/ReactAttributes.js new file mode 100644 index 0000000..e39d124 --- /dev/null +++ b/src/dom/attributes/ReactAttributes.js @@ -0,0 +1,158 @@ +/** + * Mapping of standard HTML attributes to their React counterparts + * List taken and reversed from react/src/renderers/dom/shared/HTMLDOMPropertyConfig.js + * https://github.com/facebook/react/blob/c9c3c339b757682f1154f1c915eb55e6a8766933/src/renderers/dom/shared/HTMLDOMPropertyConfig.js + * @type {Object} + */ +export default { + /** + * Standard Properties + */ + accept: 'accept', + 'accept-charset': 'acceptCharset', + accesskey: 'accessKey', + action: 'action', + allowfullscreen: 'allowFullScreen', + allowtransparency: 'allowTransparency', + alt: 'alt', + async: 'async', + autocomplete: 'autoComplete', + autoplay: 'autoPlay', + capture: 'capture', + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing', + charset: 'charSet', + challenge: 'challenge', + checked: 'checked', + classid: 'classID', + class: 'className', + cols: 'cols', + colspan: 'colSpan', + content: 'content', + contenteditable: 'contentEditable', + contextmenu: 'contextMenu', + controls: 'controls', + coords: 'coords', + crossorigin: 'crossOrigin', + data: 'data', + datetime: 'dateTime', + default: 'default', + defer: 'defer', + dir: 'dir', + disabled: 'disabled', + download: 'download', + draggable: 'draggable', + enctype: 'encType', + form: 'form', + formaction: 'formAction', + formenctype: 'formEncType', + formmethod: 'formMethod', + formnovalidate: 'formNoValidate', + formtarget: 'formTarget', + frameborder: 'frameBorder', + headers: 'headers', + height: 'height', + hidden: 'hidden', + high: 'high', + href: 'href', + hreflang: 'hrefLang', + for: 'htmlFor', + 'http-equiv': 'httpEquiv', + icon: 'icon', + id: 'id', + inputmode: 'inputMode', + integrity: 'integrity', + is: 'is', + keyparams: 'keyParams', + keytype: 'keyType', + kind: 'kind', + label: 'label', + lang: 'lang', + list: 'list', + loop: 'loop', + low: 'low', + manifest: 'manifest', + marginheight: 'marginHeight', + marginwidth: 'marginWidth', + max: 'max', + maxlength: 'maxLength', + media: 'media', + mediagroup: 'mediaGroup', + method: 'method', + min: 'min', + minlength: 'minLength', + multiple: 'multiple', + muted: 'muted', + name: 'name', + nonce: 'nonce', + novalidate: 'noValidate', + open: 'open', + optimum: 'optimum', + pattern: 'pattern', + placeholder: 'placeholder', + poster: 'poster', + preload: 'preload', + radiogroup: 'radioGroup', + readonly: 'readOnly', + rel: 'rel', + required: 'required', + reversed: 'reversed', + role: 'role', + rows: 'rows', + rowspan: 'rowSpan', + sandbox: 'sandbox', + scope: 'scope', + scoped: 'scoped', + scrolling: 'scrolling', + seamless: 'seamless', + selected: 'selected', + shape: 'shape', + size: 'size', + sizes: 'sizes', + span: 'span', + spellcheck: 'spellCheck', + src: 'src', + srcdoc: 'srcDoc', + srclang: 'srcLang', + srcset: 'srcSet', + start: 'start', + step: 'step', + style: 'style', + summary: 'summary', + tabindex: 'tabIndex', + target: 'target', + title: 'title', + type: 'type', + usemap: 'useMap', + value: 'value', + width: 'width', + wmode: 'wmode', + wrap: 'wrap', + /** + * RDFa Properties + */ + about: 'about', + datatype: 'datatype', + inlist: 'inlist', + prefix: 'prefix', + property: 'property', + resource: 'resource', + typeof: 'typeof', + vocab: 'vocab', + /** + * Non-standard Properties + */ + autocapitalize: 'autoCapitalize', + autocorrect: 'autoCorrect', + autosave: 'autoSave', + color: 'color', + itemprop: 'itemProp', + itemscope: 'itemScope', + itemtype: 'itemType', + itemid: 'itemID', + itemref: 'itemRef', + results: 'results', + security: 'security', + unselectable: 'unselectable', + autofocus: 'autoFocus' +}; diff --git a/src/utils/isVoidElement.js b/src/dom/elements/VoidElements.js similarity index 57% rename from src/utils/isVoidElement.js rename to src/dom/elements/VoidElements.js index 5b3fa09..39cd398 100644 --- a/src/utils/isVoidElement.js +++ b/src/dom/elements/VoidElements.js @@ -1,4 +1,9 @@ -const voidElements = [ +/** + * List of void elements + * These elements are not allowed to have children + * @type {Array} + */ +export default [ 'area', 'base', 'br', @@ -16,9 +21,3 @@ const voidElements = [ 'track', 'wbr' ]; - -export default function isVoidElement(element) { - - return voidElements.indexOf(element) >= 0; - -} diff --git a/src/elementTypes/TagElementType.js b/src/elementTypes/TagElementType.js index 6109dd9..7db95d1 100644 --- a/src/elementTypes/TagElementType.js +++ b/src/elementTypes/TagElementType.js @@ -2,7 +2,7 @@ import React from 'react'; import ProcessNodes from '../utils/ProcessNodes'; import GeneratePropsFromAttributes from '../utils/GeneratePropsFromAttributes'; import TransformTagName from '../utils/TransformTagName'; -import isVoidElement from '../utils/isVoidElement'; +import VoidElements from '../dom/elements/VoidElements'; /** * Converts any element (excluding style - see StyleElementType - and script) to a react element. @@ -21,7 +21,7 @@ export default function TagElementType(node, key) { // If the node is not a void element and has children then process them let children = null; - if (!isVoidElement(tagName)) { + if (VoidElements.indexOf(tagName) === -1) { children = ProcessNodes(node.children); } diff --git a/src/utils/HtmlAttributesToReact.js b/src/utils/HtmlAttributesToReact.js index 0ad2c13..5a4987e 100644 --- a/src/utils/HtmlAttributesToReact.js +++ b/src/utils/HtmlAttributesToReact.js @@ -1,160 +1,23 @@ +import BooleanAttributes from '../dom/attributes/BooleanAttributes'; +import ReactAttributes from '../dom/attributes/ReactAttributes'; + /** - * Mapping of standard HTML attributes to their React counterparts - * List taken and reversed from react/src/renderers/dom/shared/HTMLDOMPropertyConfig.js - * https://github.com/facebook/react/blob/c9c3c339b757682f1154f1c915eb55e6a8766933/src/renderers/dom/shared/HTMLDOMPropertyConfig.js - * @type {Object} + * Returns the parsed attribute value taking into account things like boolean attributes + * + * @param {String} attribute The name of the attribute + * @param {*} value The value of the attribute from the HTML + * @returns {*} The parsed attribute value */ -const attributeMap = { - /** - * Standard Properties - */ - accept: 'accept', - 'accept-charset': 'acceptCharset', - accesskey: 'accessKey', - action: 'action', - allowfullscreen: 'allowFullScreen', - allowtransparency: 'allowTransparency', - alt: 'alt', - async: 'async', - autocomplete: 'autoComplete', - autoplay: 'autoPlay', - capture: 'capture', - cellpadding: 'cellPadding', - cellspacing: 'cellSpacing', - charset: 'charSet', - challenge: 'challenge', - checked: 'checked', - classid: 'classID', - class: 'className', - cols: 'cols', - colspan: 'colSpan', - content: 'content', - contenteditable: 'contentEditable', - contextmenu: 'contextMenu', - controls: 'controls', - coords: 'coords', - crossorigin: 'crossOrigin', - data: 'data', - datetime: 'dateTime', - default: 'default', - defer: 'defer', - dir: 'dir', - disabled: 'disabled', - download: 'download', - draggable: 'draggable', - enctype: 'encType', - form: 'form', - formaction: 'formAction', - formenctype: 'formEncType', - formmethod: 'formMethod', - formnovalidate: 'formNoValidate', - formtarget: 'formTarget', - frameborder: 'frameBorder', - headers: 'headers', - height: 'height', - hidden: 'hidden', - high: 'high', - href: 'href', - hreflang: 'hrefLang', - for: 'htmlFor', - 'http-equiv': 'httpEquiv', - icon: 'icon', - id: 'id', - inputmode: 'inputMode', - integrity: 'integrity', - is: 'is', - keyparams: 'keyParams', - keytype: 'keyType', - kind: 'kind', - label: 'label', - lang: 'lang', - list: 'list', - loop: 'loop', - low: 'low', - manifest: 'manifest', - marginheight: 'marginHeight', - marginwidth: 'marginWidth', - max: 'max', - maxlength: 'maxLength', - media: 'media', - mediagroup: 'mediaGroup', - method: 'method', - min: 'min', - minlength: 'minLength', - multiple: 'multiple', - muted: 'muted', - name: 'name', - nonce: 'nonce', - novalidate: 'noValidate', - open: 'open', - optimum: 'optimum', - pattern: 'pattern', - placeholder: 'placeholder', - poster: 'poster', - preload: 'preload', - radiogroup: 'radioGroup', - readonly: 'readOnly', - rel: 'rel', - required: 'required', - reversed: 'reversed', - role: 'role', - rows: 'rows', - rowspan: 'rowSpan', - sandbox: 'sandbox', - scope: 'scope', - scoped: 'scoped', - scrolling: 'scrolling', - seamless: 'seamless', - selected: 'selected', - shape: 'shape', - size: 'size', - sizes: 'sizes', - span: 'span', - spellcheck: 'spellCheck', - src: 'src', - srcdoc: 'srcDoc', - srclang: 'srcLang', - srcset: 'srcSet', - start: 'start', - step: 'step', - style: 'style', - summary: 'summary', - tabindex: 'tabIndex', - target: 'target', - title: 'title', - type: 'type', - usemap: 'useMap', - value: 'value', - width: 'width', - wmode: 'wmode', - wrap: 'wrap', - /** - * RDFa Properties - */ - about: 'about', - datatype: 'datatype', - inlist: 'inlist', - prefix: 'prefix', - property: 'property', - resource: 'resource', - typeof: 'typeof', - vocab: 'vocab', - /** - * Non-standard Properties - */ - autocapitalize: 'autoCapitalize', - autocorrect: 'autoCorrect', - autosave: 'autoSave', - color: 'color', - itemprop: 'itemProp', - itemscope: 'itemScope', - itemtype: 'itemType', - itemid: 'itemID', - itemref: 'itemRef', - results: 'results', - security: 'security', - unselectable: 'unselectable', - autofocus: 'autoFocus' +const getParsedAttributeValue = function(attribute, value) { + + // if the attribute if a boolean then it's value should be the same as it's name + // e.g. disabled="disabled" + if (BooleanAttributes.indexOf(attribute) >= 0) { + value = attribute; + } + + return value; + }; /** @@ -170,11 +33,18 @@ export default function HtmlAttributesToReact(attributes) { .keys(attributes) .reduce( (mappedAttributes, attribute) => { + // lowercase the attribute name and find it in the react attribute map const lowerCaseAttribute = attribute.toLowerCase(); - const key = attributeMap[lowerCaseAttribute] || lowerCaseAttribute; - mappedAttributes[key] = attributes[attribute]; + + // format the attribute name + const name = ReactAttributes[lowerCaseAttribute] || lowerCaseAttribute; + + // add the parsed attribute value to the mapped attributes + mappedAttributes[name] = getParsedAttributeValue(name, attributes[attribute]); + return mappedAttributes; + }, {} ); diff --git a/test/integration/integration.spec.js b/test/integration/integration.spec.js index 29aa92b..40f6ced 100644 --- a/test/integration/integration.spec.js +++ b/test/integration/integration.spec.js @@ -77,4 +77,10 @@ describe('Integration tests: ', () => { test('

test

', '

test

'); }); + it('should convert boolean attribute values', () => { + test('', ''); + test('', ''); + test('', ''); + }); + }); diff --git a/test/unit/elementTypes/TagElementType.spec.js b/test/unit/elementTypes/TagElementType.spec.js index ed21742..f706438 100644 --- a/test/unit/elementTypes/TagElementType.spec.js +++ b/test/unit/elementTypes/TagElementType.spec.js @@ -1,11 +1,11 @@ const GeneratePropsFromAttributes = jasmine.createSpy('GeneratePropsFromAttributes').and.callFake(attrs => attrs); const ProcessNodes = jasmine.createSpy('ProcessNodes').and.returnValue('children'); -const isVoidElement = jasmine.createSpy('isVoidElement').and.returnValue(false); +const VoidElements = ['void']; const TagElementType = require('inject!elementTypes/TagElementType')({ '../utils/GeneratePropsFromAttributes': GeneratePropsFromAttributes, '../utils/ProcessNodes': ProcessNodes, - '../utils/isVoidElement': isVoidElement + '../dom/elements/VoidElements': VoidElements }).default; describe('Testing `elementTypes/TagElementType', () => { @@ -13,7 +13,6 @@ describe('Testing `elementTypes/TagElementType', () => { beforeEach(() => { GeneratePropsFromAttributes.calls.reset(); ProcessNodes.calls.reset(); - isVoidElement.calls.reset(); }); it('should return a React element corresponding to the node name', () => { @@ -45,7 +44,6 @@ describe('Testing `elementTypes/TagElementType', () => { }, children: 'child' }; - isVoidElement.and.returnValue(true); const voidElement = TagElementType(voidNode, 'key'); expect(voidElement.type).toBe('void'); diff --git a/test/unit/utils/HtmlAttributesToReact.spec.js b/test/unit/utils/HtmlAttributesToReact.spec.js index c3dd310..db074f6 100644 --- a/test/unit/utils/HtmlAttributesToReact.spec.js +++ b/test/unit/utils/HtmlAttributesToReact.spec.js @@ -19,7 +19,10 @@ describe('Testing `utils/HtmlAttributesToReact`', () => { 'aria-role': 'role', // it should also use non specified attributes (although react will filter these out) testattribute: 'testAttribute', - 'UPPER-CASE-TEST-ATTRIBUTE': 'upperTestAttribute' + 'UPPER-CASE-TEST-ATTRIBUTE': 'upperTestAttribute', + // boolean attributes + disabled: '', + checked: '' }; const expectedReactAttributes = { @@ -32,7 +35,9 @@ describe('Testing `utils/HtmlAttributesToReact`', () => { 'data-test': 'test', 'aria-role': 'role', testattribute: 'testAttribute', - 'upper-case-test-attribute': 'upperTestAttribute' + 'upper-case-test-attribute': 'upperTestAttribute', + disabled: 'disabled', + checked: 'checked' }; expect(HtmlAttributesToReact(htmlAttributes)).toEqual(expectedReactAttributes); diff --git a/test/unit/utils/isVoidElement.spec.js b/test/unit/utils/isVoidElement.spec.js deleted file mode 100644 index 4dda1cb..0000000 --- a/test/unit/utils/isVoidElement.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -import isVoidElement from 'utils/isVoidElement'; - -describe('Testing `utils/isVoidElement`', () => { - - it('should return whether the element is a void element', () => { - expect(isVoidElement('img')).toBe(true); - expect(isVoidElement('br')).toBe(true); - expect(isVoidElement('div')).toBe(false); - expect(isVoidElement('p')).toBe(false); - }); - -});