Skip to content

Commit 47765a3

Browse files
committed
feat: add h2 hyperscript function for assisting snabbdom/react migration
1 parent 2205231 commit 47765a3

File tree

10 files changed

+637
-437
lines changed

10 files changed

+637
-437
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
/lib
55
/dist
66
/.cache
7+
fixtures
8+
support
9+
plugins
710
node_modules
811
package-lock.json
912
pnpm-lock.yaml

cypress.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{
22
"baseUrl": "http://localhost:1234",
3-
"video": false
3+
"chromeWebSecurity": false,
4+
"defaultCommandTimeout": 10000,
5+
"modifyObstructiveCode": false,
6+
"video": false,
7+
"fixturesFolder": false
48
}

cypress/integration/test.spec.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
/// <reference types="cypress" />
22

3+
const { watchFile } = require("fs")
4+
35
context('Page load', () => {
46
beforeEach(() => {
57
cy.visit('/')
8+
cy.wait(500)
69
})
710
describe('React integration', () => {
11+
812
it('Should mount', () => {
913
cy.get('#app')
1014
.should('exist', 'success')
1115
})
16+
it('Should have foo property on button', () => {
17+
cy.get('.clicker')
18+
// .its('foo')
19+
// .should('eq', 3)
20+
.then(($el) => {
21+
cy.wrap($el[0].foo).should('eq', 3)
22+
})
23+
})
1224
})
1325
})

example/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import xs from 'xstream';
22
import {createElement} from 'react';
33
import {render} from 'react-dom';
4-
import {h, makeComponent} from '../src/index';
4+
import {h2, makeComponent} from '../src/index';
55

66
function main(sources) {
77
const init$ = xs.of(() => 0);
@@ -20,9 +20,9 @@ function main(sources) {
2020
.fold((state, fn) => fn(state));
2121

2222
const vdom$ = count$.map(i =>
23-
h('div', [
24-
h('h1', `Hello ${i} times`),
25-
h('button', {sel: btnSel}, 'Reset'),
23+
h2('div', [
24+
h2('h1', `Hello ${i} times`),
25+
h2('button', {sel: btnSel, className: 'clicker', domProps: {foo: 3}}, 'Reset'),
2626
]),
2727
);
2828

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@
4949
"compile": "npm run compile-cjs && npm run compile-es6",
5050
"compile-cjs": "tsc --module commonjs --outDir ./lib/cjs",
5151
"compile-es6": "echo 'TODO' : tsc --module es6 --outDir ./lib/es6",
52-
"test": "$(npm bin)/mocha test/*.ts --require ts-node/register --recursive; cypress run",
52+
"test": "$(npm bin)/mocha test/*.ts --require ts-node/register --recursive",
53+
"full-test": "npm test; npm run cypress:run",
5354
"start": "parcel example/index.html",
54-
"serve-test": "start-server-and-test start http://localhost:1234 test"
55+
"serve-test": "start-server-and-test start http://localhost:1234 full-test",
56+
"cypress:open": "cypress open",
57+
"cypress:run": "cypress run"
5558
}
5659
}

src/h2.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {
2+
createElement,
3+
ReactElement,
4+
ReactNode,
5+
ElementType,
6+
ReactHTML,
7+
Attributes,
8+
Component,
9+
ComponentType,
10+
createRef,
11+
forwardRef
12+
} from 'react';
13+
import {incorporate} from './incorporate';
14+
15+
export type PropsExtensions = {
16+
sel?: string | symbol;
17+
domProps?: any
18+
};
19+
20+
type PropsLike<P> = P & PropsExtensions & Attributes;
21+
22+
type Children = string | Array<ReactNode>;
23+
24+
export function domPropify(Comp: any): ComponentType<any> {
25+
class DomProps extends Component<any, any> {
26+
private ref: any;
27+
private domProps: any;
28+
constructor(props) {
29+
super(props);
30+
this.domProps = this.props.domProps;
31+
this.ref = props.forwardedRef || createRef();
32+
}
33+
34+
public componentDidMount() {
35+
if (this.domProps && this.ref) {
36+
Object.entries(this.domProps).forEach(([key, val]) => {
37+
this.ref.current[key] = val;
38+
});
39+
}
40+
}
41+
42+
render() {
43+
const p: any = {ref: this.ref, ...this.props};
44+
delete p.forwardedRef
45+
delete p.domProps;
46+
return createElement(Comp, p);
47+
}
48+
}
49+
50+
return forwardRef((props, ref) => {
51+
return createElement(DomProps, {...props, forwardedRef: ref});
52+
});
53+
}
54+
55+
export function domHookify(Comp: any): ComponentType<any> {
56+
class DomHooks extends Component<any, any> {
57+
private ref: any;
58+
private hooks: any;
59+
constructor(props) {
60+
super(props);
61+
this.hooks = this.props.domHooks;
62+
this.ref = props.forwardedRef || createRef();
63+
}
64+
65+
public componentDidMount() {
66+
if (this.hooks && this.hooks.insert && this.ref) {
67+
this.hooks.insert({elm: this.ref.current})
68+
}
69+
}
70+
71+
public componentDidUpdate() {
72+
if (this.hooks && this.hooks.update && this.ref) {
73+
this.hooks.update({elm: this.ref.current})
74+
}
75+
}
76+
77+
public componentWillUnmount() {
78+
if (this.hooks && this.hooks.destroy && this.ref) {
79+
this.hooks.destroy({elm: this.ref.current})
80+
}
81+
}
82+
83+
render() {
84+
const p: any = {ref: this.ref, ...this.props};
85+
delete p.forwardedRef
86+
delete p.domHooks;
87+
return createElement(Comp, p);
88+
}
89+
}
90+
91+
return forwardRef((props, ref) => {
92+
return createElement(DomHooks, {...props, forwardedRef: ref});
93+
});
94+
}
95+
96+
function createElementSpreading<P = any>(
97+
type: ElementType<P> | keyof ReactHTML,
98+
props: PropsLike<P> | null,
99+
children: Children,
100+
): ReactElement<P> {
101+
if (typeof children === 'string') {
102+
return createElement(type, props, children);
103+
} else {
104+
return createElement(type, props, ...children);
105+
}
106+
}
107+
108+
function hyperscriptProps<P = any>(
109+
type: ElementType<P> | keyof ReactHTML,
110+
props: PropsLike<P>,
111+
): ReactElement<P> {
112+
if (!props.sel) {
113+
return createElement(type, props);
114+
} else {
115+
return createElement(domHookify(domPropify(incorporate(type))), props);
116+
}
117+
}
118+
119+
function hyperscriptChildren<P = any>(
120+
type: ElementType<P> | keyof ReactHTML,
121+
children: Children,
122+
): ReactElement<P> {
123+
return createElementSpreading(type, null, children);
124+
}
125+
126+
function hyperscriptPropsChildren<P = any>(
127+
type: ElementType<P> | keyof ReactHTML,
128+
props: PropsLike<P>,
129+
children: Children,
130+
): ReactElement<P> {
131+
if (!props.sel) {
132+
return createElementSpreading(type, props, children);
133+
} else {
134+
return createElementSpreading(domHookify(domPropify(incorporate(type))), props, children);
135+
}
136+
}
137+
138+
export function h2<P = any>(
139+
type: ElementType<P> | keyof ReactHTML,
140+
a?: PropsLike<P> | Children,
141+
b?: Children,
142+
): ReactElement<P> {
143+
if (a === undefined && b === undefined) {
144+
return createElement(type, null);
145+
}
146+
if (b === undefined && (typeof a === 'string' || Array.isArray(a))) {
147+
return hyperscriptChildren(type, a as Array<ReactNode>);
148+
}
149+
if (b === undefined && typeof a === 'object' && !Array.isArray(a)) {
150+
return hyperscriptProps(type, a);
151+
}
152+
if (
153+
a !== undefined &&
154+
typeof a !== 'string' &&
155+
!Array.isArray(a) &&
156+
b !== undefined
157+
) {
158+
return hyperscriptPropsChildren(type, a, b);
159+
} else {
160+
throw new Error('Unexpected usage of h() function');
161+
}
162+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export {ScopeContext} from './context';
33
export {Scope} from './scope';
44
export {ReactSource} from './ReactSource';
55
export {h} from './h';
6+
export {h2} from './h2'
67
export {incorporate} from './incorporate';
78
export {StreamRenderer} from './StreamRenderer';

0 commit comments

Comments
 (0)