Skip to content

Commit

Permalink
feat(template-compiler): support leading hyphen in attr name (#1687)
Browse files Browse the repository at this point in the history
* feat(template-compiler): support leading hyphen in attr name
  • Loading branch information
apapko authored Jan 22, 2020
1 parent 0baa228 commit 242f0fb
Show file tree
Hide file tree
Showing 17 changed files with 114 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -465,10 +465,10 @@ export const ParserDiagnostics = {
url: '',
},

ATTRIBUTE_NAME_MUST_START_WITH_ALPHABETIC_CHARACTER: {
ATTRIBUTE_NAME_MUST_START_WITH_ALPHABETIC_OR_HYPHEN_CHARACTER: {
code: 1124,
message:
'{0} is not valid attribute for {1}. Attribute name must start with alphabetic character.',
'{0} is not valid attribute for {1}. Attribute name must start with alphabetic character or a hyphen.',
level: DiagnosticLevel.Error,
url: '',
},
Expand Down
1 change: 0 additions & 1 deletion packages/@lwc/template-compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@babel/types": "~7.1.5",
"@lwc/errors": "1.1.16",
"@lwc/shared": "1.1.16",
"camelcase": "~5.0.0",
"esutils": "^2.0.2",
"he": "^1.1.1",
"parse5-with-errors": "4.0.3-beta1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<x-button
-class="r"
-data-xx="foo"
-aria-hidden="hidden"
-role="xx"
-foo-bar="x"
--foo-zaz="z"
-foo_bar_baz="baz"
></x-button>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import _xButton from 'x/button';
import { registerTemplate } from 'lwc';

function tmpl($api, $cmp, $slotset, $ctx) {
const { c: api_custom_element } = $api;
return [
api_custom_element(
'x-button',
_xButton,
{
props: {
Class: 'r',
DataXx: 'foo',
AriaHidden: 'hidden',
Role: 'xx',
FooBar: 'x',
FooZaz: 'z',
Foo_bar_baz: 'baz'
},
key: 0
},
[]

),
];
}

export default registerTemplate(tmpl);
tmpl.stylesheets = [];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"warnings": []
}
4 changes: 2 additions & 2 deletions packages/@lwc/template-compiler/src/__tests__/parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ describe('props and attributes', () => {
expect(warnings[0]).toMatchObject({
level: DiagnosticLevel.Error,
message:
'LWC1124: _leading is not valid attribute for x-button. Attribute name must start with alphabetic character.',
'LWC1124: _leading is not valid attribute for x-button. Attribute name must start with alphabetic character or a hyphen.',
});
});

Expand All @@ -635,7 +635,7 @@ describe('props and attributes', () => {
expect(warnings[0]).toMatchObject({
level: DiagnosticLevel.Error,
message:
'LWC1124: 2_under is not valid attribute for x-button. Attribute name must start with alphabetic character.',
'LWC1124: 2_under is not valid attribute for x-button. Attribute name must start with alphabetic character or a hyphen.',
});
});

Expand Down
5 changes: 3 additions & 2 deletions packages/@lwc/template-compiler/src/codegen/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import * as babylon from '@babel/parser';
import * as t from '@babel/types';
import * as esutils from 'esutils';
import toCamelCase from 'camelcase';
import { isUndefined } from 'util';

import { toPropertyName } from '../shared/utils';

type RenderPrimitive =
| 'iterator'
| 'flatten'
Expand Down Expand Up @@ -213,7 +214,7 @@ export default class CodeGen {
}

private _toValidIdentifier(name: string) {
return esutils.keyword.isIdentifierES6(name) ? name : toCamelCase(name);
return esutils.keyword.isIdentifierES6(name) ? name : toPropertyName(name);
}

private _renderApiCall(
Expand Down
4 changes: 2 additions & 2 deletions packages/@lwc/template-compiler/src/codegen/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import * as t from '@babel/types';
import toCamelCase from 'camelcase';
import { toPropertyName } from '../shared/utils';

import State from '../state';
import { isElement, isComponentProp } from '../shared/ir';
Expand All @@ -15,7 +15,7 @@ import { kebabcaseToCamelcase } from '../shared/naming';
import CodeGen from './codegen';

export function identifierFromComponentName(name: string): t.Identifier {
return t.identifier(`_${toCamelCase(name)}`);
return t.identifier(`_${toPropertyName(name)}`);
}

export { kebabcaseToCamelcase };
Expand Down
15 changes: 5 additions & 10 deletions packages/@lwc/template-compiler/src/parser/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import * as parse5 from 'parse5-with-errors';
import camelcase from 'camelcase';

import { ParserDiagnostics, generateCompilerError } from '@lwc/errors';

import { toPropertyName } from '../shared/utils';

import {
EXPRESSION_SYMBOL_END,
EXPRESSION_SYMBOL_START,
Expand Down Expand Up @@ -289,13 +289,8 @@ function shouldCamelCaseAttribute(element: IRElement, attrName: string) {
}

export function attributeToPropertyName(element: IRElement, attrName: string): string {
let propName = attrName;
if (shouldCamelCaseAttribute(element, attrName)) {
const attrToSplit = ATTRS_PROPS_TRANFORMS[propName] || propName;
propName = attrToSplit
.split('_')
.map(camelcase)
.join('_');
if (!shouldCamelCaseAttribute) {
return attrName;
}
return propName;
return toPropertyName(ATTRS_PROPS_TRANFORMS[attrName] || attrName);
}
12 changes: 5 additions & 7 deletions packages/@lwc/template-compiler/src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,6 @@ export default function parse(source: string, state: State): TemplateParseResult
warnAt(ParserDiagnostics.INVALID_HTML_ATTRIBUTE, [name, tag], location);
}

// disallow attr name that ends with a special character.
if (name.match(/[^a-z0-9]$/)) {
const node = element.__original as parse5.AST.Default.Element;
warnAt(ParserDiagnostics.ATTRIBUTE_NAME_MUST_END_WITH_ALPHA_NUMERIC_CHARACTER, [
Expand All @@ -771,13 +770,12 @@ export default function parse(source: string, state: State): TemplateParseResult
return;
}

// disallow attr name that doesn't start with an alphabetic character.
if (name.match(/^[^a-z]/)) {
if (!/^-*[a-z]/.test(name)) {
const node = element.__original as parse5.AST.Default.Element;
warnAt(ParserDiagnostics.ATTRIBUTE_NAME_MUST_START_WITH_ALPHABETIC_CHARACTER, [
name,
treeAdapter.getTagName(node),
]);
warnAt(
ParserDiagnostics.ATTRIBUTE_NAME_MUST_START_WITH_ALPHABETIC_OR_HYPHEN_CHARACTER,
[name, treeAdapter.getTagName(node)]
);
return;
}

Expand Down
23 changes: 23 additions & 0 deletions packages/@lwc/template-compiler/src/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
export function toPropertyName(attr: string) {
let prop = '';
let shouldUpperCaseNext = false;

for (let i = 0; i < attr.length; i++) {
const char = attr.charAt(i);

if (char === '-') {
shouldUpperCaseNext = true;
} else {
prop += shouldUpperCaseNext ? char.toUpperCase() : char;
shouldUpperCaseNext = false;
}
}

return prop;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
{Upper}
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement, api } from 'lwc';

export default class UppercaseCharacterPublicPropChild extends LightningElement {
@api Upper = 'default value';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<attrs-uppercase-child -upper="uppercase value from parent component"></attrs-uppercase-child>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class UppercaseCharacterPublicPropParent extends LightningElement {}
14 changes: 14 additions & 0 deletions packages/integration-karma/test/template/properties/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import RadioButton from 'misc/radioButton';
import InputValue from 'live/inputValue';
import InputChecked from 'live/inputChecked';
import SpecialCharacterPublicProp from 'attrs/specialCharacter';
import UppercaseCharacterPublicPropParent from 'attrs/uppercaseParent';

describe('live properties', () => {
describe('input [checked] property', () => {
Expand Down Expand Up @@ -68,6 +69,19 @@ describe('custom properties', () => {
);
});
});

it('should allow attribute name with a leading uppercase character', () => {
const elm = createElement('attrs-uppercase-parent', {
is: UppercaseCharacterPublicPropParent,
});
document.body.appendChild(elm);

return Promise.resolve().then(() => {
expect(elm.shadowRoot.querySelector('attrs-uppercase-child').Upper).toEqual(
'uppercase value from parent component'
);
});
});
});

describe('regression', () => {
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3902,7 +3902,7 @@ camelcase@^4.1.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=

camelcase@^5.0.0, camelcase@~5.0.0:
camelcase@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
Expand Down

0 comments on commit 242f0fb

Please sign in to comment.