diff --git a/README.md b/README.md index e6b90b39..68bc5a8d 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 { + protected render(): DNode { + const { clear, properties: { completed, count, activeCount, activeFilter } } = this; + + return ( + + ); + } +} +``` + +**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. diff --git a/src/tsx.ts b/src/tsx.ts new file mode 100644 index 00000000..36e41b5d --- /dev/null +++ b/src/tsx.ts @@ -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

{ + static type = REGISTRY_ITEM; + properties: P; + name: string; +} + +export function fromRegistry

(tag: string): Constructor> { + return class extends FromRegistry

{ + 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); + } +} diff --git a/tests/unit/all.ts b/tests/unit/all.ts index b6c713f1..5136c891 100644 --- a/tests/unit/all.ts +++ b/tests/unit/all.ts @@ -10,3 +10,4 @@ import './main'; import './diff'; import './RegistryHandler'; import './Injector'; +import './tsx'; diff --git a/tests/unit/testIntegration.tsx b/tests/unit/testIntegration.tsx new file mode 100644 index 00000000..1651f0dd --- /dev/null +++ b/tests/unit/testIntegration.tsx @@ -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 { + render() { + const { hello } = this.properties; + return ( +

+
{ hello }
+
+ ); + } + } + class Bar extends WidgetBase { + render() { + return ; + } + } + + class Qux extends WidgetBase { + render() { + const LazyFoo = fromRegistry('LazyFoo'); + return ; + } + } + + 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'); + } +}); diff --git a/tests/unit/tsx.ts b/tests/unit/tsx.ts new file mode 100644 index 00000000..b77dfd9e --- /dev/null +++ b/tests/unit/tsx.ts @@ -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('tag'); + assert.strictEqual(( 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 = 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 = 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('tag'); + const node: 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 = 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 = 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 = tsx('div', null); + assert.deepEqual(node.tag, 'div'); + assert.deepEqual(node.properties, {}); + assert.deepEqual(node.children, []); + assert.strictEqual(node.type, HNODE); + } + } +}); diff --git a/tsconfig.json b/tsconfig.json index 09111fb3..85031030 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,8 @@ "declaration": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, + "jsx": "react", + "jsxFactory": "tsx", "lib": [ "dom", "es5", @@ -25,6 +27,7 @@ }, "include": [ "./src/**/*.ts", + "./src/**/*.tsx", "./tests/**/*.ts", "./typings/index.d.ts" ]