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

[WC] add support for custom web components with complex needs #18

Merged
merged 2 commits into from
Aug 13, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,25 @@ If you have a custom element you wish to add support to, you can register it
manually with the following:

```js
new PersistentStateRegistry().supportedTags.push('my-custom-input-element');
new PersistentStateRegistry().registerCustomElement({
// the tag name of your custom web component
name: 'my-custom-input-element',

// this is the property that <persistent-state> will initialize on your component with any stored values
updateProperty: 'customValue',

// this is the name of the event your component fires when it's internal input value changes
changeEvent: 'my-custom-input-element::input-event-name',

// This is a callback for the PersistentStateRegistry to manage changes from your element.
// The return value from this callback will be what is stored/loaded from memory
onChange: (customEvent) => {
return customEvent.detail.customValue
}
});
```

In this example, `<persistent-state>` will only work if `<my-custom-input-element>`
has a `value` attribute and fires an `input` event when the value changes.
In this example, `<persistent-state>` will initialize `<my-custom-input-element>`'s `customValue` property with data from the storage when it loads, and store the value returned from the `onChange` callback when the `my-custom-input-element::input-event-name` event fires on the element.

<details>
<summary><strong>Here is an exhaustive list of all the support <code>input</code> types</strong></summary>
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "persistent-state",
"version": "1.6.0",
"version": "1.7.0",
"description": "A native web component that holds onto the state of input elements during a session and/or between sessions.",
"main": "persistent-state.js",
"authors": [
Expand Down
33 changes: 33 additions & 0 deletions demo/custom-webcomponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
(() => {
const name = 'this-is-a-custom-wc'

class CustomInputComponent extends HTMLElement {
// when this is updated by persistent-state, this web component will update it's internals
set customValue (value) {
this.textInput.value = value || '';
}

connectedCallback () {
this.attachShadow({mode: 'open'}); // this will hide the input from <persistent-state>
this.shadowRoot.innerHTML = `
<h6>This is a custom Web Component with custom events to make it hard to use with persistent state</h6>
`
this.textInput = document.createElement('input');
this.shadowRoot.appendChild(this.textInput);

this.textInput.addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent(`${name}::input`, {
bubbles: true,
composed: true,
detail: { internalInputValue: e.currentTarget.value }
}))
})
}
}

if ('customElements' in window) {
customElements.define(name, CustomInputComponent);
} else {
document.registerElement(name, {prototype: Object.create(CustomInputComponent.prototype)});
}
})()
20 changes: 20 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<html>
<head>
<link rel="import" href="../persistent-state.html">
<script src="./custom-webcomponent.js"></script>
</head>

<body>
Expand Down Expand Up @@ -88,6 +89,16 @@ <h3><code>&lt;input type="hidden"&gt;</code></h3>
<input type="hidden" id="hidden-input">
</persistent-state>
</section>

<section>
<hr>
<h3><code>Custom WebComponents</code></h3>
<persistent-state id="custom-wc">
<this-is-a-custom-wc></this-is-a-custom-wc>
</persistent-state>
</section>


<section>
<hr>
<h3>Not Supported</h3>
Expand All @@ -104,6 +115,15 @@ <h4>Selection Types</h4>

<script>
window.hiddenInputElem = document.getElementById('hidden-input');

new PersistentStateRegistry().registerCustomElement({
name: 'this-is-a-custom-wc',
updateProperty: 'customValue',
changeEvent: 'this-is-a-custom-wc::input',
onChange: (customEvent) => {
return customEvent.detail.internalInputValue
}
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "persistent-state",
"version": "1.6.0",
"version": "1.7.0",
"description": "A native web component that holds onto the state of input elements during a session and/or between sessions.",
"main": "persistent-state.js",
"scripts": {
Expand Down
40 changes: 34 additions & 6 deletions persistent-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ window.PersistentStateRegistry = (() => {
constructor () {
if (instance) return instance;
this.supportedTags = ["input", "select", "textarea"];
this.customElementTags = [];
this.customElementConfigs = {};
this.supportedInputTypes = [
"checkbox",
"color",
Expand Down Expand Up @@ -68,7 +70,7 @@ window.PersistentStateRegistry = (() => {
static supported(elem) {
if (!instance) new PersistentStateRegistry();
let tagName = elem.tagName.toLowerCase();
let tagSupported = (instance.supportedTags.includes(tagName));
let tagSupported = (instance.supportedTags.includes(tagName) || instance.customElementTags.includes(tagName));
if (tagSupported) {
if (tagName === 'input') {
return instance.supportedInputTypes.includes(elem.type);
Expand All @@ -77,6 +79,12 @@ window.PersistentStateRegistry = (() => {
}
return false;
}

registerCustomElement(config) {
let name = config.name.toLowerCase();
this.customElementTags.push(name);
this.customElementConfigs[name] = config;
}
}

return PersistentStateRegistry;
Expand All @@ -95,13 +103,18 @@ class PersistentState extends HTMLElement {
this.type = this.getAttribute("type") || "default";
this.observers = [];
this._resetCallbacks = []
this._elements = this.storage.supportedTags.map(e=>[...this.querySelectorAll(e)]).reduce((a,c)=>a.concat(c), []);
this._elements.forEach(this.init.bind(this));

this._setupElementsList()
document.addEventListener("DOMContentLoaded", () => {
this._elements = this.storage.supportedTags.map(e=>[...this.querySelectorAll(e)]).reduce((a,c)=>a.concat(c), []);
this._elements.forEach(this.init.bind(this));
this._setupElementsList()
});
}

_setupElementsList () {
this._elements = this.storage.supportedTags.map(e=>[...this.querySelectorAll(e)]).reduce((a,c)=>a.concat(c), []);
this._elements = [...this._elements, ...this.storage.customElementTags.map(e=>[...this.querySelectorAll(e)]).reduce((a,c)=>a.concat(c), [])];
this._elements.forEach(this.init.bind(this));
}

get _storageId () {
return this.getAttribute("key") || this.id || "GLOBAL";
Expand Down Expand Up @@ -133,7 +146,8 @@ class PersistentState extends HTMLElement {
}

setupObservers (key, elem) {
if ('radio' === elem.type || 'SELECT' === elem.tagName) {
const tagName = elem.tagName.toLowerCase()
if ('radio' === elem.type || 'select' === tagName) {
elem.addEventListener('change', (e) => {
this.storage.set(key, e.currentTarget.value, this.type, this._storageId)
});
Expand All @@ -160,6 +174,15 @@ class PersistentState extends HTMLElement {
this._resetCallbacks.push(() => {
this.storage.reset(this.type, key, this._storageId)
})
} else if (this.storage.customElementTags.includes(tagName)) {
let config = this.storage.customElementConfigs[tagName]
elem.addEventListener(config.changeEvent, (e) => {
let value = config.onChange(e);
this.storage.set(key, value, tagName, this._storageId)
});
this._resetCallbacks.push(() => {
this.storage.reset(tagName, key, this._storageId)
})
} else {
elem.addEventListener('input', (e) => {
this.storage.set(key, e.currentTarget.value, this.type, this._storageId)
Expand All @@ -171,13 +194,18 @@ class PersistentState extends HTMLElement {
}

initializeValue (key, elem) {
const tagName = elem.tagName.toLowerCase();
if ('radio' === elem.type) {
let checkedItem = this.storage.get(key, this.type, this._storageId, null);
if (checkedItem && checkedItem === elem.value) {
elem.checked = true;
}
} else if ('checkbox' === elem.type) {
elem.checked = (this.storage.get(key, this.type, this._storageId, false) === 'true');
} else if (this.storage.customElementTags.includes(tagName)) {
let config = this.storage.customElementConfigs[tagName];
let value = (this.storage.get(key, tagName, this._storageId, '') || '');
elem[config.updateProperty] = value || elem[config.updateProperty];
} else {
elem.value = (this.storage.get(key, this.type, this._storageId, '') || '') || elem.value;
}
Expand Down
5 changes: 5 additions & 0 deletions test/persistent-state_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>persistent-state test</title>
<script src="../demo/custom-webcomponent.js"></script>
<script src="../persistent-state.js"></script>
</head>
<body>
Expand Down Expand Up @@ -33,6 +34,10 @@
<option value="Fried Rice">Fried Rice</option>
</select>
</persistent-state>

<persistent-state id="test-custom-wc">
<this-is-a-custom-wc></this-is-a-custom-wc>
</persistent-state>
</div>
</body>
</html>