@@ -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 | ### DjxWidgetBase<Attrs, Events> |
|
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 | ### Markup (.tsx) |
|
187 | ### Markup (.tsx) | |
175 |
|
188 | |||
@@ -178,10 +191,16 Add to your `tsconfig.json` the followin | |||||
178 | ```json |
|
191 | ```json | |
179 | { |
|
192 | { | |
180 | "compilerOptions": { |
|
193 | "compilerOptions": { | |
181 |
"types": [ |
|
194 | "types": [ | |
|
195 | "@implab/djx", | |||
|
196 | "@implab/dojo-typings" | |||
|
197 | ], | |||
|
198 | "skipLibCheck": true, | |||
182 | "experimentalDecorators": true, |
|
199 | "experimentalDecorators": true, | |
183 | "jsxFactory": "createElement", |
|
200 | "jsxFactory": "createElement", | |
184 | "jsx": "react", |
|
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 | You are ready to go! |
|
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 | case "active": |
|
409 | case "active": | |
410 | if (hasValue) |
|
410 | if (hasValue) | |
411 | s.next(lastValue); // if hasValue is true, |
|
411 | s.next(lastValue); // if hasValue is true, | |
412 |
|
|
412 | // lastValue has a valid value | |
413 | subscribers.push(s); |
|
413 | subscribers.push(s); | |
414 | return () => { |
|
414 | return () => { | |
415 | if (_subscribers === subscribers) { |
|
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 | const fusedProducer = fuse(producer); |
|
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 | }; No newline at end of file |
|
483 | }; |
@@ -1,3 +1,4 | |||||
1 | import "./declare-tests"; |
|
1 | import "./declare-tests"; | |
2 | import "./observable-tests"; |
|
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 | "typescript": "4.8.3" |
|
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 | "node_modules/@eslint/eslintrc": { |
|
31 | "node_modules/@eslint/eslintrc": { | |
41 | "version": "1.3.1", |
|
32 | "version": "1.3.1", | |
42 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz", |
|
33 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz", | |
@@ -113,8 +104,13 | |||||
113 | } |
|
104 | } | |
114 | }, |
|
105 | }, | |
115 | "node_modules/@implab/djx": { |
|
106 | "node_modules/@implab/djx": { | |
116 | "resolved": "../djx/build/npm/package", |
|
107 | "resolved": "file:../djx/build/npm/package", | |
117 |
" |
|
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 | "node_modules/@implab/dojo-typings": { |
|
115 | "node_modules/@implab/dojo-typings": { | |
120 | "version": "1.0.2", |
|
116 | "version": "1.0.2", | |
@@ -2783,7 +2779,7 | |||||
2783 | "requires": {} |
|
2779 | "requires": {} | |
2784 | }, |
|
2780 | }, | |
2785 | "@implab/djx": { |
|
2781 | "@implab/djx": { | |
2786 | "version": "file:../djx/build/npm/package", |
|
2782 | "dev": true, | |
2787 | "requires": {} |
|
2783 | "requires": {} | |
2788 | }, |
|
2784 | }, | |
2789 | "@implab/dojo-typings": { |
|
2785 | "@implab/dojo-typings": { |
General Comments 0
You need to be logged in to leave comments.
Login now