Skip to content

Commit

Permalink
feat!: allow options to define refs and props
Browse files Browse the repository at this point in the history
  • Loading branch information
arnoson committed Mar 24, 2023
1 parent c96d9c5 commit 8072694
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 92 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"editor.formatOnSave": true
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div data-simple-component="test" data-message="hello"></div>
<script type="module" src="./src/dev.ts"></script>
</body>
</html>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"devDependencies": {
"bumpp": "^8.2.1",
"jsdom": "^20.0.0",
"prettier": "^2.8.7",
"shx": "^0.3.4",
"tsup": "^6.0.1",
"typescript": "^4.8.4",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { registerComponent, mountComponents } from '.'

const options = {
props: { message: String, propWithDefault: 123 }
}

registerComponent('test', options, ({ props }) => {
console.log(props.propWithDefault)
})

mountComponents()
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { registerComponent } from './registerComponent'
export { registerComponent, defineOptions } from './registerComponent'
export { mountComponent, mountComponents } from './mountComponent'
export { defineProps } from './props'
export * from './types'
53 changes: 49 additions & 4 deletions src/mountComponent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,47 @@
import { getComponent } from './registerComponent'
import { SimpleElement, SimpleRefs, SimpleRefsAll } from './types'
import { isBuiltInTypeConstructor } from './utils'
import { walkComponent } from './walkComponent'

const getRefsAll = (el: HTMLElement): SimpleRefsAll => {
const refs: SimpleRefsAll = {}
const parseProp = (value: any, type: string) =>
type === 'string' ? String(value) : JSON.parse(value)

const stringifyProp = (value: any) =>
typeof value === 'string' ? value : JSON.stringify(value)

const getDefaultProp = (definition: any) =>
definition instanceof Function ? definition() : definition

const createPropsProxy = (
el: HTMLElement,
definitions: Record<string, any>
) => {
const get = (_: Object, key: string) => {
const value = el.dataset[key]
const definition = definitions[key]
if (!definition) return value

const isConstructor = isBuiltInTypeConstructor(definition)
const providesDefault = !isConstructor
if (value === undefined && providesDefault)
return getDefaultProp(definition)

const type = isConstructor
? definition.prototype.constructor.name.toLowerCase()
: typeof definition
return parseProp(value, type)
}

const set = (_: Object, key: string, value: unknown) => {
el.dataset[key] = stringifyProp(value)
return true
}

return new Proxy({}, { get, set })
}

const getRefsAll = (el: HTMLElement): SimpleRefsAll<any> => {
const refs: SimpleRefsAll<any> = {}
walkComponent(el, el => {
const { ref } = el.dataset
if (ref) {
Expand Down Expand Up @@ -33,10 +71,17 @@ export const mountComponent = (el: HTMLElement, isChild = false) => {

const component = getComponent(el)
if (component) {
const simpleEl = el as SimpleElement<ReturnType<typeof component>>
simpleEl.$component = component({ el, refs, refsAll }) || {}
const simpleEl = el as SimpleElement<typeof component>
const propsDefinitions = component.options.props

const props = propsDefinitions
? createPropsProxy(el, propsDefinitions)
: el.dataset

simpleEl.$props = props
simpleEl.$refs = refs
simpleEl.$refsAll = refsAll
simpleEl.$component = component.setup({ el, refs, refsAll, props }) || {}
}
}

Expand Down
57 changes: 0 additions & 57 deletions src/props.ts

This file was deleted.

38 changes: 28 additions & 10 deletions src/registerComponent.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import { SimpleComponent } from './types'
import {
SimpleComponent,
SimpleComponentOptions,
SimpleComponentSetup
} from './types'

export const components: Record<string, SimpleComponent<any>> = {}
export const getComponent = (el: HTMLElement) => {
const name = el.dataset.simpleComponent
return name ? components[name] : undefined
}

export const registerComponent = <
T extends HTMLElement,
C extends SimpleComponent<T>
>(
name: string,
component: C
) => {
if (typeof component !== 'function') {
export const defineOptions = (options: SimpleComponentOptions) => options

export function registerComponent<
Setup extends SimpleComponentSetup<Options>,
Options extends SimpleComponentOptions = {}
>(name: string, setup: Setup): SimpleComponent<any>

export function registerComponent<
Setup extends SimpleComponentSetup<Options>,
Options extends SimpleComponentOptions = {}
>(name: string, options: Options, setup: Setup): SimpleComponent<any>

export function registerComponent<
Setup extends SimpleComponentSetup<Options>,
Options extends SimpleComponentOptions = {}
>(name: string, arg1: Options | Setup, arg2?: Setup): SimpleComponent<Options> {
const hasBothArgs = arg2 !== undefined
const options = (hasBothArgs ? arg1 : {}) as Options
const setup = (hasBothArgs ? arg2 : arg1) as Setup

if (typeof setup !== 'function')
throw new Error(`Component ${name} is not a function.`)
}

const component = { options, setup }
components[name] = component
return component
}
80 changes: 65 additions & 15 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,75 @@
export type SimpleRefs = Record<string, HTMLElement | undefined>
type HTMLElementConstructor = typeof HTMLElement

export type SimpleRefsAll = Record<string, HTMLElement[]>
type HTMLElementType<T extends HTMLElementConstructor> = T['prototype']

export type DefineRefs<T> = SimpleRefs & Partial<T>
type HTMLElementsTypes<T extends Record<string, HTMLElementConstructor>> = {
[K in keyof T]: HTMLElementType<T[K]>
}

type BuiltInType = Number | String | Array<any> | Object | Boolean

export type DefineRefsAll<T> = SimpleRefsAll & T
type BuiltInTypeConstructor =
| NumberConstructor
| StringConstructor
| ArrayConstructor
| ObjectConstructor
| BooleanConstructor

type PropTypes<Definitions extends SimpleComponentOptions['props']> = {
[K in keyof Definitions]: Definitions[K] extends BuiltInTypeConstructor
? ReturnType<Definitions[K]> | undefined
: Definitions[K] extends () => any
? ReturnType<Definitions[K]>
: Definitions[K]
}

export type SimpleInstance<C extends SimpleComponent> = ReturnType<C>
export type SimpleRefs = Record<string, HTMLElement>

export type SimpleElement<C extends SimpleComponent, T = HTMLElement> = T & {
$component: SimpleInstance<C>
$refs: SimpleRefs,
$refsAll: SimpleRefsAll
export type SimpleRefsAll<R extends SimpleRefs> = {
[K in keyof R]: R[K][]
}

export interface SimpleComponentPayload<T> {
el: T
refs: SimpleRefs
refsAll: SimpleRefsAll
export interface SimpleComponentOptions {
el?: HTMLElementConstructor
props?: Record<string, BuiltInType | BuiltInTypeConstructor>
refs?: Record<string, HTMLElementConstructor>
events?: Record<string, any>
}

export type SimpleComponentPayload<
Options extends SimpleComponentOptions,
Refs extends SimpleRefs = HTMLElementsTypes<NonNullable<Options['refs']>>
> = {
el: Options['el'] extends HTMLElementConstructor
? HTMLElementType<Options['el']>
: HTMLElement

props: PropTypes<Options['props']>

refs: Record<string, HTMLElement | undefined> & Partial<Refs>

refsAll: Record<string, HTMLElement[]> & SimpleRefsAll<Refs>
}

export type SimpleComponent<T = HTMLElement> = (
payload: SimpleComponentPayload<T>
export type SimpleComponentSetup<O extends SimpleComponentOptions> = (
payload: SimpleComponentPayload<O>
) => any

export type SimpleComponent<Options extends SimpleComponentOptions> = {
setup: SimpleComponentSetup<Options>
options: Options
}

export type SimpleInstance<Component extends SimpleComponent<any>> = ReturnType<
Component['setup']
>

export type SimpleElement<
Component extends SimpleComponent<any>,
Options extends SimpleComponentOptions = Component['options'],
Payload extends SimpleComponentPayload<any> = SimpleComponentPayload<Options>
> = Payload['el'] & {
$component: SimpleInstance<Component>
$refs: Payload['refs']
$refsAll: Payload['refsAll']
}
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isBuiltInTypeConstructor = (value: any) =>
[Number, String, Boolean, Array, Object].includes(value)
13 changes: 10 additions & 3 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ it('mounts a component', () => {
div.dataset.simpleComponent = 'test'

mountComponent(div)
expect(component).toBeCalledWith({ el: div, refs: {}, refsAll: {} })
expect(component).toBeCalledWith({
el: div,
refs: {},
refsAll: {},
props: {}
})
})

it('mounts multiple components', () => {
Expand Down Expand Up @@ -99,7 +104,9 @@ it('provides a record of groups of refs with the same name', () => {
document.querySelector('#ref2'),
document.querySelector('#ref3')
]
expect(component).toBeCalledWith(expect.objectContaining({ refsAll: { myRef } }))
expect(component).toBeCalledWith(
expect.objectContaining({ refsAll: { myRef } })
)
})

it(`parses props`, () => {
Expand Down Expand Up @@ -187,7 +194,7 @@ it(`exposes the component's refsAll`, () => {

const myRef = [
document.getElementById('ref1'),
document.getElementById('ref2'),
document.getElementById('ref2')
]

mountComponents(document.body)
Expand Down

0 comments on commit 8072694

Please sign in to comment.