Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Lit-style lowercase version of props to attributes #9

Merged
merged 3 commits into from
Jun 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ svelteRetag({
tagname: 'hello-world',

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

To align with future versions of Svelte, attributes are automatically converted to lowercase (following
the [Lit-style naming convention](https://lit.dev/docs/components/properties/#observed-attributes)). So, `greetPerson`
on your component would be automatically made available as `greetperson` on your custom element.

```html
<hello-world name="Cris"></hello-world>
<hello-world greetperson="Cris"></hello-world>
```

### Options

| Option | Default | Description |
|------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| component | _(required)_ | The constructor for your Svelte component (from `import`) |
| 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)) |
| attributes | `[]` | array - List of attributes to reactively forward to your component (does not reflect changes inside the component) |
| 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. |
| 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`). |
| Option | Default | Description |
|------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| component | _(required)_ | The constructor for your Svelte component (from `import`) |
| 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)) |
| 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)). |
| 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. |
| 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`). |

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

Milestones:
Expand Down
35 changes: 32 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export default function(opts) {
});
}

// Inspect the component early on to get its available properties (statically available).
const propInstance = new opts.component({ target: document.createElement('div') });
const propMap = new Map();
for(let key of Object.keys(propInstance.$$.props)) {
propMap.set(key.toLowerCase(), key);
}
propInstance.$destroy();


/**
* Defines the actual custom element responsible for rendering the provided Svelte component.
*/
Expand Down Expand Up @@ -157,8 +166,27 @@ export default function(opts) {
attributeChangedCallback(name, oldValue, newValue) {
this._debug('attributes changed', { name, oldValue, newValue });

// If instance already available, pass it through immediately.
if (this.componentInstance && newValue !== oldValue) {
this.componentInstance.$set({ [name]: newValue });
let translatedName = this._translateAttribute(name);
this.componentInstance.$set({ [translatedName]: newValue });
}
}

/**
* Converts the provided lowercase attribute name to the correct case-sensitive component prop name, if possible.
*
* @param {string} attributeName
* @returns {string}
*/
_translateAttribute(attributeName) {
// In the unlikely scenario that a browser somewhere doesn't do this for us (or maybe we're in a quirks mode or something...)
attributeName = attributeName.toLowerCase();
if (propMap.has(attributeName)) {
return propMap.get(attributeName);
} else {
this._debug(`_translateAttribute(): ${attributeName} not found`);
return attributeName;
}
}

Expand Down Expand Up @@ -195,8 +223,9 @@ export default function(opts) {
};

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

// Instantiate component into our root now, which is either the "light DOM" (i.e. directly under this element) or
// in the shadow DOM.
Expand Down
18 changes: 18 additions & 0 deletions tests/TestAttributes.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script>
export let lowercase = 'default';
export let camelCase = 'default';
export let UPPERCASE = 'default';
</script>

{#if lowercase}
<div>lowercase: {lowercase}</div>
{/if}

{#if camelCase}
<div>camelCase: {camelCase}</div>
{/if}

{#if UPPERCASE}
<div>UPPERCASE: {UPPERCASE}</div>
{/if}

94 changes: 94 additions & 0 deletions tests/TestAttributes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, beforeAll, afterEach, test, expect } from 'vitest';
import TestAttributes from './TestAttributes.svelte';
import svelteRetag from '../index';
import { normalizeWhitespace } from './test-utils.js';

let el = null;

describe('Test custom element attributes', () => {
beforeAll(() => {
svelteRetag({ component: TestAttributes, tagname: 'test-attribs', shadow: false });
});

afterEach(() => {
if (el) {
el.remove();
}
});

let allSetOutput = `
<test-attribs lowercase="SET" camelcase="SET" uppercase="SET">
<svelte-retag>
<div>lowercase: SET</div>
<div>camelCase: SET</div>
<div>UPPERCASE: SET</div>
<!--<TestAttributes>-->
</svelte-retag>
</test-attribs>
`;

test('all: lowercase attributes', () => {

el = document.createElement('div');
el.innerHTML = '<test-attribs lowercase="SET" camelcase="SET" uppercase="SET"></test-attribs>';
document.body.appendChild(el);

expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(allSetOutput));
});

test('all: uppercase attributes', () => {

el = document.createElement('div');
el.innerHTML = '<test-attribs LOWERCASE="SET" CAMELCASE="SET" UPPERCASE="SET"></test-attribs>';
document.body.appendChild(el);

expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(allSetOutput));
});

test('all: mixed case attributes', () => {

el = document.createElement('div');
el.innerHTML = '<test-attribs lOwErCaSe="SET" cAmElCaSe="SET" uPpErCaSe="SET"></test-attribs>';
document.body.appendChild(el);

expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(allSetOutput));
});

test('explicitly empty', () => {

el = document.createElement('div');
el.innerHTML = '<test-attribs lowercase="" camelcase="" uppercase=""></test-attribs>';
document.body.appendChild(el);

let expectedOutput = `
<test-attribs lowercase="" camelcase="" uppercase="">
<svelte-retag>
<!--<TestAttributes>-->
</svelte-retag>
</test-attribs>
`;

expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(expectedOutput));
});

test('implicitly empty', () => {

el = document.createElement('div');
el.innerHTML = '<test-attribs></test-attribs>';
document.body.appendChild(el);

let expectedOutput = `
<test-attribs>
<svelte-retag>
<div>lowercase: default</div>
<div>camelCase: default</div>
<div>UPPERCASE: default</div>
<!--<TestAttributes>-->
</svelte-retag>
</test-attribs>
`;

expect(normalizeWhitespace(el.innerHTML)).toBe(normalizeWhitespace(expectedOutput));
});

});