diff --git a/packages/moon/dist/moon.js b/packages/moon/dist/moon.js index 36827683..71b24f84 100644 --- a/packages/moon/dist/moon.js +++ b/packages/moon/dist/moon.js @@ -118,6 +118,7 @@ * Escape text to make it usable in a JavaScript string literal. * * @param {string} text + * @returns {string} Escaped text */ function escapeText(text) { @@ -125,10 +126,37 @@ return escapeTextMap[match]; }); } + /** + * Normalize an attribute key to a DOM property. + * + * Moon attribute keys should follow camelCase by convention instead of using + * standard HTML attribute keys. However, standard HTML attributes are + * supported. They should typically be used for custom attributes, data-* + * attributes, or aria-* attributes. + * + * @param {string} key + * @returns {string} Normalized key + */ + + + function normalizeAttributeKey(key) { + switch (key) { + case "class": + return "className"; + + case "for": + return "htmlFor"; + + default: + // Other keys should ideally be camelCased. + return key; + } + } /** * Scope an expression to use variables within the `data` object. * * @param {string} expression + * @returns {Object} Scoped expression and static status */ @@ -297,7 +325,7 @@ while ((attributeExec = attributeRE.exec(attributesText)) !== null) { // Store the match and captured groups. var attributeMatch = attributeExec[0]; - var attributeKey = attributeExec[1]; + var attributeKey = normalizeAttributeKey(attributeExec[1]); var attributeValue = attributeExec[2]; var attributeExpression = attributeExec[3]; @@ -981,7 +1009,11 @@ info[0](event, info[1]); }); } else if (key !== "children" && value !== false) { - element.setAttribute(key, value); + if (key in element) { + element[key] = value; + } else { + element.setAttribute(key, value); + } } }; @@ -1209,6 +1241,8 @@ // otherwise. if (value === false) { nodeOldElement.removeAttribute(key); + } else if (key in nodeOldElement) { + nodeOldElement[key] = value; } else { nodeOldElement.setAttribute(key, value); } diff --git a/packages/moon/dist/moon.min.js b/packages/moon/dist/moon.min.js index 0f34ddf2..8c60e8a3 100644 --- a/packages/moon/dist/moon.min.js +++ b/packages/moon/dist/moon.min.js @@ -4,4 +4,4 @@ * Released under the MIT License * https://kbrsh.github.io/moon */ -!function(e,n){"undefined"==typeof module?e.Moon=n():module.exports=n()}(this,function(){"use strict";var b={element:0,text:1,component:2};var y,A=/^\s+$/,S=/<([\w\d-_]+)([^>]*?)(\/?)>/g,k=/([\w\d-_:@]*)(?:=(?:("[^"]*"|'[^']*')|{([^{}]*)}))?/g,n=/"[^"]*"|'[^']*'|\d+[a-zA-Z$_]\w*|\.[a-zA-Z$_]\w*|[a-zA-Z$_]\w*:|([a-zA-Z$_]\w*)/g,C=/&|>|<| |"|\\|"|\n|\r/g,r=["NaN","false","in","null","this","true","typeof","undefined","window"],q={"&":"&",">":">","<":"<"," ":" ",""":'\\"',"\\":"\\\\",'"':'\\"',"\n":"\\n","\r":"\\r"};function P(e){var t=!0;return{value:e.replace(n,function(e,n){return void 0===n||-1!==r.indexOf(n)?e:(t=!1,"$"===n[0]?n:"data."+n)}),isStatic:t}}function t(e){e=e.trim();for(var n,t=[],r=0;r",r+2),u=e.slice(r+2,i);0,t.push({type:"tagClose",value:u}),r=i+1;continue}if("!"===o&&"-"===e[r+2]&&"-"===e[r+3]){var l=e.indexOf("--\x3e",r+4);0,r=l+3;continue}S.lastIndex=r;var d=S.exec(e);0;for(var f=d[0],s=d[1],v=d[2],p=d[3],c={},h=void 0;null!==(h=k.exec(v));){var m=h[0],g=h[1],w=h[2],b=h[3];0===m.length?k.lastIndex+=1:(c[g]=void 0===b?{value:void 0===w?'""':w,isStatic:!0}:P(b),"@"===g[0]&&(c[g].value="["+c[g].value+",data]"))}t.push({type:"tagOpen",value:s,attributes:c,closed:"/"===p}),r+=f.length}else if("{"===a){var y="";for(r+=1;r]*?)(\/?)>/g,k=/([\w\d-_:@]*)(?:=(?:("[^"]*"|'[^']*')|{([^{}]*)}))?/g,n=/"[^"]*"|'[^']*'|\d+[a-zA-Z$_]\w*|\.[a-zA-Z$_]\w*|[a-zA-Z$_]\w*:|([a-zA-Z$_]\w*)/g,C=/&|>|<| |"|\\|"|\n|\r/g,r=["NaN","false","in","null","this","true","typeof","undefined","window"],q={"&":"&",">":">","<":"<"," ":" ",""":'\\"',"\\":"\\\\",'"':'\\"',"\n":"\\n","\r":"\\r"};function P(e){switch(e){case"class":return"className";case"for":return"htmlFor";default:return e}}function F(e){var t=!0;return{value:e.replace(n,function(e,n){return void 0===n||-1!==r.indexOf(n)?e:(t=!1,"$"===n[0]?n:"data."+n)}),isStatic:t}}function t(e){e=e.trim();for(var n,t=[],r=0;r",r+2),u=e.slice(r+2,i);0,t.push({type:"tagClose",value:u}),r=i+1;continue}if("!"===o&&"-"===e[r+2]&&"-"===e[r+3]){var l=e.indexOf("--\x3e",r+4);0,r=l+3;continue}S.lastIndex=r;var d=S.exec(e);0;for(var s=d[0],f=d[1],c=d[2],v=d[3],p={},h=void 0;null!==(h=k.exec(c));){var m=h[0],g=P(h[1]),w=h[2],b=h[3];0===m.length?k.lastIndex+=1:(p[g]=void 0===b?{value:void 0===w?'""':w,isStatic:!0}:F(b),"@"===g[0]&&(p[g].value="["+p[g].value+",data]"))}t.push({type:"tagOpen",value:f,attributes:p,closed:"/"===v}),r+=s.length}else if("{"===a){var y="";for(r+=1;r escapeTextMap[match]); } +/** + * Normalize an attribute key to a DOM property. + * + * Moon attribute keys should follow camelCase by convention instead of using + * standard HTML attribute keys. However, standard HTML attributes are + * supported. They should typically be used for custom attributes, data-* + * attributes, or aria-* attributes. + * + * @param {string} key + * @returns {string} Normalized key + */ +function normalizeAttributeKey(key) { + switch (key) { + case "class": + return "className"; + case "for": + return "htmlFor"; + default: + // Other keys should ideally be camelCased. + return key; + } +} + /** * Scope an expression to use variables within the `data` object. * * @param {string} expression + * @returns {Object} Scoped expression and static status */ function scopeExpression(expression) { let isStatic = true; @@ -244,7 +269,7 @@ export function lex(input) { ) { // Store the match and captured groups. const attributeMatch = attributeExec[0]; - const attributeKey = attributeExec[1]; + const attributeKey = normalizeAttributeKey(attributeExec[1]); const attributeValue = attributeExec[2]; const attributeExpression = attributeExec[3]; diff --git a/packages/moon/src/executor/executor.js b/packages/moon/src/executor/executor.js index a376a960..22865e5a 100644 --- a/packages/moon/src/executor/executor.js +++ b/packages/moon/src/executor/executor.js @@ -56,7 +56,11 @@ function executeCreate(node) { info[0](event, info[1]); }); } else if (key !== "children" && value !== false) { - element.setAttribute(key, value); + if (key in element) { + element[key] = value; + } else { + element.setAttribute(key, value); + } } } @@ -288,6 +292,8 @@ function executePatch(patches) { // otherwise. if (value === false) { nodeOldElement.removeAttribute(key); + } else if (key in nodeOldElement) { + nodeOldElement[key] = value; } else { nodeOldElement.setAttribute(key, value); } diff --git a/packages/moon/test/compiler/lexer.test.js b/packages/moon/test/compiler/lexer.test.js index 861596e5..4db65b6e 100644 --- a/packages/moon/test/compiler/lexer.test.js +++ b/packages/moon/test/compiler/lexer.test.js @@ -47,11 +47,11 @@ test("lex expression", () => { }); test("lex attributes", () => { - expect(lex(`
`)).toEqual([{"attributes": {"id": {"value": "\"test-id\"", "isStatic": true}, "class": {"value": "'test-class'", "isStatic": true}, "dynamic": {"value": "true", "isStatic": true}, "local": {"value": "$local", "isStatic": false}, "self": {"value": "\"\"", "isStatic": true}}, "closed": false, "type": "tagOpen", "value": "div"}]); + expect(lex(`
`)).toEqual([{"attributes": {"id": {"value": "\"test-id\"", "isStatic": true}, "className": {"value": "'test-class'", "isStatic": true}, "htmlFor": {"value": "'input'", "isStatic": true}, "dynamic": {"value": "true", "isStatic": true}, "local": {"value": "$local", "isStatic": false}, "self": {"value": "\"\"", "isStatic": true}}, "closed": false, "type": "tagOpen", "value": "div"}]); }); test("lex events", () => { - expect(lex(`
`)).toEqual([{"attributes": {"id": {"value": "\"test-id\"", "isStatic": true}, "class": {"value": "'test-class'", "isStatic": true}, dynamic: {"value": "true", "isStatic": true}, "@event": {"value": "[data.doSomething,data]", "isStatic": false}, self: {"value": "\"\"", "isStatic": true}}, "closed": false, "type": "tagOpen", "value": "div"}]); + expect(lex(`
`)).toEqual([{"attributes": {"id": {"value": "\"test-id\"", "isStatic": true}, "className": {"value": "'test-class'", "isStatic": true}, dynamic: {"value": "true", "isStatic": true}, "@event": {"value": "[data.doSomething,data]", "isStatic": false}, self: {"value": "\"\"", "isStatic": true}}, "closed": false, "type": "tagOpen", "value": "div"}]); }); test("lex comments", () => { @@ -64,7 +64,7 @@ test("opening tag token to string", () => { }); test("opening tag token with attributes to string", () => { - const input = `
`; + const input = `
`; expect(tokenString(lex(input)[0])).toBe(input.replace("dynamic", "data.dynamic")); }); @@ -74,7 +74,7 @@ test("self-closing tag token to string", () => { }); test("self-closing tag token with attributes to string", () => { - const input = ``; + const input = ``; expect(tokenString(lex(input)[0])).toBe(input.replace("dynamic", "data.dynamic")); });