@aerogel/core 0.1.0 → 0.1.1-next.1e4498f367b830c7a83435800066bb8261d179f5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui/UI.ts CHANGED
@@ -1,13 +1,19 @@
1
- import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
2
- import { markRaw, nextTick, unref } from 'vue';
3
- import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
4
- import type { Component } from 'vue';
5
- import type { ClosureArgs } from '@noeldemartin/utils';
1
+ import { after, facade, fail, isDevelopment, uuid } from '@noeldemartin/utils';
2
+ import { markRaw, unref } from 'vue';
3
+ import type { Constructor } from '@noeldemartin/utils';
4
+ import type { Component, ComputedOptions, MethodOptions } from 'vue';
6
5
 
7
- import App from '@aerogel/core/services/App';
8
6
  import Events from '@aerogel/core/services/Events';
7
+ import { closeModal, createModal, modals, showModal } from '@aerogel/core/ui/modals';
8
+ import type { GetModalProps, GetModalResponse } from '@aerogel/core/ui/modals';
9
+ import type { AcceptRefs } from '@aerogel/core/utils';
10
+ import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
11
+ import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
12
+ import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
13
+ import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
9
14
  import type {
10
15
  ConfirmModalCheckboxes,
16
+ ConfirmModalEmits,
11
17
  ConfirmModalExpose,
12
18
  ConfirmModalProps,
13
19
  } from '@aerogel/core/components/contracts/ConfirmModal';
@@ -15,42 +21,30 @@ import type {
15
21
  ErrorReportModalExpose,
16
22
  ErrorReportModalProps,
17
23
  } from '@aerogel/core/components/contracts/ErrorReportModal';
18
- import type { AcceptRefs } from '@aerogel/core/utils';
19
- import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
20
- import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
21
- import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
22
- import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
23
- import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
24
+ import type {
25
+ PromptModalEmits,
26
+ PromptModalExpose,
27
+ PromptModalProps,
28
+ } from '@aerogel/core/components/contracts/PromptModal';
24
29
 
25
30
  import Service from './UI.state';
26
31
  import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
27
- import type { UIModal, UIToast } from './UI.state';
32
+ import type { UIToast } from './UI.state';
28
33
 
29
- interface ModalCallbacks<T = unknown> {
30
- willClose(result: T | undefined): void;
31
- hasClosed(result: T | undefined): void;
32
- }
33
-
34
- export type ModalResult<T> = ModalExposeResult<ComponentExposed<T>>;
35
- export type ModalExposeResult<T> = T extends { close(result?: infer Result): Promise<void> } ? Result : unknown;
36
- export type UIComponent<Props = {}, Exposed = {}> = { new (...args: ClosureArgs): Exposed & { $props: Props } };
34
+ export type UIComponent<Props = {}, Exposed = {}, Emits = {}> = Constructor<{ $emit?: Emits } & Exposed> &
35
+ Component<Props, {}, {}, ComputedOptions, MethodOptions, {}, {}>;
37
36
 
38
37
  export interface UIComponents {
39
38
  'alert-modal': UIComponent<AlertModalProps, AlertModalExpose>;
40
- 'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose>;
39
+ 'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose, ConfirmModalEmits>;
41
40
  'error-report-modal': UIComponent<ErrorReportModalProps, ErrorReportModalExpose>;
42
41
  'loading-modal': UIComponent<LoadingModalProps, LoadingModalExpose>;
43
- 'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose>;
42
+ 'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose, PromptModalEmits>;
44
43
  'router-link': UIComponent;
45
44
  'startup-crash': UIComponent;
46
45
  toast: UIComponent<ToastProps, ToastExpose>;
47
46
  }
48
47
 
49
- export interface UIModalContext {
50
- modal: UIModal;
51
- childIndex?: number;
52
- }
53
-
54
48
  export type ConfirmOptions = AcceptRefs<{
55
49
  acceptText?: string;
56
50
  acceptVariant?: ButtonVariant;
@@ -91,7 +85,6 @@ export interface ToastOptions {
91
85
 
92
86
  export class UIService extends Service {
93
87
 
94
- private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
95
88
  private components: Partial<UIComponents> = {};
96
89
 
97
90
  public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
@@ -153,11 +146,11 @@ export class UIService extends Service {
153
146
  };
154
147
 
155
148
  const properties = getProperties();
156
- const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
157
- const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
149
+ const { response } = await this.modal(this.requireComponent('confirm-modal'), properties);
150
+ const confirmed = typeof response === 'object' ? response[0] : (response ?? false);
158
151
  const checkboxes =
159
- typeof result === 'object'
160
- ? result[1]
152
+ typeof response === 'object'
153
+ ? response[1]
161
154
  : Object.entries(properties.checkboxes ?? {}).reduce(
162
155
  (values, [checkbox, { default: defaultValue }]) => ({
163
156
  [checkbox]: defaultValue ?? false,
@@ -205,8 +198,8 @@ export class UIService extends Service {
205
198
  } as PromptModalProps;
206
199
  };
207
200
 
208
- const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
209
- const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
201
+ const { response } = await this.modal(this.requireComponent('prompt-modal'), getProperties());
202
+ const result = trim && typeof response === 'string' ? response?.trim() : response;
210
203
 
211
204
  return result ?? null;
212
205
  }
@@ -253,7 +246,9 @@ export class UIService extends Service {
253
246
  return operationPromise;
254
247
  }
255
248
 
256
- const modal = await this.modal(this.requireComponent('loading-modal'), props);
249
+ const modal = createModal(this.requireComponent('loading-modal'), props);
250
+
251
+ showModal(modal);
257
252
 
258
253
  try {
259
254
  const result = await operationPromise;
@@ -262,7 +257,7 @@ export class UIService extends Service {
262
257
 
263
258
  return result;
264
259
  } finally {
265
- await this.closeModal(modal.id);
260
+ await closeModal(modal.id, { removeAfter: 1000 });
266
261
  }
267
262
  }
268
263
 
@@ -278,98 +273,28 @@ export class UIService extends Service {
278
273
  }
279
274
 
280
275
  public modal<T extends Component>(
281
- ...args: {} extends ComponentProps<T>
282
- ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
283
- : [component: T, props: AcceptRefs<ComponentProps<T>>]
284
- ): Promise<UIModal<ModalResult<T>>>;
285
-
286
- public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
287
- const id = uuid();
288
- const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
289
- const modal: UIModal<ModalResult<T>> = {
290
- id,
291
- closing: false,
292
- properties: props ?? {},
293
- component: markRaw(component),
294
- beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
295
- afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
296
- };
297
- const modals = this.modals.concat(modal);
298
-
299
- this.modalCallbacks[modal.id] = callbacks;
300
-
301
- this.setState({ modals });
302
-
303
- await nextTick();
304
-
305
- return modal;
306
- }
307
-
308
- public modalForm<T extends Component>(
309
- ...args: {} extends ComponentProps<T>
310
- ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
311
- : [component: T, props: AcceptRefs<ComponentProps<T>>]
312
- ): Promise<ModalResult<T> | undefined>;
276
+ component: T & object extends GetModalProps<T> ? T : never,
277
+ props?: GetModalProps<T>
278
+ ): Promise<GetModalResponse<T>>;
313
279
 
314
- public async modalForm<T extends Component>(
315
- component: T,
316
- props?: ComponentProps<T>,
317
- ): Promise<ModalResult<T> | undefined> {
318
- const modal = await this.modal<T>(component, props as ComponentProps<T>);
319
- const result = await modal.beforeClose;
320
-
321
- return result;
322
- }
323
-
324
- public async closeModal(id: string, result?: unknown): Promise<void> {
325
- if (!App.isMounted()) {
326
- await this.removeModal(id, result);
327
-
328
- return;
329
- }
280
+ public modal<T extends Component>(
281
+ component: T & object extends GetModalProps<T> ? never : T,
282
+ props: GetModalProps<T>
283
+ ): Promise<GetModalResponse<T>>;
330
284
 
331
- await Events.emit('close-modal', { id, result });
285
+ public modal<T extends Component>(component: T, componentProps?: GetModalProps<T>): Promise<GetModalResponse<T>> {
286
+ return showModal(component, componentProps ?? {});
332
287
  }
333
288
 
334
289
  public async closeAllModals(): Promise<void> {
335
- while (this.modals.length > 0) {
336
- await this.closeModal(required(this.modals[this.modals.length - 1]).id);
337
- }
290
+ await Promise.all(modals.value.map(({ id }) => closeModal(id, { removeAfter: 1000 })));
338
291
  }
339
292
 
340
293
  protected override async boot(): Promise<void> {
341
- this.watchModalEvents();
342
294
  this.watchMountedEvent();
343
295
  this.watchViewportBreakpoints();
344
296
  }
345
297
 
346
- private async removeModal(id: string, result?: unknown): Promise<void> {
347
- this.setState(
348
- 'modals',
349
- this.modals.filter((m) => m.id !== id),
350
- );
351
-
352
- this.modalCallbacks[id]?.hasClosed?.(result);
353
-
354
- delete this.modalCallbacks[id];
355
- }
356
-
357
- private watchModalEvents(): void {
358
- Events.on('modal-will-close', ({ modal: { id }, result }) => {
359
- const modal = this.modals.find((_modal) => id === _modal.id);
360
-
361
- if (modal) {
362
- modal.closing = true;
363
- }
364
-
365
- this.modalCallbacks[id]?.willClose?.(result);
366
- });
367
-
368
- Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
369
- await this.removeModal(id, result);
370
- });
371
- }
372
-
373
298
  private watchMountedEvent(): void {
374
299
  Events.once('application-mounted', async () => {
375
300
  if (!globalThis.document || !globalThis.getComputedStyle) {
@@ -405,11 +330,3 @@ export class UIService extends Service {
405
330
  }
406
331
 
407
332
  export default facade(UIService);
408
-
409
- declare module '@aerogel/core/services/Events' {
410
- export interface EventsPayload {
411
- 'close-modal': { id: string; result?: unknown };
412
- 'modal-will-close': { modal: UIModal; result?: unknown };
413
- 'modal-has-closed': { modal: UIModal; result?: unknown };
414
- }
415
- }
package/src/ui/index.ts CHANGED
@@ -14,6 +14,7 @@ import type { Component } from 'vue';
14
14
 
15
15
  const services = { $ui: UI };
16
16
 
17
+ export * from './modals';
17
18
  export * from './UI';
18
19
  export * from './utils';
19
20
  export { default as UI } from './UI';
@@ -0,0 +1,36 @@
1
+ import { after } from '@noeldemartin/utils';
2
+ import { injectModal, useModal as useModalBase } from '@noeldemartin/vue-modals';
3
+
4
+ export {
5
+ createModal,
6
+ showModal,
7
+ injectModal,
8
+ closeModal,
9
+ modals,
10
+ ModalComponent,
11
+ ModalsPortal,
12
+ type GetModalProps,
13
+ type GetModalResponse,
14
+ type ModalController,
15
+ } from '@noeldemartin/vue-modals';
16
+
17
+ const instances = new WeakSet();
18
+
19
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
20
+ export function useModal<T = never>() {
21
+ const instance = injectModal<T>();
22
+ const { close, remove, ...modal } = useModalBase<T>(instances.has(instance) ? {} : { removeOnClose: false });
23
+
24
+ instances.add(instance);
25
+
26
+ return {
27
+ ...modal,
28
+ async close(result?: T) {
29
+ close(result);
30
+
31
+ await after(1000);
32
+
33
+ remove();
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { nextTick, watchEffect } from 'vue';
3
+
4
+ import { reactiveSet } from './reactiveSet';
5
+
6
+ describe('Vue reactiveSet', () => {
7
+
8
+ it('watches updates', async () => {
9
+ // Arrange
10
+ const set = reactiveSet();
11
+ let updates = 0;
12
+
13
+ watchEffect(() => (set.has('foo'), updates++));
14
+
15
+ // Act
16
+ set.add('foo');
17
+ await nextTick();
18
+
19
+ set.add('bar');
20
+ await nextTick();
21
+
22
+ set.add('baz');
23
+ await nextTick();
24
+
25
+ set.reset();
26
+ await nextTick();
27
+
28
+ // Assert
29
+ expect(updates).toEqual(5);
30
+ });
31
+
32
+ });
@@ -0,0 +1,53 @@
1
+ import { fail } from '@noeldemartin/utils';
2
+ import { customRef } from 'vue';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
5
+ export function reactiveSet<T>(initial?: T[] | Set<T>) {
6
+ let set: Set<T> = new Set(initial);
7
+ let trigger: () => void;
8
+ let track: () => void;
9
+ const ref = customRef((_track, _trigger) => {
10
+ track = _track;
11
+ trigger = _trigger;
12
+
13
+ return {
14
+ get: () => set,
15
+ set: () => fail('Attempted to write read-only reactive set'),
16
+ };
17
+ });
18
+
19
+ return {
20
+ values() {
21
+ track();
22
+
23
+ return Array.from(ref.value.values());
24
+ },
25
+ has(item: T) {
26
+ track();
27
+
28
+ return ref.value.has(item);
29
+ },
30
+ add(item: T) {
31
+ trigger();
32
+
33
+ ref.value.add(item);
34
+ },
35
+ delete(item: T) {
36
+ trigger();
37
+
38
+ ref.value.delete(item);
39
+ },
40
+ clear() {
41
+ trigger();
42
+
43
+ ref.value.clear();
44
+ },
45
+ reset(items?: T[] | Set<T>) {
46
+ trigger();
47
+
48
+ set = new Set(items);
49
+ },
50
+ };
51
+ }
52
+
53
+ export type ReactiveSet<T = unknown> = ReturnType<typeof reactiveSet<T>>;
@@ -4,6 +4,7 @@ export * from './composition/events';
4
4
  export * from './composition/forms';
5
5
  export * from './composition/hooks';
6
6
  export * from './composition/persistent';
7
+ export * from './composition/reactiveSet';
7
8
  export * from './composition/state';
8
9
  export * from './markdown';
9
10
  export * from './types';
@@ -1,14 +0,0 @@
1
- <template>
2
- <aside v-if="modal">
3
- <ModalContext :child-index="1" :modal />
4
- </aside>
5
- </template>
6
-
7
- <script setup lang="ts">
8
- import { computed } from 'vue';
9
-
10
- import ModalContext from '@aerogel/core/components/ui/ModalContext.vue';
11
- import UI from '@aerogel/core/ui/UI';
12
-
13
- const modal = computed(() => UI.modals[0] ?? null);
14
- </script>
@@ -1,31 +0,0 @@
1
- <template>
2
- <component :is="modal.component" v-bind="modalProperties" />
3
- </template>
4
-
5
- <script setup lang="ts">
6
- import { computed, provide, toRef, unref } from 'vue';
7
-
8
- import type { UIModal } from '@aerogel/core/ui/UI.state';
9
- import type { UIModalContext } from '@aerogel/core/ui/UI';
10
- import type { AcceptRefs } from '@aerogel/core/utils/vue';
11
-
12
- const props = defineProps<{
13
- modal: UIModal;
14
- childIndex?: number;
15
- }>();
16
-
17
- const modalProperties = computed(() => {
18
- const properties = {} as typeof props.modal.properties;
19
-
20
- for (const property in props.modal.properties) {
21
- properties[property] = unref(props.modal.properties[property]);
22
- }
23
-
24
- return properties;
25
- });
26
-
27
- provide<AcceptRefs<UIModalContext>>('modal', {
28
- modal: toRef(props, 'modal'),
29
- childIndex: toRef(props, 'childIndex'),
30
- });
31
- </script>