@@ -0,0 +1,78 | |||
|
1 | import { observe, subject } from "./observable"; | |
|
2 | import * as tap from "tap"; | |
|
3 | ||
|
4 | tap.test("Subject tests", t => { | |
|
5 | ||
|
6 | let nextEvent: (value: string) => void = () => void (0); | |
|
7 | ||
|
8 | const subj1 = observe(subject<string>(({ next }) => { | |
|
9 | t.comment("Start subject"); | |
|
10 | ||
|
11 | nextEvent = next; | |
|
12 | ||
|
13 | return () => { | |
|
14 | nextEvent = () => void (0); | |
|
15 | t.comment("Stop subject"); | |
|
16 | }; | |
|
17 | })); | |
|
18 | ||
|
19 | const h1 = subj1.subscribe({ | |
|
20 | next: v => t.comment(`h1 next: ${v}`) | |
|
21 | }); | |
|
22 | ||
|
23 | nextEvent("first"); | |
|
24 | ||
|
25 | const h2 = subj1.subscribe({ | |
|
26 | next: v => t.comment(`h2 next: ${v}`) | |
|
27 | }); | |
|
28 | ||
|
29 | nextEvent("second"); | |
|
30 | ||
|
31 | h1.unsubscribe(); | |
|
32 | ||
|
33 | nextEvent("third"); | |
|
34 | ||
|
35 | h2.unsubscribe(); | |
|
36 | ||
|
37 | t.pass("Subject finished"); | |
|
38 | t.end(); | |
|
39 | }).catch(e => console.error(e)); | |
|
40 | ||
|
41 | ||
|
42 | tap.test("Subject tests #2", t => { | |
|
43 | ||
|
44 | let nextEvent: (value: string) => void = () => void (0); | |
|
45 | ||
|
46 | const subj1 = observe(subject<string>(({ next, complete }) => { | |
|
47 | t.comment("Start subject"); | |
|
48 | ||
|
49 | complete(); | |
|
50 | nextEvent = next; | |
|
51 | ||
|
52 | return () => { | |
|
53 | nextEvent = () => void (0); | |
|
54 | t.comment("Stop subject"); | |
|
55 | }; | |
|
56 | })); | |
|
57 | ||
|
58 | const h1 = subj1.subscribe({ | |
|
59 | next: v => t.comment(`h1 next: ${v}`) | |
|
60 | }); | |
|
61 | ||
|
62 | nextEvent("first"); | |
|
63 | ||
|
64 | const h2 = subj1.subscribe({ | |
|
65 | next: v => t.comment(`h2 next: ${v}`) | |
|
66 | }); | |
|
67 | ||
|
68 | nextEvent("second"); | |
|
69 | ||
|
70 | h1.unsubscribe(); | |
|
71 | ||
|
72 | nextEvent("third"); | |
|
73 | ||
|
74 | h2.unsubscribe(); | |
|
75 | ||
|
76 | t.pass("Subject finished"); | |
|
77 | t.end(); | |
|
78 | }).catch(e => console.error(e)); No newline at end of file |
@@ -169,7 +169,20 desired methods and use the special form | |||
|
169 | 169 | |
|
170 | 170 | ### DjxWidgetBase<Attrs, Events> |
|
171 | 171 | |
|
172 | TODO | |
|
172 | This is the base class for the djx widgets. It declares the abstract method | |
|
173 | `render()` which is used to render the content of the widget, like `_TemplatedMixin`. | |
|
174 | ||
|
175 | This class extends `dijit/_WidgetBase` and contains logic from `_AttachMixin` thus | |
|
176 | it is capable to handle `data-dojo-attach-*` attributes from the rendered markup. | |
|
177 | ||
|
178 | ```tsx | |
|
179 | @djclass | |
|
180 | export class MyFirstWidget extends djbase(DjxWidgetBase) { | |
|
181 | render() { | |
|
182 | return <h1>My first widget</h1>; | |
|
183 | } | |
|
184 | } | |
|
185 | ``` | |
|
173 | 186 | |
|
174 | 187 | ### Markup (.tsx) |
|
175 | 188 | |
@@ -178,10 +191,16 Add to your `tsconfig.json` the followin | |||
|
178 | 191 | ```json |
|
179 | 192 | { |
|
180 | 193 | "compilerOptions": { |
|
181 |
"types": [ |
|
|
194 | "types": [ | |
|
195 | "@implab/djx", | |
|
196 | "@implab/dojo-typings" | |
|
197 | ], | |
|
198 | "skipLibCheck": true, | |
|
182 | 199 | "experimentalDecorators": true, |
|
183 | 200 | "jsxFactory": "createElement", |
|
184 | 201 | "jsx": "react", |
|
202 | "target": "ES5", // minimal supported version | |
|
203 | "lib": ["ES2015", "DOM"] | |
|
185 | 204 | } |
|
186 | 205 | } |
|
187 | 206 | |
@@ -195,4 +214,93 import { createElement } from "@implab/d | |||
|
195 | 214 | |
|
196 | 215 | You are ready to go! |
|
197 | 216 | |
|
198 | TODO | |
|
217 | ### Adding reactive behavior: refs, watch(...) and watchFor(...) | |
|
218 | ||
|
219 | This library adds some reactive traits to update the generated DOM of the widget. | |
|
220 | Dojo 1.x adds some standard options to deal with dynamic changes: | |
|
221 | ||
|
222 | * `data-dojo-attach-point` allows to get reference to an element (or a nested widget) | |
|
223 | * widget attribute mappings, allows to bind widget's property to a property of | |
|
224 | the element, referenced by `data-dojo-attach-point`. | |
|
225 | ||
|
226 | The typical implementation of this technique would look like | |
|
227 | ||
|
228 | ```tsx | |
|
229 | import { createElement } from "@implab/djx/tsx"; | |
|
230 | import {djclass, djbase, bind} from "@implab/djx/declare"; | |
|
231 | ||
|
232 | @djclass | |
|
233 | export class MyFirstWidget extends djbase(DjxWidgetBase) { | |
|
234 | ||
|
235 | // @bind will generate special attribute mapping | |
|
236 | // _setCaptionAttr = { node: "captionNode", type: "innerHTML" } | |
|
237 | @bind({ node: "captionNode", type: "innerHTML" }) | |
|
238 | caption = "My first widget"; | |
|
239 | ||
|
240 | render() { | |
|
241 | return <h1 data-dojo-attach-point="captionNode"/>; | |
|
242 | } | |
|
243 | } | |
|
244 | ``` | |
|
245 | ||
|
246 | Despite this is a natural way for the dojo it has some disadvantages: | |
|
247 | ||
|
248 | 1. The compiler doesn't check existence of the attach-point. | |
|
249 | 2. Attribute mappings support only simple mappings, it's difficult to update the | |
|
250 | complex rendition. | |
|
251 | ||
|
252 | This library helps you to get both goals with special trait `watch(...)` | |
|
253 | ||
|
254 | ```tsx | |
|
255 | import { createElement } from "@implab/djx/tsx"; | |
|
256 | import { djclass, djbase} from "@implab/djx/declare" | |
|
257 | ||
|
258 | @djclass | |
|
259 | export class MyFirstWidget extends djbase(DjxWidgetBase) { | |
|
260 | ||
|
261 | caption = "My first widget"; | |
|
262 | ||
|
263 | render() { | |
|
264 | return <h1>{watch(this,"caption", value => value)}</h1>; | |
|
265 | } | |
|
266 | } | |
|
267 | ``` | |
|
268 | ||
|
269 | In this example we replaced attach-point with simple call to `watch` function | |
|
270 | which renders string value to text representation (text node). It will create a | |
|
271 | rendition which will observe the `caption` property of the widget and update its | |
|
272 | contents according to the value changes of the property. | |
|
273 | ||
|
274 | The key feature of this approach that the rendering function within `watch` may | |
|
275 | return a complex rendition. | |
|
276 | ||
|
277 | ```tsx | |
|
278 | // inside some widget | |
|
279 | render() { | |
|
280 | return <section> | |
|
281 | {watch(this,"user", value => value && [ | |
|
282 | <UserInfo user={value}/>, | |
|
283 | <LogoutButton click={this._logoutClick}/> | |
|
284 | ])} | |
|
285 | </section>; | |
|
286 | } | |
|
287 | ||
|
288 | private readonly _logoutClick = () => { /* do logout */ } | |
|
289 | ||
|
290 | ``` | |
|
291 | ||
|
292 | The `watch` function has two forms: | |
|
293 | ||
|
294 | * `watch(stateful, prop, render)` - observes the specified property of the | |
|
295 | `dojo/Stateful` object (or widget) | |
|
296 | * `watch(observable, render)` - observes the specified observable. It supports | |
|
297 | `rxjs` or `@implab/djx/observable` observables. | |
|
298 | ||
|
299 | The `render` callback may return almost anything which will be converted to DOM: | |
|
300 | ||
|
301 | * `boolean`, `null`, `undefined` - ignored, | |
|
302 | * `string` - converted to text node, | |
|
303 | * `array` - converted to DocumentFragment of its elements, | |
|
304 | * DOM Nodes and widgets are left intact, | |
|
305 | * any other kind of value will cause an error. | |
|
306 |
@@ -409,7 +409,7 export const stateful = <T>(producer: Pr | |||
|
409 | 409 | case "active": |
|
410 | 410 | if (hasValue) |
|
411 | 411 | s.next(lastValue); // if hasValue is true, |
|
412 |
|
|
|
412 | // lastValue has a valid value | |
|
413 | 413 | subscribers.push(s); |
|
414 | 414 | return () => { |
|
415 | 415 | if (_subscribers === subscribers) { |
@@ -428,10 +428,56 export const stateful = <T>(producer: Pr | |||
|
428 | 428 | }; |
|
429 | 429 | }; |
|
430 | 430 | |
|
431 | const subject = <T>(producer: Producer<T>): Producer<T> => { | |
|
431 | /** Create the producer which will be called once when the first subscriber is | |
|
432 | * attached, next subscribers would share the same producer. When all | |
|
433 | * subscribers are removed the producer will be cleaned up. | |
|
434 | * | |
|
435 | * Use this wrapper to prevent spawning multiple producers. | |
|
436 | * | |
|
437 | * @param producer The source producer | |
|
438 | * @returns The wrapped producer | |
|
439 | */ | |
|
440 | export const subject = <T>(producer: Producer<T>): Producer<T> => { | |
|
432 | 441 | const fusedProducer = fuse(producer); |
|
433 | 442 | |
|
434 | return () => { | |
|
443 | let subscribers: Sink<T>[] = []; | |
|
444 | ||
|
445 | let cleanup = noop; | |
|
435 | 446 | |
|
447 | const sink: Sink<T> = { | |
|
448 | isClosed: () => false, | |
|
449 | complete: () => { | |
|
450 | const _subscribers = subscribers; | |
|
451 | subscribers = []; | |
|
452 | _subscribers.forEach(s => s.complete()); | |
|
453 | cleanup(); | |
|
454 | }, | |
|
455 | error: e => { | |
|
456 | const _subscribers = subscribers; | |
|
457 | subscribers = []; | |
|
458 | _subscribers.forEach(s => s.error(e)); | |
|
459 | cleanup(); | |
|
460 | }, | |
|
461 | next: v => { | |
|
462 | const _subscribers = subscribers; | |
|
463 | _subscribers.forEach(s => s.next(v)); | |
|
464 | } | |
|
465 | }; | |
|
466 | ||
|
467 | return client => { | |
|
468 | const _subscribers = subscribers; | |
|
469 | subscribers.push(client); | |
|
470 | if (subscribers.length === 1) | |
|
471 | cleanup = fusedProducer(sink) ?? noop; | |
|
472 | ||
|
473 | return () => { | |
|
474 | if (_subscribers === subscribers) { | |
|
475 | const pos = subscribers.indexOf(client); | |
|
476 | if (pos >= 0) | |
|
477 | subscribers.splice(pos,1); | |
|
478 | if (!subscribers.length) | |
|
479 | cleanup(); | |
|
480 | } | |
|
481 | }; | |
|
436 | 482 | }; |
|
437 | 483 | }; No newline at end of file |
@@ -1,3 +1,4 | |||
|
1 | 1 | import "./declare-tests"; |
|
2 | 2 | import "./observable-tests"; |
|
3 | import "./state-tests"; No newline at end of file | |
|
3 | import "./state-tests"; | |
|
4 | import "./subject-tests"; No newline at end of file |
@@ -28,15 +28,6 | |||
|
28 | 28 | "typescript": "4.8.3" |
|
29 | 29 | } |
|
30 | 30 | }, |
|
31 | "../djx/build/npm/package": { | |
|
32 | "name": "@implab/djx", | |
|
33 | "dev": true, | |
|
34 | "license": "BSD-2-Clause", | |
|
35 | "peerDependencies": { | |
|
36 | "@implab/core-amd": "^1.4.6", | |
|
37 | "dojo": "^1.10.0" | |
|
38 | } | |
|
39 | }, | |
|
40 | 31 | "node_modules/@eslint/eslintrc": { |
|
41 | 32 | "version": "1.3.1", |
|
42 | 33 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz", |
@@ -113,8 +104,13 | |||
|
113 | 104 | } |
|
114 | 105 | }, |
|
115 | 106 | "node_modules/@implab/djx": { |
|
116 | "resolved": "../djx/build/npm/package", | |
|
117 |
" |
|
|
107 | "resolved": "file:../djx/build/npm/package", | |
|
108 | "dev": true, | |
|
109 | "license": "BSD-2-Clause", | |
|
110 | "peerDependencies": { | |
|
111 | "@implab/core-amd": "^1.4.6", | |
|
112 | "dojo": "^1.10.0" | |
|
113 | } | |
|
118 | 114 | }, |
|
119 | 115 | "node_modules/@implab/dojo-typings": { |
|
120 | 116 | "version": "1.0.2", |
@@ -2783,7 +2779,7 | |||
|
2783 | 2779 | "requires": {} |
|
2784 | 2780 | }, |
|
2785 | 2781 | "@implab/djx": { |
|
2786 | "version": "file:../djx/build/npm/package", | |
|
2782 | "dev": true, | |
|
2787 | 2783 | "requires": {} |
|
2788 | 2784 | }, |
|
2789 | 2785 | "@implab/dojo-typings": { |
General Comments 0
You need to be logged in to leave comments.
Login now