Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Commit

Permalink
tsx syntax support (#475)
Browse files Browse the repository at this point in the history
* add initial tsx support

* tweak tsx for consumer usage

* add test for tsx

* add unit tests

* readme for tsx

* review feedback
  • Loading branch information
agubler authored May 6, 2017
1 parent 20022ee commit dfd4c66
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ We also provide a suite of pre-built widgets to use in your applications: [(@doj
- [`v` & `w`](#v--w)
- [`v`](#v)
- [`w`](#w)
- [tsx](#tsx)
- [Writing custom widgets](#writing-custom-widgets)
- [Public API](#public-api)
- [The 'properties' lifecycle](#the-properties-lifecycle)
Expand Down Expand Up @@ -173,6 +174,51 @@ w('my-widget', properties, children);
The example above that uses a string for the `widgetConstructor `, is taking advantage of the [widget registry](#widget-registry) functionality.
The widget registry allows for the lazy loading of widgets.
### tsx
In additional to the programatic functions `v` and `w`, widget-core optionally supports the use of the `jsx` syntax known as [`tsx`](https://www.typescriptlang.org/docs/handbook/jsx.html) in TypeScript.
To start to use `jsx` in your project the widgets need to be named with a `.tsx` extension and some configuration is required in the project's `tsconfig.json`:
Add the configuration options for `jsx`:
```
"jsx": "react",
"jsxFactory": "tsx",
```
Include `.tsx` files in the project:
```
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
```
Once the project is configured, `tsx` can be used in a widget's `render` function simply by importing the `tsx` function as `import { tsx } from '@dojo/widget-core/tsx';`
```tsx
class MyWidgetWithTsx extends WidgetBase<MyProperties> {
protected render(): DNode {
const { clear, properties: { completed, count, activeCount, activeFilter } } = this;

return (
<footer classes={this.classes(css.footer)}>
<span classes={this.classes(css.count)}>
<strong>{`${activeCount}`}</strong>
<span>{`${count}`}</span>
</span>
<TodoFilter activeFilter={activeFilter} />
{ completed ? ( <button onclick={clear} /> ) : ( null ) }
</footer>
);
}
}
```
**Note:** Unfortunately `tsx` is not directly used within the module so will report as an unused import so would be needed to be ignored by linters.
### Writing Custom Widgets
The `WidgetBase` class provides the functionality needed to create Custom Widgets.
Expand Down
55 changes: 55 additions & 0 deletions src/tsx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { v, w } from './d';
import { Constructor, DNode } from './interfaces';
import { WNode, VirtualDomProperties } from './interfaces';

declare global {
namespace JSX {
type Element = WNode;
interface ElementAttributesProperty {
properties: {};
}
interface IntrinsicElements {
[key: string]: VirtualDomProperties;
}
}
}

export const REGISTRY_ITEM = Symbol('Identifier for an item from the Widget Registry.');

export class FromRegistry<P> {
static type = REGISTRY_ITEM;
properties: P;
name: string;
}

export function fromRegistry<P>(tag: string): Constructor<FromRegistry<P>> {
return class extends FromRegistry<P> {
properties: P;
static type = REGISTRY_ITEM;
name = tag;
};
}

function spreadChildren(children: any[], child: any): any[] {
if (Array.isArray(child)) {
return child.reduce(spreadChildren, children);
}
else {
return [ ...children, child ];
}
}

export function tsx(tag: any, properties = {}, ...children: any[]): DNode {
children = children.reduce(spreadChildren, []);
properties = properties === null ? {} : properties;
if (typeof tag === 'string') {
return v(tag, properties, children);
}
else if (tag.type === REGISTRY_ITEM) {
const registryItem = new tag();
return w(registryItem.name, properties, children);
}
else {
return w(tag, properties, children);
}
}
1 change: 1 addition & 0 deletions tests/unit/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ import './main';
import './diff';
import './RegistryHandler';
import './Injector';
import './tsx';
54 changes: 54 additions & 0 deletions tests/unit/testIntegration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as registerSuite from 'intern!object';
import * as assert from 'intern/chai!assert';
import { WidgetBase } from '../../src/WidgetBase';
import { fromRegistry, registry } from '../../src/d';
import { WidgetProperties } from '../../src/interfaces';
import { VNode } from '@dojo/interfaces/vdom';
import * as tsx from './../../src/tsx';

registerSuite({
name: 'tsx',
'can use tsx'() {
interface FooProperties extends WidgetProperties {
hello: string;
}
class Foo extends WidgetBase<FooProperties> {
render() {
const { hello } = this.properties;
return (
<header classes={{ background: true }} >
<div>{ hello }</div>
</header>
);
}
}
class Bar extends WidgetBase<any> {
render() {
return <Foo hello='world' />;
}
}

class Qux extends WidgetBase<any> {
render() {
const LazyFoo = fromRegistry<FooProperties>('LazyFoo');
return <LazyFoo hello='cool' />;
}
}

const bar = new Bar();
const barRender = bar.__render__() as VNode;
const barChild = barRender.children![0];
assert.equal(barRender.vnodeSelector, 'header');
assert.equal(barChild.text, 'world');

const qux = new Qux();
const firstQuxRender = qux.__render__();
assert.equal(firstQuxRender, null);

registry.define('LazyFoo', Foo);
const secondQuxRender = qux.__render__() as VNode;
const secondQuxChild = secondQuxRender.children![0];
assert.equal(secondQuxRender.vnodeSelector, 'header');
assert.equal(secondQuxChild.text, 'cool');
}
});
63 changes: 63 additions & 0 deletions tests/unit/tsx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as registerSuite from 'intern!object';
import * as assert from 'intern/chai!assert';
import { WidgetBase } from './../../src/WidgetBase';
import { HNode, WidgetProperties, WNode } from '../../src/interfaces';
import { tsx, fromRegistry, REGISTRY_ITEM } from '../../src/tsx';
import { HNODE } from './../../src/d';

registerSuite({
name: 'tsx',
'create a registry wrapper'() {
const RegistryWrapper = fromRegistry<WidgetProperties>('tag');
assert.strictEqual((<any> RegistryWrapper).type, REGISTRY_ITEM);
const registryWrapper = new RegistryWrapper();
assert.strictEqual(registryWrapper.name, 'tag');
// These will always be undefined but show the type inference of properties.
registryWrapper.properties = {};
assert.isUndefined(registryWrapper.properties.key);
assert.isUndefined(registryWrapper.properties.bind);
},
tsx: {
'tsx generate a HNode'() {
const node: HNode = <HNode> tsx('div', { hello: 'world' }, [ 'child' ]);
assert.deepEqual(node.tag, 'div');
assert.deepEqual(node.properties, { hello: 'world' });
assert.deepEqual(node.children, [ 'child' ]);
assert.strictEqual(node.type, HNODE);
},
'tsx generate a WNode'() {
const node: WNode = <WNode> tsx(WidgetBase, { hello: 'world' }, [ 'child' ]);
assert.deepEqual(node.widgetConstructor, WidgetBase);
assert.deepEqual(node.properties, { hello: 'world' });
assert.deepEqual(node.children, [ 'child' ]);
},
'tsx generate a WNode from a RegistryWrapper'() {
const RegistryWrapper = fromRegistry<WidgetProperties>('tag');
const node: WNode = <WNode> tsx(RegistryWrapper, { hello: 'world' }, [ 'child' ]);
assert.deepEqual(node.widgetConstructor, 'tag');
assert.deepEqual(node.properties, { hello: 'world' });
assert.deepEqual(node.children, [ 'child' ]);
},
'children arrays are spread correctly'() {
const node: HNode = <HNode> tsx('div', { hello: 'world' }, [ 'child', [ 'child-2', [ 'child-3' ] ] ]);
assert.deepEqual(node.tag, 'div');
assert.deepEqual(node.properties, { hello: 'world' });
assert.deepEqual(node.children, [ 'child', 'child-2', 'child-3' ]);
assert.strictEqual(node.type, HNODE);
},
'defaults properties to empty object'() {
const node: HNode = <HNode> tsx('div');
assert.deepEqual(node.tag, 'div');
assert.deepEqual(node.properties, {});
assert.deepEqual(node.children, []);
assert.strictEqual(node.type, HNODE);
},
'defaults `null` properties to empty object'() {
const node: HNode = <HNode> tsx('div', <any> null);
assert.deepEqual(node.tag, 'div');
assert.deepEqual(node.properties, {});
assert.deepEqual(node.children, []);
assert.strictEqual(node.type, HNODE);
}
}
});
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"jsxFactory": "tsx",
"lib": [
"dom",
"es5",
Expand All @@ -25,6 +27,7 @@
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./tests/**/*.ts",
"./typings/index.d.ts"
]
Expand Down

0 comments on commit dfd4c66

Please sign in to comment.