-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinline.ts
More file actions
158 lines (147 loc) · 4.45 KB
/
inline.ts
File metadata and controls
158 lines (147 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
const patterns = {
tag: /^([a-zA-Z0-9-_]+)/,
id: /\#([a-zA-Z0-9-_]+)/,
class: /\.([a-zA-Z0-9-_]+)/g,
};
/**
* Extracts the HTML tag name from a CSS selector string.
* Handles selectors with IDs (#), classes (.), and attributes ([).
*
* @example
* ExtractTagName<'div#app'> // 'div'
* ExtractTagName<'button.btn'> // 'button'
* ExtractTagName<'.container'> // '' (empty, defaults to 'div')
*/
type ExtractTagName<S extends string> = S extends `${infer Tag}#${string}`
? Tag
: S extends `${infer Tag}.${string}`
? Tag
: S extends `${infer Tag}[${string}`
? Tag
: S;
/**
* Maps a CSS selector to its corresponding HTMLElement type.
* Returns HTMLDivElement as default for unknown tags or class-only selectors.
*
* @example
* ElementFromSelector<'input'> // HTMLInputElement
* ElementFromSelector<'a'> // HTMLAnchorElement
* ElementFromSelector<'.container'> // HTMLDivElement
*/
type ElementFromSelector<S extends string> =
ExtractTagName<S> extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[ExtractTagName<S>]
: HTMLDivElement;
/**
* Valid property types that can follow the selector argument.
* All types are optional and can be mixed in any order.
*/
type Props<S extends string> = [
selector: S,
...(
| HTMLElement
| ((el: ElementFromSelector<S>) => void)
| string
| Partial<ElementFromSelector<S>>
)[],
];
/**
* Creates a typed HTMLElement from a CSS selector with flexible property arguments.
*
* @template S - The selector string type, used for element type inference
* @param props - Selector followed by any number of properties in any order:
* - **selector** (string): CSS selector (tag#id.class1.class2)
* - Tag defaults to 'div' if omitted
* - Examples: 'button.btn', '#app', 'input#email.form-control'
* - **string**: Text content (creates text node)
* - **HTMLElement**: Child element (appends to children)
* - **object**: Element properties (assigned via Object.assign)
* - **function**: Callback receiving the typed element for side effects
*
* @returns Typed HTMLElement matching the selector's tag name
* - 'input' → HTMLInputElement
* - 'button' → HTMLButtonElement
* - '.container' → HTMLDivElement (default)
*
* @example
* // Simple text content
* $('h1', 'Hello World')
* // <h1>Hello World</h1>
*
* @example
* // Element with ID and classes
* $('input#email.form-control')
* // <input id="email" class="form-control">
*
* @example
* // Properties object (fully typed)
* $('input', { type: 'email', placeholder: 'Enter email', required: true })
* // <input type="email" placeholder="Enter email" required>
*
* @example
* // Nested children
* $('.card',
* $('h2', 'Title'),
* $('p', 'Description')
* )
* // <div class="card">
* // <h2>Title</h2>
* // <p>Description</p>
* // </div>
*
* @example
* // Callback for event listeners or imperative logic
* $('button', (el) => {
* el.addEventListener('click', () => console.log('clicked'));
* })
*
* @example
* // Mixed arguments (order independent)
* $('button.btn',
* { type: 'submit', disabled: false },
* 'Submit',
* (el) => el.addEventListener('click', handler)
* )
*
* @example
* // Type inference in callbacks
* $('canvas', { width: 800, height: 600 }, (canvas) => {
* const ctx = canvas.getContext('2d'); // canvas is HTMLCanvasElement
* ctx?.fillRect(0, 0, 100, 100);
* })
*
* @example
* // Dynamic children with spread
* $('ul', ...items.map(item => $('li', item.text)))
*/
export const $ = <S extends string>(
...props: Props<S>
): ElementFromSelector<S> => {
const [selector, ...rest] = props;
const tag = selector.match(patterns.tag)?.[1] as keyof HTMLElementTagNameMap ?? 'div';
const el = document.createElement(tag);
const id = selector.match(patterns.id)?.[1];
if (id) {
el.id = id;
}
const classes = selector.match(patterns.class)?.map((i) => i.slice(1));
if (classes) {
el.classList.add(...classes);
}
rest.forEach((prop) => {
if (prop instanceof HTMLElement) {
// Directly append child element.
el.appendChild(prop);
} else if (typeof prop === 'function') {
// Call the function with the element as argument.
prop(el as ElementFromSelector<S>);
} else if (typeof prop === 'string') {
// Append the text node as child.
el.appendChild(document.createTextNode(prop));
} else {
// Set the properties.
Object.assign(el, prop);
}
});
return el as ElementFromSelector<S>;
};