diff --git a/djx/package-lock.json b/djx/package-lock.json --- a/djx/package-lock.json +++ b/djx/package-lock.json @@ -25,6 +25,7 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.29.4", "requirejs": "2.3.6", + "rxjs": "7.5.6", "tap": "16.3.0", "typescript": "4.8.3", "yaml": "~1.7.2" @@ -3994,6 +3995,21 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -9572,6 +9588,23 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + } + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/djx/package.json b/djx/package.json --- a/djx/package.json +++ b/djx/package.json @@ -24,6 +24,7 @@ "@types/requirejs": "2.1.31", "@types/yaml": "1.2.0", "@types/tap": "15.0.7", + "rxjs": "7.5.6", "dojo": "1.16.0", "@implab/dojo-typings": "1.0.3", "@typescript-eslint/eslint-plugin": "^5.23.0", diff --git a/djx/src/main/ts/observable.ts b/djx/src/main/ts/observable.ts --- a/djx/src/main/ts/observable.ts +++ b/djx/src/main/ts/observable.ts @@ -1,3 +1,6 @@ +import { PromiseOrValue } from "@implab/core-amd/interfaces"; +import { isPromise } from "@implab/core-amd/safe"; + /** * The interface for the consumer of an observable sequence */ @@ -19,11 +22,30 @@ export interface Observer { } /** - * The group of functions to feed an observable. This methods are provided to + * The group of functions to feed an observable. These methods are provided to * the producer to generate a stream of events. */ export type Sink = { - [k in keyof Observer]: (this: void, ...args: Parameters[k]>) => void; + /** + * Call to send the next element in the sequence + */ + next: (value: T) => void; + + /** + * Call to notify about the error occurred in the sequence. + */ + error: (e: unknown) => void; + + /** + * Call to signal the end of the sequence. + */ + complete: () => void; + + /** + * Checks whether the sink is accepting new elements. It's safe to + * send elements to the closed sink. + */ + isClosed: () => boolean; }; export type Producer = (sink: Sink) => (void | (() => void)); @@ -64,6 +86,8 @@ export interface Observable extends S * @param initial */ scan(accumulator: (acc: A, value: T) => A, initial: A): Observable; + + cat(...seq: Subscribable[]): Observable; } const noop = () => { }; @@ -73,75 +97,133 @@ const sink = (consumer: Partial false }; }; -const fuse = ({ next, error, complete }: Sink) => { +/** Wraps the producer to handle tear down logic and subscription management + * + * @param producer The producer to wrap + * @returns The wrapper producer + */ +const fuse = (producer: Producer) => ({ next, error, complete }: Sink) => { let done = false; - return { + let cleanup = noop; + + const _fin = (fn: (...args: A) => void) => + (...args: A) => done ? + void (0) : + (done = true, cleanup(), fn(...args)); + + const safeSink = { next: (value: T) => { !done && next(value); }, - error: (e: unknown) => { !done && (done = true, error(e)); }, - complete: () => { !done && (done = true, complete()); } + error: _fin(error), + complete: _fin(complete), + isClosed: () => done }; + cleanup = producer(safeSink) ?? noop; + return done ? + (cleanup(), noop) : + _fin(noop); }; const _observe = (producer: Producer): Observable => ({ subscribe: (consumer: Partial>) => ({ unsubscribe: producer(sink(consumer)) ?? noop }), - map: (mapper) => _observe(({ next, error, complete }) => + map: (mapper) => _observe(({ next, ...rest }) => producer({ next: next !== noop ? (v: T) => next(mapper(v)) : noop, - error, - complete + ...rest + }) + ), + filter: (predicate) => _observe(({ next, ...rest }) => + producer({ + next: next !== noop ? (v: T) => predicate(v) ? next(v) : void (0) : noop, + ...rest }) ), - filter: (predicate) => _observe(({ next, error, complete }) => - producer({ - next: next !== noop ? - (v: T) => predicate(v) ? next(v) : void (0) : noop, - error, - complete - }) - ), - scan: (accumulator, initial) => _observe(({ next, error, complete }) => { + scan: (accumulator, initial) => _observe(({ next, ...rest }) => { let _acc = initial; return producer({ - next: next !== noop ? - (v: T) => next(_acc = accumulator(_acc, v)) : noop, - error, - complete + next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop, + ...rest }); + }), + + cat: (...seq) => _observe(({ next, complete: final, ...rest }) => { + let cleanup: () => void; + const complete = () => { + const continuation = seq.shift(); + if (continuation) { + // if we have a next sequence, subscribe to it + const subscription = continuation.subscribe({ next, complete, ...rest }); + cleanup = subscription.unsubscribe.bind(subscription); + } else { + // otherwise notify the consumer about completion + final(); + } + }; + + cleanup = producer({ next, complete, ...rest }) ?? noop; + + return () => cleanup(); }) }); -export const observe = (producer: Producer): Observable => ({ - subscribe: (consumer: Partial>) => ({ - unsubscribe: producer(fuse(sink(consumer))) ?? noop - }), - map: (mapper) => _observe(({ next, error, complete }) => - producer(fuse({ - next: next !== noop ? - (v: T) => next(mapper(v)) : noop, - error, - complete - })) - ), - filter: (predicate) => _observe(({ next, error, complete }) => - producer(fuse({ - next: next !== noop ? - (v: T) => predicate(v) ? next(v) : void (0) : noop, - error, - complete - })) - ), - scan: (accumulator, initial) => observe(({ next, error, complete }) => { - let _acc = initial; - return producer(fuse({ - next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop, - error, - complete - })); - }) -}); +export interface OrderUpdate { + /** The item is being updated */ + item: T; + + /** The previous index of the item, -1 in case it is inserted */ + prevIndex: number; + + /** The new index of the item, -1 in case it is deleted */ + newIndex: number; +} + +interface ObservableResults { + /** + * Allows observation of results + */ + observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): { + remove(): void; + }; +} + +interface Queryable { + query(...args: A): PromiseOrValue; +} + +export const isObservableResults = (v: object): v is ObservableResults => + v && (typeof (v as { observe?: unknown; }).observe === "function"); + +export const observe = (producer: Producer) => _observe(fuse(producer)); + +export const empty = observe(({ complete }) => complete()); + +export const query = (store: Queryable) => + (...args: A) => { + return observe>(({ next, complete, error }) => { + try { + const results = store.query(...args); + if (isPromise(results)) { + results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }))) + .then(undefined, error); + } else { + results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })); + } + + if (isObservableResults(results)) { + const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex })); + return () => h.remove(); + } else { + complete(); + } + } catch (err) { + error(err); + } + }); + + }; diff --git a/djx/src/main/ts/store/Observable.ts b/djx/src/main/ts/store/Observable.ts deleted file mode 100644 --- a/djx/src/main/ts/store/Observable.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default class Observable> { - -} \ No newline at end of file diff --git a/djx/src/main/ts/tsx.ts b/djx/src/main/ts/tsx.ts --- a/djx/src/main/ts/tsx.ts +++ b/djx/src/main/ts/tsx.ts @@ -7,7 +7,7 @@ import Stateful = require("dojo/Stateful import _WidgetBase = require("dijit/_WidgetBase"); import { DjxWidgetBase } from "./tsx/DjxWidgetBase"; import { WatchRendition } from "./tsx/WatchRendition"; -import { Observable, observe, Subscribable } from "./observable"; +import { Observable, observe, OrderUpdate, Subscribable } from "./observable"; import djAttr = require("dojo/dom-attr"); import djClass = require("dojo/dom-class"); import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition"; @@ -45,17 +45,6 @@ export interface EventSelector { target: HTMLElement; } -export interface QueryResultUpdate { - /** The item is being updated */ - item: T; - - /** The previous index of the item, -1 in case it is inserted */ - prevIndex: number; - - /** The new index of the item, -1 in case it is deleted */ - newIndex: number; -} - export type DojoMouseEvent = MouseEvent & EventSelector & EventDetails; type StatefulProps = T extends Stateful ? A : @@ -112,7 +101,7 @@ export function watch( } } -export const watchFor = (source: T[] | Subscribable>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => { +export const watchFor = (source: T[] | Subscribable>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => { return new WatchForRendition({ ...opts, subject: source, diff --git a/djx/src/main/ts/tsx/WatchForRendition.ts b/djx/src/main/ts/tsx/WatchForRendition.ts --- a/djx/src/main/ts/tsx/WatchForRendition.ts +++ b/djx/src/main/ts/tsx/WatchForRendition.ts @@ -9,8 +9,7 @@ import { collectNodes, destroy as safeDe import { IDestroyable } from "@implab/core-amd/interfaces"; import { play } from "../play"; import * as fx from "dojo/fx"; -import { isSubsribable, Subscribable } from "../observable"; -import { QueryResultUpdate } from "../tsx"; +import { isObservableResults, isSubsribable, OrderUpdate, Subscribable } from "../observable"; const trace = TraceSource.get(mid); @@ -22,16 +21,7 @@ interface ItemRendition { destroy(): void; } -interface ObservableResults { - /** - * Allows observation of results - */ - observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): { - remove(): void; - }; -} - -interface RenderTask extends QueryResultUpdate { +interface RenderTask extends OrderUpdate { animate: boolean; } @@ -44,13 +34,11 @@ export interface AnimationAttrs { } export interface WatchForRenditionAttrs extends AnimationAttrs { - subject: T[] | Subscribable>; + subject: T[] | Subscribable>; component: (arg: T, index: number) => unknown; } -const isObservable = (v: ArrayLike): v is ArrayLike & ObservableResults => - v && (typeof (v as { observe?: unknown; }).observe === "function"); const noop = () => { }; @@ -72,7 +60,7 @@ export class WatchForRendition extend private readonly _itemRenditions: ItemRendition[] = []; - private readonly _subject: T[] | Subscribable>; + private readonly _subject: T[] | Subscribable>; private readonly _renderTasks: RenderTask[] = []; @@ -109,7 +97,7 @@ export class WatchForRendition extend const result = this._subject; if (result) { - if (isSubsribable>(result)) { + if (isSubsribable>(result)) { let animate = false; const subscription = result.subscribe({ next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate }) @@ -117,7 +105,7 @@ export class WatchForRendition extend scope.own(subscription); animate = this._animate; } else { - if (isObservable(result)) + if (isObservableResults(result)) scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true)); for (let i = 0, n = result.length; i < n; i++) diff --git a/djx/src/test/ts/observable-tests.ts b/djx/src/test/ts/observable-tests.ts --- a/djx/src/test/ts/observable-tests.ts +++ b/djx/src/test/ts/observable-tests.ts @@ -49,4 +49,4 @@ subj1 t.equal(consumer2.value, 8, "Should map"); t.equal(maps, 3, "The map chain should not be executed after completion"); -t.ok(consumer2.completed, "The completion signal should pass through"); \ No newline at end of file +t.ok(consumer2.completed, "The completion signal should pass through"); diff --git a/playground/build.gradle b/playground/build.gradle --- a/playground/build.gradle +++ b/playground/build.gradle @@ -94,6 +94,9 @@ task copyModules(type: Copy) { pack("@implab/djx") pack("@implab/core-amd") + into("@js-joda/core") { + from(npm.module("@js-joda/core/dist")) + } pack("dojo") pack("dijit") into("rxjs") { diff --git a/playground/package-lock.json b/playground/package-lock.json --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@implab/djx-playground", "dependencies": { + "@js-joda/core": "5.3.1", "dijit": "1.17.3", "dojo": "1.17.3", "requirejs": "2.3.6", @@ -121,6 +122,11 @@ "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==", "dev": true }, + "node_modules/@js-joda/core": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.3.1.tgz", + "integrity": "sha512-iHHyIRLEfXLqBN+BkyH8u8imMYr4ihRbFDEk8toqTwUECETVQFCTh2U59Sw2oMoRVaS3XRIb7pyCulltq2jFVA==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2786,6 +2792,11 @@ "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==", "dev": true }, + "@js-joda/core": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.3.1.tgz", + "integrity": "sha512-iHHyIRLEfXLqBN+BkyH8u8imMYr4ihRbFDEk8toqTwUECETVQFCTh2U59Sw2oMoRVaS3XRIb7pyCulltq2jFVA==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/playground/package.json b/playground/package.json --- a/playground/package.json +++ b/playground/package.json @@ -2,6 +2,7 @@ "name": "@implab/djx-playground", "private": true, "dependencies": { + "@js-joda/core": "5.3.1", "dijit": "1.17.3", "dojo": "1.17.3", "requirejs": "2.3.6", diff --git a/playground/src/bundle/config.js b/playground/src/bundle/config.js --- a/playground/src/bundle/config.js +++ b/playground/src/bundle/config.js @@ -10,6 +10,11 @@ requirejs.config({ name: "rxjs", location: "rxjs", main: "rxjs.umd.min" + }, + { + name: "@js-joda/core", + location: "@js-joda/core", + main: "js-joda" } ], deps: ["app"] diff --git a/playground/src/main/ts/logging.ts b/playground/src/main/ts/logging.ts new file mode 100644 --- /dev/null +++ b/playground/src/main/ts/logging.ts @@ -0,0 +1,11 @@ +import { TraceSource } from "@implab/core-amd/log/TraceSource"; + +const delegate = unknown }, K extends string>(target: T, key: K): OmitThisParameter => target[key].bind(target) as OmitThisParameter; + +export const log = (trace: TraceSource) => delegate(trace, "log"); + +export const debug = (trace: TraceSource) => delegate(trace, "debug"); + +export const warn = (trace: TraceSource) => delegate(trace, "warn"); + +export const error = (trace: TraceSource) => delegate(trace, "error"); diff --git a/playground/src/main/ts/main.ts b/playground/src/main/ts/main.ts --- a/playground/src/main/ts/main.ts +++ b/playground/src/main/ts/main.ts @@ -4,4 +4,5 @@ import "@implab/djx/css!dijit/themes/dij import "@implab/djx/css!dijit/themes/tundra/tundra.css"; const w = new MainWidget(); -w.placeAt(document.body); \ No newline at end of file +w.placeAt(document.body); +w.load(); diff --git a/playground/src/main/ts/model/Appointment.ts b/playground/src/main/ts/model/Appointment.ts --- a/playground/src/main/ts/model/Appointment.ts +++ b/playground/src/main/ts/model/Appointment.ts @@ -1,12 +1,14 @@ import { Contact } from "./Contact"; -type AppointmentRole = "organizer" | "speaker" | "participant"; +export type AppointmentRole = "organizer" | "speaker" | "participant"; export interface Member extends Contact { role: AppointmentRole; } export interface Appointment { + id: string; + title: string; startAt: Date; diff --git a/playground/src/main/ts/model/MainContext.ts b/playground/src/main/ts/model/MainContext.ts --- a/playground/src/main/ts/model/MainContext.ts +++ b/playground/src/main/ts/model/MainContext.ts @@ -1,88 +1,33 @@ import Memory = require("dojo/store/Memory"); import Observable = require("dojo/store/Observable"); -import { Appointment, Member } from "./Appointment"; +import { Appointment, AppointmentRole, Member } from "./Appointment"; import { Contact } from "./Contact"; import { Uuid } from "@implab/core-amd/Uuid"; -import { Observable as RxjsObservable } from "rxjs"; -import { QueryResultUpdate } from "@implab/djx/tsx"; -import {isPromise} from "@implab/core-amd/safe"; +import { query } from "@implab/djx/observable"; +import { IDestroyable } from "@implab/core-amd/interfaces"; +import { delay } from "@implab/core-amd/safe"; -type AppointmentRecord = Omit & {id: string}; +type AppointmentRecord = Omit & { id: string }; type ContactRecord = Contact; type MemberRecord = Member & { appointmentId: string; }; -export interface ObservableResults { - /** - * Allows observation of results - */ - observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): { - remove(): void; - }; -} +const item = (map: (x: T) => T2) => ({ item, ...props }: U) => ({ item: map(item), ...props }); -export function isObservable(v: unknown): v is ObservableResults { - return !!v && (typeof (v as {observe?: unknown}).observe === "function"); -} - -export function observe(results: T[], includeObjectUpdates?: boolean): RxjsObservable>; -export function observe(results: PromiseLike, includeObjectUpdates?: boolean): PromiseLike>>; -export function observe(results: unknown[] | PromiseLike, includeObjectUpdates = true) { - // results может быть асинхронным, т.е. до завершения - // получения результатов store может быть обновлен. В любом - // случае, если между подключением хотя бы одного наблюдателя - // была выполнена команда обновления, results считается устаревшим - // и не может быть использован для отслеживания обновлений. - // Конкретно с dojo/store/Observable тут вообще возникает проблема: - // 1. Синхронные store типа Memory будут давать ошибку на методах - // обновления (add,put,remove) - // 2. Асинхронные store типа JsonRest будут выдавать предупреждения - // о необработанной ошибке в Promise при обращении к методам - // обновления (add,put,remove) - - const _subscribe = (items: unknown[]) => new RxjsObservable>(subscriber => { - items - .forEach((value, newIndex) => subscriber.next({ item: value, newIndex, prevIndex: -1})); - - try { - if (isObservable(results)) { - const h = results.observe( - (value, prevIndex, newIndex) => subscriber.next({ - item: value, - prevIndex, - newIndex - }), - includeObjectUpdates - ); - - return () => { h.remove(); }; - } - } catch (err) { - subscriber.error(err); - } - }); - - return isPromise(results) ? - results.then(_subscribe) : - _subscribe(results || []); -} - - - - -export class MainContext { +export class MainContext implements IDestroyable { private readonly _appointments = new Observable(new Memory()); private readonly _contacts = new Observable(new Memory()); private readonly _members = new Observable(new Memory()); - createAppointment(title: string, startAt: Date, duration: number, members: Member[]) { + async createAppointment(title: string, startAt: Date, duration: number, members: Member[]) { + await delay(1000); const id = Uuid(); this._appointments.add({ - id: Uuid(), + id, startAt, duration, title @@ -92,16 +37,35 @@ export class MainContext { this._members.add({ appointmentId: id, ...member - }, {id: Uuid()}) as void + }, { id: Uuid() }) as void ); } - queryAppointments(dateFrom: Date, dateTo: Date) { - //this._appointments.query().map() + queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) { + return query(this._appointments)(({ startAt }) => + (!dateFrom || dateFrom <= startAt) && + (!dateTo || startAt <= dateTo) + ).map(item(this._mapAppointment)); } - private readonly _mapAppointment = ({startAt, title, duration, id}: AppointmentRecord) => ({ + async addMember(appointmentId: string, member: Member) { + await delay(1000); + this._members.add({ + appointmentId, + ...member + }); + } + private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({ + id, + title, + startAt, + duration, + getMembers: (role?: AppointmentRole) => this._members.query(role ? { appointmentId: id, role } : { appointmentId: id }) }); + destroy() { + + } + } diff --git a/playground/src/main/ts/model/MainModel.ts b/playground/src/main/ts/model/MainModel.ts --- a/playground/src/main/ts/model/MainModel.ts +++ b/playground/src/main/ts/model/MainModel.ts @@ -1,20 +1,37 @@ -import { BehaviorSubject, Observer, Unsubscribable, Subscribable } from "rxjs"; -import { IDestroyable} from "@implab/core-amd/interfaces" - -interface State { - color: string; +import { id as mid } from "module"; +import { BehaviorSubject, Observer, Unsubscribable } from "rxjs"; +import { IDestroyable } from "@implab/core-amd/interfaces"; +import { OrderUpdate, Observable } from "@implab/djx/observable"; +import { Appointment, Member } from "./Appointment"; +import { MainContext } from "./MainContext"; +import { LocalDate } from "@js-joda/core"; +import { error } from "../logging"; +import { TraceSource } from "@implab/core-amd/log/TraceSource"; - label: string; +const trace = TraceSource.get(mid); + +export interface State { + appointments: Observable>; - current: number; + dateTo: LocalDate; - max: number; + dateFrom: LocalDate; + + title: string; } export default class MainModel implements IDestroyable { private readonly _state: BehaviorSubject; - constructor(initialState: State) { - this._state = new BehaviorSubject(initialState); + + private readonly _context = new MainContext(); + + constructor() { + this._state = new BehaviorSubject({ + dateTo: LocalDate.now(), + dateFrom: LocalDate.now().minusMonths(1), + appointments: this._context.queryAppointments(), + title: "Appointments" + }); } getState() { return this._state.getValue(); @@ -22,13 +39,25 @@ export default class MainModel implement subscribe(observer: Partial>): Unsubscribable { return this._state.subscribe(observer); - } - + } + protected dispatch(command: Partial) { const state = this.getState(); - this._state.next({...state, ... command}); + this._state.next({ ...state, ...command }); + } + + addMember(appointmentId: string, member: Member) { + this._context.addMember(appointmentId, member).catch(error(trace)); } - load() { } - destroy() { } + addAppointment(title: string, startAt: Date, duration: number) { + this._context.createAppointment(title,startAt, duration, []).catch(error(trace)); + } + + load() { + } + + destroy() { + this._context.destroy(); + } } \ No newline at end of file diff --git a/playground/src/main/ts/view/MainWidget.tsx b/playground/src/main/ts/view/MainWidget.tsx --- a/playground/src/main/ts/view/MainWidget.tsx +++ b/playground/src/main/ts/view/MainWidget.tsx @@ -1,81 +1,72 @@ import { djbase, djclass } from "@implab/djx/declare"; import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase"; -import { createElement, watch, prop, attach, all, bind, toggleClass } from "@implab/djx/tsx"; -import ProgressBar from "./ProgressBar"; +import { bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx"; +import MainModel from "../model/MainModel"; +import { OrderUpdate, Observable } from "@implab/djx/observable"; +import { Appointment } from "../model/Appointment"; +import { LocalDate } from "@js-joda/core"; import Button = require("dijit/form/Button"); -import { interval } from "rxjs"; - -const Counter = ({ children }: { children: unknown[] }) => Counter: {children}; @djclass export default class MainWidget extends djbase(DjxWidgetBase) { - titleNode?: HTMLHeadingElement; + appointments?: Observable>; + + model: MainModel; - progressBar?: ProgressBar; + dateTo?: LocalDate; - count = 0; + dateFrom?: LocalDate; - showCounter = false; + constructor(opts?: Partial & ThisType, srcNode?: string | Node) { + super(opts, srcNode); - counterNode?: HTMLInputElement; + const model = this.model = new MainModel(); + this.own(model); + model.subscribe({ next: x => this.set(x) }); + } - paused = false; render() { return
-

Hi!

-
- {watch(prop(this, "showCounter"), flag => flag && - [ - x/10) - ), - attach(this, "counterNode") - )} /> s, - " | ", - , - " | ", -
- +

+ {watch(prop(this, "appointments"), items => items && +
    + {watchFor(items, ({ id, title, getMembers }) => +
  • {title} +
      + {watchFor(getMembers(), ({ role, name, position }) => +
    • {name}({position})
    • + )} +
    +
    + +
    +
  • + )} +
+ )} +
+ +

; } - postCreate(): void { - super.postCreate(); - - const h = setInterval( - () => { - this.set("count", this.count + 1); - }, - 100 - ); - this.own({ - destroy: () => { - clearInterval(h); - } - }); + load() { + this.model.load(); } - private readonly _onPauseClick = () => { - this.set("paused", !this.paused); + private readonly _onAddMemberClick = (appointmentId: string) => { + this.model.addMember(appointmentId, { + email: "some-mail", + name: "Member Name", + position: "Member position", + role: "participant" + }); }; - private readonly _onToggleCounterClick = () => { - this.set("showCounter", !this.showCounter); + private readonly _onAddAppointmentClick = () => { + this.model.addAppointment("Appointment", new Date, 30); }; } diff --git a/playground/src/tsconfig.json b/playground/src/tsconfig.json --- a/playground/src/tsconfig.json +++ b/playground/src/tsconfig.json @@ -11,6 +11,8 @@ "@implab/djx", "@implab/dojo-typings" ], - "skipLibCheck": true + "skipLibCheck": true, + "target": "ES5", + "lib": ["ES2015"] } } \ No newline at end of file