##// END OF EJS Templates
added form/Form widget and form/_FormMixin, this is a rewritten version of the corresponding dojo classed....
cin -
r134:f139e2153e0d v1.9.0-rc1 default
parent child
Show More

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

@@ -0,0 +1,134
1
2 import { djbase, djclass } from "../declare";
3 import { attach, createElement } from "../tsx";
4 import { DjxWidgetBase } from "../tsx/DjxWidgetBase";
5 import _FormMixin from "./_FormMixin";
6
7
8 /** This widget represents a document section containing interactive controls
9 * for submitting information.
10 */
11 @djclass
12 export default class Form<T extends object> extends djbase(DjxWidgetBase, _FormMixin) {
13
14 /** Name of form for scripting. */
15 name?: string;
16
17 /** The URL that processes the form submission. */
18 action?: string;
19
20 /** The HTTP method to submit the form with. */
21 method?: "POST" | "GET" | "DIALOG";
22
23 /** If the value of the method attribute is post, enctype is the MIME type
24 * of the form submission.
25 */
26 enctype?: string;
27
28 /** Comma-separated content types the server accepts.
29 *
30 * @deprecated This attribute has been deprecated and should not be used.
31 * Instead, use the accept attribute on <input type=file> elements.
32 */
33 accept?: string;
34
35 /** Space-separated character encodings the server accepts. The browser
36 * uses them in the order in which they are listed.
37 */
38 "accept-charset"?: string;
39
40 /** Indicates where to display the response after submitting the form. It
41 * is a name/keyword for a browsing context (for example, tab, window, or
42 * iframe).
43 */
44 target?: string;
45
46 /** Indicates whether input elements can by default have their values
47 * automatically completed by the browser. `autocomplete` attributes on form
48 * elements override it on `<form>`.
49 */
50 autocomplete?: "off" | "on";
51
52
53 /** This Boolean attribute indicates that the form shouldn't be validated
54 * when submitted. If this attribute is not set the form is validated, it
55 * can be overridden by a `formnovalidate` attribute on a `<button>`,
56 * `<input type="submit">`, or `<input type="image">` element belonging to
57 * the form.
58 */
59 novalidate?: boolean;
60
61 value?: T;
62
63 containerNode?: HTMLFormElement;
64
65 _resetting = false;
66
67 constructor(opts?: Partial<Form<T>>, srcRef?: string | Node) {
68 super(opts, srcRef);
69 }
70
71 render() {
72 return <form onreset={this._onReset} onsubmit={this._onSubmit} ref={attach(this, "containerNode")}>
73 </form>;
74 }
75
76 postCreate() {
77 super.postCreate();
78
79 this.watch("value", (_prop, _oldValue, newValue) => this.onValueChange(newValue));
80 }
81
82 private readonly _onReset = (evt: Event) => {
83 if (!this._resetting) {
84 // re-dispatch event to replace default reset behavior
85 try {
86 this._resetting = true;
87 evt.preventDefault();
88 if(this.containerNode?.dispatchEvent(new Event(evt.type, evt)))
89 super.reset();
90 } finally {
91 this._resetting = false;
92 }
93 } else {
94 this.onReset(evt);
95 }
96 };
97
98 private readonly _onSubmit = (evt: SubmitEvent) => {
99 if(!this.novalidate && !this.validate()) {
100 evt.preventDefault();
101 evt.stopPropagation();
102 } else {
103 this.onSubmit(evt);
104 }
105 };
106
107 // eslint-disable-next-line @typescript-eslint/no-unused-vars
108 onSubmit(evt: Event) {
109 }
110
111 // eslint-disable-next-line @typescript-eslint/no-unused-vars
112 onReset(evt: Event) {
113 }
114
115 // eslint-disable-next-line @typescript-eslint/no-unused-vars
116 onValueChange(value?: T) {
117 }
118
119 /** Resets the form. */
120 reset() {
121 if (!this.containerNode)
122 throw new Error("Can't reset the destroyed form");
123 this.containerNode.reset();
124 }
125
126 /** Programmatically submits form, no additional events emitted
127 * or checks made.
128 */
129 submit() {
130 if (!this.containerNode)
131 throw new Error("Can't submit the destroyed form");
132 this.containerNode.submit();
133 }
134 }
@@ -0,0 +1,319
1 import { id as mid } from "module";
2 import { djbase, djclass } from "../declare";
3 import { whenRendered } from "../tsx/render";
4 import { TraceSource } from "@implab/core-amd/log/TraceSource";
5 import _WidgetBase = require("dijit/_WidgetBase");
6 import _FormValueMixin = require("dijit/form/_FormValueMixin");
7 import { scrollIntoView } from "dojo/window";
8 import { get } from "@implab/core-amd/safe";
9
10 const trace = TraceSource.get(mid);
11
12 const isValueWidget = <W>(w: W): w is W & _FormValueMixin =>
13 typeof w === "object" && w !== null && "value" in w;
14
15 const testMember = <T extends object>(member: keyof T, type?: "function" | "boolean" | "string" | "number" | "object") => type ?
16 <V extends object>(v: V): v is V & T => typeof v === "object" && v !== null && (typeof (v as V & T)[member] === type) :
17 <V extends object>(v: V): v is V & T => typeof v === "object" && v !== null && (member in v);
18
19 // reset method support
20 type Resettable = { reset(): void };
21
22 const isResettable = testMember<Resettable>("reset", "function");
23
24 // validation support
25 type Validatable = { validate(): boolean };
26
27 const isValidatable = testMember<Validatable>("validate", "function");
28
29 // checkbox/toggle button/radio widgets support
30 type CheckedProp = { checked: boolean };
31
32 const hasCheckedProp = testMember<CheckedProp>("checked", "boolean");
33
34 // multi-select support
35 type MultipleProp = { multiple: unknown };
36
37 const hasMultipleProp = testMember<MultipleProp>("multiple");
38
39 // declared class
40 type DeclaredClassProp = { declaredClass: string };
41
42 const hasDeclaredClassProp = testMember<DeclaredClassProp>("declaredClass", "string");
43
44 // state
45
46 type StateProp = { state: string };
47
48 const hasStateProp = testMember<StateProp>("state", "string");
49
50 // common type for form members
51 type FormValueWidget = _WidgetBase & _FormValueMixin;
52
53 /** Traverses child widgets collecting form inputs.
54 *
55 * @param children Widgets to traverse.
56 * @returns The array of form inputs.
57 */
58 const collectDescendantFormWidgets = (children: _WidgetBase[]): (FormValueWidget)[] => children
59 .map(child =>
60 isValueWidget(child) ?
61 [child] :
62 collectDescendantFormWidgets(child.getChildren())
63 )
64 .reduce((res, part) => res.concat(part), []);
65
66 // helper functions to manipulate the form value
67
68 /** checks whether the value is and object and returns it otherwise returns
69 * a new empty object. Useful to auto-crete a nested objects on demand.
70 */
71 const _obj = (v: unknown) => (typeof v === "object" && v !== null ? v : {}) as Record<string, unknown>;
72
73 /** Combines the values
74 *
75 * @param prev The previous value, `undefined` if none
76 * @param value The new value to store
77 * @param hint The hint how to combine values
78 */
79 const _combine = (prev: unknown, value: unknown, hint?: string) =>
80 // write the value as an array and append new values to it
81 hint === "array" ? prev === undefined ? [value] : ([] as unknown[]).concat(prev, value) :
82 // write the value as is and convert it the array when new values are appended
83 hint === "append" ? prev === undefined ? value : ([] as unknown[]).concat(prev, value) :
84 // write the value only if the previous one is undefined
85 hint === "unset" ? prev === undefined ? value : prev :
86 // overwrite
87 value;
88
89 /** Merges the specified value to the object. The function takes a path for the
90 * new value and creates a nested objects if needed. The hint is used to control
91 * how to store a new or update the existing value.
92 */
93 const _merge = (
94 [prop, ...rest]: string[],
95 obj: Record<string, unknown>,
96 value: unknown,
97 hint?: string
98 ): unknown => ({
99 ...obj,
100 [prop]: rest.length > 0 ?
101 _merge(rest, _obj(obj[prop]), value) :
102 _combine(obj[prop], value, hint)
103 });
104
105 /** Merges the specified value to the object. The function takes a path for the
106 * new value and creates a nested objects if needed. The hint is used to control
107 * how to store a new or update the existing value.
108 *
109 * @param name The path of the property to assign, `x.y.z`
110 * @param value The value for the specified property
111 * @param hint The hint how to store the value. The valid values are:
112 * `array`, `append`, `unset`.
113 */
114 const _assign = (name: string, obj: object, value: unknown, hint?: string) =>
115 _merge(name.split("."), _obj(obj), value, hint) as object;
116
117 @djclass
118 abstract class _FormMixin extends djbase<_WidgetBase<Record<string, Event>>>() {
119
120 /** The form value. When assigned in the constructor is considered as
121 * initial (reset value) value of the form.
122 */
123 value?: object | undefined;
124
125 _resetValue?: object | undefined;
126
127 private _pending: { value: object; priority?: boolean | null } | undefined;
128
129 state: "Error" | "Incomplete" | "" = "";
130
131 _onChangeDelayTimer = { remove: () => { } };
132
133 /** Fill in form values from according to an Object (in the format returned
134 * by get('value'))
135 *
136 * This method schedules updating form values after
137 */
138 _setValueAttr(value: object, priority?: boolean | null) {
139 if (!this._pending) {
140 Promise.resolve() // delay value update
141 .then(whenRendered) // await for the rendering to complete
142 .then(() => {
143 if (this._pending) { // double check
144 const { value, priority } = this._pending;
145 this._pending = undefined;
146 this._setValueImpl(value, priority);
147 }
148 }).catch(e => trace.error(e));
149 }
150 this._pending = { value, priority };
151 }
152
153 getDescendantFormWidgets() {
154 return collectDescendantFormWidgets(this.getChildren());
155 }
156
157 /**
158 * Resets contents of the form to initial value if any. If an input doesn't
159 * have a corresponding initial value it is reset to its default value.
160 */
161 reset() {
162 this.getDescendantFormWidgets()
163 .filter(isResettable)
164 .forEach(w => w.reset());
165 if (this._resetValue)
166 this.set("value", this._resetValue);
167 }
168
169 /**
170 * returns if the form is valid - same as isValid - but
171 * provides a few additional (ui-specific) features:
172 *
173 * 1. it will highlight any sub-widgets that are not valid
174 * 2. it will call focus() on the first invalid sub-widget
175 */
176 validate() {
177 const [firstError] = this.getDescendantFormWidgets()
178 .map(w => {
179 w._hasBeenBlurred = true;
180 const valid = w.disabled || !isValidatable(w) || w.validate();
181 return valid ? null : w;
182 })
183 .filter(Boolean);
184
185 if (firstError) {
186 scrollIntoView(firstError.containerNode || firstError.domNode);
187 firstError.focus();
188 return false;
189 } else {
190 return true;
191 }
192 }
193
194 _setValueImpl(obj: object, priority?: boolean | null) {
195 const map = this.getDescendantFormWidgets()
196 .filter(w => w.name)
197 .reduce((g, w) => {
198 const entry = g[w.name];
199 return {
200 ...g,
201 [w.name]: entry ? entry.concat(w) : [w]
202 };
203 }, {} as Record<string, FormValueWidget[]>);
204
205 Object.keys(map).forEach(name => {
206 const widgets = map[name];
207 const _values = get(name, obj) as unknown;
208
209 if (_values !== undefined) {
210 const values = ([] as unknown[]).concat(_values);
211
212 const [w] = widgets; // at least one widget per group
213 if (hasCheckedProp(w)) {
214 widgets.forEach(w =>
215 w.set("value", values.indexOf(w._get("value")) >= 0, priority)
216 );
217 } else if (hasMultipleProp(w) && w.multiple) {
218 w.set("value", values, priority);
219 } else {
220 widgets.forEach((w, i) => w.set("value", values[i], priority));
221 }
222 }
223 });
224 }
225
226 _getValueAttr() {
227 return this.getDescendantFormWidgets()
228 .map(widget => {
229 const name = widget.name;
230 if (name && !widget.disabled) {
231
232 const value = widget.get("value") as unknown;
233
234 if (hasCheckedProp(widget)) {
235 if (hasDeclaredClassProp(widget) && /Radio/.test(widget.declaredClass)) {
236 // radio button
237 if (value !== false) {
238 return { name, value };
239 } else {
240 // give radio widgets a default of null
241 return { name, value: null, hint: "unset" };
242 }
243 } else {
244 // checkbox/toggle button
245 if (value !== false)
246 return { name, value, hint: "array" };
247 }
248 } else {
249 return { name, value, hint: "append" };
250 }
251 }
252 return {};
253 })
254 .reduce((obj, { name, value, hint }) => name ? _assign(name, obj, value, hint) : obj, {});
255 }
256
257 /** Returns true if all of the widgets are valid.
258 * @deprecated will be removed
259 */
260 isValid() {
261 return this.state === "";
262 }
263
264 /** Triggered when the validity state changes */
265 // eslint-disable-next-line @typescript-eslint/no-unused-vars
266 onValidStateChange(validity: boolean) {
267 }
268
269 _getState() {
270 const states = this.getDescendantFormWidgets().filter(hasStateProp).map(w => w.get("state"));
271
272 return states.indexOf("Error") >= 0 ? "Error" :
273 states.indexOf("Incomplete") >= 0 ? "Incomplete" : "";
274 }
275
276 _onChildChange(attr: string) {
277 // summary:
278 // Called when child's value or disabled state changes
279
280 // The unit tests expect state update to be synchronous, so update it immediately.
281 if (!attr || attr == "state" || attr == "disabled") {
282 this._set("state", this._getState());
283 }
284
285 // Use defer() to collapse value changes in multiple children into a single
286 // update to my value. Multiple updates will occur on:
287 // 1. Form.set()
288 // 2. Form.reset()
289 // 3. user selecting a radio button (which will de-select another radio button,
290 // causing two onChange events)
291 if (!attr || attr == "value" || attr == "disabled" || attr == "checked") {
292 if (this._onChangeDelayTimer) {
293 this._onChangeDelayTimer.remove();
294 }
295 this._onChangeDelayTimer = this.defer(() => {
296 this._onChangeDelayTimer = { remove: () => { } };
297 this._set("value", this.get("value"));
298 }, 10);
299 }
300 }
301
302 postCreate() {
303 super.postCreate();
304
305 this._resetValue = this.value;
306
307 this.on("attrmodified-state, attrmodified-disabled, attrmodified-value, attrmodified-checked", this._onAttrModified);
308 this.watch("state", (prop, ov, nv) => this.onValidStateChange(nv === ""));
309 }
310
311 private readonly _onAttrModified = ({ target, type }: Event) => {
312 // ignore events that I fire on myself because my children changed
313 if (target !== this.domNode) {
314 this._onChildChange(type.replace("attrmodified-", ""));
315 }
316 };
317 }
318
319 export default _FormMixin; No newline at end of file
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,16 +1,18
1 {
1 {
2 "java.configuration.updateBuildConfiguration": "automatic",
2 "java.configuration.updateBuildConfiguration": "automatic",
3 "files.exclude": {
3 "files.exclude": {
4 "**/.classpath": true,
4 "**/.classpath": true,
5 "**/.project": true,
5 "**/.project": true,
6 "**/.settings": true,
6 "**/.settings": true,
7 "**/.factorypath": true
7 "**/.factorypath": true
8 },
8 },
9 "cSpell.words": [
9 "cSpell.words": [
10 "attrmodified",
10 "dijit",
11 "dijit",
11 "djbase",
12 "djbase",
12 "djclass",
13 "djclass",
13 "Unsubscribable",
14 "Unsubscribable",
15 "Validatable",
14 "wpos"
16 "wpos"
15 ]
17 ]
16 } No newline at end of file
18 }
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,43 +1,43
1 {
1 {
2 "name": "@implab/djx",
2 "name": "@implab/djx",
3 "version": "0.0.1-dev",
3 "version": "0.0.1-dev",
4 "description": "Supports using dojo version 1 with typescript and .tsx files",
4 "description": "Supports using dojo version 1 with typescript and .tsx files",
5 "keywords": [
5 "keywords": [
6 "dojo",
6 "dojo",
7 "tsx",
7 "tsx",
8 "typescript",
8 "typescript",
9 "widgets"
9 "widgets"
10 ],
10 ],
11 "author": "Implab team",
11 "author": "Implab team",
12 "license": "BSD-2-Clause",
12 "license": "BSD-2-Clause",
13 "repository": "https://code.implab.org/implab/implabjs-djx",
13 "repository": "https://code.implab.org/implab/implabjs-djx",
14 "publishConfig": {
14 "publishConfig": {
15 "access": "public"
15 "access": "public"
16 },
16 },
17 "peerDependencies": {
17 "peerDependencies": {
18 "@implab/core-amd": "^1.4.6",
18 "@implab/core-amd": "^1.4.6",
19 "dojo": "^1.10.0"
19 "dojo": "^1.10.0"
20 },
20 },
21 "devDependencies": {
21 "devDependencies": {
22 "@implab/core-amd": "^1.4.6",
22 "@implab/core-amd": "^1.4.6",
23 "@types/chai": "4.1.3",
23 "@types/chai": "4.1.3",
24 "@types/requirejs": "2.1.31",
24 "@types/requirejs": "2.1.31",
25 "@types/yaml": "1.2.0",
25 "@types/yaml": "1.2.0",
26 "@types/tap": "15.0.7",
26 "@types/tap": "15.0.7",
27 "rxjs": "7.5.6",
27 "rxjs": "7.5.6",
28 "dojo": "1.16.0",
28 "dojo": "1.16.0",
29 "@implab/dojo-typings": "1.0.3",
29 "@implab/dojo-typings": "1.0.7",
30 "@typescript-eslint/eslint-plugin": "^5.23.0",
30 "@typescript-eslint/eslint-plugin": "^5.23.0",
31 "@typescript-eslint/parser": "^5.23.0",
31 "@typescript-eslint/parser": "^5.23.0",
32 "eslint": "^8.28.0",
32 "eslint": "^8.28.0",
33 "eslint-config-standard": "^17.0.0",
33 "eslint-config-standard": "^17.0.0",
34 "eslint-plugin-import": "^2.26.0",
34 "eslint-plugin-import": "^2.26.0",
35 "eslint-plugin-n": "^15.2.0",
35 "eslint-plugin-n": "^15.2.0",
36 "eslint-plugin-promise": "^6.0.0",
36 "eslint-plugin-promise": "^6.0.0",
37 "eslint-plugin-react": "^7.29.4",
37 "eslint-plugin-react": "^7.29.4",
38 "requirejs": "2.3.6",
38 "requirejs": "2.3.6",
39 "typescript": "4.8.3",
39 "typescript": "4.8.3",
40 "yaml": "~1.7.2",
40 "yaml": "~1.7.2",
41 "tap": "16.3.0"
41 "tap": "16.3.0"
42 }
42 }
43 }
43 }
@@ -1,196 +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, priority?: boolean): 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, false)
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
150 /** Creates refHook to toggle the specified css class on the target element
151 *
151 *
152 * @param className
152 * @param className
153 * @param subj a boolean observable value. When the value is false the 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
154 * is removed, when the value is true, the className is added to the target element
155 * @returns refHook
155 * @returns refHook
156 */
156 */
157 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
157 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
158 let h = { unsubscribe() { } };
158 let h = { unsubscribe() { } };
159 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
159 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
160 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
160 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
161 if (el) {
161 if (el) {
162 h = subj.subscribe({
162 h = subj.subscribe({
163 next: v => djClass.toggle(el, className, v || false)
163 next: v => djClass.toggle(el, className, v || false)
164 });
164 });
165 } else {
165 } else {
166 h.unsubscribe();
166 h.unsubscribe();
167 }
167 }
168 };
168 };
169 };
169 };
170
170
171 /** Combines multiple hooks to the single one */
171 /** Combines multiple hooks to the single one */
172 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));
173
173
174 /** 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.
175 * This decorator can be applied to DjxWidgetBase subclass methods.
175 * This decorator can be applied to DjxWidgetBase subclass methods.
176 *
176 *
177 * ```
177 * ```
178 * @on("click")
178 * @on("click")
179 * _onClick(eventObj: MouseEvent) {
179 * _onClick(eventObj: MouseEvent) {
180 * // ...
180 * // ...
181 * }
181 * }
182 * ```
182 * ```
183 */
183 */
184 export const on = <E extends string>(...eventNames: E[]) =>
184 export const on = <E extends string>(...eventNames: E[]) =>
185 <K extends string,
185 <K extends string,
186 T extends DjxWidgetBase<object, { [p in E]: EV }>,
186 T extends DjxWidgetBase<object, { [p in E]: EV }>,
187 EV extends Event
187 EV extends Event
188 >(
188 >(
189 target: T,
189 target: T,
190 key: K,
190 key: K,
191 // eslint-disable-next-line @typescript-eslint/no-unused-vars
191 // eslint-disable-next-line @typescript-eslint/no-unused-vars
192 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
192 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
193 ) => {
193 ) => {
194 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
194 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
195 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
195 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
196 };
196 };
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now