readme.md
337 lines
| 9.2 KiB
| text/x-minidsrc
|
MarkdownLexer
/ djx / readme.md
cin
|
r65 | # @implab/djx | ||
## SYNOPSIS | ||||
cin
|
r129 | ```jsx | ||
cin
|
r65 | import { djbase, djclass, bind, prototype, AbstractConstructor } from "@implab/djx/declare"; | ||
import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase"; | ||||
import { createElement } from "@implab/djx/tsx"; | ||||
interface MyWidgetAttrs { | ||||
title: string; | ||||
counter: number; | ||||
} | ||||
interface MyWidgetEvents { | ||||
"count-inc": Event; | ||||
"count-dec": Event; | ||||
} | ||||
@djclass | ||||
export class MyWidget extends djbase( | ||||
DjxWidgetBase as AbstractConstructor<DjxWidgetBase<MyWidgetAttrs, MyWidgetEvents>> | ||||
) { | ||||
@bind({ node: "titleNode", type: "innerHTML" }) | ||||
title = ""; | ||||
@prototype() | ||||
counter = 0; | ||||
render() { | ||||
const Frame = (props: any) => <div>{props.children}</div>; | ||||
return <div | ||||
className="myWidget" | ||||
tabIndex={3} | ||||
style={ alignContent: "center", border: "1px solid" } | ||||
> | ||||
<h1 data-dojo-attach-point="titleNode"></h1> | ||||
<Frame> | ||||
<span class="up-button" onclick={e => this._onIncClick(e)}>[+]</span> | ||||
<span class="down-button" onclick={() => this._onDecClick()}>[-]</span> | ||||
</Frame> | ||||
</div>; | ||||
} | ||||
_onIncClick(e: MouseEvent) { | ||||
this.emit("count-inc", { bubbles: false }); | ||||
} | ||||
_onDecClick() { | ||||
this.emit("count-dec", { bubbles: false }); | ||||
} | ||||
} | ||||
``` | ||||
## DESCRIPTION | ||||
This package provides you with the tools to glue your good-fellow dojo with modern | ||||
techniques of building the webapp. The core concept is to built around widgets and | ||||
using .tsx to write it. Here are some features: | ||||
* `djbase()`, `@djaclass` - traits to declare your classes with `dojo/_base/declare` | ||||
* `@implab/djx/tsx` - traits to build the rendering of your widgets with tsx | ||||
* `DjxWidgetBase` - abstract class which supports tsx markup and | ||||
`data-dojo-attach-*` attributes. | ||||
* `@bind(...)` - annotations provide an easy way of using standard dojo widget | ||||
attribute bindings. | ||||
### djbase, @djclass | ||||
These two traits provide convenient way of using `dojo/_base/declare` in Typescript | ||||
for declaring your classes. | ||||
`djbase(...constructors)` - this method accepts a list of constructors in its | ||||
parameters and returns the **fake** base type which then can be used to derive | ||||
your own class. This allows you to provide the Typescript with the correct | ||||
information about the base type and even use `super`!. The only caveat of | ||||
this approach is that you **MUST** decorate your class with `@djclass` annotation. | ||||
Consider the following example: | ||||
```ts | ||||
import { djbase, djclass } from "@implab/djx/declare"; | ||||
import { FooMixin } from "./FooMixin"; | ||||
import { BarMixin } from "./BarMixin"; | ||||
import { BoxMixin } from "./BoxMixin"; | ||||
@djclass | ||||
export class Baz extends djbase(FooMixin, BarMixin, BoxMixin) { | ||||
writeHello(out: string[]) { | ||||
out.push("-> Baz"); | ||||
super.writeHello(out); | ||||
out.push("<- Baz"); | ||||
} | ||||
} | ||||
``` | ||||
All mixins are declared like the one below: | ||||
```ts | ||||
import { djclass, djbase } from "@implab/djx/declare"; | ||||
interface Super { | ||||
writeHello(out: string[]): void; | ||||
} | ||||
@djclass | ||||
export class BarMixin extends djbase<Super>() { | ||||
writeHello(out: string[]) { | ||||
out.push("-> Bar"); | ||||
super.writeHello(out); | ||||
out.push("<- Bar"); | ||||
} | ||||
} | ||||
``` | ||||
finally create an instance and call the `writeHello` method | ||||
```ts | ||||
const baz = new Baz(); | ||||
const data: string[] = []; | ||||
baz.writeHello(data); | ||||
console.log(data.join("\n")); | ||||
``` | ||||
you will get the following output: | ||||
```text | ||||
-> Baz | ||||
-> Box | ||||
-> Bar | ||||
-> Foo | ||||
<- Foo | ||||
<- Bar | ||||
<- Box | ||||
<- Baz | ||||
``` | ||||
Let's take a closer look at the `Baz` declaration it uses `djbase` to derive | ||||
from three mixins and the class is decorated with `@djclass` to accomplish the | ||||
declaration and make a real constructor. | ||||
To allow access to the next sibling method (in terms of multiple inheritance) | ||||
Dojo provides `this.inherited(arguments)` method but this approach leads to the | ||||
problem with 'strict' mode of ES5 and eliminates the type information about a | ||||
calling method. This library solves the problem calling inherited/next method by | ||||
utilizing `super` keyword. Under the hood there are proxy methods generated in | ||||
the prototype of the declared class which make calls to `this.inherited(...)` | ||||
method. This technique is compatible with 'strict' mode. | ||||
Mixins are declared similar, they also may have the base types although | ||||
the most common case is declaring the mixin without any base classes. To allow | ||||
the mixin to access the next method declare the interface with | ||||
desired methods and use the special form of `djbase<Super>()` without arguments. | ||||
### DjxWidgetBase<Attrs, Events> | ||||
cin
|
r125 | This is the base class for the djx widgets. It declares the abstract method | ||
`render()` which is used to render the content of the widget, like `_TemplatedMixin`. | ||||
This class extends `dijit/_WidgetBase` and contains logic from `_AttachMixin` thus | ||||
it is capable to handle `data-dojo-attach-*` attributes from the rendered markup. | ||||
cin
|
r129 | ```jsx | ||
cin
|
r125 | @djclass | ||
export class MyFirstWidget extends djbase(DjxWidgetBase) { | ||||
render() { | ||||
return <h1>My first widget</h1>; | ||||
} | ||||
} | ||||
``` | ||||
cin
|
r65 | |||
### Markup (.tsx) | ||||
Add to your `tsconfig.json` the following options | ||||
```json | ||||
{ | ||||
"compilerOptions": { | ||||
cin
|
r125 | "types": [ | ||
"@implab/djx", | ||||
"@implab/dojo-typings" | ||||
], | ||||
"skipLibCheck": true, | ||||
cin
|
r65 | "experimentalDecorators": true, | ||
"jsxFactory": "createElement", | ||||
"jsx": "react", | ||||
cin
|
r129 | "target": "ES5", | ||
cin
|
r125 | "lib": ["ES2015", "DOM"] | ||
cin
|
r65 | } | ||
} | ||||
``` | ||||
Import `createElement` into your `.tsx` file | ||||
```ts | ||||
import { createElement } from "@implab/djx/tsx"; | ||||
``` | ||||
You are ready to go! | ||||
cin
|
r125 | ### Adding reactive behavior: refs, watch(...) and watchFor(...) | ||
This library adds some reactive traits to update the generated DOM of the widget. | ||||
Dojo 1.x adds some standard options to deal with dynamic changes: | ||||
* `data-dojo-attach-point` allows to get reference to an element (or a nested widget) | ||||
* widget attribute mappings, allows to bind widget's property to a property of | ||||
the element, referenced by `data-dojo-attach-point`. | ||||
The typical implementation of this technique would look like | ||||
cin
|
r129 | ```jsx | ||
cin
|
r125 | import { createElement } from "@implab/djx/tsx"; | ||
import {djclass, djbase, bind} from "@implab/djx/declare"; | ||||
@djclass | ||||
export class MyFirstWidget extends djbase(DjxWidgetBase) { | ||||
// @bind will generate special attribute mapping | ||||
// _setCaptionAttr = { node: "captionNode", type: "innerHTML" } | ||||
@bind({ node: "captionNode", type: "innerHTML" }) | ||||
caption = "My first widget"; | ||||
render() { | ||||
return <h1 data-dojo-attach-point="captionNode"/>; | ||||
} | ||||
} | ||||
``` | ||||
Despite this is a natural way for the dojo it has some disadvantages: | ||||
1. The compiler doesn't check existence of the attach-point. | ||||
2. Attribute mappings support only simple mappings, it's difficult to update the | ||||
complex rendition. | ||||
This library helps you to get both goals with special trait `watch(...)` | ||||
cin
|
r129 | ```jsx | ||
cin
|
r125 | import { createElement } from "@implab/djx/tsx"; | ||
import { djclass, djbase} from "@implab/djx/declare" | ||||
@djclass | ||||
export class MyFirstWidget extends djbase(DjxWidgetBase) { | ||||
caption = "My first widget"; | ||||
render() { | ||||
return <h1>{watch(this,"caption", value => value)}</h1>; | ||||
} | ||||
} | ||||
``` | ||||
In this example we replaced attach-point with simple call to `watch` function | ||||
which renders string value to text representation (text node). It will create a | ||||
rendition which will observe the `caption` property of the widget and update its | ||||
contents according to the value changes of the property. | ||||
The key feature of this approach that the rendering function within `watch` may | ||||
return a complex rendition. | ||||
cin
|
r129 | ```jsx | ||
cin
|
r125 | // inside some widget | ||
render() { | ||||
return <section> | ||||
{watch(this,"user", value => value && [ | ||||
<UserInfo user={value}/>, | ||||
<LogoutButton click={this._logoutClick}/> | ||||
])} | ||||
</section>; | ||||
} | ||||
private readonly _logoutClick = () => { /* do logout */ } | ||||
``` | ||||
The `watch` function has two forms: | ||||
* `watch(stateful, prop, render)` - observes the specified property of the | ||||
`dojo/Stateful` object (or widget) | ||||
* `watch(observable, render)` - observes the specified observable. It supports | ||||
`rxjs` or `@implab/djx/observable` observables. | ||||
The `render` callback may return almost anything which will be converted to DOM: | ||||
* `boolean`, `null`, `undefined` - ignored, | ||||
* `string` - converted to text node, | ||||
* `array` - converted to DocumentFragment of its elements, | ||||
* DOM Nodes and widgets are left intact, | ||||
* any other kind of value will cause an error. | ||||
cin
|
r129 | The watch method allows to observe a single value, for the large sets of data | ||
this isn't suitable well and may lead to performance issues. Dojo provides | ||||
observable stores to being able to track individual changes. The library provides | ||||
`watchFor(observable, render)` method to render observable query results and | ||||
handle changes on per item basis. | ||||
```jsx | ||||
// inside some widget | ||||
staff = new Observable(new Memory<Employee>()), | ||||
getStuff() { | ||||
return this.staff.query(); | ||||
} | ||||
addEmployee(employee: Employee) { | ||||
this.staff.add(employee); // the rendition will update automatically | ||||
} | ||||
render() { | ||||
return <table> | ||||
<thead> | ||||
<tr><th>Name</th><th>Position</th><th>Salary</th></tr> | ||||
</thead> | ||||
<tbody> | ||||
{watchFor(this.getStaff(), ({name, position, salary}) => | ||||
<tr><td>{name}</td><td>{position}</td><td>{salary}</td></tr> | ||||
)} | ||||
</tbody> | ||||
</table> | ||||
} | ||||
``` | ||||