Skip to content

Commit

Permalink
Merge branch 'issue-16-lit-style-attributes'
Browse files Browse the repository at this point in the history
  • Loading branch information
patricknelson committed Jun 10, 2023
2 parents 6729c80 + ac19a4f commit 9e7c3de
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 13 deletions.
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));
});

});

0 comments on commit 9e7c3de

Please sign in to comment.