##// END OF EJS Templates
added observable subject producer
cin -
r125:cede47727a1b v1.7.0 default
parent child
Show More
@@ -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": ["@implab/djx"],
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 // lastValue has a valid value
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 "link": true
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