Skip to content

Commit 9d93073

Browse files
author
arnoson
committed
Add defineProps()
1 parent 5bd8118 commit 9d93073

File tree

5 files changed

+216
-5
lines changed

5 files changed

+216
-5
lines changed

README.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A very simple way to attach javascript to the DOM. When even [petite-vue](https://github.com/vuejs/petite-vue) or [alpine.js](https://github.com/alpinejs/alpine/) would be too much.
44

5-
💾 less than 0.5kb (minify and gzip)
5+
💾 ~ 0.6kb (minify and gzip)
66

77
## Installation
88

@@ -15,19 +15,27 @@ npm i @very-simple/components
1515
```js
1616
// components/gallery.js
1717

18-
import { registerComponent } from '@very-simple/components'
18+
import { registerComponent, defineProps } from '@very-simple/components'
1919

20+
const props = defineProps({ loop: Boolean })
2021
registerComponent('gallery', ({ el, ref, refs }) => {
22+
// Props are read from el's dataset and automatically converted to the correct
23+
// type. Default values are also possible, see documentation.
24+
const { loop } = props(el)
25+
2126
// Multiple HTML elements can have the same `ref` name. They will be
2227
// grouped in `refs` ...
2328
const { slides } = refs
29+
2430
// ... whereas `ref` only stores a single element per name.
2531
const { prev, next } = ref
2632

2733
let currentIndex = 0
2834
const maxIndex = slides.length - 1
2935

3036
const selectSlide = index => {
37+
if (!loop && (index < 0 || index > maxIndex)) return
38+
3139
currentIndex = index < 0 ? maxIndex : index > maxIndex ? 0 : index
3240
slides.forEach((el, index) => (el.hidden = index !== currentIndex))
3341
}
@@ -44,7 +52,7 @@ registerComponent('gallery', ({ el, ref, refs }) => {
4452
```html
4553
<!-- index.html -->
4654

47-
<div id="my-gallery" data-simple-component="gallery">
55+
<div id="my-gallery" data-simple-component="gallery" data-loop="true">
4856
<div data-ref="slides">A</div>
4957
<div data-ref="slides">B</div>
5058
<div data-ref="slides">C</div>
@@ -92,6 +100,44 @@ mountComponent(el: HTMLElement)
92100
mountComponent(root?: HTMLElement)
93101
```
94102
103+
### Define Props
104+
105+
Define properties to automatically convert `el.dataset` properties to the
106+
correct type and enable autocompletion.
107+
108+
```ts
109+
// You can either define a prop's type by proving a constructor ...
110+
defineProps({ answer: Number, enabled: Boolean })
111+
112+
// ... or by providing a default value.
113+
defineProps({ answer: 42, enabled: true })
114+
115+
// For objects and arrays the default value can be wrapped inside a function
116+
defineProps({ list: () => [1, 2, 3] })
117+
```
118+
119+
Example:
120+
121+
```ts
122+
const props = defineProps({
123+
enabled: true,
124+
message: String,
125+
tags: () => ['default']
126+
})
127+
128+
export default registerComponent('my-component', ({ el }) => {
129+
const { enabled, message, tags } = props(el)
130+
})
131+
```
132+
133+
```html
134+
<div
135+
data-simple-component="my-component"
136+
data-message="Hello"
137+
data-tags='[ "very", "simple", "components" ]'
138+
></div>
139+
```
140+
95141
### Ignore Elements
96142

97143
Sometimes it is useful to skip big DOM elements when searching for components

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { registerComponent } from './registerComponent'
22
export { mountComponent, mountComponents } from './mountComponent'
3+
export { defineProps } from './props'

src/props.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
type Constructor =
2+
| NumberConstructor
3+
| StringConstructor
4+
| ArrayConstructor
5+
| ObjectConstructor
6+
| BooleanConstructor
7+
8+
type Props<T> = {
9+
[K in keyof T]: T[K] extends Constructor
10+
? ReturnType<T[K]> | undefined
11+
: T[K] extends () => any
12+
? ReturnType<T[K]>
13+
: T[K]
14+
}
15+
16+
const isConstructor = (value: any) =>
17+
[Number, String, Boolean, Array, Object].includes(value)
18+
19+
const parse = (value: any, type: string) =>
20+
type === 'string'
21+
? String(value)
22+
: type === 'number'
23+
? Number(value)
24+
: type === 'boolean'
25+
? value !== 'false'
26+
: JSON.parse(value)
27+
28+
const getDefaultValue = (definition: any) =>
29+
definition instanceof Function ? definition() : definition
30+
31+
export const defineProps = <T>(definitions: T) => {
32+
return (el: HTMLElement) => {
33+
const props = Object.fromEntries(
34+
Object.entries(definitions).map(([key, definition]) => {
35+
const serializedValue = el.dataset[key]
36+
const providesDefault = !isConstructor(definition)
37+
38+
const defaultValue = providesDefault
39+
? getDefaultValue(definition)
40+
: undefined
41+
42+
const type = providesDefault
43+
? typeof defaultValue
44+
: definition.prototype.constructor.name.toLowerCase()
45+
46+
const value =
47+
serializedValue === undefined
48+
? defaultValue
49+
: parse(serializedValue, type)
50+
51+
return [key, value]
52+
})
53+
)
54+
55+
return props as Props<T>
56+
}
57+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Vitest Snapshot v1
2+
3+
exports[`interferes prop types from default values 1`] = `
4+
{
5+
"array": [
6+
1,
7+
2,
8+
3,
9+
],
10+
"bool": true,
11+
"number": 1,
12+
"obj": {
13+
"hello": "world",
14+
},
15+
"string": "text",
16+
}
17+
`;
18+
19+
exports[`parses props 1`] = `
20+
{
21+
"array": [
22+
1,
23+
2,
24+
3,
25+
],
26+
"bool": true,
27+
"number": 1,
28+
"obj": {
29+
"hello": "world",
30+
},
31+
"string": "text",
32+
}
33+
`;
34+
35+
exports[`provides default values for props 1`] = `
36+
{
37+
"array": [],
38+
"bool": true,
39+
"number": 10,
40+
"obj": {},
41+
"string": "hello",
42+
}
43+
`;

tests/index.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { it, expect, vi } from 'vitest'
2-
import { registerComponent, mountComponent, mountComponents } from '../src'
2+
import {
3+
registerComponent,
4+
mountComponent,
5+
mountComponents,
6+
defineProps
7+
} from '../src'
38

49
it('mounts a component', () => {
510
const component = vi.fn()
@@ -96,6 +101,65 @@ it('provides a record of groups of refs with the same name', () => {
96101
expect(component).toBeCalledWith(expect.objectContaining({ refs: { myRef } }))
97102
})
98103

104+
it(`parses props`, () => {
105+
document.body.innerHTML = `
106+
<div
107+
data-number="1"
108+
data-string="text"
109+
data-bool="true"
110+
data-array="[1,2,3]"
111+
data-obj='{ "hello": "world" }'
112+
></div>
113+
`
114+
const el = document.querySelector('div')
115+
const readProps = defineProps({
116+
number: Number,
117+
string: String,
118+
bool: Boolean,
119+
array: Array,
120+
obj: Object
121+
})
122+
123+
expect(readProps(el!)).toMatchSnapshot()
124+
})
125+
126+
it(`provides default values for props`, () => {
127+
document.body.innerHTML = `<div></div>`
128+
const el = document.querySelector('div')
129+
130+
const readProps = defineProps({
131+
number: 10,
132+
string: 'hello',
133+
bool: true,
134+
array: () => [],
135+
obj: () => ({})
136+
})
137+
138+
expect(readProps(el!)).toMatchSnapshot()
139+
})
140+
141+
it('interferes prop types from default values', () => {
142+
document.body.innerHTML = `
143+
<div
144+
data-number="1"
145+
data-string="text"
146+
data-bool="true"
147+
data-array="[1,2,3]"
148+
data-obj='{ "hello": "world" }'
149+
></div>
150+
`
151+
const el = document.querySelector('div')
152+
const readProps = defineProps({
153+
number: 42,
154+
string: 'default',
155+
bool: false,
156+
array: () => [],
157+
obj: () => ({})
158+
})
159+
160+
expect(readProps(el!)).toMatchSnapshot()
161+
})
162+
99163
it(`exposes the component function's return value`, () => {
100164
const exposed = { test: () => {} }
101165
registerComponent('test', () => exposed)
@@ -104,5 +168,5 @@ it(`exposes the component function's return value`, () => {
104168
<div data-simple-component="test" id="my-id"></div>
105169
`
106170
mountComponents(document.body)
107-
expect(document.getElementById('my-id').$component).toBe(exposed)
171+
expect(document.getElementById('my-id')?.$component).toBe(exposed)
108172
})

0 commit comments

Comments
 (0)