##// END OF EJS Templates
corrected tear down logic handling in observables. Added support for observable query results
cin -
r110:1a190b3a757d v1.4.0 default
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,42 +1,43
1 {
1 {
2 "name": "@implab/djx",
2 "name": "@implab/djx",
3 "version": "0.0.1-dev",
3 "version": "0.0.1-dev",
4 "description": "Supports using dojo version 1 with typescript and .tsx files",
4 "description": "Supports using dojo version 1 with typescript and .tsx files",
5 "keywords": [
5 "keywords": [
6 "dojo",
6 "dojo",
7 "tsx",
7 "tsx",
8 "typescript",
8 "typescript",
9 "widgets"
9 "widgets"
10 ],
10 ],
11 "author": "Implab team",
11 "author": "Implab team",
12 "license": "BSD-2-Clause",
12 "license": "BSD-2-Clause",
13 "repository": "https://code.implab.org/implab/implabjs-djx",
13 "repository": "https://code.implab.org/implab/implabjs-djx",
14 "publishConfig": {
14 "publishConfig": {
15 "access": "public"
15 "access": "public"
16 },
16 },
17 "peerDependencies": {
17 "peerDependencies": {
18 "@implab/core-amd": "^1.4.0",
18 "@implab/core-amd": "^1.4.0",
19 "dojo": "^1.10.0"
19 "dojo": "^1.10.0"
20 },
20 },
21 "devDependencies": {
21 "devDependencies": {
22 "@implab/core-amd": "^1.4.0",
22 "@implab/core-amd": "^1.4.0",
23 "@types/chai": "4.1.3",
23 "@types/chai": "4.1.3",
24 "@types/requirejs": "2.1.31",
24 "@types/requirejs": "2.1.31",
25 "@types/yaml": "1.2.0",
25 "@types/yaml": "1.2.0",
26 "@types/tap": "15.0.7",
26 "@types/tap": "15.0.7",
27 "rxjs": "7.5.6",
27 "dojo": "1.16.0",
28 "dojo": "1.16.0",
28 "@implab/dojo-typings": "1.0.3",
29 "@implab/dojo-typings": "1.0.3",
29 "@typescript-eslint/eslint-plugin": "^5.23.0",
30 "@typescript-eslint/eslint-plugin": "^5.23.0",
30 "@typescript-eslint/parser": "^5.23.0",
31 "@typescript-eslint/parser": "^5.23.0",
31 "eslint": "^8.23.0",
32 "eslint": "^8.23.0",
32 "eslint-config-standard": "^17.0.0",
33 "eslint-config-standard": "^17.0.0",
33 "eslint-plugin-import": "^2.26.0",
34 "eslint-plugin-import": "^2.26.0",
34 "eslint-plugin-n": "^15.2.0",
35 "eslint-plugin-n": "^15.2.0",
35 "eslint-plugin-promise": "^6.0.0",
36 "eslint-plugin-promise": "^6.0.0",
36 "eslint-plugin-react": "^7.29.4",
37 "eslint-plugin-react": "^7.29.4",
37 "requirejs": "2.3.6",
38 "requirejs": "2.3.6",
38 "typescript": "4.8.3",
39 "typescript": "4.8.3",
39 "yaml": "~1.7.2",
40 "yaml": "~1.7.2",
40 "tap": "16.3.0"
41 "tap": "16.3.0"
41 }
42 }
42 }
43 }
@@ -1,147 +1,229
1 import { PromiseOrValue } from "@implab/core-amd/interfaces";
2 import { isPromise } from "@implab/core-amd/safe";
3
1 /**
4 /**
2 * The interface for the consumer of an observable sequence
5 * The interface for the consumer of an observable sequence
3 */
6 */
4 export interface Observer<T> {
7 export interface Observer<T> {
5 /**
8 /**
6 * Called for the next element in the sequence
9 * Called for the next element in the sequence
7 */
10 */
8 next: (value: T) => void;
11 next: (value: T) => void;
9
12
10 /**
13 /**
11 * Called once when the error occurs in the sequence.
14 * Called once when the error occurs in the sequence.
12 */
15 */
13 error: (e: unknown) => void;
16 error: (e: unknown) => void;
14
17
15 /**
18 /**
16 * Called once at the end of the sequence.
19 * Called once at the end of the sequence.
17 */
20 */
18 complete: () => void;
21 complete: () => void;
19 }
22 }
20
23
21 /**
24 /**
22 * The group of functions to feed an observable. This methods are provided to
25 * The group of functions to feed an observable. These methods are provided to
23 * the producer to generate a stream of events.
26 * the producer to generate a stream of events.
24 */
27 */
25 export type Sink<T> = {
28 export type Sink<T> = {
26 [k in keyof Observer<T>]: (this: void, ...args: Parameters<Observer<T>[k]>) => void;
29 /**
30 * Call to send the next element in the sequence
31 */
32 next: (value: T) => void;
33
34 /**
35 * Call to notify about the error occurred in the sequence.
36 */
37 error: (e: unknown) => void;
38
39 /**
40 * Call to signal the end of the sequence.
41 */
42 complete: () => void;
43
44 /**
45 * Checks whether the sink is accepting new elements. It's safe to
46 * send elements to the closed sink.
47 */
48 isClosed: () => boolean;
27 };
49 };
28
50
29 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
51 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
30
52
31 export interface Unsubscribable {
53 export interface Unsubscribable {
32 unsubscribe(): void;
54 unsubscribe(): void;
33 }
55 }
34
56
35 export const isUnsubsribable = (v: unknown): v is Unsubscribable =>
57 export const isUnsubsribable = (v: unknown): v is Unsubscribable =>
36 v !== null && v !== undefined && typeof (v as Unsubscribable).unsubscribe === "function";
58 v !== null && v !== undefined && typeof (v as Unsubscribable).unsubscribe === "function";
37
59
38 export const isSubsribable = <T = unknown>(v: unknown): v is Subscribable<T> =>
60 export const isSubsribable = <T = unknown>(v: unknown): v is Subscribable<T> =>
39 v !== null && v !== undefined && typeof (v as Subscribable<unknown>).subscribe === "function";
61 v !== null && v !== undefined && typeof (v as Subscribable<unknown>).subscribe === "function";
40
62
41 export interface Subscribable<T> {
63 export interface Subscribable<T> {
42 subscribe(consumer: Partial<Observer<T>>): Unsubscribable;
64 subscribe(consumer: Partial<Observer<T>>): Unsubscribable;
43 }
65 }
44
66
45 /** The observable source of items. */
67 /** The observable source of items. */
46 export interface Observable<T> extends Subscribable<T> {
68 export interface Observable<T> extends Subscribable<T> {
47 /** Transforms elements of the sequence with the specified mapper
69 /** Transforms elements of the sequence with the specified mapper
48 *
70 *
49 * @param mapper The mapper used to transform the values
71 * @param mapper The mapper used to transform the values
50 */
72 */
51 map<T2>(mapper: (value: T) => T2): Observable<T2>;
73 map<T2>(mapper: (value: T) => T2): Observable<T2>;
52
74
53 /** Filters elements of the sequence. The resulting sequence will
75 /** Filters elements of the sequence. The resulting sequence will
54 * contain only elements which match the specified predicate.
76 * contain only elements which match the specified predicate.
55 *
77 *
56 * @param predicate The filter predicate.
78 * @param predicate The filter predicate.
57 */
79 */
58 filter(predicate: (value: T) => boolean): Observable<T>;
80 filter(predicate: (value: T) => boolean): Observable<T>;
59
81
60 /** Applies accumulator to each value in the sequence and
82 /** Applies accumulator to each value in the sequence and
61 * emits the accumulated value for each source element
83 * emits the accumulated value for each source element
62 *
84 *
63 * @param accumulator
85 * @param accumulator
64 * @param initial
86 * @param initial
65 */
87 */
66 scan<A>(accumulator: (acc: A, value: T) => A, initial: A): Observable<A>;
88 scan<A>(accumulator: (acc: A, value: T) => A, initial: A): Observable<A>;
89
90 cat(...seq: Subscribable<T>[]): Observable<T>;
67 }
91 }
68
92
69 const noop = () => { };
93 const noop = () => { };
70
94
71 const sink = <T>(consumer: Partial<Observer<T>>) => {
95 const sink = <T>(consumer: Partial<Observer<T>>) => {
72 const { next, error, complete } = consumer;
96 const { next, error, complete } = consumer;
73 return {
97 return {
74 next: next ? next.bind(consumer) : noop,
98 next: next ? next.bind(consumer) : noop,
75 error: error ? error.bind(consumer) : noop,
99 error: error ? error.bind(consumer) : noop,
76 complete: complete ? complete.bind(consumer) : noop
100 complete: complete ? complete.bind(consumer) : noop,
101 isClosed: () => false
77 };
102 };
78 };
103 };
79
104
80 const fuse = <T>({ next, error, complete }: Sink<T>) => {
105 /** Wraps the producer to handle tear down logic and subscription management
106 *
107 * @param producer The producer to wrap
108 * @returns The wrapper producer
109 */
110 const fuse = <T>(producer: Producer<T>) => ({ next, error, complete }: Sink<T>) => {
81 let done = false;
111 let done = false;
82 return {
112 let cleanup = noop;
113
114 const _fin = <A extends unknown[]>(fn: (...args: A) => void) =>
115 (...args: A) => done ?
116 void (0) :
117 (done = true, cleanup(), fn(...args));
118
119 const safeSink = {
83 next: (value: T) => { !done && next(value); },
120 next: (value: T) => { !done && next(value); },
84 error: (e: unknown) => { !done && (done = true, error(e)); },
121 error: _fin(error),
85 complete: () => { !done && (done = true, complete()); }
122 complete: _fin(complete),
123 isClosed: () => done
86 };
124 };
125 cleanup = producer(safeSink) ?? noop;
126 return done ?
127 (cleanup(), noop) :
128 _fin(noop);
87 };
129 };
88
130
89 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
131 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
90 subscribe: (consumer: Partial<Observer<T>>) => ({
132 subscribe: (consumer: Partial<Observer<T>>) => ({
91 unsubscribe: producer(sink(consumer)) ?? noop
133 unsubscribe: producer(sink(consumer)) ?? noop
92 }),
134 }),
93 map: (mapper) => _observe(({ next, error, complete }) =>
135 map: (mapper) => _observe(({ next, ...rest }) =>
94 producer({
136 producer({
95 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
137 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
96 error,
138 ...rest
97 complete
139 })
140 ),
141 filter: (predicate) => _observe(({ next, ...rest }) =>
142 producer({
143 next: next !== noop ? (v: T) => predicate(v) ? next(v) : void (0) : noop,
144 ...rest
98 })
145 })
99 ),
146 ),
100 filter: (predicate) => _observe(({ next, error, complete }) =>
147 scan: (accumulator, initial) => _observe(({ next, ...rest }) => {
101 producer({
102 next: next !== noop ?
103 (v: T) => predicate(v) ? next(v) : void (0) : noop,
104 error,
105 complete
106 })
107 ),
108 scan: (accumulator, initial) => _observe(({ next, error, complete }) => {
109 let _acc = initial;
148 let _acc = initial;
110 return producer({
149 return producer({
111 next: next !== noop ?
150 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
112 (v: T) => next(_acc = accumulator(_acc, v)) : noop,
151 ...rest
113 error,
114 complete
115 });
152 });
153 }),
154
155 cat: (...seq) => _observe(({ next, complete: final, ...rest }) => {
156 let cleanup: () => void;
157 const complete = () => {
158 const continuation = seq.shift();
159 if (continuation) {
160 // if we have a next sequence, subscribe to it
161 const subscription = continuation.subscribe({ next, complete, ...rest });
162 cleanup = subscription.unsubscribe.bind(subscription);
163 } else {
164 // otherwise notify the consumer about completion
165 final();
166 }
167 };
168
169 cleanup = producer({ next, complete, ...rest }) ?? noop;
170
171 return () => cleanup();
116 })
172 })
117 });
173 });
118
174
119 export const observe = <T>(producer: Producer<T>): Observable<T> => ({
175 export interface OrderUpdate<T> {
120 subscribe: (consumer: Partial<Observer<T>>) => ({
176 /** The item is being updated */
121 unsubscribe: producer(fuse(sink(consumer))) ?? noop
177 item: T;
122 }),
178
123 map: (mapper) => _observe(({ next, error, complete }) =>
179 /** The previous index of the item, -1 in case it is inserted */
124 producer(fuse({
180 prevIndex: number;
125 next: next !== noop ?
181
126 (v: T) => next(mapper(v)) : noop,
182 /** The new index of the item, -1 in case it is deleted */
127 error,
183 newIndex: number;
128 complete
184 }
129 }))
185
130 ),
186 interface ObservableResults<T> {
131 filter: (predicate) => _observe(({ next, error, complete }) =>
187 /**
132 producer(fuse({
188 * Allows observation of results
133 next: next !== noop ?
189 */
134 (v: T) => predicate(v) ? next(v) : void (0) : noop,
190 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
135 error,
191 remove(): void;
136 complete
192 };
137 }))
193 }
138 ),
194
139 scan: (accumulator, initial) => observe(({ next, error, complete }) => {
195 interface Queryable<T, A extends unknown[]> {
140 let _acc = initial;
196 query(...args: A): PromiseOrValue<T[]>;
141 return producer(fuse({
197 }
142 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
198
143 error,
199 export const isObservableResults = <T>(v: object): v is ObservableResults<T> =>
144 complete
200 v && (typeof (v as { observe?: unknown; }).observe === "function");
145 }));
201
146 })
202 export const observe = <T>(producer: Producer<T>) => _observe(fuse(producer));
203
204 export const empty = observe<never>(({ complete }) => complete());
205
206 export const query = <T, A extends unknown[]>(store: Queryable<T, A>) =>
207 (...args: A) => {
208 return observe<OrderUpdate<T>>(({ next, complete, error }) => {
209 try {
210 const results = store.query(...args);
211 if (isPromise(results)) {
212 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
213 .then(undefined, error);
214 } else {
215 results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
216 }
217
218 if (isObservableResults<T>(results)) {
219 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }));
220 return () => h.remove();
221 } else {
222 complete();
223 }
224 } catch (err) {
225 error(err);
226 }
147 });
227 });
228
229 };
@@ -1,198 +1,187
1 import { Constructor } from "@implab/core-amd/interfaces";
1 import { Constructor } from "@implab/core-amd/interfaces";
2 import { HtmlRendition } from "./tsx/HtmlRendition";
2 import { HtmlRendition } from "./tsx/HtmlRendition";
3 import { WidgetRendition } from "./tsx/WidgetRendition";
3 import { WidgetRendition } from "./tsx/WidgetRendition";
4 import { isElementNode, isWidget, isWidgetConstructor, Rendition } from "./tsx/traits";
4 import { isElementNode, isWidget, isWidgetConstructor, Rendition } from "./tsx/traits";
5 import { FunctionRendition } from "./tsx/FunctionRendition";
5 import { FunctionRendition } from "./tsx/FunctionRendition";
6 import Stateful = require("dojo/Stateful");
6 import Stateful = require("dojo/Stateful");
7 import _WidgetBase = require("dijit/_WidgetBase");
7 import _WidgetBase = require("dijit/_WidgetBase");
8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
9 import { WatchRendition } from "./tsx/WatchRendition";
9 import { WatchRendition } from "./tsx/WatchRendition";
10 import { Observable, observe, Subscribable } from "./observable";
10 import { Observable, observe, OrderUpdate, Subscribable } from "./observable";
11 import djAttr = require("dojo/dom-attr");
11 import djAttr = require("dojo/dom-attr");
12 import djClass = require("dojo/dom-class");
12 import djClass = require("dojo/dom-class");
13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
14
14
15 export function createElement<T extends Constructor | string | ((props: object) => Element)>(elementType: T, ...args: unknown[]): Rendition {
15 export function createElement<T extends Constructor | string | ((props: object) => Element)>(elementType: T, ...args: unknown[]): Rendition {
16 if (typeof elementType === "string") {
16 if (typeof elementType === "string") {
17 const ctx = new HtmlRendition(elementType);
17 const ctx = new HtmlRendition(elementType);
18 if (args)
18 if (args)
19 args.forEach(x => ctx.visitNext(x));
19 args.forEach(x => ctx.visitNext(x));
20
20
21 return ctx;
21 return ctx;
22 } else if (isWidgetConstructor(elementType)) {
22 } else if (isWidgetConstructor(elementType)) {
23 const ctx = new WidgetRendition(elementType);
23 const ctx = new WidgetRendition(elementType);
24 if (args)
24 if (args)
25 args.forEach(x => ctx.visitNext(x));
25 args.forEach(x => ctx.visitNext(x));
26
26
27 return ctx;
27 return ctx;
28 } else if (typeof elementType === "function") {
28 } else if (typeof elementType === "function") {
29 const ctx = new FunctionRendition(elementType as (props: unknown) => Element);
29 const ctx = new FunctionRendition(elementType as (props: unknown) => Element);
30 if (args)
30 if (args)
31 args.forEach(x => ctx.visitNext(x));
31 args.forEach(x => ctx.visitNext(x));
32
32
33 return ctx;
33 return ctx;
34 } else {
34 } else {
35 throw new Error(`The element type '${String(elementType)}' is unsupported`);
35 throw new Error(`The element type '${String(elementType)}' is unsupported`);
36 }
36 }
37 }
37 }
38
38
39 export interface EventDetails<T = unknown> {
39 export interface EventDetails<T = unknown> {
40 detail: T;
40 detail: T;
41 }
41 }
42
42
43 export interface EventSelector {
43 export interface EventSelector {
44 selectorTarget: HTMLElement;
44 selectorTarget: HTMLElement;
45 target: HTMLElement;
45 target: HTMLElement;
46 }
46 }
47
47
48 export interface QueryResultUpdate<T> {
49 /** The item is being updated */
50 item: T;
51
52 /** The previous index of the item, -1 in case it is inserted */
53 prevIndex: number;
54
55 /** The new index of the item, -1 in case it is deleted */
56 newIndex: number;
57 }
58
59 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
48 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
60
49
61 type StatefulProps<T> = T extends Stateful<infer A> ? A :
50 type StatefulProps<T> = T extends Stateful<infer A> ? A :
62 T extends _WidgetBase ? T : never;
51 T extends _WidgetBase ? T : never;
63
52
64
53
65 /**
54 /**
66 * Observers the property and calls render callback each change.
55 * Observers the property and calls render callback each change.
67 *
56 *
68 * @param target The target object which property will be observed.
57 * @param target The target object which property will be observed.
69 * @param prop The name of the property.
58 * @param prop The name of the property.
70 * @param render The callback which will be called every time the value is changed
59 * @param render The callback which will be called every time the value is changed
71 * @returns Rendition which is created instantly
60 * @returns Rendition which is created instantly
72 */
61 */
73 export function watch<W extends _WidgetBase, K extends keyof W>(
62 export function watch<W extends _WidgetBase, K extends keyof W>(
74 target: W,
63 target: W,
75 prop: K,
64 prop: K,
76 render: (model: W[K]) => unknown
65 render: (model: W[K]) => unknown
77 ): Rendition;
66 ): Rendition;
78 /**
67 /**
79 * Observers the property and calls render callback each change.
68 * Observers the property and calls render callback each change.
80 *
69 *
81 * @param target The target object which property will be observed.
70 * @param target The target object which property will be observed.
82 * @param prop The name of the property.
71 * @param prop The name of the property.
83 * @param render The callback which will be called every time the value is changed
72 * @param render The callback which will be called every time the value is changed
84 * @returns Rendition which is created instantly
73 * @returns Rendition which is created instantly
85 */
74 */
86 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
75 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
87 target: T,
76 target: T,
88 prop: K,
77 prop: K,
89 render: (model: StatefulProps<T>[K]) => unknown
78 render: (model: StatefulProps<T>[K]) => unknown
90 ): Rendition;
79 ): Rendition;
91 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
80 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
92 export function watch(
81 export function watch(
93 ...args: [Stateful, string, (model: unknown) => unknown] |
82 ...args: [Stateful, string, (model: unknown) => unknown] |
94 [Subscribable<unknown>, (model: unknown) => unknown]
83 [Subscribable<unknown>, (model: unknown) => unknown]
95 ) {
84 ) {
96 if (args.length === 3) {
85 if (args.length === 3) {
97 const [target, prop, render] = args;
86 const [target, prop, render] = args;
98 return new WatchRendition(
87 return new WatchRendition(
99 render,
88 render,
100 observe(({next}) => {
89 observe(({next}) => {
101 const h = target.watch(
90 const h = target.watch(
102 prop,
91 prop,
103 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
92 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
104 );
93 );
105 next(target.get(prop));
94 next(target.get(prop));
106 return () => h.remove();
95 return () => h.remove();
107 })
96 })
108 );
97 );
109 } else {
98 } else {
110 const [subj, render] = args;
99 const [subj, render] = args;
111 return new WatchRendition(render, subj);
100 return new WatchRendition(render, subj);
112 }
101 }
113 }
102 }
114
103
115 export const watchFor = <T>(source: T[] | Subscribable<QueryResultUpdate<T>>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
104 export const watchFor = <T>(source: T[] | Subscribable<OrderUpdate<T>>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
116 return new WatchForRendition({
105 return new WatchForRendition({
117 ...opts,
106 ...opts,
118 subject: source,
107 subject: source,
119 component: render
108 component: render
120 });
109 });
121 };
110 };
122
111
123
112
124 export const prop: {
113 export const prop: {
125 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
114 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
126 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
115 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
127 } = (target: Stateful, name: string) => {
116 } = (target: Stateful, name: string) => {
128 return observe(({next}) => {
117 return observe(({next}) => {
129 const h = target.watch(
118 const h = target.watch(
130 name,
119 name,
131 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
120 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
132 );
121 );
133 next(target.get(name));
122 next(target.get(name));
134 return () => h.remove();
123 return () => h.remove();
135 });
124 });
136 };
125 };
137
126
138 export const attach = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
127 export const attach = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
139
128
140 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
129 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
141 let h = { unsubscribe() { } };
130 let h = { unsubscribe() { } };
142
131
143 return (el: Element | { set(name: K, value: T): void; } | undefined) => {
132 return (el: Element | { set(name: K, value: T): void; } | undefined) => {
144 if (el) {
133 if (el) {
145 if (isElementNode(el)) {
134 if (isElementNode(el)) {
146 h = subj.subscribe({
135 h = subj.subscribe({
147 next: value => djAttr.set(el, attr, value)
136 next: value => djAttr.set(el, attr, value)
148 });
137 });
149 } else {
138 } else {
150 h = subj.subscribe({
139 h = subj.subscribe({
151 next: value => el.set(attr, value)
140 next: value => el.set(attr, value)
152 });
141 });
153 }
142 }
154 } else {
143 } else {
155 h.unsubscribe();
144 h.unsubscribe();
156 }
145 }
157 };
146 };
158 };
147 };
159
148
160 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
149 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
161 let h = { unsubscribe() { } };
150 let h = { unsubscribe() { } };
162 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
151 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
163 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
152 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
164 if (el) {
153 if (el) {
165 h = subj.subscribe({
154 h = subj.subscribe({
166 next: v => djClass.toggle(el, className, v)
155 next: v => djClass.toggle(el, className, v)
167 });
156 });
168 } else {
157 } else {
169 h.unsubscribe();
158 h.unsubscribe();
170 }
159 }
171 };
160 };
172 };
161 };
173
162
174 export const all = <T, A extends JSX.Ref<T>[]>(...cbs: A): JSX.Ref<T> => (arg: T | undefined) => cbs.forEach(cb => cb(arg));
163 export const all = <T, A extends JSX.Ref<T>[]>(...cbs: A): JSX.Ref<T> => (arg: T | undefined) => cbs.forEach(cb => cb(arg));
175
164
176 /** Decorates the method which will be registered as the handle for the specified event.
165 /** Decorates the method which will be registered as the handle for the specified event.
177 * This decorator can be applied to DjxWidgetBase subclass methods.
166 * This decorator can be applied to DjxWidgetBase subclass methods.
178 *
167 *
179 * ```
168 * ```
180 * @on("click")
169 * @on("click")
181 * _onClick(eventObj: MouseEvent) {
170 * _onClick(eventObj: MouseEvent) {
182 * // ...
171 * // ...
183 * }
172 * }
184 * ```
173 * ```
185 */
174 */
186 export const on = <E extends string>(...eventNames: E[]) =>
175 export const on = <E extends string>(...eventNames: E[]) =>
187 <K extends string,
176 <K extends string,
188 T extends DjxWidgetBase<object, { [p in E]: EV }>,
177 T extends DjxWidgetBase<object, { [p in E]: EV }>,
189 EV extends Event
178 EV extends Event
190 >(
179 >(
191 target: T,
180 target: T,
192 key: K,
181 key: K,
193 // eslint-disable-next-line @typescript-eslint/no-unused-vars
182 // eslint-disable-next-line @typescript-eslint/no-unused-vars
194 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
183 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
195 ) => {
184 ) => {
196 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
185 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
197 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
186 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
198 };
187 };
@@ -1,215 +1,203
1 import { id as mid } from "module";
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 import { argumentNotNull } from "@implab/core-amd/safe";
3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render } from "./render";
4 import { getScope, render } from "./render";
5 import { RenditionBase } from "./RenditionBase";
5 import { RenditionBase } from "./RenditionBase";
6 import { Scope } from "./Scope";
6 import { Scope } from "./Scope";
7 import { Cancellation } from "@implab/core-amd/Cancellation";
7 import { Cancellation } from "@implab/core-amd/Cancellation";
8 import { collectNodes, destroy as safeDestroy, isDocumentFragmentNode, isElementNode, isMounted, placeAt, startupWidgets } from "./traits";
8 import { collectNodes, destroy as safeDestroy, isDocumentFragmentNode, isElementNode, isMounted, placeAt, startupWidgets } from "./traits";
9 import { IDestroyable } from "@implab/core-amd/interfaces";
9 import { IDestroyable } from "@implab/core-amd/interfaces";
10 import { play } from "../play";
10 import { play } from "../play";
11 import * as fx from "dojo/fx";
11 import * as fx from "dojo/fx";
12 import { isSubsribable, Subscribable } from "../observable";
12 import { isObservableResults, isSubsribable, OrderUpdate, Subscribable } from "../observable";
13 import { QueryResultUpdate } from "../tsx";
14
13
15 const trace = TraceSource.get(mid);
14 const trace = TraceSource.get(mid);
16
15
17 interface ItemRendition {
16 interface ItemRendition {
18 nodes: Node[];
17 nodes: Node[];
19
18
20 scope: IDestroyable;
19 scope: IDestroyable;
21
20
22 destroy(): void;
21 destroy(): void;
23 }
22 }
24
23
25 interface ObservableResults<T> {
24 interface RenderTask<T> extends OrderUpdate<T> {
26 /**
27 * Allows observation of results
28 */
29 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
30 remove(): void;
31 };
32 }
33
34 interface RenderTask<T> extends QueryResultUpdate<T> {
35 animate: boolean;
25 animate: boolean;
36 }
26 }
37
27
38 export interface AnimationAttrs {
28 export interface AnimationAttrs {
39 animate?: boolean;
29 animate?: boolean;
40
30
41 animateIn?: (nodes: Node[]) => Promise<void>;
31 animateIn?: (nodes: Node[]) => Promise<void>;
42
32
43 animateOut?: (nodes: Node[]) => Promise<void>;
33 animateOut?: (nodes: Node[]) => Promise<void>;
44 }
34 }
45
35
46 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
36 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
47 subject: T[] | Subscribable<QueryResultUpdate<T>>;
37 subject: T[] | Subscribable<OrderUpdate<T>>;
48
38
49 component: (arg: T, index: number) => unknown;
39 component: (arg: T, index: number) => unknown;
50 }
40 }
51
41
52 const isObservable = <T>(v: ArrayLike<T>): v is ArrayLike<T> & ObservableResults<T> =>
53 v && (typeof (v as { observe?: unknown; }).observe === "function");
54
42
55 const noop = () => { };
43 const noop = () => { };
56
44
57 const fadeIn = (nodes: Node[]) => Promise.all(nodes
45 const fadeIn = (nodes: Node[]) => Promise.all(nodes
58 .filter(isElementNode)
46 .filter(isElementNode)
59 .map(el => play(fx.fadeIn({ node: el as HTMLElement })))
47 .map(el => play(fx.fadeIn({ node: el as HTMLElement })))
60 ).then(noop);
48 ).then(noop);
61
49
62 const fadeOut = (nodes: Node[]) => Promise.all(nodes
50 const fadeOut = (nodes: Node[]) => Promise.all(nodes
63 .filter(isElementNode)
51 .filter(isElementNode)
64 .map(el => play(fx.fadeOut({ node: el as HTMLElement })))
52 .map(el => play(fx.fadeOut({ node: el as HTMLElement })))
65 ).then(noop);
53 ).then(noop);
66
54
67
55
68 export class WatchForRendition<T> extends RenditionBase<Node> {
56 export class WatchForRendition<T> extends RenditionBase<Node> {
69 private readonly _component: (arg: T, index: number) => unknown;
57 private readonly _component: (arg: T, index: number) => unknown;
70
58
71 private readonly _node: Node;
59 private readonly _node: Node;
72
60
73 private readonly _itemRenditions: ItemRendition[] = [];
61 private readonly _itemRenditions: ItemRendition[] = [];
74
62
75 private readonly _subject: T[] | Subscribable<QueryResultUpdate<T>>;
63 private readonly _subject: T[] | Subscribable<OrderUpdate<T>>;
76
64
77 private readonly _renderTasks: RenderTask<T>[] = [];
65 private readonly _renderTasks: RenderTask<T>[] = [];
78
66
79 private readonly _animate: boolean;
67 private readonly _animate: boolean;
80
68
81 private readonly _animateIn: (nodes: Node[]) => Promise<void>;
69 private readonly _animateIn: (nodes: Node[]) => Promise<void>;
82
70
83 private readonly _animateOut: (nodes: Node[]) => Promise<void>;
71 private readonly _animateOut: (nodes: Node[]) => Promise<void>;
84
72
85 private _ct = Cancellation.none;
73 private _ct = Cancellation.none;
86
74
87 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
75 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
88 super();
76 super();
89 argumentNotNull(component, "component");
77 argumentNotNull(component, "component");
90 argumentNotNull(subject, "component");
78 argumentNotNull(subject, "component");
91
79
92 this._component = component;
80 this._component = component;
93
81
94 this._subject = subject;
82 this._subject = subject;
95
83
96 this._node = document.createComment("[WatchFor]");
84 this._node = document.createComment("[WatchFor]");
97 this._animate = !!animate;
85 this._animate = !!animate;
98 this._animateIn = animateIn ?? fadeIn;
86 this._animateIn = animateIn ?? fadeIn;
99 this._animateOut = animateOut ?? fadeOut;
87 this._animateOut = animateOut ?? fadeOut;
100 }
88 }
101
89
102 protected _create() {
90 protected _create() {
103 const scope = getScope();
91 const scope = getScope();
104 scope.own(() => {
92 scope.own(() => {
105 this._itemRenditions.forEach(safeDestroy);
93 this._itemRenditions.forEach(safeDestroy);
106 safeDestroy(this._node);
94 safeDestroy(this._node);
107 });
95 });
108
96
109 const result = this._subject;
97 const result = this._subject;
110
98
111 if (result) {
99 if (result) {
112 if (isSubsribable<QueryResultUpdate<T>>(result)) {
100 if (isSubsribable<OrderUpdate<T>>(result)) {
113 let animate = false;
101 let animate = false;
114 const subscription = result.subscribe({
102 const subscription = result.subscribe({
115 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
103 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
116 });
104 });
117 scope.own(subscription);
105 scope.own(subscription);
118 animate = this._animate;
106 animate = this._animate;
119 } else {
107 } else {
120 if (isObservable(result))
108 if (isObservableResults<T>(result))
121 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
109 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
122
110
123 for (let i = 0, n = result.length; i < n; i++)
111 for (let i = 0, n = result.length; i < n; i++)
124 this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
112 this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
125 }
113 }
126 }
114 }
127 this._ct = new Cancellation(cancel => scope.own(cancel));
115 this._ct = new Cancellation(cancel => scope.own(cancel));
128 }
116 }
129
117
130 private readonly _onItemUpdated = (item: RenderTask<T>) => {
118 private readonly _onItemUpdated = (item: RenderTask<T>) => {
131 if (!this._renderTasks.length) {
119 if (!this._renderTasks.length) {
132 // schedule a new job
120 // schedule a new job
133 this._renderTasks.push(item);
121 this._renderTasks.push(item);
134 this._render().catch(e => trace.error(e));
122 this._render().catch(e => trace.error(e));
135 } else {
123 } else {
136 // update existing job
124 // update existing job
137 this._renderTasks.push(item);
125 this._renderTasks.push(item);
138 }
126 }
139 };
127 };
140
128
141 private async _render() {
129 private async _render() {
142 // fork
130 // fork
143 await Promise.resolve();
131 await Promise.resolve();
144 // don't render destroyed rendition
132 // don't render destroyed rendition
145 if (this._ct.isRequested())
133 if (this._ct.isRequested())
146 return;
134 return;
147
135
148 this._renderTasks.forEach(this._onRenderItem);
136 this._renderTasks.forEach(this._onRenderItem);
149 this._renderTasks.length = 0;
137 this._renderTasks.length = 0;
150 }
138 }
151
139
152 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
140 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
153 const animate = _animate && prevIndex !== newIndex;
141 const animate = _animate && prevIndex !== newIndex;
154
142
155 if (prevIndex > -1) {
143 if (prevIndex > -1) {
156 // if we need to delete previous rendition
144 // if we need to delete previous rendition
157 const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
145 const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
158 if (animate) {
146 if (animate) {
159 this._animateOut(nodes)
147 this._animateOut(nodes)
160 .then(destroy)
148 .then(destroy)
161 .catch(e => trace.error(e));
149 .catch(e => trace.error(e));
162 } else {
150 } else {
163 destroy();
151 destroy();
164 }
152 }
165 }
153 }
166
154
167 if (newIndex > -1) {
155 if (newIndex > -1) {
168 // if we need to create the new rendition
156 // if we need to create the new rendition
169
157
170 // 1. create a new scope for rendering a content
158 // 1. create a new scope for rendering a content
171 const scope = new Scope();
159 const scope = new Scope();
172
160
173 // 2. render the content
161 // 2. render the content
174 const itemNode = render(this._component(item, newIndex), scope);
162 const itemNode = render(this._component(item, newIndex), scope);
175
163
176 // 3. track nodes
164 // 3. track nodes
177 const nodes = isDocumentFragmentNode(itemNode) ?
165 const nodes = isDocumentFragmentNode(itemNode) ?
178 collectNodes(itemNode.childNodes) :
166 collectNodes(itemNode.childNodes) :
179 [itemNode];
167 [itemNode];
180
168
181 // 5. insert node at the correct position
169 // 5. insert node at the correct position
182
170
183 const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
171 const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
184
172
185 if (beforeNode)
173 if (beforeNode)
186 placeAt(itemNode, beforeNode, "before");
174 placeAt(itemNode, beforeNode, "before");
187 else
175 else
188 placeAt(itemNode, this._node, "before");
176 placeAt(itemNode, this._node, "before");
189
177
190 // 6. store information about rendition
178 // 6. store information about rendition
191 this._itemRenditions.splice(newIndex, 0, {
179 this._itemRenditions.splice(newIndex, 0, {
192 scope,
180 scope,
193 nodes,
181 nodes,
194 destroy: () => {
182 destroy: () => {
195 scope.destroy();
183 scope.destroy();
196 nodes.forEach(safeDestroy);
184 nodes.forEach(safeDestroy);
197 }
185 }
198 });
186 });
199
187
200 // 7. startup widgets if needed
188 // 7. startup widgets if needed
201 if (isMounted(this._node))
189 if (isMounted(this._node))
202 nodes.forEach(n => startupWidgets(n));
190 nodes.forEach(n => startupWidgets(n));
203
191
204 // 8. optionally play the animation
192 // 8. optionally play the animation
205 if (animate)
193 if (animate)
206 this._animateIn(nodes).catch(e => trace.error(e));
194 this._animateIn(nodes).catch(e => trace.error(e));
207 }
195 }
208 };
196 };
209
197
210 protected _getDomNode() {
198 protected _getDomNode() {
211 if (!this._node)
199 if (!this._node)
212 throw new Error("The instance of the widget isn't created");
200 throw new Error("The instance of the widget isn't created");
213 return this._node;
201 return this._node;
214 }
202 }
215 }
203 }
@@ -1,52 +1,52
1 import { observe } from "./observable";
1 import { observe } from "./observable";
2 import * as t from "tap";
2 import * as t from "tap";
3
3
4 const subj1 = observe<number>(({ next, complete }) => {
4 const subj1 = observe<number>(({ next, complete }) => {
5 next(1);
5 next(1);
6 complete();
6 complete();
7 next(2);
7 next(2);
8 });
8 });
9
9
10 const consumer1 = {
10 const consumer1 = {
11 sum: 0,
11 sum: 0,
12 next(v: number) {
12 next(v: number) {
13 this.sum += v;
13 this.sum += v;
14 }
14 }
15 };
15 };
16
16
17 subj1.subscribe(consumer1);
17 subj1.subscribe(consumer1);
18 t.equal(consumer1.sum, 1, "Should get only one value");
18 t.equal(consumer1.sum, 1, "Should get only one value");
19
19
20 subj1.subscribe(consumer1);
20 subj1.subscribe(consumer1);
21 t.equal(consumer1.sum, 2, "Should get the value again");
21 t.equal(consumer1.sum, 2, "Should get the value again");
22
22
23 const consumer2 = {
23 const consumer2 = {
24 value: 0,
24 value: 0,
25 completed: false,
25 completed: false,
26 next(v: number) { this.value = v; },
26 next(v: number) { this.value = v; },
27 complete() { this.completed = true; }
27 complete() { this.completed = true; }
28 };
28 };
29
29
30 let maps = 0;
30 let maps = 0;
31
31
32 subj1
32 subj1
33 .map(v => {
33 .map(v => {
34 t.comment(`map1: ${v * 2}`);
34 t.comment(`map1: ${v * 2}`);
35 maps++;
35 maps++;
36 return v * 2;
36 return v * 2;
37 })
37 })
38 .map (v => {
38 .map (v => {
39 t.comment(`map2: ${v * 2}`);
39 t.comment(`map2: ${v * 2}`);
40 maps++;
40 maps++;
41 return v * 2;
41 return v * 2;
42 })
42 })
43 .map(v => {
43 .map(v => {
44 t.comment(`map3: ${v * 2}`);
44 t.comment(`map3: ${v * 2}`);
45 maps++;
45 maps++;
46 return v * 2;
46 return v * 2;
47 })
47 })
48 .subscribe(consumer2);
48 .subscribe(consumer2);
49
49
50 t.equal(consumer2.value, 8, "Should map");
50 t.equal(consumer2.value, 8, "Should map");
51 t.equal(maps, 3, "The map chain should not be executed after completion");
51 t.equal(maps, 3, "The map chain should not be executed after completion");
52 t.ok(consumer2.completed, "The completion signal should pass through"); No newline at end of file
52 t.ok(consumer2.completed, "The completion signal should pass through");
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now