##// END OF EJS Templates
type fixes for i18n, store
cin -
r120:bc1b4dd8ca1a v1.6.2 default
parent child
Show More

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

@@ -0,0 +1,8
1 import { bundle } from "../i18n";
2
3 export const { i18n, load, define } = bundle({
4 foo: "foo",
5 bar: "bar"
6 }, {
7 ru: () => import("./ru/nls-test")
8 }); No newline at end of file
@@ -0,0 +1,5
1 import { define } from "../nls-test";
2
3 export default define({
4 foo: "Фу"
5 }); No newline at end of file
@@ -1,60 +1,60
1 import { MapOf, PromiseOrValue } from "@implab/core-amd/interfaces";
1 import { MapOf, PromiseOrValue } from "@implab/core-amd/interfaces";
2 import { argumentNotEmptyString, isPromise, mixin } from "@implab/core-amd/safe";
2 import { argumentNotEmptyString, isPromise, mixin } from "@implab/core-amd/safe";
3
3
4 export type LocaleProvider<T> = () => PromiseOrValue<T | { default: T }>;
4 export type LocaleProvider<T> = () => PromiseOrValue<T | {default: T}>;
5
5
6 function when<T, T2>(value: PromiseOrValue<T>, cb: (v: T) => PromiseOrValue<T2>): PromiseOrValue<T2> {
6 function when<T, T2>(value: PromiseOrValue<T>, cb: (v: T) => PromiseOrValue<T2>): PromiseOrValue<T2> {
7 return isPromise(value) ?
7 return isPromise(value) ?
8 value.then(cb) :
8 value.then(cb) :
9 cb(value);
9 cb(value);
10 }
10 }
11
11
12 const loadPackage = <T extends object>(localeData: LocaleProvider<Partial<T>> | undefined) =>
12 const loadPackage = <T extends object>(localeData: LocaleProvider<Partial<T>> | undefined) =>
13 localeData ?
13 localeData ?
14 when(localeData(), data => data && "default" in data ? data.default : data) :
14 when(localeData(), data => data && "default" in data ? data.default : data) :
15 undefined;
15 undefined;
16
16
17 const chainObjects = <T extends object>(o1: T, o2: Partial<T> | undefined): T =>
17 const chainObjects = <T extends object>(o1: T, o2: Partial<T> | undefined): T =>
18 o2 ? mixin(Object.create(o1) as T, o2) : o1;
18 o2 ? mixin(Object.create(o1) as T, o2) : o1;
19
19
20 export class NlsBundle<T extends object> {
20 export class NlsBundle<T extends object> {
21 private readonly _locales: MapOf<LocaleProvider<Partial<T>>>;
21 private readonly _locales: MapOf<LocaleProvider<Partial<T>>>;
22
22
23 private readonly _default: T;
23 private readonly _default: T;
24
24
25 private _cache: MapOf<PromiseOrValue<T>>;
25 private _cache: MapOf<PromiseOrValue<T>>;
26
26
27 constructor(defNls: T, locales?: MapOf<LocaleProvider<Partial<T>>>) {
27 constructor(defNls: T, locales?: MapOf<LocaleProvider<Partial<T>>>) {
28 this._default = defNls;
28 this._default = defNls;
29 this._locales = locales || {};
29 this._locales = locales || {};
30 this._cache = {};
30 this._cache = {};
31 }
31 }
32
32
33 getLocale(locale: string) {
33 getLocale(locale: string) {
34 argumentNotEmptyString(locale, "locale");
34 argumentNotEmptyString(locale, "locale");
35 const _loc = locale;
35 const _loc = locale;
36
36
37 // en-US => ["en", "en-US"]
37 // en-US => ["en", "en-US"]
38 const locales = _loc.split(/-|_/).map((x, i, a) => a.slice(0, i + 1).join("-"));
38 const locales = _loc.split(/-|_/).map((x, i, a) => a.slice(0, i + 1).join("-"));
39 return this._resolveLocale(locales);
39 return this._resolveLocale(locales);
40 }
40 }
41
41
42 _resolveLocale(locales: string[]): PromiseOrValue<T> {
42 _resolveLocale(locales: string[]): PromiseOrValue<T> {
43 if (!locales.length)
43 if (!locales.length)
44 return this._default;
44 return this._default;
45
45
46 const locale = locales.pop();
46 const locale = locales.pop();
47 if (!locale)
47 if (!locale)
48 throw new Error("The locale can't be empty");
48 throw new Error("The locale can't be empty");
49
49
50 if (this._cache[locale])
50 if (this._cache[locale])
51 return this._cache[locale];
51 return this._cache[locale];
52
52
53 const data = loadPackage(this._locales[locale]);
53 const data = loadPackage(this._locales[locale]);
54 const parent = this._resolveLocale(locales);
54 const parent = this._resolveLocale(locales);
55
55
56 return this._cache[locale] = when(data, x => {
56 return this._cache[locale] = when(data, x => {
57 return when(parent, y => this._cache[locale] = chainObjects(y, x));
57 return when(parent, y => this._cache[locale] = chainObjects(y, x));
58 });
58 });
59 }
59 }
60 }
60 }
@@ -1,51 +1,56
1 import { id as mid} from "module";
1 import { id as mid} from "module";
2 import { MapOf } from "@implab/core-amd/interfaces";
2 import { PromiseOrValue } from "@implab/core-amd/interfaces";
3 import { LocaleProvider, NlsBundle } from "./NlsBundle";
3 import { NlsBundle } from "./NlsBundle";
4 import { isPromise } from "@implab/core-amd/safe";
4 import { isPromise } from "@implab/core-amd/safe";
5 import { locale as sysLocale } from "dojo/_base/kernel";
5 import { locale as sysLocale } from "dojo/_base/kernel";
6 import { TraceSource } from "@implab/core-amd/log/TraceSource";
6 import { TraceSource } from "@implab/core-amd/log/TraceSource";
7
7
8 const trace = TraceSource.get(mid);
8 const trace = TraceSource.get(mid);
9
9
10 trace.debug("Current sysLocale: {0}", sysLocale);
10 trace.debug("Current sysLocale: {0}", sysLocale);
11
11
12 export interface OnLoad {
12 export interface OnLoad {
13 (result?: unknown): void;
13 (result?: unknown): void;
14 error(err: unknown): void;
14 error(err: unknown): void;
15 }
15 }
16
16
17 export function bundle<T extends object>(nls: T, locales?: MapOf<LocaleProvider<object>>) {
17 export const bundle = <T extends object>(nls: T, locales?: Record<string, () => PromiseOrValue<object>>) : {
18 i18n: (locale?: string) => T;
19 define: (pack: Partial<T>) => object;
20 load: (id: string, require: Require, cb: OnLoad, config: {isBuild?: boolean}) => void;
21 } => {
18 const nlsBundle = new NlsBundle(nls, locales);
22 const nlsBundle = new NlsBundle(nls, locales);
19
23
20 const fn = (_locale?: string) => {
24 const fn = (_locale?: string) => {
21 const locale = _locale || sysLocale;
25 const locale = _locale || sysLocale;
22 const result = nlsBundle.getLocale(locale);
26 const result = nlsBundle.getLocale(locale);
23
27
24 if (isPromise(result))
28 if (isPromise(result))
25 throw new Error(`The bundle '${locale}' isn't loaded`);
29 throw new Error(`The bundle '${locale}' isn't loaded`);
26 else
30 else
27 return result;
31 return result;
28 };
32 };
29
33
30 fn.define = (pack: Partial<T>) => pack;
34 fn.i18n = fn;
35 fn.define = (pack: Partial<T>): object => pack;
31 fn.load = async (id: string, require: Require, cb: OnLoad, config: {isBuild?: boolean}) => {
36 fn.load = async (id: string, require: Require, cb: OnLoad, config: {isBuild?: boolean}) => {
32 const locale = id || sysLocale;
37 const locale = id || sysLocale;
33 if (config && config.isBuild) {
38 if (config && config.isBuild) {
34 cb();
39 cb();
35 } else {
40 } else {
36 try {
41 try {
37 await nlsBundle.getLocale(locale);
42 await nlsBundle.getLocale(locale);
38 cb();
43 cb();
39 } catch (e) {
44 } catch (e) {
40 if(cb.error) {
45 if(cb.error) {
41 cb.error(e);
46 cb.error(e);
42 } else {
47 } else {
43 // in case the loader doesn't support error reporting
48 // in case the loader doesn't support error reporting
44 trace.error("Error loading {0}: {1}", locale, e);
49 trace.error("Error loading {0}: {1}", locale, e);
45 }
50 }
46 }
51 }
47 }
52 }
48 };
53 };
49
54
50 return fn;
55 return fn;
51 }
56 };
@@ -1,58 +1,58
1 import { PromiseOrValue } from "@implab/core-amd/interfaces";
1 import { PromiseOrValue } from "@implab/core-amd/interfaces";
2 import { isPromise } from "@implab/core-amd/safe";
2 import { isPromise } from "@implab/core-amd/safe";
3 import { observe, Observable } from "./observable";
3 import { observe, Observable } from "./observable";
4
4
5 export interface OrderedUpdate<T> {
5 export interface OrderedUpdate<T> {
6 /** The item is being updated */
6 /** The item is being updated */
7 readonly item: T;
7 readonly item: T;
8
8
9 /** The previous index of the item, -1 in case it is inserted */
9 /** The previous index of the item, -1 in case it is inserted */
10 readonly prevIndex: number;
10 readonly prevIndex: number;
11
11
12 /** The new index of the item, -1 in case it is deleted */
12 /** The new index of the item, -1 in case it is deleted */
13 readonly newIndex: number;
13 readonly newIndex: number;
14
14
15 }
15 }
16
16
17 export type QueryResults<T> = Observable<OrderedUpdate<T>>;
17 export type QueryResults<T> = Observable<OrderedUpdate<T>>;
18
18
19 interface DjObservableResults<T> {
19 interface DjObservableResults<T> {
20 /**
20 /**
21 * Allows observation of results
21 * Allows observation of results
22 */
22 */
23 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
23 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
24 remove(): void;
24 remove(): void;
25 };
25 };
26 }
26 }
27
27
28 interface Queryable<T, Q, O> {
28 interface Queryable<T, Q, O> {
29 query(query?: Q, options?: O): PromiseOrValue<T[]>;
29 query(query?: Q, options?: O): PromiseOrValue<T[]>;
30 }
30 }
31
31
32 export const isDjObservableResults = <T>(v: object): v is DjObservableResults<T> =>
32 export const isDjObservableResults = <T>(v: object): v is DjObservableResults<T> =>
33 v && (typeof (v as { observe?: unknown; }).observe === "function");
33 v && (typeof (v as { observe?: unknown; }).observe === "function");
34
34
35 export const query = <T, Q, O>(store: Queryable<T, Q, O>, includeUpdates = true) =>
35 export const query = <T, Q, O>(store: Queryable<T, Q, O>, includeUpdates = true) =>
36 (query?: Q, options?: O & { observe: boolean }) => {
36 (query?: Q, options?: O & { observe?: boolean }) => {
37 return observe<OrderedUpdate<T>>(({ next, complete, error, isClosed }) => {
37 return observe<OrderedUpdate<T>>(({ next, complete, error, isClosed }) => {
38 try {
38 try {
39 const results = store.query(query, options);
39 const results = store.query(query, options);
40 if (isPromise(results)) {
40 if (isPromise(results)) {
41 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
41 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
42 .then(undefined, error);
42 .then(undefined, error);
43 } else {
43 } else {
44 results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
44 results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
45 }
45 }
46
46
47 if (!isClosed() && (options?.observe !== false) && isDjObservableResults<T>(results)) {
47 if (!isClosed() && (options?.observe !== false) && isDjObservableResults<T>(results)) {
48 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }), includeUpdates);
48 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }), includeUpdates);
49 return () => h.remove();
49 return () => h.remove();
50 } else {
50 } else {
51 complete();
51 complete();
52 }
52 }
53 } catch (err) {
53 } catch (err) {
54 error(err);
54 error(err);
55 }
55 }
56 });
56 });
57
57
58 };
58 };
@@ -1,188 +1,196
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, 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 import { OrderedUpdate } from "./store";
14 import { OrderedUpdate } from "./store";
15
15
16 export function createElement<T extends Constructor | string | ((props: object) => Element)>(elementType: T, ...args: unknown[]): Rendition {
16 export function createElement<T extends Constructor | string | ((props: object) => Element)>(elementType: T, ...args: unknown[]): Rendition {
17 if (typeof elementType === "string") {
17 if (typeof elementType === "string") {
18 const ctx = new HtmlRendition(elementType);
18 const ctx = new HtmlRendition(elementType);
19 if (args)
19 if (args)
20 args.forEach(x => ctx.visitNext(x));
20 args.forEach(x => ctx.visitNext(x));
21
21
22 return ctx;
22 return ctx;
23 } else if (isWidgetConstructor(elementType)) {
23 } else if (isWidgetConstructor(elementType)) {
24 const ctx = new WidgetRendition(elementType);
24 const ctx = new WidgetRendition(elementType);
25 if (args)
25 if (args)
26 args.forEach(x => ctx.visitNext(x));
26 args.forEach(x => ctx.visitNext(x));
27
27
28 return ctx;
28 return ctx;
29 } else if (typeof elementType === "function") {
29 } else if (typeof elementType === "function") {
30 const ctx = new FunctionRendition(elementType as (props: unknown) => Element);
30 const ctx = new FunctionRendition(elementType as (props: unknown) => Element);
31 if (args)
31 if (args)
32 args.forEach(x => ctx.visitNext(x));
32 args.forEach(x => ctx.visitNext(x));
33
33
34 return ctx;
34 return ctx;
35 } else {
35 } else {
36 throw new Error(`The element type '${String(elementType)}' is unsupported`);
36 throw new Error(`The element type '${String(elementType)}' is unsupported`);
37 }
37 }
38 }
38 }
39
39
40 export interface EventDetails<T = unknown> {
40 export interface EventDetails<T = unknown> {
41 detail: T;
41 detail: T;
42 }
42 }
43
43
44 export interface EventSelector {
44 export interface EventSelector {
45 selectorTarget: HTMLElement;
45 selectorTarget: HTMLElement;
46 target: HTMLElement;
46 target: HTMLElement;
47 }
47 }
48
48
49 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
49 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
50
50
51 type StatefulProps<T> = T extends Stateful<infer A> ? A :
51 type StatefulProps<T> = T extends Stateful<infer A> ? A :
52 T extends _WidgetBase ? T : never;
52 T extends _WidgetBase ? T : never;
53
53
54
54
55 /**
55 /**
56 * Observers the property and calls render callback each change.
56 * Observers the property and calls render callback each change.
57 *
57 *
58 * @param target The target object which property will be observed.
58 * @param target The target object which property will be observed.
59 * @param prop The name of the property.
59 * @param prop The name of the property.
60 * @param render The callback which will be called every time the value is changed
60 * @param render The callback which will be called every time the value is changed
61 * @returns Rendition which is created instantly
61 * @returns Rendition which is created instantly
62 */
62 */
63 export function watch<W extends _WidgetBase, K extends keyof W>(
63 export function watch<W extends _WidgetBase, K extends keyof W>(
64 target: W,
64 target: W,
65 prop: K,
65 prop: K,
66 render: (model: W[K]) => unknown
66 render: (model: W[K]) => unknown
67 ): Rendition;
67 ): Rendition;
68 /**
68 /**
69 * Observers the property and calls render callback each change.
69 * Observers the property and calls render callback each change.
70 *
70 *
71 * @param target The target object which property will be observed.
71 * @param target The target object which property will be observed.
72 * @param prop The name of the property.
72 * @param prop The name of the property.
73 * @param render The callback which will be called every time the value is changed
73 * @param render The callback which will be called every time the value is changed
74 * @returns Rendition which is created instantly
74 * @returns Rendition which is created instantly
75 */
75 */
76 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
76 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
77 target: T,
77 target: T,
78 prop: K,
78 prop: K,
79 render: (model: StatefulProps<T>[K]) => unknown
79 render: (model: StatefulProps<T>[K]) => unknown
80 ): Rendition;
80 ): Rendition;
81 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
81 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
82 export function watch(
82 export function watch(
83 ...args: [Stateful, string, (model: unknown) => unknown] |
83 ...args: [Stateful, string, (model: unknown) => unknown] |
84 [Subscribable<unknown>, (model: unknown) => unknown]
84 [Subscribable<unknown>, (model: unknown) => unknown]
85 ) {
85 ) {
86 if (args.length === 3) {
86 if (args.length === 3) {
87 const [target, prop, render] = args;
87 const [target, prop, render] = args;
88 return new WatchRendition(
88 return new WatchRendition(
89 render,
89 render,
90 observe(({next}) => {
90 observe(({next}) => {
91 const h = target.watch(
91 const h = target.watch(
92 prop,
92 prop,
93 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
93 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
94 );
94 );
95 next(target.get(prop));
95 next(target.get(prop));
96 return () => h.remove();
96 return () => h.remove();
97 })
97 })
98 );
98 );
99 } else {
99 } else {
100 const [subj, render] = args;
100 const [subj, render] = args;
101 return new WatchRendition(render, subj);
101 return new WatchRendition(render, subj);
102 }
102 }
103 }
103 }
104
104
105 export const watchFor = <T>(source: T[] | Subscribable<OrderedUpdate<T>> | null | undefined, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
105 export const watchFor = <T>(source: T[] | Subscribable<OrderedUpdate<T>> | null | undefined, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
106 return new WatchForRendition({
106 return new WatchForRendition({
107 ...opts,
107 ...opts,
108 subject: source,
108 subject: source,
109 component: render
109 component: render
110 });
110 });
111 };
111 };
112
112
113
113
114 export const prop: {
114 export const prop: {
115 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
115 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
116 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
116 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
117 } = (target: Stateful, name: string) => {
117 } = (target: Stateful, name: string) => {
118 return observe(({next}) => {
118 return observe(({next}) => {
119 const h = target.watch(
119 const h = target.watch(
120 name,
120 name,
121 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
121 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
122 );
122 );
123 next(target.get(name));
123 next(target.get(name));
124 return () => h.remove();
124 return () => h.remove();
125 });
125 });
126 };
126 };
127
127
128 export const attach = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
128 export const attach = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
129
129
130 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
130 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
131 let h = { unsubscribe() { } };
131 let h = { unsubscribe() { } };
132
132
133 return (el: Element | { set(name: K, value: T): void; } | undefined) => {
133 return (el: Element | { set(name: K, value: T): void; } | undefined) => {
134 if (el) {
134 if (el) {
135 if (isElementNode(el)) {
135 if (isElementNode(el)) {
136 h = subj.subscribe({
136 h = subj.subscribe({
137 next: value => djAttr.set(el, attr, value)
137 next: value => djAttr.set(el, attr, value)
138 });
138 });
139 } else {
139 } else {
140 h = subj.subscribe({
140 h = subj.subscribe({
141 next: value => el.set(attr, value)
141 next: value => el.set(attr, value)
142 });
142 });
143 }
143 }
144 } else {
144 } else {
145 h.unsubscribe();
145 h.unsubscribe();
146 }
146 }
147 };
147 };
148 };
148 };
149
149
150 /** Creates refHook to toggle the specified css class on the target element
151 *
152 * @param className
153 * @param subj a boolean observable value. When the value is false the className
154 * is removed, when the value is true, the className is added to the target element
155 * @returns refHook
156 */
150 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
157 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
151 let h = { unsubscribe() { } };
158 let h = { unsubscribe() { } };
152 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
159 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
153 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
160 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
154 if (el) {
161 if (el) {
155 h = subj.subscribe({
162 h = subj.subscribe({
156 next: v => djClass.toggle(el, className, v)
163 next: v => djClass.toggle(el, className, v || false)
157 });
164 });
158 } else {
165 } else {
159 h.unsubscribe();
166 h.unsubscribe();
160 }
167 }
161 };
168 };
162 };
169 };
163
170
171 /** Combines multiple hooks to the single one */
164 export const all = <T, A extends JSX.Ref<T>[]>(...cbs: A): JSX.Ref<T> => (arg: T | undefined) => cbs.forEach(cb => cb(arg));
172 export const all = <T, A extends JSX.Ref<T>[]>(...cbs: A): JSX.Ref<T> => (arg: T | undefined) => cbs.forEach(cb => cb(arg));
165
173
166 /** Decorates the method which will be registered as the handle for the specified event.
174 /** Decorates the method which will be registered as the handle for the specified event.
167 * This decorator can be applied to DjxWidgetBase subclass methods.
175 * This decorator can be applied to DjxWidgetBase subclass methods.
168 *
176 *
169 * ```
177 * ```
170 * @on("click")
178 * @on("click")
171 * _onClick(eventObj: MouseEvent) {
179 * _onClick(eventObj: MouseEvent) {
172 * // ...
180 * // ...
173 * }
181 * }
174 * ```
182 * ```
175 */
183 */
176 export const on = <E extends string>(...eventNames: E[]) =>
184 export const on = <E extends string>(...eventNames: E[]) =>
177 <K extends string,
185 <K extends string,
178 T extends DjxWidgetBase<object, { [p in E]: EV }>,
186 T extends DjxWidgetBase<object, { [p in E]: EV }>,
179 EV extends Event
187 EV extends Event
180 >(
188 >(
181 target: T,
189 target: T,
182 key: K,
190 key: K,
183 // eslint-disable-next-line @typescript-eslint/no-unused-vars
191 // eslint-disable-next-line @typescript-eslint/no-unused-vars
184 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
192 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
185 ) => {
193 ) => {
186 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
194 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
187 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
195 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
188 };
196 };
@@ -1,195 +1,197
1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { isPromise } from "@implab/core-amd/safe";
2 import { isPromise } from "@implab/core-amd/safe";
3 import { id as mid } from "module";
3 import { id as mid } from "module";
4 import { IScope, Scope } from "./Scope";
4 import { IScope, Scope } from "./Scope";
5 import { isNode, isRendition, isWidget } from "./traits";
5 import { isNode, isRendition, isWidget } from "./traits";
6
6
7 const trace = TraceSource.get(mid);
7 const trace = TraceSource.get(mid);
8
8
9 interface Context {
9 interface Context {
10 readonly scope: IScope;
10 readonly scope: IScope;
11
11
12 readonly hooks?: (() => void)[];
12 readonly hooks?: (() => void)[];
13 }
13 }
14
14
15 let _context: Context = {
15 let _context: Context = {
16 scope: Scope.dummy
16 scope: Scope.dummy
17 };
17 };
18
18
19 let _renderCount = 0;
19 let _renderCount = 0;
20 let _renderId = 1;
20 let _renderId = 1;
21 let _renderedHooks: (() => void)[] = [];
21 let _renderedHooks: (() => void)[] = [];
22
22
23
23
24 const guard = (cb: () => unknown) => {
24 const guard = (cb: () => unknown) => {
25 try {
25 try {
26 const result = cb();
26 const result = cb();
27 if (isPromise(result)) {
27 if (isPromise(result)) {
28 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
28 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
29 result.then(warn, warn);
29 result.then(warn, warn);
30 }
30 }
31 } catch (e) {
31 } catch (e) {
32 trace.error(e);
32 trace.error(e);
33 }
33 }
34 };
34 };
35
35
36 /**
36 /**
37 *
37 *
38 * @param scope
38 * @param scope
39 * @returns
39 * @returns
40 */
40 */
41 export const beginRender = (scope = getScope()) => {
41 export const beginRender = (scope = getScope()) => {
42 const prev = _context;
42 const prev = _context;
43 _renderCount++;
43 _renderCount++;
44 const renderId = _renderId++;
44 const renderId = _renderId++;
45 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
45 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
46 if (_renderCount === 1)
46 if (_renderCount === 1)
47 onRendering();
47 onRendering();
48
48
49 _context = {
49 _context = {
50 scope,
50 scope,
51 hooks: []
51 hooks: []
52 };
52 };
53 return endRender(prev, _context, renderId);
53 return endRender(prev, _context, renderId);
54 };
54 };
55
55
56 /**
56 /**
57 * Method for a deferred rendering. Returns a promise with `beginRender()` function.
57 * Method for a deferred rendering. Returns a promise with `beginRender()` function.
58 * Call to `scheduleRender` will save the current context, and will increment pending
58 * Call to `scheduleRender` will save the current context, and will increment pending
59 * operations counter.
59 * operations counter.
60 *
60 *
61 * @example
61 * @example
62 *
62 *
63 * const begin = await scheduleRender();
63 * const begin = await scheduleRender();
64 * const end = begin();
64 * const end = begin();
65 * try {
65 * try {
66 * // do some DOM manipulations
66 * // do some DOM manipulations
67 * } finally {
67 * } finally {
68 * end();
68 * end();
69 * }
69 * }
70 *
70 *
71 * @param scope
71 * @param scope
72 * @returns
72 * @returns
73 */
73 */
74 export const scheduleRender = async (scope = getScope()) => {
74 export const scheduleRender = async (scope = getScope()) => {
75 const prev = _context;
75 const prev = _context;
76 _renderCount++;
76 _renderCount++;
77 const renderId = _renderId ++;
77 const renderId = _renderId ++;
78 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
78 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
79 if (_renderCount === 1)
79 if (_renderCount === 1)
80 onRendering();
80 onRendering();
81
81
82 await Promise.resolve();
82 await Promise.resolve();
83
83
84 return () => {
84 return () => {
85 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
85 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
86 _context = {
86 _context = {
87 scope,
87 scope,
88 hooks: []
88 hooks: []
89 };
89 };
90 return endRender(prev, _context, renderId);
90 return endRender(prev, _context, renderId);
91 };
91 };
92 };
92 };
93
93
94 /**
94 /**
95 * Completes render operation
95 * Completes render operation
96 */
96 */
97 const endRender = (prev: Context, current: Context, renderId: number) => () => {
97 const endRender = (prev: Context, current: Context, renderId: number) => () => {
98 if (_context !== current)
98 if (_context !== current)
99 trace.error("endRender mismatched beginRender call");
99 trace.error("endRender mismatched beginRender call");
100
100
101 const { hooks } = _context;
101 const { hooks } = _context;
102 if (hooks)
102 if (hooks)
103 hooks.forEach(guard);
103 hooks.forEach(guard);
104
104
105 _renderCount--;
105 _renderCount--;
106 _context = prev;
106 _context = prev;
107
107
108 trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
108 trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
109 if (_renderCount === 0)
109 if (_renderCount === 0)
110 onRendered();
110 onRendered();
111 };
111 };
112
112
113 // called when the first beginRender is called for this iteration
113 // called when the first beginRender is called for this iteration
114 const onRendering = () => {
114 const onRendering = () => {
115 setTimeout(() => {
115 setTimeout(() => {
116 if (_renderCount !== 0)
116 if (_renderCount !== 0)
117 trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
117 trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
118 });
118 });
119 };
119 };
120
120
121 // called when all render operations are complete
121 // called when all render operations are complete
122 const onRendered = () => {
122 const onRendered = () => {
123 _renderedHooks.forEach(guard);
123 _renderedHooks.forEach(guard);
124 _renderedHooks = [];
124 _renderedHooks = [];
125 };
125 };
126
126
127 export const whenRendered = () => new Promise<void>((resolve) => {
127 export const whenRendered = () => new Promise<void>((resolve) => {
128 if (_renderCount)
128 if (_renderCount)
129 _renderedHooks.push(resolve);
129 _renderedHooks.push(resolve);
130 else
130 else
131 resolve();
131 resolve();
132 });
132 });
133
133
134 export const renderHook = (hook: () => void) => {
134 export const renderHook = (hook: () => void) => {
135 const { hooks } = _context;
135 const { hooks } = _context;
136 if (hooks)
136 if (hooks)
137 hooks.push(hook);
137 hooks.push(hook);
138 else
138 else
139 guard(hook);
139 guard(hook);
140 };
140 };
141
141
142 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
142 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
143 const { hooks, scope } = _context;
143 const { hooks, scope } = _context;
144 if (hooks)
144 if (hooks)
145 hooks.push(() => ref(value));
145 hooks.push(() => ref(value));
146 else
146 else
147 guard(() => ref(value));
147 guard(() => ref(value));
148
148
149 scope.own(() => ref(undefined));
149 scope.own(() => ref(undefined));
150 };
150 };
151
151
152 /** Returns the current scope */
152 /** Returns the current scope */
153 export const getScope = () => _context.scope;
153 export const getScope = () => _context.scope;
154
154
155 /** Schedules the rendition to be rendered to the DOM Node
155 /** Schedules the rendition to be rendered to the DOM Node
156 * @param rendition The rendition to be rendered
156 * @param rendition The rendition to be rendered
157 * @param scope The scope
157 * @param scope The scope
158 */
158 */
159 export const render = (rendition: unknown, scope = Scope.dummy) => {
159 export const render = (rendition: unknown, scope = Scope.dummy) => {
160 const complete = beginRender(scope);
160 const complete = beginRender(scope);
161 try {
161 try {
162 return getItemDom(rendition);
162 return getItemDom(rendition);
163 } finally {
163 } finally {
164 complete();
164 complete();
165 }
165 }
166 };
166 };
167
167
168 const emptyFragment = document.createDocumentFragment();
169
168 /** Renders DOM element for different types of the argument. */
170 /** Renders DOM element for different types of the argument. */
169 export const getItemDom = (v: unknown) => {
171 export const getItemDom = (v: unknown) => {
170 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
172 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
171 // primitive types converted to the text nodes
173 // primitive types converted to the text nodes
172 return document.createTextNode(v.toString());
174 return document.createTextNode(v.toString());
173 } else if (isNode(v)) {
175 } else if (isNode(v)) {
174 // nodes are kept as is
176 // nodes are kept as is
175 return v;
177 return v;
176 } else if (isRendition(v)) {
178 } else if (isRendition(v)) {
177 // renditions are instantiated
179 // renditions are instantiated
178 return v.getDomNode();
180 return v.getDomNode();
179 } else if (isWidget(v)) {
181 } else if (isWidget(v)) {
180 // widgets are converted to it's markup
182 // widgets are converted to it's markup
181 return v.domNode;
183 return v.domNode;
182 } else if (typeof v === "boolean" || v === null || v === undefined) {
184 } else if (typeof v === "boolean" || v === null || v === undefined) {
183 // null | undefined | boolean are removed
185 // null | undefined | boolean are removed
184 return document.createDocumentFragment();
186 return emptyFragment;
185 } else if (v instanceof Array) {
187 } else if (v instanceof Array) {
186 // arrays will be translated to document fragments
188 // arrays will be translated to document fragments
187 const fragment = document.createDocumentFragment();
189 const fragment = document.createDocumentFragment();
188 v.map(item => getItemDom(item))
190 v.map(item => getItemDom(item))
189 .forEach(node => fragment.appendChild(node));
191 .forEach(node => fragment.appendChild(node));
190 return fragment;
192 return fragment;
191 } else {
193 } else {
192 // bug: explicit error otherwise
194 // bug: explicit error otherwise
193 throw new Error(`Invalid parameter: ${String(v)}`);
195 throw new Error(`Invalid parameter: ${String(v)}`);
194 }
196 }
195 };
197 };
@@ -1,87 +1,87
1 import { djbase, djclass, bind, prototype, AbstractConstructor } from "../declare";
1 import { djbase, djclass, bind, prototype, AbstractConstructor } from "../declare";
2
2
3 import { DjxWidgetBase } from "../tsx/DjxWidgetBase";
3 import { DjxWidgetBase } from "../tsx/DjxWidgetBase";
4 import { createElement, on } from "../tsx";
4 import { createElement, on } from "../tsx";
5 import { argumentNotNull } from "@implab/core-amd/safe";
5 import { argumentNotNull } from "@implab/core-amd/safe";
6
6
7 interface MyWidgetAttrs {
7 interface MyWidgetAttrs {
8 title: string;
8 title: string;
9
9
10 counter: number;
10 counter: number;
11 }
11 }
12
12
13 interface MyWidgetEvents {
13 interface MyWidgetEvents {
14 "count-inc": Event & {
14 "count-inc": Event & {
15 detail: number;
15 detail: number;
16 };
16 };
17
17
18 "count-dec": Event & {
18 "count-dec": Event & {
19 detail: number;
19 detail: number;
20 };
20 };
21 }
21 }
22
22
23 interface FrameProps {
23 interface FrameProps {
24 ref?: JSX.Ref<HTMLDivElement>;
24 ref?: JSX.Ref<HTMLDivElement>;
25 children?: unknown[];
25 children?: unknown[];
26 }
26 }
27
27
28 const Frame = ({children, ref}: FrameProps) => <div ref={ref} >{children}</div>;
28 const Frame = ({children, ref}: FrameProps) => <div ref={ref} >{children}</div>;
29
29
30 @djclass
30 @djclass
31 export class MyWidget extends djbase(DjxWidgetBase as AbstractConstructor<DjxWidgetBase<MyWidgetAttrs, MyWidgetEvents>>) {
31 export class MyWidget extends djbase(DjxWidgetBase as AbstractConstructor<DjxWidgetBase<MyWidgetAttrs, MyWidgetEvents>>) {
32
32
33 @bind({ node: "titleNode", type: "innerHTML" })
33 @bind({ node: "titleNode", type: "innerHTML" })
34 title = "";
34 title = "";
35
35
36 @prototype(0)
36 @prototype(0)
37 counter = 0;
37 counter = 0;
38
38
39 frameNode?: HTMLDivElement;
39 frameNode?: HTMLDivElement;
40
40
41 render() {
41 render() {
42
42
43 return <div className="myWidget" onsubmit={this._onSubmit} tabIndex={3} style={{ alignContent: "center", border: "1px solid" }} >
43 return <div className="myWidget" onsubmit={this._onSubmit} tabIndex={3} style={{ alignContent: "center", border: "1px solid" }} >
44 <h1 data-dojo-attach-point="titleNode"></h1>
44 <h1 data-dojo-attach-point="titleNode"></h1>
45 <Frame ref={this._setFrameElement}>
45 <Frame ref={this._setFrameElement}>
46 <span class="up-button" onclick={this._onIncClick}>[+]</span>
46 <span class="up-button" onclick={this._onIncClick}>[+]</span>
47 <span class="down-button" onclick={() => this._onDecClick()}>[-]</span>
47 <span class="down-button" onclick={() => this._onDecClick()}>[-]</span>
48 </Frame>
48 </Frame>
49 </div>;
49 </div>;
50 }
50 }
51
51
52 private readonly _setFrameElement = (node?: HTMLDivElement) => {
52 private readonly _setFrameElement = (node?: HTMLDivElement) => {
53 this.frameNode = node;
53 this.frameNode = node;
54 };
54 };
55
55
56 postCreate() {
56 postCreate() {
57 super.postCreate();
57 super.postCreate();
58
58
59 this.on("click", () => {});
59 this.on("click", () => {});
60 }
60 }
61
61
62 private readonly _onSubmit = (evt: Event) => {
62 private readonly _onSubmit = (evt: Event) => {
63 argumentNotNull(evt, "evt");
63 argumentNotNull(evt, "evt");
64 };
64 };
65
65
66 private readonly _onIncClick = (evt: MouseEvent) => {
66 private readonly _onIncClick = (evt: MouseEvent) => {
67 argumentNotNull(evt, "evt");
67 argumentNotNull(evt, "evt");
68
68
69 this.set("counter", this.counter + 1);
69 this.set("counter", this.counter + 1);
70
70
71 this.emit("count-inc", { bubbles: false });
71 this.emit("count-inc", { bubbles: false });
72 };
72 };
73
73
74 _onDecClick() {
74 _onDecClick() {
75 this.emit("count-dec", { bubbles: false, detail: this.counter });
75 this.emit("count-dec", { bubbles: false, detail: this.counter });
76 }
76 }
77
77
78 @on("count-inc")
78 @on("count-inc")
79 private _onCounterInc(evt: Event & { detail: number; x?: number; }) {
79 protected _onCounterInc(evt: Event & { detail: number; x?: number; }) {
80 argumentNotNull(evt, "evt");
80 argumentNotNull(evt, "evt");
81 }
81 }
82
82
83 @on("click", "keydown")
83 @on("click", "keydown")
84 protected _onClick(evt: MouseEvent | KeyboardEvent) {
84 protected _onClick(evt: MouseEvent | KeyboardEvent) {
85 argumentNotNull(evt, "evt");
85 argumentNotNull(evt, "evt");
86 }
86 }
87 } No newline at end of file
87 }
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now