diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index f37d1f35..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -repos: - - repo: https://github.com/standard/standard - rev: v17.1.0 - hooks: - - id: standard diff --git a/lib/one.js b/lib/one.js index 7a4a53a9..c16359c1 100644 --- a/lib/one.js +++ b/lib/one.js @@ -1,14 +1,50 @@ +// @flow + 'use strict' -const components = {} -const store = { +// TODO: Find a way to type HTMLElement with $one. +type OneElement = any + +type Props = Array | { [string]: mixed } +type State = { [string]: mixed } +type Computed = { [string]: Function } + +type Config = { + key?: string, + uid?: number, + name?: string, + template?: TrustedHTML, + computed?: Computed, + el: OneElement, + props?: Props, + state?: State, + parent?: Partial, + store?: Store, + setup: Function, + created?: Function, + components?: Array +} + +type Components = { + [string]: Array +} + +type Store = { + state?: { [string]: mixed }, + get: (key: string) => mixed, + set: (key: string, value: mixed) => void, + length: number +} + +const components: Components = {} +const store: Store = { state: {}, - get length () { return Object.keys(store.state).length }, - get (key) { - return store.state[key] + get length() { return Object.keys(store.state ?? {}).length }, + get(key) { + return store.state?.[key] }, - set (key, value) { - if (!store.state[key]) { + set(key, value) { + if (!store.state?.[key]) { throw new Error(`One->store: You can't set "${key}", since it wasn't defined on initialization.`) } @@ -16,14 +52,10 @@ const store = { for (const k in components) { const component = components[k] - if (!component) { - continue - } - - component.forEach(c => { + component.forEach((c: Config) => { if ( - c.template?.includes(`$store.${key}`) || - c.el.textContent.includes(`$store.${key}`) + typeof c.template === 'string' && c.template.includes(`$store.${key}`) || + c.el?.textContent.includes(`$store.${key}`) ) { // TODO: Find a way to optimize that the component would re-render twice, // if it's got an event AND is using the store. @@ -38,10 +70,15 @@ const store = { let uid = 0 -function one (config) { +function one(config: Config) { const els = config.parent - ? config.parent.el.querySelectorAll(config.name) - : document.querySelectorAll(config.name) + ? config.parent.el?.querySelectorAll(config.name) + // FIXME: Don't use ?? '' here. + : document.querySelectorAll(config.name ?? '') + + if (els == null || els.length === 0) { + return + } Array.from(els).forEach(async (el) => { const { setup, created, ...c } = createInstance({ @@ -85,30 +122,40 @@ function one (config) { }) } -function template (config) { +function template(config: Partial) { + const tmpl = document.createElement('template') + // FIXME: Shouldn't bail when there's no template. // The user could've defined the template in HTML. // i.e.: config.el.children... if (!config.template) return - - const tmpl = document.createElement('template') tmpl.innerHTML = config.template if (tmpl.content.children.length !== 1) { throw new Error('One->template: You can only have exactly one root element in your template.') } - const clone = tmpl.content.children[0].cloneNode(true) - const component = components[config.name].find(c => c.uid === config.uid) + const name = config.name ?? '' + const clone: OneElement = tmpl.content.children[0].cloneNode(true) + const component = components[name].find(c => c.uid === config.uid) + + if (!component) { + throw new Error(`One->template: Could not find component ${name}.`) + } + + if (!config.el) { + throw new Error('One->template: Could not find the element.') + } - clone.$one = config.el.$one + const el: OneElement = config.el + clone.$one = config.el?.$one for (const attr of config.el.attributes) { - if (config.props[attr.name]) continue + if (config.props?.[attr.name]) continue clone.setAttribute(attr.name, attr.value) } - config.el.replaceWith(clone) + el.replaceWith(clone) config.el = clone component.el = clone @@ -116,16 +163,23 @@ function template (config) { tmpl.remove() } -function createInstance (config) { +function createInstance(config: Config): Partial { const name = config.name const el = config.el + if (!name) { + throw new Error('One->createInstance: You must define a name for your component.') + } + if (el.$one) { - return components[name].find(c => c.uid === el.$one.uid) + const component = components[name].find(c => c.uid === el.$one.uid) + if (component) { + return component + } } if (!components[name]) { - components[name] = [] + components[name] = [config] } uid = uid + 1 @@ -138,19 +192,20 @@ function createInstance (config) { parent: config.parent } - const props = {} - if (config.props) { + const props: Object = {} + if (config.props && Array.isArray(config.props)) { config.props.forEach(prop => { const val = el.getAttribute(prop) if (val) { - if (config.props[prop] == null) { + if ((config.props: Object)[prop] == null) { props[prop] = val } - el.$one.props[prop] = val } + el.$one.props[prop] = val }) } + config.props = Object.freeze({ ...props }) config.computed = {} @@ -160,8 +215,8 @@ function createInstance (config) { return config } -function query (selector) { - const el = this.el.children.length +function query(this: Partial, selector: string) { + const el = this.el?.children.length ? this.el.querySelector(selector) : this.el @@ -176,7 +231,7 @@ function query (selector) { return el } -function on (el, name, handler) { +function on(this: Partial, el: OneElement, name: string, handler: Function) { el[`on${name}`] = async () => { await handler({ store: { get: store.get, set: store.set, length: store.length }, @@ -186,11 +241,15 @@ function on (el, name, handler) { if (this.computed) { for (const k in this.computed) { - this.state[k] = this.computed[k]({ + if (!this.computed[k]) { continue } + const value: mixed = this.computed[k]({ state: this.state, props: this.props, store: this.store }) + if (value && this.state && this.state[k] !== value) { + this.state[k] = value + } } } @@ -198,9 +257,19 @@ function on (el, name, handler) { } } -function loop (el, [items, key], fn, parent = null) { +function loop( + this: Partial, + el: OneElement, + [items, key]: [string, string], + fn: Function, + parent: ?OneElement = null +) { const parentNode = parent ?? el.parentNode - const arr = this.state[items] ?? this.parent?.state?.[items] + const arr = this.state?.[items] ?? this.parent?.state?.[items] + + if (!Array.isArray(arr)) { + return + } parentNode.$one = { ...parentNode.$one, @@ -215,7 +284,7 @@ function loop (el, [items, key], fn, parent = null) { Array.from(parentNode.children).forEach((child) => { const k = child.$one?.key if (!k) { return } - if (!arr.find(x => x[key] === k)) { + if (!arr.find((x: Object) => x[key] === k)) { child.remove() } }) @@ -227,7 +296,7 @@ function loop (el, [items, key], fn, parent = null) { el.remove() } - arr.forEach((item, index) => { + arr.forEach((item: Object, index: number) => { const clone = el.cloneNode(true) if (!item[key]) { @@ -236,12 +305,13 @@ function loop (el, [items, key], fn, parent = null) { el.remove() - clone.$one = { + const config: Partial = { key: item[key], el: clone, state: item, store: this.store } + clone.$one = config clone.on = on.bind(clone.$one, clone) clone.if = fi.bind(clone.$one, clone) @@ -261,15 +331,15 @@ function loop (el, [items, key], fn, parent = null) { }) } -function fi (el) { - let original +function fi(this: Partial, el: OneElement) { + let original: ?OneElement = null const container = document.createElement('one-if') const template = document.createElement('template') const sibling = el.nextSibling return { - hide () { + hide() { if (original) { return } template.append(el) @@ -279,7 +349,7 @@ function fi (el) { original = container }, - show () { + show() { if (!original) { return } original.replaceWith(el) original = null @@ -287,29 +357,33 @@ function fi (el) { } } -function computed (name, fn) { - if (this.state[name] != null) { +function computed(this: Partial, name: string, fn: Function) { + if (this.state?.[name] != null) { throw new Error(`One->computed: You already have a state property called "${name}".`) } - if (this.computed[name] != null) { + if (this.computed?.[name] != null) { throw new Error(`One->computed: You already have a computed property called "${name}".`) } - this.state[name] = fn({ - state: this.state, - props: this.props, - store: this.store, - parent: this.parent - }) + if (this.state) { + this.state[name] = fn({ + state: this.state, + props: this.props, + store: this.store, + parent: this.parent + }) + } - this.computed[name] = fn + if (this.computed) { + this.computed[name] = fn + } } -function render (config) { +function render(config: Partial) { traverse(config.el, config) } -function traverse (el, config) { +function traverse(el: OneElement, config: Partial) { Array.from(el.attributes).forEach(attr => { if (attr.name === 'o-text') { const val = value(attr.value, config) @@ -334,12 +408,12 @@ function traverse (el, config) { } // TODO: Support for nested keys, like `foo.bar.baz`. -function value (key, config) { +function value(key: string, config: Partial) { const fromStore = key.startsWith('$store.') const fromProps = key.startsWith('$props.') if (fromStore) { - if (config.store.length === 0) { + if (config.store == null || config.store?.length === 0) { throw new Error('One->value: You are trying to access a $store value, but no store was passed to the component.') } @@ -351,17 +425,21 @@ function value (key, config) { throw new Error('One->value: You are trying to access a $props value, but the component have no props.') } - return config.props[key.replace('$props.', '')] + return (config.props: Object)[key.replace('$props.', '')] + } + + if (config.state == null) { + throw new Error('One->value: You are trying to access a state value, but the component have no state.') } return config.state[key] } -function createStore (fn) { +function createStore(fn: Function) { store.state = fn() } -function html (strings, ...values) { +function html(strings: Array, ...values: Array): string { return strings.reduce((acc, str, i) => { const val = values[i] == null ? '' : values[i] return acc + str + val diff --git a/package.json b/package.json index 51b1990c..700c7173 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "flow": "flow", "watch": "parcel watch --no-cache", "build": "parcel build", - "standard": "standard --fix", "test": "jest --passWithNoTests", "example": "parcel example/index.html -p 1337 --no-cache" }, @@ -29,8 +28,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "parcel": "^2.10.0", - "pre-commit": "^1.2.2", - "standard": "*" + "pre-commit": "^1.2.2" }, "author": "sebkolind ", "license": "ISC",