Skip to content

Commit

Permalink
more types, fix key for non-object items
Browse files Browse the repository at this point in the history
  • Loading branch information
sebkolind committed Nov 25, 2023
1 parent bd92c92 commit dd2a9b3
Showing 1 changed file with 97 additions and 54 deletions.
151 changes: 97 additions & 54 deletions lib/one.js
Original file line number Diff line number Diff line change
@@ -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> | { [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,
Expand All @@ -22,11 +26,20 @@ type Config = {
store?: Store,
setup: Function,
created?: Function,
components?: Array<Config>
components?: Array<Config>,
loop?: {
parent: OneElement,
el: OneElement,
items: string,
key: ?Key,
fn: Function
}
}

type Component = Omit<Config, 'setup'>

type Components = {
[string]: Array<Omit<Config, 'setup'>>
[string]: Array<Component>
}

type Store = {
Expand Down Expand Up @@ -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<OneElement> = config.parent
? (config.parent.el?.querySelectorAll(config.name): any)
: (document.querySelectorAll(config.name): any)

if (els == null || els.length === 0) {
return
Expand Down Expand Up @@ -123,9 +139,9 @@ function one(config: Config) {
})
}

function template(config: Omit<Config, 'setup'>) {
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...
Expand All @@ -136,8 +152,11 @@ function template(config: Omit<Config, 'setup'>) {
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) {
Expand All @@ -148,11 +167,11 @@ function template(config: Omit<Config, 'setup'>) {
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)
}

Expand All @@ -164,7 +183,7 @@ function template(config: Omit<Config, 'setup'>) {
tmpl.remove()
}

function createInstance(config: Omit<Config, 'setup'>): Omit<Config, 'setup'> {
function createInstance(config: Component): Component {
const name = config.name
const el = config.el

Expand All @@ -190,7 +209,8 @@ function createInstance(config: Omit<Config, 'setup'>): Omit<Config, 'setup'> {
uid,
props: {},
state: config.state,
parent: config.parent
parent: config.parent,
el: config.el,
}

const props: Object = {}
Expand All @@ -203,7 +223,7 @@ function createInstance(config: Omit<Config, 'setup'>): Omit<Config, 'setup'> {
props[prop] = val
}
}
el.$one.props[prop] = val
(el.$one.props: Object)[prop] = val
})
}

Expand All @@ -216,9 +236,9 @@ function createInstance(config: Omit<Config, 'setup'>): Omit<Config, 'setup'> {
return config
}

function query(this: Omit<Config, 'setup'>, 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) {
Expand All @@ -232,7 +252,7 @@ function query(this: Omit<Config, 'setup'>, selector: string) {
return el
}

function on(this: Omit<Config, 'setup'>, 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 },
Expand All @@ -258,17 +278,14 @@ function on(this: Omit<Config, 'setup'>, 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<Config, 'setup'>,
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)) {
Expand All @@ -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()
}
Expand All @@ -300,17 +317,20 @@ function loop(
el.remove()
}

arr.forEach((item: Object, index: number) => {
const clone = el.cloneNode(true)
const keys: Array<Key> = []
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
Expand All @@ -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)
}
Expand All @@ -335,33 +356,49 @@ function loop(
})
}

function fi(this: Omit<Config, 'setup'>, 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<Config, 'setup'>, 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}".`)
}
Expand All @@ -383,11 +420,11 @@ function computed(this: Omit<Config, 'setup'>, name: string, fn: Function) {
}
}

function render(config: Omit<Config, 'setup'>) {
function render(config: Component) {
traverse(config.el, config)
}

function traverse(el: OneElement, config: Omit<Config, 'setup'>) {
function traverse(el: OneElement, config: Component) {
Array.from(el.attributes).forEach(attr => {
if (attr.name === 'o-text') {
const val = value(attr.value, config)
Expand All @@ -398,21 +435,23 @@ function traverse(el: OneElement, config: Omit<Config, 'setup'>) {
}

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<OneElement> = (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<Config, 'setup'>) {
function value(key: string, config: Component) {
const fromStore = key.startsWith('$store.')
const fromProps = key.startsWith('$props.')

Expand All @@ -436,6 +475,10 @@ function value(key: string, config: Omit<Config, 'setup'>) {
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]
}

Expand Down

0 comments on commit dd2a9b3

Please sign in to comment.