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