Skip to content

Commit

Permalink
fix(html): add fallback for camel-cased property name
Browse files Browse the repository at this point in the history
  • Loading branch information
smalluban committed Apr 16, 2024
1 parent 07b9334 commit bdd8716
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 31 deletions.
25 changes: 4 additions & 21 deletions docs/component-model/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,15 @@ const update = ({ radius }) => html`
## Properties & Attributes

```javascript
html`<div propertyName="${value}"></div>`;
// el.propertyName = value, el.otherProperty = value
html`<my-element propertyName="${value}" other-property="${value}"></my-element>`;
```

Expression in the attribute set corresponding property of an element instance. Even though attributes are not case-sensitive, the template engine uses the exact name defined in the template.

### Attribute Fallback

If the property is not found in the prototype of an element, it fallbacks to the attribute value (the attribute name is not translated from camel-case to dash or in any other way). If your template contains a custom element, which only supports attributes, you can use the original name:

```javascript
html`<external-calender start-date="${myDate}"></external-calender>`
```

Custom elements defined with the library support both camel-case property and dashed attribute. However, the best option is to use the original property name, if you want to pass dynamic data to the element. On another hand, if you have a fully static value, you can set dashed attribute in the template content:

```javascript
// Attribute: static value
html`<my-calendar start-date="2020-01-01"></my-calendar>`;

// Property: the only way to create dynamically changing value
html`<my-calendar startDate=${dynamicDate}></my-calendar>`;
```
Expression as the element's attribute content set corresponding existing case-sensitive property, translated camel-cased property or fallbacks to the attribute value if property is not found. This behavior maximizes compatibility with custom elements created outside of the library.

### Mixed Values

If the attribute value contains additional characters or multiple expressions, the engine fallbacks to the attribute value with concatenated characters. It has precedence even over the special cases described below.
If the attribute value contains additional characters or multiple expressions, the attribute is always used with concatenated string value. It has precedence even over the special cases described below.

```javascript
html`<div id="el" class="button ${buttonType} ${buttonColor}"></div>`
Expand Down
40 changes: 30 additions & 10 deletions src/template/resolvers/property.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import resolveEventListener from "./event.js";
import resolveClassList from "./class.js";
import resolveStyleList from "./style.js";

function updateAttr(target, attrName, value) {
if (value === false || value === undefined || value === null) {
target.removeAttribute(attrName);
} else {
const attrValue = value === true ? "" : String(value);
target.setAttribute(attrName, attrValue);
}
}

export default function resolveProperty(attrName, propertyName, isSVG) {
if (propertyName.substr(0, 2) === "on") {
const eventType = propertyName.substr(2);
Expand All @@ -14,20 +23,31 @@ export default function resolveProperty(attrName, propertyName, isSVG) {
case "style":
return resolveStyleList;
default: {
let isProp = false;
if (isSVG) {
return (host, target, value) => {
updateAttr(target, attrName, value);
};
}

let isProp = undefined;
return (host, target, value) => {
isProp =
isProp ||
(!isSVG &&
!(target instanceof globalThis.SVGElement) &&
propertyName in target);
if (isProp === undefined) {
isProp = target.tagName !== "svg";
if (isProp) {
isProp = propertyName in target;
if (!isProp) {
propertyName = attrName.replace(/-./g, (match) =>
match[1].toUpperCase(),
);
isProp = propertyName in target;
}
}
}

if (isProp) {
target[propertyName] = value;
} else if (value === false || value === undefined || value === null) {
target.removeAttribute(attrName);
} else {
const attrValue = value === true ? "" : String(value);
target.setAttribute(attrName, attrValue);
updateAttr(target, attrName, value);
}
};
}
Expand Down
16 changes: 16 additions & 0 deletions test/spec/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ describe("html:", () => {

expect(fragment.children[0].customProperty).toBe(1);
});

it("sets property using dashed name", () => {
define({
tag: "test-html-property",
customProperty: 0,
});

const render = html`
<test-html-property title=${""} custom-property=${1}>
</test-html-property>
`;

render(fragment);

expect(fragment.children[0].customProperty).toBe(1);
});
});

describe("class expression attribute", () => {
Expand Down

0 comments on commit bdd8716

Please sign in to comment.