Skip to content

Commit c9902db

Browse files
committed
feat: attribute support
1 parent 9e0c993 commit c9902db

File tree

7 files changed

+66
-42
lines changed

7 files changed

+66
-42
lines changed

src/__test__/m.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,20 @@ describe('.m', () => {
121121
);
122122
});
123123

124+
it('should create a tag with dataset attribute', () => {
125+
expect(h('div', { attributes: { 'data-test': 'foo' } })).toEqual({
126+
tag: 'div',
127+
attributes: {
128+
'data-test': 'foo',
129+
},
130+
children: undefined,
131+
delta: undefined,
132+
flag: undefined,
133+
key: undefined,
134+
props: {},
135+
});
136+
});
137+
124138
it('should attach ns to props with children with props', () => {
125139
const vnode = {
126140
tag: 'svg',

src/__test__/patch.spec.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@ describe('.patch', () => {
1616

1717
patch(el, h('div', { id: 'el' }, 'bar'));
1818
expect(el).toEqual(createElement(h('div', { id: 'el' }, 'bar')));
19-
expect(el).toEqual(createElement(h('div', { id: 'el' }, 'bar')));
20-
patch(el, h('div', { id: 'el', class: 'new' }, 'baz'));
21-
expect(el).toEqual(createElement(h('div', { id: 'el', class: 'new' }, 'baz')));
19+
patch(el, h('div', { id: 'el', className: 'foo' }, 'baz'));
20+
expect(el).toEqual(createElement(h('div', { id: 'el', className: 'foo' }, 'baz')));
21+
22+
document.body.textContent = '';
23+
});
24+
25+
it('should patch attributes', () => {
26+
const el = createElement(h('div', { id: 'el' }, 'foo'));
27+
28+
patch(el, h('div', { attributes: { 'data-test': 'foo' } }, 'bar'));
29+
expect(el).toEqual(createElement(h('div', { attributes: { 'data-test': 'foo' } }, 'bar')));
2230

2331
document.body.textContent = '';
2432
});

src/createElement.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { OLD_VNODE_FIELD, VNode } from './structs';
22

33
/**
44
* Creates an element from a VNode
5-
* @param {VNode} vnode - VNode to convert to HTMLElement or Text
6-
* @param {boolean} attachField - Attach OLD_VNODE_FIELD
7-
* @returns {HTMLElement|Text}
85
*/
96
export const createElement = (vnode: VNode, attachField = true): HTMLElement | Text => {
107
if (typeof vnode === 'string') return document.createTextNode(vnode);
118
const el = <HTMLElement>Object.assign(document.createElement(vnode.tag), vnode.props);
129

10+
Object.entries(vnode.attributes || {}).forEach(([attrName, attrValue]) => {
11+
el.setAttribute(attrName, attrValue);
12+
});
13+
1314
vnode.children?.forEach((child) => {
1415
el.appendChild(createElement(child));
1516
});

src/jsx.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const h = (tag: string, props?: VProps, children?: VNode[], delta?: VDelta) => {
2121
}
2222
}
2323
if (typeof props?.className === 'object') {
24-
props.className = className(props.className);
24+
props.className = className(<Record<string, boolean>>(<unknown>props.className));
2525
}
2626
if (typeof props?.style === 'object') {
2727
props.style = style(props.style);
@@ -51,7 +51,7 @@ const normalizeChildren = (children: JSXVNode[], normalizedChildren: VNode[]) =>
5151
return normalizedChildren;
5252
};
5353

54-
const jsx = (tag: string | FC, props?: VProps, ...children: JSXVNode[]): VNode => {
54+
const jsx = (tag: string | FC, props?: VProps, ...children: JSXVNode[]): VNode | (() => VNode) => {
5555
let delta: VDelta | undefined;
5656
if (props) {
5757
const rawDelta = <VDelta>(<unknown>props.delta);
@@ -66,7 +66,7 @@ const jsx = (tag: string | FC, props?: VProps, ...children: JSXVNode[]): VNode =
6666
}
6767
const normalizedChildren = normalizeChildren(children, []);
6868
if (typeof tag === 'function') {
69-
return tag(props, normalizedChildren, delta);
69+
return () => tag(props, normalizedChildren, delta);
7070
} else {
7171
return h(tag, props, normalizedChildren, delta);
7272
}

src/m.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
VAttributes,
23
VDelta,
34
VDeltaOperation,
45
VDeltaOperationTypes,
@@ -10,8 +11,6 @@ import {
1011

1112
/**
1213
* Attaches ns props to svg element
13-
* @param {VElement} vnode - SVG VNode
14-
* @returns {VElement}
1514
*/
1615
export const svg = (vnode: VElement): VElement => {
1716
/* istanbul ignore next */
@@ -31,8 +30,6 @@ export const ns = (tag: string, props: VProps, children?: VNode[]): void => {
3130

3231
/**
3332
* Generates a className string based on a classObject
34-
* @param {object} classObject - Object with classes paired with boolean values to toggle
35-
* @returns {string}
3633
*/
3734
export const className = (classObject: Record<string, boolean>): string =>
3835
Object.keys(classObject)
@@ -41,8 +38,6 @@ export const className = (classObject: Record<string, boolean>): string =>
4138

4239
/**
4340
* Generates a style string based on a styleObject
44-
* @param {object} styleObject - Object with styles
45-
* @returns {string}
4641
*/
4742
export const style = (styleObject: Record<string, string>): string =>
4843
Object.entries(styleObject)
@@ -51,8 +46,6 @@ export const style = (styleObject: Record<string, string>): string =>
5146

5247
/**
5348
* Returns an insert (creation) delta operation
54-
* @param {number} positionIdx - Index of delta operation
55-
* @returns {VDeltaOperation}
5649
*/
5750
export const INSERT = (positionIdx = 0): VDeltaOperation => [
5851
VDeltaOperationTypes.INSERT,
@@ -61,8 +54,6 @@ export const INSERT = (positionIdx = 0): VDeltaOperation => [
6154

6255
/**
6356
* Returns an update (modification) delta operation
64-
* @param {number} positionIdx - Index of delta operation
65-
* @returns {VDeltaOperation}
6657
*/
6758
export const UPDATE = (positionIdx = 0): VDeltaOperation => [
6859
VDeltaOperationTypes.UPDATE,
@@ -71,8 +62,6 @@ export const UPDATE = (positionIdx = 0): VDeltaOperation => [
7162

7263
/**
7364
* Returns an delete (removal) delta operation
74-
* @param {number} positionIdx - Index of delta operation
75-
* @returns {VDeltaOperation}
7665
*/
7766
export const DELETE = (positionIdx = 0): VDeltaOperation => [
7867
VDeltaOperationTypes.DELETE,
@@ -81,11 +70,6 @@ export const DELETE = (positionIdx = 0): VDeltaOperation => [
8170

8271
/**
8372
* Helper method for creating a VNode
84-
* @param {string} tag - The tagName of an HTMLElement
85-
* @param {VProps=} props - DOM properties and attributes of an HTMLElement
86-
* @param {VNode[]=} children - Children of an HTMLElement
87-
* @param {VFlags=} flag - Compiler flag for VNode
88-
* @returns {VElement}
8973
*/
9074
export const m = (
9175
tag: string,
@@ -95,13 +79,19 @@ export const m = (
9579
delta?: VDelta,
9680
): VElement => {
9781
let key;
82+
let attributes;
9883
if (props?.key) {
9984
key = <string | undefined>props.key;
10085
delete props.key;
10186
}
87+
if (props?.attributes) {
88+
attributes = <VAttributes>props.attributes;
89+
delete props.attributes;
90+
}
10291
return {
10392
tag,
10493
props,
94+
attributes,
10595
children,
10696
key,
10797
flag,

src/patch.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,12 @@ import {
1111

1212
/**
1313
* Diffs two VNode props and modifies the DOM node based on the necessary changes
14-
* @param {HTMLElement} el - Target element to be modified
15-
* @param {VProps} oldProps - Old VNode props
16-
* @param {VProps} newProps - New VNode props
17-
* @returns {void}
1814
*/
1915
export const patchProps = (
2016
el: HTMLElement,
2117
oldProps: VProps,
2218
newProps: VProps,
19+
areAttributes: boolean,
2320
workQueue: (() => void)[],
2421
): void => {
2522
const skip = new Set<string>();
@@ -29,12 +26,20 @@ export const patchProps = (
2926
if (newPropValue) {
3027
const oldPropValue = oldProps[oldPropName];
3128
if (newPropValue !== oldPropValue) {
32-
if (typeof oldPropValue === 'function' && typeof newPropValue === 'function') {
29+
if (
30+
!areAttributes &&
31+
typeof oldPropValue === 'function' &&
32+
typeof newPropValue === 'function'
33+
) {
3334
if (oldPropValue.toString() !== newPropValue.toString()) {
3435
workQueue.push(() => (el[oldPropName] = newPropValue));
3536
}
3637
} else {
37-
workQueue.push(() => (el[oldPropName] = newPropValue));
38+
workQueue.push(() =>
39+
areAttributes
40+
? el.setAttribute(oldPropName, String(newPropValue))
41+
: (el[oldPropName] = newPropValue),
42+
);
3843
}
3944
}
4045
skip.add(oldPropName);
@@ -48,17 +53,17 @@ export const patchProps = (
4853

4954
for (const newPropName of Object.keys(newProps)) {
5055
if (!skip.has(newPropName)) {
51-
workQueue.push(() => (el[newPropName] = newProps[newPropName]));
56+
workQueue.push(() =>
57+
areAttributes
58+
? el.setAttribute(newPropName, String(newProps[newPropName]))
59+
: (el[newPropName] = newProps[newPropName]),
60+
);
5261
}
5362
}
5463
};
5564

5665
/**
5766
* Diffs two VNode children and modifies the DOM node based on the necessary changes
58-
* @param {HTMLElement} el - Target element to be modified
59-
* @param {VNode[]} oldVNodeChildren - Old VNode children
60-
* @param {VNode[]} newVNodeChildren - New VNode children
61-
* @returns {void}
6267
*/
6368
export const patchChildren = (
6469
el: HTMLElement,
@@ -186,10 +191,6 @@ export const patchChildren = (
186191

187192
/**
188193
* Diffs two VNodes and modifies the DOM node based on the necessary changes
189-
* @param {HTMLElement|Text} el - Target element to be modified
190-
* @param {VNode} newVNode - New VNode
191-
* @param {VNode=} prevVNode - Previous VNode
192-
* @returns {void}
193194
*/
194195
export const patch = (
195196
el: HTMLElement | Text,
@@ -217,6 +218,14 @@ export const patch = (
217218
el,
218219
(<VElement>oldVNode)?.props || {},
219220
(<VElement>newVNode).props || {},
221+
false,
222+
workQueue,
223+
);
224+
patchProps(
225+
el,
226+
(<VElement>oldVNode)?.attributes || {},
227+
(<VElement>newVNode).attributes || {},
228+
true,
220229
workQueue,
221230
);
222231

src/structs.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
export const OLD_VNODE_FIELD = '__m_old_vnode';
55

66
// Props can contain a standard value or a callback function (for events)
7-
export type VProps = Record<string, string | boolean | (() => void)>;
7+
export type VProps = Record<string, string | boolean | (() => void) | VAttributes>;
8+
export type VAttributes = Record<string, string>;
89
export type VNode = VElement | string;
910
export type VDeltaOperation = [VDeltaOperationTypes, number];
1011
export type VDelta = VDeltaOperation[];
@@ -13,6 +14,7 @@ export type VTask = () => void;
1314
export interface VElement {
1415
tag: string;
1516
props?: VProps;
17+
attributes?: VAttributes;
1618
children?: VNode[];
1719
key?: string;
1820
flag?: VFlags;

0 commit comments

Comments
 (0)