##// 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
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,16 +1,18
1 1 {
2 2 "java.configuration.updateBuildConfiguration": "automatic",
3 3 "files.exclude": {
4 4 "**/.classpath": true,
5 5 "**/.project": true,
6 6 "**/.settings": true,
7 7 "**/.factorypath": true
8 8 },
9 9 "cSpell.words": [
10 "attrmodified",
10 11 "dijit",
11 12 "djbase",
12 13 "djclass",
13 14 "Unsubscribable",
15 "Validatable",
14 16 "wpos"
15 17 ]
16 18 } No newline at end of file
1 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 2 "name": "@implab/djx",
3 3 "version": "0.0.1-dev",
4 4 "description": "Supports using dojo version 1 with typescript and .tsx files",
5 5 "keywords": [
6 6 "dojo",
7 7 "tsx",
8 8 "typescript",
9 9 "widgets"
10 10 ],
11 11 "author": "Implab team",
12 12 "license": "BSD-2-Clause",
13 13 "repository": "https://code.implab.org/implab/implabjs-djx",
14 14 "publishConfig": {
15 15 "access": "public"
16 16 },
17 17 "peerDependencies": {
18 18 "@implab/core-amd": "^1.4.6",
19 19 "dojo": "^1.10.0"
20 20 },
21 21 "devDependencies": {
22 22 "@implab/core-amd": "^1.4.6",
23 23 "@types/chai": "4.1.3",
24 24 "@types/requirejs": "2.1.31",
25 25 "@types/yaml": "1.2.0",
26 26 "@types/tap": "15.0.7",
27 27 "rxjs": "7.5.6",
28 28 "dojo": "1.16.0",
29 "@implab/dojo-typings": "1.0.3",
29 "@implab/dojo-typings": "1.0.7",
30 30 "@typescript-eslint/eslint-plugin": "^5.23.0",
31 31 "@typescript-eslint/parser": "^5.23.0",
32 32 "eslint": "^8.28.0",
33 33 "eslint-config-standard": "^17.0.0",
34 34 "eslint-plugin-import": "^2.26.0",
35 35 "eslint-plugin-n": "^15.2.0",
36 36 "eslint-plugin-promise": "^6.0.0",
37 37 "eslint-plugin-react": "^7.29.4",
38 38 "requirejs": "2.3.6",
39 39 "typescript": "4.8.3",
40 40 "yaml": "~1.7.2",
41 41 "tap": "16.3.0"
42 42 }
43 43 }
@@ -1,196 +1,196
1 1 import { Constructor } from "@implab/core-amd/interfaces";
2 2 import { HtmlRendition } from "./tsx/HtmlRendition";
3 3 import { WidgetRendition } from "./tsx/WidgetRendition";
4 4 import { isElementNode, isWidget, isWidgetConstructor, Rendition } from "./tsx/traits";
5 5 import { FunctionRendition } from "./tsx/FunctionRendition";
6 6 import Stateful = require("dojo/Stateful");
7 7 import _WidgetBase = require("dijit/_WidgetBase");
8 8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
9 9 import { WatchRendition } from "./tsx/WatchRendition";
10 10 import { Observable, observe, Subscribable } from "./observable";
11 11 import djAttr = require("dojo/dom-attr");
12 12 import djClass = require("dojo/dom-class");
13 13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
14 14 import { OrderedUpdate } from "./store";
15 15
16 16 export function createElement<T extends Constructor | string | ((props: object) => Element)>(elementType: T, ...args: unknown[]): Rendition {
17 17 if (typeof elementType === "string") {
18 18 const ctx = new HtmlRendition(elementType);
19 19 if (args)
20 20 args.forEach(x => ctx.visitNext(x));
21 21
22 22 return ctx;
23 23 } else if (isWidgetConstructor(elementType)) {
24 24 const ctx = new WidgetRendition(elementType);
25 25 if (args)
26 26 args.forEach(x => ctx.visitNext(x));
27 27
28 28 return ctx;
29 29 } else if (typeof elementType === "function") {
30 30 const ctx = new FunctionRendition(elementType as (props: unknown) => Element);
31 31 if (args)
32 32 args.forEach(x => ctx.visitNext(x));
33 33
34 34 return ctx;
35 35 } else {
36 36 throw new Error(`The element type '${String(elementType)}' is unsupported`);
37 37 }
38 38 }
39 39
40 40 export interface EventDetails<T = unknown> {
41 41 detail: T;
42 42 }
43 43
44 44 export interface EventSelector {
45 45 selectorTarget: HTMLElement;
46 46 target: HTMLElement;
47 47 }
48 48
49 49 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
50 50
51 51 type StatefulProps<T> = T extends Stateful<infer A> ? A :
52 52 T extends _WidgetBase ? T : never;
53 53
54 54
55 55 /**
56 56 * Observers the property and calls render callback each change.
57 57 *
58 58 * @param target The target object which property will be observed.
59 59 * @param prop The name of the property.
60 60 * @param render The callback which will be called every time the value is changed
61 61 * @returns Rendition which is created instantly
62 62 */
63 63 export function watch<W extends _WidgetBase, K extends keyof W>(
64 64 target: W,
65 65 prop: K,
66 66 render: (model: W[K]) => unknown
67 67 ): Rendition;
68 68 /**
69 69 * Observers the property and calls render callback each change.
70 70 *
71 71 * @param target The target object which property will be observed.
72 72 * @param prop The name of the property.
73 73 * @param render The callback which will be called every time the value is changed
74 74 * @returns Rendition which is created instantly
75 75 */
76 76 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
77 77 target: T,
78 78 prop: K,
79 79 render: (model: StatefulProps<T>[K]) => unknown
80 80 ): Rendition;
81 81 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
82 82 export function watch(
83 83 ...args: [Stateful, string, (model: unknown) => unknown] |
84 84 [Subscribable<unknown>, (model: unknown) => unknown]
85 85 ) {
86 86 if (args.length === 3) {
87 87 const [target, prop, render] = args;
88 88 return new WatchRendition(
89 89 render,
90 90 observe(({next}) => {
91 91 const h = target.watch(
92 92 prop,
93 93 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
94 94 );
95 95 next(target.get(prop));
96 96 return () => h.remove();
97 97 })
98 98 );
99 99 } else {
100 100 const [subj, render] = args;
101 101 return new WatchRendition(render, subj);
102 102 }
103 103 }
104 104
105 105 export const watchFor = <T>(source: T[] | Subscribable<OrderedUpdate<T>> | null | undefined, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
106 106 return new WatchForRendition({
107 107 ...opts,
108 108 subject: source,
109 109 component: render
110 110 });
111 111 };
112 112
113 113
114 114 export const prop: {
115 115 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
116 116 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
117 117 } = (target: Stateful, name: string) => {
118 118 return observe(({next}) => {
119 119 const h = target.watch(
120 120 name,
121 121 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
122 122 );
123 123 next(target.get(name));
124 124 return () => h.remove();
125 125 });
126 126 };
127 127
128 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 130 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
131 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 134 if (el) {
135 135 if (isElementNode(el)) {
136 136 h = subj.subscribe({
137 137 next: value => djAttr.set(el, attr, value)
138 138 });
139 139 } else {
140 140 h = subj.subscribe({
141 next: value => el.set(attr, value)
141 next: value => el.set(attr, value, false)
142 142 });
143 143 }
144 144 } else {
145 145 h.unsubscribe();
146 146 }
147 147 };
148 148 };
149 149
150 150 /** Creates refHook to toggle the specified css class on the target element
151 151 *
152 152 * @param className
153 153 * @param subj a boolean observable value. When the value is false the className
154 154 * is removed, when the value is true, the className is added to the target element
155 155 * @returns refHook
156 156 */
157 157 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
158 158 let h = { unsubscribe() { } };
159 159 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
160 160 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
161 161 if (el) {
162 162 h = subj.subscribe({
163 163 next: v => djClass.toggle(el, className, v || false)
164 164 });
165 165 } else {
166 166 h.unsubscribe();
167 167 }
168 168 };
169 169 };
170 170
171 171 /** Combines multiple hooks to the single one */
172 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 174 /** Decorates the method which will be registered as the handle for the specified event.
175 175 * This decorator can be applied to DjxWidgetBase subclass methods.
176 176 *
177 177 * ```
178 178 * @on("click")
179 179 * _onClick(eventObj: MouseEvent) {
180 180 * // ...
181 181 * }
182 182 * ```
183 183 */
184 184 export const on = <E extends string>(...eventNames: E[]) =>
185 185 <K extends string,
186 186 T extends DjxWidgetBase<object, { [p in E]: EV }>,
187 187 EV extends Event
188 188 >(
189 189 target: T,
190 190 key: K,
191 191 // eslint-disable-next-line @typescript-eslint/no-unused-vars
192 192 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
193 193 ) => {
194 194 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
195 195 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
196 196 };
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now