| @@ -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 | ||
| @@ -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 | |
|  | 443 | let subscribers: Sink<T>[] = []; | |
|  | 444 | ||
|  | 445 | let cleanup = noop; | |
|  | 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 | ||
|  | 434 | 473 | return () => { | 
|  | 435 | ||
|  | 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
                    
                