Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSX pragma #3

Merged
merged 10 commits into from
Jun 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/*", "./**/*"]
}