Compare commits
No commits in common. "7de03160384c2ad529635093564eab107310149e" and "0e63167c92a5edb9ea4a91902b1f69c05581913b" have entirely different histories.
7de0316038
...
0e63167c92
14 changed files with 74 additions and 398 deletions
|
@ -1,2 +0,0 @@
|
|||
[*]
|
||||
indent_style = tab
|
25
element.js
25
element.js
|
@ -6,9 +6,9 @@ export default Class => {
|
|||
const props = []
|
||||
|
||||
Object.entries(attributes).forEach(([name, attribute]) => {
|
||||
const htmlName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
||||
let htmlName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
||||
props.push(htmlName)
|
||||
const prop = {}
|
||||
let prop = {}
|
||||
|
||||
prop.get = typeof attribute.get == "function"
|
||||
? function() { return attribute.get.call(this, this.getAttribute(htmlName)) }
|
||||
|
@ -17,7 +17,7 @@ export default Class => {
|
|||
prop.set = typeof attribute.set == "function"
|
||||
? function(val) { return this.setAttribute(htmlName, attribute.set.call(this, val)) }
|
||||
: attribute.set === false
|
||||
? function() { throw(Error(`Attribute ${name} cannot be set`)) }
|
||||
? function(val) { throw(Error(`Attribute ${name} cannot be set`)) }
|
||||
: function(val) { this.setAttribute(htmlName, val) }
|
||||
|
||||
Object.defineProperty(proto, name, prop)
|
||||
|
@ -33,20 +33,9 @@ export default Class => {
|
|||
})
|
||||
|
||||
Class.prototype.attributeChangedCallback = function(name, oldValue, newValue) {
|
||||
name = name.replaceAll(/-(.)/g, (_, b) => b.toUpperCase())
|
||||
const get_transform = attributes[name]?.get
|
||||
if (`${name}Changed` in this) {
|
||||
if (typeof get_transform == "function")
|
||||
return this[`${name}Changed`](get_transform(oldValue), get_transform(newValue))
|
||||
else
|
||||
return this[`${name}Changed`](oldValue, newValue)
|
||||
}
|
||||
if (`changed` in this) {
|
||||
if (typeof get_transform == "function")
|
||||
return this.changed(name, get_transform(oldValue), get_transform(newValue))
|
||||
else
|
||||
return this.changed(name, oldValue, newValue)
|
||||
}
|
||||
name = name.replaceAll(/-(.)/g, (a, b) => b.toUpperCase())
|
||||
if (`${name}Changed` in this) this[`${name}Changed`](oldValue, newValue)
|
||||
if (`changed` in this) this.changed(name, oldValue, newValue)
|
||||
}
|
||||
|
||||
/* Enable batch-processing for dollar-methods */
|
||||
|
@ -65,7 +54,7 @@ export default Class => {
|
|||
queue.push(args)
|
||||
Class.queues.set(this, queue)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
36
index.html
36
index.html
|
@ -66,22 +66,36 @@
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<h2>State</h2>
|
||||
<h2>Listener</h2>
|
||||
<p>
|
||||
Utility class to monitor state changes on an object using a Proxy and events.
|
||||
Changes are batched and processed in a microtask.
|
||||
<a class="fancy" href="page/state.html">Read More</a>
|
||||
Like a normal object, except you can register callbacks for property changes.
|
||||
<a class="fancy" href="page/listener.html">Read More</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<h3 class="all-unset"><b>Code Sample</b>:</h3>
|
||||
<code-block>
|
||||
import listener from 'listener.js'
|
||||
|
||||
const user = listener({name: "John Doe"})
|
||||
user.listen('name', value => console.warn(`User name has changed to ${value}`))
|
||||
</code-block>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Speaker</h2>
|
||||
<p>
|
||||
Publish and subscribe to messages, relayed via micro-tasks.
|
||||
<a class="fancy" href="page/speaker.html">Read More</a>
|
||||
|
||||
<code-block>
|
||||
const state = new State()
|
||||
speaker.meepChanged = value => console.log(`meep ${value}`)
|
||||
state.proxy.meep = "Heat from fire"
|
||||
state.proxy.meep = "Fire from heat"
|
||||
state.proxy.meep = "moop"
|
||||
// outputs: "meep moop"
|
||||
const speaker = new Speaker()
|
||||
speaker.listen((...args) => console.log(...args))
|
||||
speaker.speak("First", "second", "third")
|
||||
</code-block>
|
||||
|
||||
No, this still has nothing to do with playing audio.
|
||||
No, this has nothing to do with playing audio.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ Example:
|
|||
*/
|
||||
|
||||
const registry = new WeakMap()
|
||||
|
||||
const listener = (target={}) => {
|
||||
const callbacks = new Map()
|
||||
const methods = Object.create(null)
|
||||
|
@ -17,7 +16,7 @@ const listener = (target={}) => {
|
|||
const callback = once
|
||||
? (...args) => { this.forget(name, callback); return fn(...args) }
|
||||
: fn
|
||||
const set = callbacks.get(name) ?? new Set()
|
||||
let set = callbacks.get(name) ?? new Set()
|
||||
callbacks.set(name, set)
|
||||
set.add(callback)
|
||||
return this
|
||||
|
@ -30,13 +29,13 @@ const listener = (target={}) => {
|
|||
return callbacks.delete(name)
|
||||
}
|
||||
}
|
||||
const proxy = new Proxy(target, {
|
||||
let proxy = new Proxy(target, {
|
||||
set: (target, prop, value) => {
|
||||
if (callbacks.has(null)) callbacks.get(null).forEach(callback => callback(value, target[prop], prop))
|
||||
if (callbacks.has(prop)) callbacks.get(prop).forEach(callback => callback(value, target[prop], prop))
|
||||
return Reflect.set(target, prop, value)
|
||||
},
|
||||
get: (target, prop, _value) => prop in methods
|
||||
get: (target, prop, value) => prop in methods
|
||||
? methods[prop]
|
||||
: target[prop]
|
||||
})
|
||||
|
|
|
@ -297,7 +297,6 @@
|
|||
Therefore one cannot use tagged template literals with <code>text</code>
|
||||
as this would return a document fragment which cannot be replaced.
|
||||
</div>
|
||||
If the element is a string, it is turned into a text node before insertion.
|
||||
</dd>
|
||||
<dt>Update function</dt>
|
||||
<code>
|
||||
|
|
160
page/state.html
160
page/state.html
|
@ -1,160 +0,0 @@
|
|||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<script type="module" src="codeblock.js"></script>
|
||||
<script type="module" src="scrollToTop.js"></script>
|
||||
|
||||
<scroll-to-top>
|
||||
</scroll-to-top>
|
||||
|
||||
<a class="back" href="..">Module Index</a>
|
||||
|
||||
<h1 class="js module">state.js</h1>
|
||||
|
||||
<code-block>import {State} from 'state.js'</code-block>
|
||||
|
||||
<section>
|
||||
<h2>Description</h2>
|
||||
<p>
|
||||
State objects are an event-oriented way of managing application state and reacting to changes.
|
||||
It makes use of built-in features such as <b><code>Proxy</code></b> and <b><code>EventTarget</code></b>
|
||||
to save code and give users less to remember.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By default, state changes are only queued up and processed in a microtask, allowing for batch-processing.<br>
|
||||
This means that for repeated changes to the same property, the user can decide to process all the changes in one go or even discard everything but the last value of each property.<br>
|
||||
That way one can easily avoid some types of unnecessary redraws, network requests, etc.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To make code more readable, the constructor defaults to adding an event listener that checks for a method called [prop]<code>Changed</code> in the state object, and calls it with the new value. This behaviour can be disabled by setting the <code>methods</code> option to false.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The state object also has a getter and setter pair for the `state` attribute, which simply accesses this same attribute on the proxy object.
|
||||
This is simply a convenience feature to save some typing on single-variable states.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<dl>
|
||||
<dt><code>defer</code></dt>
|
||||
<dd>Set to false to disable deferred change processing. This will emit a new event every time something changes, even if it's about to be changed again in the next line.</dd>
|
||||
<dt><code>methods</code></dt>
|
||||
<dd>Set to false to disable the default event listener that attempts to call a [prop]Changed method on the state object.</dd>
|
||||
</dl>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Examples</h2>
|
||||
|
||||
<p>
|
||||
A simple counter state that prints a new count to the console every time it gets updated.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
const counter = new State({state: 0}, {defer: false})
|
||||
|
||||
counter.stateChanged = (count) => console.log(`new count: ${count}`)
|
||||
|
||||
counter.state += 1
|
||||
counter.state += 1
|
||||
counter.state += 1
|
||||
</code-block>
|
||||
|
||||
<p>
|
||||
This example uses an event listener instead to get notified of all property changes.
|
||||
Collecting changes into a map is an easy way of de-duplicating their values and keeping only the last one.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
const state = new State({}, {methods: false})
|
||||
|
||||
state.addEventListener("change", event => {
|
||||
console.log(`There were ${event.changes.length} changes.`)
|
||||
new Map(event.changes).forEach((value, prop) => {
|
||||
console.log(`Final vaue of ${prop}: ${value}`)
|
||||
})
|
||||
})
|
||||
|
||||
state.proxy.foo = "foo 1"
|
||||
state.proxy.foo = "foo 2"
|
||||
state.proxy.bar = "bar 1"
|
||||
state.proxy.foo = "foo 3"
|
||||
state.proxy.bar = "bar 2"
|
||||
|
||||
// There were 5 changes.
|
||||
// Final vaue of foo: foo 3
|
||||
// Final vaue of bar: bar 2
|
||||
</code-block>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Storage-backed states</h2>
|
||||
|
||||
<p>
|
||||
The <code>StorageState</code> subclass of <code>State</code> implements the same API but is backed by a
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage"><code>Storage</code></a> object.
|
||||
</p>
|
||||
<p>
|
||||
By default, <code>StorageState</code> uses the
|
||||
<code>window.localStorage</code> object, but you can change this by
|
||||
passing the <code>storage</code> option in the constructor.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When using the <code>value</code> short-hand to have a different
|
||||
<code>StorageState</code> for every individual value, these would
|
||||
all override the same key in the storage. To remedy this, the
|
||||
<code>key</code> option can be used to redirect reads and writes
|
||||
of the <code>"value"</code> key to a different key in the storage.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
const greeting = new StorageState({}, {
|
||||
storage = sessionStorage,
|
||||
key: 'greeting'
|
||||
})
|
||||
|
||||
greeting.valueChanged = newValue =>
|
||||
console.log(`Greeting has changed: ${newValue}`)
|
||||
|
||||
greeting.value = "Hello, World!"
|
||||
|
||||
console.log(sessionStorage.value) // udnefined
|
||||
console.log(sessionStorage.greeting) // "Hello, World!"
|
||||
</code-block>
|
||||
|
||||
<p>
|
||||
Using a storage object comes with the disadvantage that all values must
|
||||
be stored in a serialised form, which won't work for all data and will
|
||||
break identity. Specifically, all values are converted to JSON strings
|
||||
for storage.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
const plainState = new State({})
|
||||
const storageState = new StorageState({})
|
||||
|
||||
const object = {}
|
||||
plainState.value = object
|
||||
storageState.value = object
|
||||
|
||||
console.log(plainState.value == object) // true
|
||||
console.log(storageState.value == object) // false
|
||||
</code-block>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Map-backed Storage</h2>
|
||||
|
||||
<p>
|
||||
When a fallback option is needed, for example to support clients without
|
||||
<code>localStorage</code> without breaking the page, the
|
||||
<code>MapStorage</code> object implements the <code>Storage</code> API
|
||||
and is backed by a plain JavaScript <code>Map</code> object.
|
||||
</p>
|
||||
</section>
|
|
@ -67,7 +67,7 @@ export default (parallel = 1) => {
|
|||
// Loop until there is nothing more to do:
|
||||
// a) Running queue is full and nothing can be resolved
|
||||
// b) Waiting queue is empty
|
||||
const spin = () => { while (pump()) {} }
|
||||
const spin = () => { while pump() {} }
|
||||
|
||||
return promise => {
|
||||
waiting.push(promise)
|
||||
|
|
|
@ -24,22 +24,13 @@ support inheriting from other classes for extending builtin elements.
|
|||
Generate CSS from JS objects. Yes, you can generate CSS from JSON now. Or from
|
||||
YAML. Or from whatever you want, really.
|
||||
|
||||
## State
|
||||
|
||||
Combines both Listener and Speaker into one convenient class that batches and
|
||||
defers changes by default.
|
||||
|
||||
## Listener
|
||||
|
||||
(Deprecated; use `State` instead)
|
||||
|
||||
A proxy object that fires a callback when certain (or all) properties are
|
||||
changed.
|
||||
|
||||
## Speaker
|
||||
|
||||
(Deprecated; use `State` instead)
|
||||
|
||||
Simple messaging helper that uses microtasks by default.
|
||||
|
||||
## Debounce
|
||||
|
|
34
skooma.js
34
skooma.js
|
@ -10,10 +10,10 @@ or
|
|||
html.ul([1, 2, 3, 4, 5].map(x => html.li(x)), {class: "numbers"})
|
||||
*/
|
||||
|
||||
export const empty = Symbol("Explicit empty argument for Skooma")
|
||||
|
||||
const keyToPropName = key => key.replace(/^[A-Z]/, a => "-"+a).replace(/[A-Z]/g, a => '-'+a.toLowerCase())
|
||||
|
||||
export const empty = Symbol("Explicit empty argument for Skooma")
|
||||
|
||||
const insertStyles = (rule, styles) => {
|
||||
for (const [key, value] of Object.entries(styles))
|
||||
if (typeof value == "undefined")
|
||||
|
@ -81,9 +81,9 @@ const node = (name, args, options) => {
|
|||
const custom = getCustom(args)
|
||||
if ("nameFilter" in options) name = options.nameFilter(name)
|
||||
if (options.xmlns)
|
||||
element = document.createElementNS(options.xmlns, name, custom ?? {is: custom})
|
||||
element = document.createElementNS(options.xmlns, name, {is: custom})
|
||||
else
|
||||
element = document.createElement(name, custom ?? {is: custom})
|
||||
element = document.createElement(name, {is: custom})
|
||||
parseArgs(element, null, args)
|
||||
return element
|
||||
}
|
||||
|
@ -93,34 +93,25 @@ const nameSpacedProxy = (options={}) => new Proxy(Window, {
|
|||
has: (_target, _prop) => true,
|
||||
})
|
||||
|
||||
export const html = nameSpacedProxy({nameFilter: name => name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()})
|
||||
export const svg = nameSpacedProxy({xmlns: "http://www.w3.org/2000/svg"})
|
||||
|
||||
// Other utility exports
|
||||
|
||||
export const bind = transform => {
|
||||
let element
|
||||
const inject = next => Object.defineProperty(next, 'current', {get: () => element})
|
||||
const update = (...data) => {
|
||||
const next = transform(...data)
|
||||
if (next) {
|
||||
if (typeof next == "string") {
|
||||
element.innerText = next
|
||||
return element
|
||||
} else {
|
||||
if (element) element.replaceWith(next)
|
||||
element = inject(next)
|
||||
return element
|
||||
}
|
||||
if (element) element.replaceWith(next)
|
||||
element = inject(next)
|
||||
return element
|
||||
}
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
// Wraps an event handler in a function that calls preventDefault on the event
|
||||
export const handle = fn => event => { fn(event); event.preventDefault() }
|
||||
export const handle = fn => event => { event.preventDefault(); return fn(event) }
|
||||
|
||||
export const html = nameSpacedProxy({nameFilter: name => name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()})
|
||||
export const svg = nameSpacedProxy({xmlns: "http://www.w3.org/2000/svg"})
|
||||
|
||||
// Wraps a list of elements in a document fragment
|
||||
export const fragment = (...elements) => {
|
||||
const fragment = new DocumentFragment()
|
||||
for (const element of elements)
|
||||
|
@ -128,9 +119,6 @@ export const fragment = (...elements) => {
|
|||
return fragment
|
||||
}
|
||||
|
||||
// Turns a template literal into document fragment.
|
||||
// Strings are returned as text nodes.
|
||||
// Elements are inserted in between.
|
||||
const textFromTemplate = (literals, items) => {
|
||||
const fragment = new DocumentFragment()
|
||||
for (const key in items) {
|
||||
|
|
|
@ -15,8 +15,8 @@ export class Speaker {
|
|||
speak(...args) {
|
||||
if (!this._scheduled.length) {
|
||||
queueMicrotask(() => {
|
||||
for (const args of this._scheduled) {
|
||||
for (const callback of this._callbacks) {
|
||||
for (let args of this._scheduled) {
|
||||
for (let callback of this._callbacks) {
|
||||
callback(...args)
|
||||
}
|
||||
this._retain = args
|
||||
|
@ -28,7 +28,7 @@ export class Speaker {
|
|||
}
|
||||
|
||||
forget(callback) {
|
||||
this._callbacks.delete(callback)
|
||||
this._callbacks.delete(callbacks)
|
||||
}
|
||||
|
||||
silence() {
|
||||
|
@ -38,7 +38,7 @@ export class Speaker {
|
|||
|
||||
export class ImmediateSpeaker extends Speaker {
|
||||
speak(...args) {
|
||||
for (const callback of this._callbacks) {
|
||||
for (let callback of this._callbacks) {
|
||||
callback(...args)
|
||||
}
|
||||
this._retain = args
|
||||
|
|
156
state.js
156
state.js
|
@ -1,156 +0,0 @@
|
|||
export class ChangeEvent extends Event {
|
||||
#final
|
||||
constructor(...changes) {
|
||||
super('change')
|
||||
this.changes = changes
|
||||
}
|
||||
get final() {
|
||||
if (!this.#final) {
|
||||
this.#final = new Map(this.changes)
|
||||
}
|
||||
return this.#final
|
||||
}
|
||||
}
|
||||
|
||||
export class MapStorage extends Storage {
|
||||
#map = new Map()
|
||||
key(index) {
|
||||
return [...this.#map.keys()][index]
|
||||
}
|
||||
getItem(keyName) {
|
||||
if (this.#map.has(keyName))
|
||||
return this.#map.get(keyName)
|
||||
else
|
||||
return null
|
||||
}
|
||||
setItem(keyName, keyValue) {
|
||||
this.#map.set(keyName, String(keyValue))
|
||||
}
|
||||
removeItem(keyName) {
|
||||
this.#map.delete(keyName)
|
||||
}
|
||||
clear() {
|
||||
this.#map.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export class State extends EventTarget {
|
||||
#target
|
||||
#options
|
||||
#queue
|
||||
|
||||
constructor(target={}, options={}) {
|
||||
super()
|
||||
this.#options = options
|
||||
this.#target = target
|
||||
this.proxy = new Proxy(target, {
|
||||
set: (_target, prop, value) => {
|
||||
this.emit(prop, value)
|
||||
this.set(prop, value)
|
||||
return true
|
||||
},
|
||||
get: (_target, prop) => this.get(prop),
|
||||
})
|
||||
|
||||
this.addEventListener
|
||||
|
||||
// Try running a "<name>Changed" method for every changed property
|
||||
// Can be disabled to maybe squeeze out some performance
|
||||
if (options.methods ?? true) {
|
||||
this.addEventListener("change", ({final}) => {
|
||||
final.forEach((value, prop) => {
|
||||
if (`${prop}Changed` in this) this[`${prop}Changed`](value)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// When you only need one value, you can skip the proxy.
|
||||
set value(value) { this.proxy.value = value }
|
||||
get value() { return this.proxy.value }
|
||||
|
||||
// Anounces that a prop has changed
|
||||
emit(prop, value) {
|
||||
if (this.#options.defer ?? true) {
|
||||
if (!this.#queue) {
|
||||
this.#queue = []
|
||||
queueMicrotask(() => {
|
||||
this.dispatchEvent(new ChangeEvent(...this.#queue))
|
||||
this.#queue = undefined
|
||||
})
|
||||
}
|
||||
this.#queue.push([prop, value])
|
||||
} else {
|
||||
this.dispatchEvent(new ChangeEvent([prop, value]))
|
||||
}
|
||||
}
|
||||
|
||||
set(prop, value) {
|
||||
this.#target[prop] = value
|
||||
}
|
||||
|
||||
get(prop) {
|
||||
return this.#target[prop]
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageChangeEvent extends Event {
|
||||
constructor(storage, key, value, targetState) {
|
||||
super("storagechange")
|
||||
this.storageArea = storage
|
||||
this.key = key
|
||||
this.newValue = value
|
||||
this.targetState = targetState
|
||||
}
|
||||
}
|
||||
|
||||
export class StoredState extends State {
|
||||
#storage
|
||||
#valueKey
|
||||
|
||||
constructor(init, options={}) {
|
||||
super({}, options)
|
||||
this.#storage = options.storage ?? localStorage ?? new MapStorage()
|
||||
this.#valueKey = options.key ?? 'value'
|
||||
|
||||
// Initialise storage from defaults
|
||||
for (let [prop, value] of Object.entries(init)) {
|
||||
if (prop === 'value') prop = this.#valueKey
|
||||
if (this.#storage[prop] === undefined)
|
||||
this.set(prop, value)
|
||||
}
|
||||
|
||||
// Emit change events for any changed keys
|
||||
for (let i=0; i<this.#storage.length; i++) {
|
||||
const key = this.#storage.key(i)
|
||||
const value = this.#storage[key]
|
||||
if (value !== JSON.stringify(init[key]))
|
||||
this.emit(key, value)
|
||||
}
|
||||
|
||||
// Listen for changes from other windows
|
||||
const handler = event => {
|
||||
if (event.targetState !== this && event.storageArea == this.#storage) {
|
||||
let prop = event.key
|
||||
if (prop === this.#valueKey) prop = 'value'
|
||||
this.emit(prop, JSON.parse(event.newValue))
|
||||
}
|
||||
}
|
||||
addEventListener("storage", handler)
|
||||
addEventListener("storagechange", handler)
|
||||
}
|
||||
|
||||
set(prop, value) {
|
||||
if (prop == "value") prop = this.#valueKey
|
||||
const json = JSON.stringify(value)
|
||||
dispatchEvent(new StorageChangeEvent(this.#storage, prop, json, this))
|
||||
this.#storage[prop] = json
|
||||
}
|
||||
|
||||
get(prop) {
|
||||
if (prop == "value") prop = this.#valueKey
|
||||
return JSON.parse(this.#storage[prop])
|
||||
}
|
||||
}
|
||||
|
||||
export default State
|
21
storage.js
Normal file
21
storage.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export class ObjectStorage {
|
||||
#map = new Map()
|
||||
key(index) {
|
||||
return [...this.#map.keys()][index]
|
||||
}
|
||||
getItem(keyName) {
|
||||
if (this.#map.has(keyName))
|
||||
return this.#map.get(keyName)
|
||||
else
|
||||
return null
|
||||
}
|
||||
setItem(keyName, keyValue) {
|
||||
this.#map.set(keyName, keyValue)
|
||||
}
|
||||
removeItem(keyName) {
|
||||
this.#map.delete(keyName)
|
||||
}
|
||||
clear() {
|
||||
this.#map.clear()
|
||||
}
|
||||
}
|
|
@ -7,11 +7,11 @@ Example:
|
|||
*/
|
||||
|
||||
export const template = (strings, ...args) => {
|
||||
const buf = []
|
||||
let buf = []
|
||||
for (let i=0;i<strings.length;i++) {
|
||||
buf.push(strings[i], args[i])
|
||||
}
|
||||
const template = document.createElement("template")
|
||||
let template = document.createElement("template")
|
||||
template.innerHTML = buf.join("")
|
||||
return template.content
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue