diff --git a/lib/one.js b/lib/one.js index 384dca84..f02adb32 100644 --- a/lib/one.js +++ b/lib/one.js @@ -1,16 +1,20 @@ // @flow -'use strict' - -// TODO: Find a way to type HTMLElement with $one. -type OneElement = any +type OneElement = HTMLElement & { + $one: Component, + on?: Function, + if?: Function, + for?: Function, + [string]: Function +} type Props = Array | { [string]: mixed } -type State = { [string]: mixed } +type State = Object type Computed = { [string]: Function } +type Key = string | number | boolean type Config = { - key?: string, + key?: Key, uid?: number, name?: string, template?: TrustedHTML, @@ -22,11 +26,20 @@ type Config = { store?: Store, setup: Function, created?: Function, - components?: Array + components?: Array, + loop?: { + parent: OneElement, + el: OneElement, + items: string, + key: ?Key, + fn: Function + } } +type Component = Omit + type Components = { - [string]: Array> + [string]: Array } type Store = { @@ -70,11 +83,14 @@ const store: Store = { let uid = 0 -function one(config: Config) { - const els = config.parent - ? config.parent.el?.querySelectorAll(config.name) - // FIXME: Don't use ?? '' here. - : document.querySelectorAll(config.name ?? '') +function one(config: Config): void { + if (!config.name) { + throw new Error('One->one: You must define a name for your component.') + } + + const els: NodeList = config.parent + ? (config.parent.el?.querySelectorAll(config.name): any) + : (document.querySelectorAll(config.name): any) if (els == null || els.length === 0) { return @@ -123,9 +139,9 @@ function one(config: Config) { }) } -function template(config: Omit) { +function template(config: Component) { 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... @@ -136,8 +152,11 @@ function template(config: Omit) { throw new Error('One->template: You can only have exactly one root element in your template.') } - const name = config.name ?? '' - const clone: OneElement = tmpl.content.children[0].cloneNode(true) + const clone: OneElement = (tmpl.content.children[0].cloneNode(true): any) + + const name = config.name + if (!name) return + const component = components[name].find(c => c.uid === config.uid) if (!component) { @@ -148,11 +167,11 @@ function template(config: Omit) { throw new Error('One->template: Could not find the element.') } - const el: OneElement = config.el + const el = config.el clone.$one = config.el?.$one for (const attr of config.el.attributes) { - if (config.props?.[attr.name]) continue + if (config.props && (config.props: Object)[attr.name]) continue clone.setAttribute(attr.name, attr.value) } @@ -164,7 +183,7 @@ function template(config: Omit) { tmpl.remove() } -function createInstance(config: Omit): Omit { +function createInstance(config: Component): Component { const name = config.name const el = config.el @@ -190,7 +209,8 @@ function createInstance(config: Omit): Omit { uid, props: {}, state: config.state, - parent: config.parent + parent: config.parent, + el: config.el, } const props: Object = {} @@ -203,7 +223,7 @@ function createInstance(config: Omit): Omit { props[prop] = val } } - el.$one.props[prop] = val + (el.$one.props: Object)[prop] = val }) } @@ -216,9 +236,9 @@ function createInstance(config: Omit): Omit { return config } -function query(this: Omit, selector: string) { - const el = this.el?.children.length - ? this.el.querySelector(selector) +function query(this: Component, selector: string) { + const el: ?OneElement = this.el?.children.length + ? (this.el.querySelector(selector): any) : this.el if (!el) { @@ -232,7 +252,7 @@ function query(this: Omit, selector: string) { return el } -function on(this: Omit, el: OneElement, name: string, handler: Function) { +function on(this: Component, el: OneElement, name: string, handler: Function) { el[`on${name}`] = async () => { await handler({ store: { get: store.get, set: store.set, length: store.length }, @@ -258,17 +278,14 @@ function on(this: Omit, el: OneElement, name: string, handler: } } -// TOOD: Support for items that aren't an array of objects -// i.e: [1, 2, 3] or ['foo', 'bar', 'baz'] -// How to handle the key in this case? function loop( - this: Omit, + this: Component, el: OneElement, - [items, key]: [string, string], + [items, key]: [string, ?Key], fn: Function, parent: ?OneElement = null ) { - const parentNode = parent ?? el.parentNode + const parentNode: OneElement = parent ?? (el.parentNode: any) const arr = this.state?.[items] ?? this.parent?.state?.[items] if (!Array.isArray(arr)) { @@ -285,9 +302,9 @@ function loop( // `parent` will only be defined if it's a re-render. // Or, if the user defines it, which should be discouraged. if (parent) { - Array.from(parentNode.children).forEach((child) => { - const k = child.$one?.key - if (!k) { return } + Array.from(parentNode.children).forEach((child: any) => { + const k: OneElement = child.$one?.key + if (!k) return if (!arr.find((x: Object) => x[key] === k)) { child.remove() } @@ -300,17 +317,20 @@ function loop( el.remove() } - arr.forEach((item: Object, index: number) => { - const clone = el.cloneNode(true) + const keys: Array = [] + arr.forEach((item: Object | string | number | boolean, index: number) => { + const clone: OneElement = (el.cloneNode(true): any) + const k = getKey(item, key).toString() - if (!item[key]) { - throw new Error(`One->for: The key "${key}" does not exist on ${JSON.stringify(item)}`) + if (keys.includes(k)) { + throw new Error(`One->for: There are multiple items with the key "${k}" when looping "${items}".`) } + keys.push(k) el.remove() const config = { - key: item[key], + key: k, el: clone, state: item, store: this.store @@ -323,8 +343,9 @@ function loop( render(clone.$one) - if (parentNode.children[index]) { - render(parentNode.children[index].$one) + const child: OneElement = (parentNode.children[index]: any) + if (child) { + render(child.$one) } else { parentNode.append(clone) } @@ -335,33 +356,49 @@ function loop( }) } -function fi(this: Omit, el: OneElement) { +function getKey(item: Object | string | number | boolean, key: ?Key) { + if (!key && typeof item === 'object') { + throw new Error(`One->for: You must define a key for "${JSON.stringify(item)}"`) + } + + if (key && typeof item === 'object' && !item[key]) { + throw new Error(`One->for: The key "${key.toString()}" does not exist on ${JSON.stringify(item)}`) + } + + if (typeof item === 'object') { + return item[key] + } + + return item +} + +function fi(this: Component, el: OneElement) { let original: ?OneElement = null - const container = document.createElement('one-if') + const container: OneElement = (document.createElement('one-if'): any) const template = document.createElement('template') - const sibling = el.nextSibling + const sibling: OneElement = (el.nextSibling: any) return { hide() { - if (original) { return } + if (original) return template.append(el) container.append(template) - sibling.parentNode.insertBefore(container, sibling) + sibling.parentNode?.insertBefore(container, sibling) original = container }, show() { - if (!original) { return } + if (!original) return original.replaceWith(el) original = null } } } -function computed(this: Omit, name: string, fn: Function) { +function computed(this: Component, name: string, fn: Function) { if (this.state?.[name] != null) { throw new Error(`One->computed: You already have a state property called "${name}".`) } @@ -383,11 +420,11 @@ function computed(this: Omit, name: string, fn: Function) { } } -function render(config: Omit) { +function render(config: Component) { traverse(config.el, config) } -function traverse(el: OneElement, config: Omit) { +function traverse(el: OneElement, config: Component) { Array.from(el.attributes).forEach(attr => { if (attr.name === 'o-text') { const val = value(attr.value, config) @@ -398,21 +435,23 @@ function traverse(el: OneElement, config: Omit) { } if (attr.name === 'o-for') { + if (!el.$one?.loop) return const { el: elm, items, key, fn, parent } = el.$one.loop loop.bind(config)(elm, [items, key], fn, parent) } }) if (el.children.length) { - Array.from(el.children).forEach(c => { - if (c.$one?.uid) { return } + const children: Array = (Array.from(el.children): any) + children.forEach((c) => { + if (c.$one?.uid) return traverse(c, config) }) } } // TODO: Support for nested keys, like `foo.bar.baz`. -function value(key: string, config: Omit) { +function value(key: string, config: Component) { const fromStore = key.startsWith('$store.') const fromProps = key.startsWith('$props.') @@ -436,6 +475,10 @@ function value(key: string, config: Omit) { throw new Error('One->value: You are trying to access a state value, but the component have no state.') } + if (typeof config.state !== 'object') { + return config.state + } + return config.state[key] }