Skip to content

Commit 9e7c3de

Browse files
committed
Merge branch 'issue-16-lit-style-attributes'
2 parents 6729c80 + ac19a4f commit 9e7c3de

File tree

4 files changed

+158
-13
lines changed

4 files changed

+158
-13
lines changed

README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ svelteRetag({
5252
tagname: 'hello-world',
5353

5454
// Optional:
55-
attributes: ['greeting', 'name'],
55+
attributes: ['greetperson'],
5656
shadow: false,
5757
href: '/your/stylesheet.css', // Only necessary if shadow is true
5858
});
@@ -62,19 +62,23 @@ Now anywhere you use the `<hello-world>` tag, you'll get a Svelte component. Not
6262
name
6363
to [anything containing a dash](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements).
6464

65+
To align with future versions of Svelte, attributes are automatically converted to lowercase (following
66+
the [Lit-style naming convention](https://lit.dev/docs/components/properties/#observed-attributes)). So, `greetPerson`
67+
on your component would be automatically made available as `greetperson` on your custom element.
68+
6569
```html
66-
<hello-world name="Cris"></hello-world>
70+
<hello-world greetperson="Cris"></hello-world>
6771
```
6872

6973
### Options
7074

71-
| Option | Default | Description |
72-
|------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
73-
| component | _(required)_ | The constructor for your Svelte component (from `import`) |
74-
| tagname | _(required)_ | The custom element tag name to use ([must contain a dash](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)) |
75-
| attributes | `[]` | array - List of attributes to reactively forward to your component (does not reflect changes inside the component) |
76-
| shadow | `false` | boolean - Should this component use shadow DOM.<br/> **Note:** Only basic support for shadow DOM is currently provided. See https://github.com/patricknelson/svelte-retag/issues/6. |
77-
| href | `''` | link to your stylesheet - Allows you to ensure your styles are included in the shadow DOM (thus only required when `shadow` is set to `true`). |
75+
| Option | Default | Description |
76+
|------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
77+
| component | _(required)_ | The constructor for your Svelte component (from `import`) |
78+
| tagname | _(required)_ | The custom element tag name to use ([must contain a dash](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)) |
79+
| attributes | `[]` | array - List of attributes to reactively forward to your component (does not reflect changes inside the component). <br> **Important:** Attributes must be the lowercase version of your Svelte component props ([similar to Lit](https://lit.dev/docs/components/properties/#observed-attributes)). |
80+
| shadow | `false` | boolean - Should this component use shadow DOM.<br/> **Note:** Only basic support for shadow DOM is currently provided. See https://github.com/patricknelson/svelte-retag/issues/6. |
81+
| href | `''` | link to your stylesheet - Allows you to ensure your styles are included in the shadow DOM (thus only required when `shadow` is set to `true`). |
7882

7983
**Note:** For portability, `svelte-retag`'s API is fully backward compatible
8084
with [`svelte-tag@^1.0.0`](https://github.com/crisward/svelte-tag).
@@ -88,7 +92,7 @@ On the immediate horizon:
8892
- [x] Fix nested slot support (https://github.com/patricknelson/svelte-retag/pull/5)
8993
- [x] Better support for slots during early execution of IIFE compiled packages, i.e. use `MutationObserver` to watch
9094
for light DOM slots during initial parsing (see https://github.com/patricknelson/svelte-retag/issues/7)
91-
- [ ] Support Lit-style lowercase props (see https://github.com/crisward/svelte-tag/issues/16)
95+
- [x] Support Lit-style lowercase props (see https://github.com/crisward/svelte-tag/issues/16)
9296
- [ ] Lower priority: Support context (see https://github.com/crisward/svelte-tag/issues/8)
9397

9498
Milestones:

index.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ export default function(opts) {
3939
});
4040
}
4141

42+
// Inspect the component early on to get its available properties (statically available).
43+
const propInstance = new opts.component({ target: document.createElement('div') });
44+
const propMap = new Map();
45+
for(let key of Object.keys(propInstance.$$.props)) {
46+
propMap.set(key.toLowerCase(), key);
47+
}
48+
propInstance.$destroy();
49+
50+
4251
/**
4352
* Defines the actual custom element responsible for rendering the provided Svelte component.
4453
*/
@@ -157,8 +166,27 @@ export default function(opts) {
157166
attributeChangedCallback(name, oldValue, newValue) {
158167
this._debug('attributes changed', { name, oldValue, newValue });
159168

169+
// If instance already available, pass it through immediately.
160170
if (this.componentInstance && newValue !== oldValue) {
161-
this.componentInstance.$set({ [name]: newValue });
171+
let translatedName = this._translateAttribute(name);
172+
this.componentInstance.$set({ [translatedName]: newValue });
173+
}
174+
}
175+
176+
/**
177+
* Converts the provided lowercase attribute name to the correct case-sensitive component prop name, if possible.
178+
*
179+
* @param {string} attributeName
180+
* @returns {string}
181+
*/
182+
_translateAttribute(attributeName) {
183+
// In the unlikely scenario that a browser somewhere doesn't do this for us (or maybe we're in a quirks mode or something...)
184+
attributeName = attributeName.toLowerCase();
185+
if (propMap.has(attributeName)) {
186+
return propMap.get(attributeName);
187+
} else {
188+
this._debug(`_translateAttribute(): ${attributeName} not found`);
189+
return attributeName;
162190
}
163191
}
164192

@@ -195,8 +223,9 @@ export default function(opts) {
195223
};
196224

197225
// Populate custom element attributes into the props object.
198-
// TODO: Inspect component and normalize to lowercase for Lit-style props (https://github.com/crisward/svelte-tag/issues/16)
199-
Array.from(this.attributes).forEach(attr => props[attr.name] = attr.value);
226+
for(let attr of [...this.attributes]) {
227+
props[this._translateAttribute(attr.name)] = attr.value
228+
}
200229

201230
// Instantiate component into our root now, which is either the "light DOM" (i.e. directly under this element) or
202231
// in the shadow DOM.

tests/TestAttributes.svelte

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
export let lowercase = 'default';
3+
export let camelCase = 'default';
4+
export let UPPERCASE = 'default';
5+
</script>
6+
7+
{#if lowercase}
8+
<div>lowercase: {lowercase}</div>
9+
{/if}
10+
11+
{#if camelCase}
12+
<div>camelCase: {camelCase}</div>
13+
{/if}
14+
15+
{#if UPPERCASE}
16+
<div>UPPERCASE: {UPPERCASE}</div>
17+
{/if}
18+

tests/TestAttributes.test.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, beforeAll, afterEach, test, expect } from 'vitest';
2+
import TestAttributes from './TestAttributes.svelte';
3+
import svelteRetag from '../index';
4+
import { normalizeWhitespace } from './test-utils.js';
5+
6+
let el = null;
7+
8+
describe('Test custom element attributes', () => {
9+
beforeAll(() => {
10+
svelteRetag({ component: TestAttributes, tagname: 'test-attribs', shadow: false });
11+
});
12+
13+
afterEach(() => {
14+
if (el) {
15+
el.remove();
16+
}
17+
});
18+
19+
let allSetOutput = `
20+
<test-attribs lowercase="SET" camelcase="SET" uppercase="SET">
21+
<svelte-retag>
22+
<div>lowercase: SET</div>
23+
<div>camelCase: SET</div>
24+
<div>UPPERCASE: SET</div>
25+
<!--<TestAttributes>-->
26+
</svelte-retag>
27+
</test-attribs>
28+
`;
29+
30+
test('all: lowercase attributes', () => {
31+
32+
el = document.createElement('div');
33+
el.innerHTML = '<test-attribs lowercase="SET" camelcase="SET" uppercase="SET"></test-attribs>';
34+
document.body.appendChild(el);
35+
36+
expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(allSetOutput));
37+
});
38+
39+
test('all: uppercase attributes', () => {
40+
41+
el = document.createElement('div');
42+
el.innerHTML = '<test-attribs LOWERCASE="SET" CAMELCASE="SET" UPPERCASE="SET"></test-attribs>';
43+
document.body.appendChild(el);
44+
45+
expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(allSetOutput));
46+
});
47+
48+
test('all: mixed case attributes', () => {
49+
50+
el = document.createElement('div');
51+
el.innerHTML = '<test-attribs lOwErCaSe="SET" cAmElCaSe="SET" uPpErCaSe="SET"></test-attribs>';
52+
document.body.appendChild(el);
53+
54+
expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(allSetOutput));
55+
});
56+
57+
test('explicitly empty', () => {
58+
59+
el = document.createElement('div');
60+
el.innerHTML = '<test-attribs lowercase="" camelcase="" uppercase=""></test-attribs>';
61+
document.body.appendChild(el);
62+
63+
let expectedOutput = `
64+
<test-attribs lowercase="" camelcase="" uppercase="">
65+
<svelte-retag>
66+
<!--<TestAttributes>-->
67+
</svelte-retag>
68+
</test-attribs>
69+
`;
70+
71+
expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(expectedOutput));
72+
});
73+
74+
test('implicitly empty', () => {
75+
76+
el = document.createElement('div');
77+
el.innerHTML = '<test-attribs></test-attribs>';
78+
document.body.appendChild(el);
79+
80+
let expectedOutput = `
81+
<test-attribs>
82+
<svelte-retag>
83+
<div>lowercase: default</div>
84+
<div>camelCase: default</div>
85+
<div>UPPERCASE: default</div>
86+
<!--<TestAttributes>-->
87+
</svelte-retag>
88+
</test-attribs>
89+
`;
90+
91+
expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(expectedOutput));
92+
});
93+
94+
});

0 commit comments

Comments
 (0)