Skip to content

Commit

Permalink
Add local storage for persistence and cross tab events.
Browse files Browse the repository at this point in the history
  • Loading branch information
treeder committed Oct 19, 2023
1 parent 255495c commit f018e9d
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 5 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run:
python -m http.server 3000
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# state
JavaScript client side state using modules. Super simple.

Super lightweight JavaScript state library using ESM modules. Modern and easy to use.

Features:

* Uses localStorage so state remains intact even if user leaves and comes back.
* Can listen for state changes from anywhere, even across browser tabs!

## Usage

Import this library:

```js
```js
<script type="module">
import state from 'https://cdn.jsdelivr.net/gh/treeder/state@0/state.js'
Expand Down
44 changes: 44 additions & 0 deletions example/count-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/***
* This one just shows the counter value from the state.
*/
/***
* This one just shows the counter value from the state AND lets you increment it.
*/
import { LitElement, html } from 'lit'
import '@material/web/button/filled-button.js'
import state from '../state.js'

class CountComponent extends LitElement {
static properties = {
counter: { type: Number }
}

constructor() {
super()
this.counter = 0
}

connectedCallback() {
super.connectedCallback()
this.counter = state.get('counter')
console.log("got counter 1:", this.counter)
state.addEventListener('counter', (e) => {
console.log("counter event", e)
this.counter = e.detail.value
})
}

render() {
return html`
<div>${this.counter}</div>
`
}

increment() {
this.counter++
state.set('counter', this.counter)
}

}

customElements.define('count-component', CountComponent)
42 changes: 42 additions & 0 deletions example/counter-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/***
* This one just shows the counter value from the state AND lets you increment it.
*/
import { LitElement, html } from 'lit'
import '@material/web/button/filled-button.js'
import state from '../state.js'

class CounterComponent extends LitElement {
static properties = {
counter: { type: Number }
}

constructor() {
super();
this.counter = 0
}

connectedCallback() {
super.connectedCallback()
this.counter = state.get('counter')
console.log("got counter 2:", this.counter)
state.addEventListener('counter', (e) => {
console.log("counter event", e)
this.counter = e.detail.value
})
}

render() {
return html`
<div>${this.counter}</div>
<md-filled-button @click=${this.increment}>Increment</md-filled-button>
`
}

increment() {
this.counter++
state.set('counter', this.counter)
}

}

customElements.define('counter-component', CounterComponent)
76 changes: 76 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>

<head>
<title>State Demo Page</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="importmap">
{
"imports": {
"lit": "https://cdn.jsdelivr.net/npm/lit@2/index.js",
"lit/": "https://cdn.jsdelivr.net/npm/lit@2/",
"@material/web/": "https://cdn.jsdelivr.net/npm/@material/web@1/",
"@lit/localize": "https://cdn.jsdelivr.net/npm/@lit/localize/lit-localize.js",
"@lit/reactive-element": "https://cdn.jsdelivr.net/npm/@lit/reactive-element@1/reactive-element.js",
"@lit/reactive-element/": "https://cdn.jsdelivr.net/npm/@lit/reactive-element@1/",
"lit-element/lit-element.js": "https://cdn.jsdelivr.net/npm/lit-element@3/lit-element.js",
"lit-html": "https://cdn.jsdelivr.net/npm/lit-html@2/lit-html.js",
"lit-html/": "https://cdn.jsdelivr.net/npm/lit-html@2/",
"tslib": "https://cdn.jsdelivr.net/npm/tslib@2/tslib.es6.mjs"
}
}
</script>

</head>

<body>
<script type="module">
import './counter-component.js'
import './count-component.js'
</script>

<style>
/* Set up the flex container */
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #333;
color: #fff;
padding: 10px;
}

/* Style the links */
.nav-link {
color: #fff;
text-decoration: none;
margin: 0 10px;
}

/* Style the active link */
.nav-link.active {
font-weight: bold;
}
</style>

<div class="nav-container">
<div>
<a href="#" class="nav-link active">Home</a>
<a href="#" class="nav-link">About</a>
<a href="#" class="nav-link">Contact</a>
</div>
<div>
<div>Count: <count-component></count-component></div>
<a href="#" class="nav-link">Login</a>
<a href="#" class="nav-link">Sign Up</a>
</div>
</div>

<h1>Welcome to the state demo</h1>
<p>
<counter-component></counter-component>
</p>
</body>

</html>
54 changes: 51 additions & 3 deletions state.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@

class State extends EventTarget { // implements EventTarget (partially anyways) https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
export class State extends EventTarget { // implements EventTarget (partially anyways) https://developer.mozilla.org/en-US/docs/Web/API/EventTarget

static stateKey = 'state'
static statePrefix = 'state.'

constructor() {
super()

this.stateMap = new Map()
this.loadState()

window.addEventListener('storage', (e) => {
// console.log("storage event", e)
if (e.key.startsWith(State.statePrefix)) {
let key = e.key.substring(State.statePrefix.length)
this.dispatchEvent(new CustomEvent(key, {
detail: {
action: e.newValue ? 'set' : 'delete',
key: key,
value: e.newValue,
},
}))
}
})
}

loadState() {
// NOTE: I may drop the 'state' object from local storage and just use individual keys, better for event handling.
const storedState = localStorage.getItem(State.stateKey)
if (!storedState) return null
// console.log("storedState:", storedState)
let state = null
try {
state = JSON.parse(storedState)
// console.log("storedState:", state)
} catch (err) {
console.error("error parsing stored state:", err)
}
this.stateMap = new Map(Object.entries(state))

// the alternative way using individual keys
for (let key in localStorage) {
let value = localStorage.getItem(key)
this.stateMap.set(key, value)
}

return state
}

saveState() {
localStorage.setItem(State.stateKey, JSON.stringify(Object.fromEntries(this.stateMap)))
}

set(key, value) {
let m = this.stateMap.set(key, value)
localStorage.setItem(`${State.statePrefix}${key}`, JSON.stringify(value))
this.saveState()
this.dispatchEvent(new CustomEvent(key, {
detail: {
action: 'set',
Expand All @@ -21,6 +67,8 @@ class State extends EventTarget { // implements EventTarget (partially anyways)

delete(key) {
let r = this.stateMap.delete(key)
localStorage.removeItem(`${State.statePrefix}${key}`)
this.saveState()
this.dispatchEvent(new CustomEvent(key, {
detail: {
action: 'delete',
Expand All @@ -36,5 +84,5 @@ class State extends EventTarget { // implements EventTarget (partially anyways)

}

const state = new State()
export const state = new State()
export default state

0 comments on commit f018e9d

Please sign in to comment.