Skip to content

Commit 2b15bc1

Browse files
committed
fix: modal, form & dropdown performance
1 parent daf1e4b commit 2b15bc1

File tree

7 files changed

+125
-99
lines changed

7 files changed

+125
-99
lines changed

packages/common-helpers/src/form/input.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,24 @@ function getDefault<V extends iFormValue = iFormValue>(
6666
export class FormInputDefault<T extends eFormTypeSimple | eFormTypeComplex = eFormTypeSimple>
6767
implements iFormInputDefault<T>
6868
{
69+
// private
70+
private _options!: iSelectOption[];
6971
// public
7072
public type!: T;
7173
// public readonly
7274
public readonly required!: boolean;
73-
public readonly options!: iSelectOption[];
7475
public readonly placeholder!: string;
7576
public readonly icon!: tFormIcon;
7677
public readonly autocomplete!: tFormAutocomplete;
7778
public readonly min!: number;
7879
public readonly max!: number;
7980

80-
constructor(formInput: iFormInputDefault<T>) {
81+
constructor(
82+
formInput: iFormInputDefault<T>,
83+
private _rerender?: (fi?: Partial<iFormInputDefault<T>>) => void
84+
) {
8185
this.required = formInput.required ?? false;
82-
this.options = formInput.options?.map(toOption) ?? [];
86+
this._options = formInput.options?.map(toOption) ?? [];
8387
this.min = formInput.min ?? 1;
8488

8589
// max cannot be lower than min or more than options if they exist
@@ -92,6 +96,28 @@ export class FormInputDefault<T extends eFormTypeSimple | eFormTypeComplex = eFo
9296
if (formInput.icon) this.icon = getIcon(formInput.icon, formInput.type);
9397
if (formInput.autocomplete) this.autocomplete = formInput.autocomplete;
9498
}
99+
100+
get options(): iSelectOption[] {
101+
return this._options;
102+
}
103+
set options(updatedOptions: iSelectOption[] | undefined) {
104+
this._options = updatedOptions || [];
105+
this.rerender();
106+
}
107+
108+
/** Rerender component */
109+
public rerender(): void {
110+
this._rerender?.(this);
111+
}
112+
113+
/**
114+
* set rerender function
115+
*/
116+
public setRerender(rerender: (fi?: Partial<iFormInputDefault<T>>) => void) {
117+
this._rerender = rerender;
118+
119+
return this;
120+
}
95121
}
96122

97123
export class FormInput<V extends iFormValue = iFormValue>
@@ -101,8 +127,6 @@ export class FormInput<V extends iFormValue = iFormValue>
101127
// private
102128
private _values!: (V | V[])[];
103129
private _defaults?: [iFormInputDefault, iFormInputDefault, ...iFormInputDefault[]];
104-
/** Rerender component */
105-
private _rerender?: () => void;
106130
// public readonly
107131
public readonly name!: string;
108132
public readonly title!: string;
@@ -115,9 +139,10 @@ export class FormInput<V extends iFormValue = iFormValue>
115139
*/
116140
constructor(
117141
formInput: iFormInput<V>,
118-
private _onUpdatedValues?: (updatedValues: (V | V[])[]) => void
142+
private _onUpdatedValues?: (updatedValues: (V | V[])[]) => void,
143+
rerender?: (fi?: Partial<iFormInput<V>>) => void
119144
) {
120-
super(formInput);
145+
super(formInput, rerender);
121146

122147
const values = Array(this.min).fill(getDefault(formInput.type, formInput.defaults));
123148

@@ -150,15 +175,16 @@ export class FormInput<V extends iFormValue = iFormValue>
150175
updatedDefaults: [iFormInputDefault, iFormInputDefault, ...iFormInputDefault[]] | undefined
151176
) {
152177
this._defaults = updatedDefaults;
153-
// rerender on defaults change
154-
this._rerender?.();
178+
this.rerender();
155179
}
156180

157181
/**
158182
* set rerender function
183+
*
184+
* @override
159185
*/
160-
public setRerender(rerender: () => void) {
161-
this._rerender = rerender;
186+
public setRerender(rerender: (fi?: Partial<iFormInput<V>>) => void) {
187+
super.setRerender(rerender);
162188

163189
return this;
164190
}
@@ -193,11 +219,12 @@ export class FormInput<V extends iFormValue = iFormValue>
193219
) {
194220
const oldFormInput: iFormInput<V> = {
195221
...this,
196-
values: this._values,
197-
defaults: this._defaults,
222+
options: this.options,
223+
values: this.values,
224+
defaults: this.defaults,
198225
};
199226

200-
return new FormInput({ ...oldFormInput, ...overrides }, onUpdatedValues);
227+
return new FormInput({ ...oldFormInput, ...overrides }, onUpdatedValues, this.rerender);
201228
}
202229

203230
/**

packages/components-vue/src/components/Dropdown.vue

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
computed,
2525
ref,
2626
watch,
27-
onMounted,
2827
onUnmounted,
2928
type Component as VueComponent,
3029
type DefineComponent,
@@ -96,12 +95,15 @@
9695
});
9796
9897
function setModel(value = !model.value) {
98+
if (value) document.addEventListener("click", clickOutside, true);
99+
99100
return (model.value = value);
100101
}
101102
102103
function closeDropdown() {
103104
emit("close");
104105
emit("update:model-value", setModel(false));
106+
document.removeEventListener("click", clickOutside, true);
105107
}
106108
function clickOutside(e: MouseEvent) {
107109
const target = e.target as HTMLElement;
@@ -130,10 +132,5 @@
130132
},
131133
{ immediate: false }
132134
);
133-
onMounted(() => {
134-
document.addEventListener("click", clickOutside, true);
135-
});
136-
onUnmounted(() => {
137-
document.removeEventListener("click", clickOutside, true);
138-
});
135+
onUnmounted(closeDropdown);
139136
</script>

packages/components-vue/src/components/Modal.vue

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<slot v-if="$slots.toggle" name="toggle" v-bind="{ toggleModal, model }"></slot>
33
<Teleport v-if="!disabled" :id="modalId" :key="modalId" :to="target || 'body'">
4-
<dialog ref="modalRef" @close="closeAndResetModal" @mousedown="clickOutside">
4+
<dialog ref="modalRef" @close="closeModal" @mousedown="clickOutside">
55
<div
66
v-show="!loading && !hide"
77
class="modal"
@@ -21,7 +21,7 @@
2121
<ActionLink
2222
:theme="theme"
2323
:aria-label="cancelButtonOptions.title"
24-
@click.stop="closeAndResetModal"
24+
@click.stop="closeModal()"
2525
>
2626
<IconFa name="xmark" size="20" />
2727
</ActionLink>
@@ -35,7 +35,7 @@
3535
:theme="theme"
3636
:aria-label="saveButtonOptions.title"
3737
:class="saveButtonOptions.btnClass"
38-
@click="emit('save', closeAndResetModal, $event)"
38+
@click="emit('save', closeModal, $event)"
3939
>
4040
{{ saveButtonOptions.title }}
4141
</ActionButton>
@@ -46,7 +46,7 @@
4646
:class="cancelButtonOptions.btnClass"
4747
data-dismiss="modal"
4848
round=":sm-inv"
49-
@click.stop="closeAndResetModal"
49+
@click.stop="closeModal()"
5050
>
5151
<IconFa name="xmark" hidden="-full:sm" />
5252
<IconFa name="xmark" regular hidden="-full:sm" />
@@ -66,11 +66,7 @@
6666
<p class="--txtColor-light --txtShadow --txtSize-sm">
6767
{{ props.hideMessage ? props.hideMessage : t("modal_taking_too_long") }}
6868
</p>
69-
<ActionButton
70-
:theme="theme"
71-
:aria-label="t('close')"
72-
@click="closeAndResetModal"
73-
>
69+
<ActionButton :theme="theme" :aria-label="t('close')" @click="closeModal()">
7470
{{ t("close") }}
7571
</ActionButton>
7672
</div>
@@ -192,16 +188,39 @@
192188
...(!!props.cancelButton && props.cancelButton),
193189
}));
194190
195-
function closeAndResetModal() {
191+
function closeModal(success?: boolean) {
196192
modalRef.value?.close();
197193
loadingTooLong.value = false;
194+
localModel.value = false;
195+
resolver.value?.(success); // resolve promise early
198196
emit("update:model-value", false);
199197
emit("close");
200198
}
201199
function clickOutside(e: Event) {
202200
if (modalRef.value !== e.target) return;
203201
204-
closeAndResetModal();
202+
closeModal();
203+
}
204+
/**
205+
* Opens modal if requirements are met
206+
*/
207+
function openModal() {
208+
localModel.value = true;
209+
modalRef.value?.showModal();
210+
211+
// close modal if requirements are not meet
212+
if (!props.loading && props.hide) {
213+
Swal.fire({
214+
title: t("swal.modal_unauthorized"),
215+
text: props.hideMessage || t("swal.modal_unauthorized_text"),
216+
icon: "warning",
217+
});
218+
219+
return closeModal();
220+
}
221+
222+
// display message if loading longer than usual
223+
setTimeout(() => (loadingTooLong.value = props.loading), 3000);
205224
}
206225
/**
207226
* Toggles modal
@@ -210,55 +229,27 @@
210229
function toggleModal(success?: boolean) {
211230
return new Promise<boolean | undefined>((resolve) => {
212231
if (model.value) {
213-
// old promise
214-
resolver.value?.(success);
215-
// current promise
216-
resolve(success);
217-
} else resolver.value = resolve;
218-
219-
model.value = !model.value;
232+
closeModal(success); // close & resolve old promise
233+
resolve(undefined); // bypass promise
234+
} else {
235+
resolver.value = resolve;
236+
openModal();
237+
}
220238
});
221239
}
222240
223-
/**
224-
* Modal model
225-
*/
226-
const model = computed({
227-
get() {
228-
return !props.disabled && localModel.value;
229-
},
230-
set(value) {
231-
if (!value) closeAndResetModal();
232-
else {
233-
modalRef.value?.showModal();
234-
235-
// close modal if requirements are not meet
236-
if (!props.loading && props.hide) {
237-
value = false;
238-
closeAndResetModal();
239-
Swal.fire({
240-
title: t("swal.modal_unauthorized"),
241-
text: props.hideMessage || t("swal.modal_unauthorized_text"),
242-
icon: "warning",
243-
});
244-
}
245-
246-
// display message if loading longer than usual
247-
setTimeout(() => (loadingTooLong.value = props.loading), 3000);
248-
}
249-
250-
localModel.value = value;
251-
},
252-
});
241+
/** Modal model */
242+
const model = computed(() => !props.disabled && (props.modelValue || localModel.value));
253243
254244
// lifecycle
255245
onMounted(() => {
256-
if (props.modelValue) model.value = props.modelValue;
246+
watch(
247+
() => props.modelValue,
248+
(show) => {
249+
if (show) openModal();
250+
},
251+
{ immediate: true }
252+
);
257253
});
258-
onUnmounted(closeAndResetModal);
259-
watch(
260-
() => props.modelValue,
261-
(show) => (model.value = show),
262-
{ immediate: false }
263-
);
254+
onUnmounted(closeModal);
264255
</script>

packages/components-vue/src/components/base/Select.vue

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,14 @@
7676
}
7777
7878
// lifecycle
79-
80-
// set single option as value
81-
if (selectOptions.value.length === 1) emit("update:model-value", selectOptions.value[0].value);
82-
8379
watch(
8480
selectOptions,
8581
(options) => {
8682
// set single option as value
87-
if (options.length === 1) emit("update:model-value", options[0].value);
83+
if (options.length === 1 && props.modelValue !== options[0].value) {
84+
emit("update:model-value", options[0].value);
85+
}
8886
},
89-
{ immediate: false }
87+
{ immediate: true }
9088
);
9189
</script>

packages/components-vue/src/components/form/Input.vue

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
>
6060
<div
6161
v-if="input.defaults && input.defaults.length >= 2"
62-
class="flx --flxColumn --flxRow-wrap:md --flx-start-stretch --flx"
62+
class="flx --flxColumn --flxRow-wrap:md --flx-start-stretch --flx --gap-5"
6363
>
6464
<!-- Recursion -->
6565
<Input
@@ -68,12 +68,14 @@
6868
input.defaults?.[index].placeholder || input.defaults?.[index].type || index
6969
"
7070
:input="
71-
input.setRerender($forceUpdate).clone({
72-
...input.defaults[index], // sub input
73-
multiple: false,
74-
defaults: undefined,
75-
values: [models[i].value[index]],
76-
})
71+
input
72+
.clone({
73+
...input.defaults[index], // sub input
74+
multiple: false,
75+
defaults: undefined,
76+
values: [models[i].value[index]],
77+
})
78+
.setRerender($forceUpdate)
7779
"
7880
:theme="theme"
7981
class="--width-180 --flx"

0 commit comments

Comments
 (0)