Skip to content

Commit

Permalink
Support JSX as a pragma
Browse files Browse the repository at this point in the history
PR #3

* Upgrade to @cycle/[email protected]

* Upgrade typescript

* Add jsx-factory

* Add tests

* Update readme

* Remove unused import

* Rename JsxFactory -> jsxFactory

* Fix typo in readme

* Fix dangling quote
  • Loading branch information
sliptype authored and staltz committed Jun 6, 2019
1 parent 8b1504a commit 2a06a07
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 5 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
"@types/node": "^10.5.2",
"@types/react": "16.4.x",
"@types/react-dom": "16.x.x",
"react": "16.5.2",
"react-dom": "16.5.2",
"karma": "2.0.2",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1",
Expand All @@ -40,8 +38,10 @@
"karma-typescript": "^3.0.12",
"karma-typescript-es6-transform": "^1.0.4",
"mocha": "^5.2.0",
"react": "16.5.2",
"react-dom": "16.5.2",
"ts-node": "^7.0.1",
"typescript": "~3.1.3"
"typescript": "^3.4.5"
},
"publishConfig": {
"access": "public"
Expand Down
83 changes: 83 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,89 @@ There are also shortcuts for (MVI) intent selectors:
- `div('inc', props, [child1])` becomes `h('div', {sel: 'inc'}, [child1])`
- etc

## JSX

### Babel

Add the following to your webpack config:

```js
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
plugins: [
['transform-react-jsx', { pragma: 'jsxFactory.createElement' }],
]
}
}
]
},
```

If you used `create-cycle-app` you may have to eject to modify the config.

### Automatically providing jsxFactory

You can avoid having to import `jsxFactory` in every jsx file by allowing webpack to provide it:

```js
plugins: [
new webpack.ProvidePlugin({
jsxFactory: ['react-dom', 'jsxFactory']
})
],
```

### Typescript

Add the following to your `tsconfig.json`:

```js
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "jsxFactory.createElement"
}
}
```

If webpack is providing `jsxFactory` you will need to add typings to `custom-typings.d.ts`:

```js
declare var jsxFactory: any;
```


## Usage

```js
import { jsxFactory } from '@cycle/react-dom';

function view(state$: Stream<State>): Stream<ReactElement> {
return state$.map(({ count }) => (
<div>
<h2>Counter: {count}</h2>
<button sel="add">Add</button>
<button sel="subtract">Subtract</button>
</div>
));
}
```

## Notes

Please ensure you are depending on compatible versions of `@cycle/react` and `@cycle/react-dom`. They should both be at least version `2.1.x`.

```
yarn list @cycle/react
```

should return a single result.


## License

MIT, Andre 'Staltz' Medeiros 2018
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,5 @@ export const tr: HyperScriptHelperFn = hh.tr;
export const u: HyperScriptHelperFn = hh.u;
export const ul: HyperScriptHelperFn = hh.ul;
export const video: HyperScriptHelperFn = hh.video;

export { default as jsxFactory } from './jsx-factory';
36 changes: 36 additions & 0 deletions src/jsx-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createElement, ReactElement, ReactType } from 'react';
import { incorporate } from '@cycle/react';
export { Attributes } from 'react';

declare global {
namespace JSX {
interface IntrinsicAttributes {
sel?: string | symbol;
}
}
namespace React {
interface ClassAttributes<T> extends Attributes {
sel?: string | symbol;
}
}
}

type PropsExtensions = {
sel?: string | symbol;
}

function createIncorporatedElement<P = any>(
type: ReactType<P>,
props: P & PropsExtensions | null,
...children: Array<string | ReactElement<any>>
): ReactElement<P> {
if (!props || !props.sel) {
return createElement(type, props, ...children);
} else {
return createElement(incorporate(type), props, ...children);
}
}

export default {
createElement: createIncorporatedElement
}
231 changes: 231 additions & 0 deletions test/jsx-factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { createElement, Attributes, ReactElement, ReactType } from 'react';
const assert = require('assert');
import {ReactSource} from '@cycle/react';
import {makeDOMDriver, jsxFactory} from '../src/index';
import {run} from '@cycle/run';
import xs from 'xstream';

function createRenderTarget(id: string | null = null) {
const element = document.createElement('div');
element.className = 'cycletest';
if (id) {
element.id = id;
}
document.body.appendChild(element);
return element;
}

describe('jsx-factory', function() {
it('w/ nothing', done => {
function main(sources: {react: ReactSource}) {
return {
react: xs.of(<h1></h1>),
};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const h1 = target.querySelector('h1') as HTMLElement;
assert.strictEqual(!!h1, true);
assert.strictEqual(h1.tagName, 'H1');
done();
}, 100);
});

it('w/ text child', done => {
function main(sources: {react: ReactSource}) {
return {
react: xs.of(<h1>heading 1</h1>),
};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const h1 = target.querySelector('h1') as HTMLElement;
assert.strictEqual(!!h1, true);
assert.strictEqual(h1.innerHTML, 'heading 1');
done();
}, 100);
});

it('w/ children array', done => {
function main(sources: {react: ReactSource}) {
return {
react: xs.of(
<section>
<h1>heading 1</h1>
<h2>heading 2</h2>
<h3>heading 3</h3>
</section>
),
};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const section = target.querySelector('section') as HTMLElement;
assert.strictEqual(!!section, true);
assert.strictEqual(section.children.length, 3);
done();
}, 100);
});

it('w/ props', done => {
function main(sources: {react: ReactSource}) {
return {
react: xs.of(<section data-foo="bar"/>),
};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const section = target.querySelector('section') as HTMLElement;
assert.strictEqual(!!section, true);
assert.strictEqual(section.dataset.foo, 'bar');
done();
}, 100);
});

it('w/ props and children', done => {
function main(sources: {react: ReactSource}) {
return {
react: xs.of(
<section data-foo="bar">
<h1>heading 1</h1>
<h2>heading 2</h2>
<h3>heading 3</h3>
</section>
),
};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const section = target.querySelector('section') as HTMLElement;
assert.strictEqual(!!section, true);
assert.strictEqual(section.dataset.foo, 'bar');
assert.strictEqual(section.children.length, 3);
done();
}, 100);
});

it('w/ className', done => {
function main(sources: {react: ReactSource}) {
return {
react: xs.of(<section className="foo"/>),
};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const section = target.querySelector('section') as HTMLElement;
assert.strictEqual(!!section, true);
assert.strictEqual(section.className, 'foo');
done();
}, 100);
});

it('w/ symbol selector', done => {
function main(sources: {react: ReactSource}) {

const inc = Symbol();
const inc$ = sources.react.select(inc).events('click');
const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0);
const vdom$ = count$.map((i: number) => (
<div>
<h1>{'' + i}</h1>
<button sel={inc}/>
</div>
));

return {react: vdom$};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const button = target.querySelector('button') as HTMLElement;
const h1 = target.querySelector('h1') as HTMLElement;
assert.strictEqual(!!button, true);
assert.strictEqual(!!h1, true);
assert.strictEqual(h1.innerHTML, '0');
button.click();
setTimeout(() => {
assert.strictEqual(h1.innerHTML, '1');
done();
}, 100);
}, 100);
});

it('renders functional component', done => {
const Test = () => (<h1>Functional</h1>);

function main() {
const vdom$ = xs.of(<Test/>);
return {react: vdom$};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const h1 = target.querySelector('h1') as HTMLElement;
assert.strictEqual(!!h1, true);
done();
}, 100);
});

it('renders class component', done => {
class Test extends React.Component {
render() {
return (<h1>Class</h1>);
}
}

function main() {
const vdom$ = xs.of(<Test/>);
return {react: vdom$};
}

const target = createRenderTarget();
run(main, {
react: makeDOMDriver(target),
});

setTimeout(() => {
const h1 = target.querySelector('h1') as HTMLElement;
assert.strictEqual(!!h1, true);
done();
}, 100);
});

});
6 changes: 4 additions & 2 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"rootDir": "./",
"outDir": "test/out",
"skipLibCheck": true,
"lib": ["dom", "es5", "scripthost", "es2015"]
"lib": ["dom", "es5", "scripthost", "es2015"],
"jsx": "react",
"jsxFactory": "jsxFactory.createElement"
},
"files": ["../src/index.ts", "render.ts", "hyperscript-helpers.ts"]
"include": ["../src/*", "./**/*"]
}

0 comments on commit 2a06a07

Please sign in to comment.