diff --git a/README.md b/README.md index 69db3c3..693bfbc 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,21 @@ A library for developing React applications using Model-View-ViewModel inspired * [ICollectionChange\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ICollectionChange) * [ItemRemovedCallback\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ItemRemovedCallback) * [EventDispatcher](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/EventDispatcher) -* **Observable Collection** +* **Observable Collections** * [IReadOnlyObservableCollection\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IReadOnlyObservableCollection) * [IObservableCollection\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IObservableCollection) * [ReadOnlyObservableCollection\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ReadOnlyObservableCollection) * [ObservableCollection\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ObservableCollection) * **ViewModels** * [ViewModel](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ViewModel) + * [isViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/isViewModel) * **Forms** * [IFormFieldViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IFormFieldViewModel) * [IFormFieldViewModelConfig\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IFormFieldViewModelConfig) * [FormFieldViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldViewModel) - * [FormFieldCollectionViewModel](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldCollectionViewModel) - * [FormFieldSet](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldSet) - * [DynamicFormFieldCollectionViewModel](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/DynamicFormFieldCollectionViewModel) + * [FormFieldCollectionViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldCollectionViewModel) + * [FormFieldSet\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldSet) + * [DynamicFormFieldCollectionViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/DynamicFormFieldCollectionViewModel) * **Validation** * [IReadOnlyValidatable](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IReadOnlyValidatable) * [IValidatable](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IValidatable) @@ -53,7 +54,8 @@ A library for developing React applications using Model-View-ViewModel inspired * [useEvent\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useEvent) * [ViewModelType\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ViewModelType) * [ViewModelFactory\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ViewModelFactory) - * [useViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useViewModel) + * [useViewModel\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useViewModel) + * [useViewModelMemo\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useViewModelMemo) * [useObservableCollection\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useObservableCollection) * [useValidators\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useValidators) * [useCollectionValidators\](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useCollectionValidators) diff --git a/package.json b/package.json index af40dc6..56bfa22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-model-view-viewmodel", - "version": "2.2.0-rc.2", + "version": "2.2.0-rc.3", "description": "A library for developing React applications using Model-View-ViewModel inspired by .NET", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/form-field-view-model.ts b/src/form-field-view-model.ts index bc7e428..30f4af9 100644 --- a/src/form-field-view-model.ts +++ b/src/form-field-view-model.ts @@ -35,7 +35,7 @@ export interface IFormFieldViewModelConfig { /** The initial value of the field. */ readonly initialValue: TValue; /** Optional, a validation config without the target as this is the field that is being initialized. */ - readonly validationConfig?: Omit>, "target">; + readonly validationConfig?: Omit>, 'target'>; /** Optional, a set of validators for the field. */ readonly validators?: readonly ValidatorCallback>[]; } diff --git a/src/hooks/use-view-model-factory.ts b/src/hooks/use-view-model-factory.ts deleted file mode 100644 index 1005ace..0000000 --- a/src/hooks/use-view-model-factory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { INotifyPropertiesChanged } from '../events'; -import { type ViewModelFactory, useViewModel } from './use-view-model'; - -/** Ensures a unique instance per component that is generated by the factory is created and watches the view model for changes. Returns the view model instance. - * @deprecated In future versions this hook will be removed, switch to {@link useViewModel}. - * @template TViewModel The type of view model to create. - * @param viewModelFactory The view model factory callback that initializes the instance. - * @param watchedProperties Optional, a render will be requested when only one of these properties has changed. - */ -export function useViewModelFactory(viewModelFactory: ViewModelFactory, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel { - return useViewModel(viewModelFactory, [], watchedProperties); -} \ No newline at end of file diff --git a/src/hooks/use-view-model-memo.ts b/src/hooks/use-view-model-memo.ts new file mode 100644 index 0000000..2e740b0 --- /dev/null +++ b/src/hooks/use-view-model-memo.ts @@ -0,0 +1,31 @@ +import type { INotifyPropertiesChanged } from '../events'; +import { type DependencyList, useMemo } from 'react'; +import { useViewModel } from './use-view-model'; + +/** Represents a view model factory callback. + * @template TViewModel The type of view model to create. + */ +export type ViewModelFactory = () => TViewModel; + +/** Ensures a unique instance per component that is generated by the factory is created and watches the view model for changes. Returns the view model instance. + * @template TViewModel The type of view model to create. + * @param viewModelFactory The view model factory callback that initializes the instance. + * @param deps Dependencies of the callback, whenever these change the callback is called again, similar to {@link useMemo}. + * @param watchedProperties Optional, a render will be requested when only one of these properties has changed. + */ +export function useViewModelMemo(viewModelFactory: ViewModelFactory, deps: DependencyList, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel { + const viewModel = useMemo(viewModelFactory, deps); + useViewModel(viewModel, watchedProperties); + + return viewModel; +} + +/** Ensures a unique instance per component that is generated by the factory is created and watches the view model for changes. Returns the view model instance. + * @deprecated In future versions this hook will be removed, switch to {@link useViewModelMemo}. + * @template TViewModel The type of view model to create. + * @param viewModelFactory The view model factory callback that initializes the instance. + * @param watchedProperties Optional, a render will be requested when only one of these properties has changed. + */ +export function useViewModelFactory(viewModelFactory: ViewModelFactory, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel { + return useViewModelMemo(viewModelFactory, [], watchedProperties); +} \ No newline at end of file diff --git a/src/hooks/use-view-model.ts b/src/hooks/use-view-model.ts index e6d3d07..d144ce9 100644 --- a/src/hooks/use-view-model.ts +++ b/src/hooks/use-view-model.ts @@ -1,5 +1,6 @@ import type { INotifyPropertiesChanged, IEventHandler } from '../events'; import { type DependencyList, useMemo, useState, useEffect } from 'react'; +import { isViewModel } from '../view-model'; /** Represents a view model type. * @template TViewModel The type of view model. @@ -9,11 +10,6 @@ export type ViewModelType = () => TViewModel; - /** * Watches a view model for property changes. * @template TViewModel The type of view model to watch. @@ -31,60 +27,27 @@ export function useViewModel(viewMo * @param watchedProperties Optional, when provided, a render will be requested when only one of these properties has changed. * @returns Returns the initialized view model instance. */ -export function useViewModel(viewModelType: ViewModelType, constructorArgs?: ConstructorParameters>, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel; - -/** - * Creates a new instance of a view model using the provided callback and watches for property changes. - * @template TViewModel The type of view model that is created. - * @param viewModelFactory A callback that provides the view model instance. - * @param deps Dependencies of the callback, whenever these change the callback is called again, similar to {@link useMemo}. - * @param watchedProperties Optional, when provided, a render will be requested when only one of these properties has changed. - * @returns Returns the initialized view model instance. - */ -export function useViewModel(viewModelFactory: ViewModelFactory, deps: DependencyList, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel; +export function useViewModel(viewModelType: ViewModelType, constructorArgs: ConstructorParameters>, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel; -export function useViewModel(typeViewModelOrFactory: TViewModel | ViewModelType | ViewModelFactory, constructorArgsOrDepsOrWatchedProperties?: ConstructorParameters> | DependencyList | readonly (keyof TViewModel)[], watchedProperties?: readonly (keyof TViewModel)[]): void | TViewModel { - const isViewModelCase = isViewModel(typeViewModelOrFactory); - const constructorArgsOrDeps: TConstructorArgs | DependencyList = isViewModelCase - ? [typeViewModelOrFactory] - : (constructorArgsOrDepsOrWatchedProperties || []); +export function useViewModel(viewModelOrViewModelType: TViewModel | ViewModelType, constructorArgsOrWatchedProperties?: ConstructorParameters> | readonly (keyof TViewModel)[], watchedProperties?: readonly (keyof TViewModel)[]): void | TViewModel { + const isViewModelCase = isViewModel(viewModelOrViewModelType); + const dependencies: DependencyList = isViewModelCase + ? [isViewModelCase, viewModelOrViewModelType] + : [isViewModelCase, ...(constructorArgsOrWatchedProperties || [])]; const viewModel = useMemo( - () => { - if (isViewModelCase) - return typeViewModelOrFactory; - - try { - const viewModelType = typeViewModelOrFactory as ViewModelType; - const constructorArgsAndDeps = constructorArgsOrDeps as TConstructorArgs; - return new viewModelType(...constructorArgsAndDeps); - } - catch { - const viewModelFactory = typeViewModelOrFactory as ViewModelFactory; - return viewModelFactory(); - } - }, - constructorArgsOrDeps + () => isViewModelCase ? viewModelOrViewModelType : new viewModelOrViewModelType(...(constructorArgsOrWatchedProperties || []) as TConstructorArgs), + dependencies ); const actualWatchedProperties = isViewModelCase - ? (constructorArgsOrDepsOrWatchedProperties as readonly (keyof TViewModel)[]) + ? (constructorArgsOrWatchedProperties as readonly (keyof TViewModel)[]) : watchedProperties; useViewModelProperties(viewModel, actualWatchedProperties); return viewModel; } -/** - * Checkes whether the provided instance is a view model (implements {@link INotifyPropertiesChanged}). - * @template TViewModel The type of view model to check, defaults to {@link INotifyPropertiesChanged}. - * @param maybeViewModel The value to check if is a view model. - * @returns Returns `true` if the provided instance implements {@link INotifyPropertiesChanged}; otherwise `false`. - */ -export function isViewModel(maybeViewModel: any): maybeViewModel is TViewModel { - return maybeViewModel !== undefined && maybeViewModel !== null && !(maybeViewModel instanceof Function) && 'propertiesChanged' in maybeViewModel; -} - type Destructor = () => void; type EffectResult = void | Destructor; diff --git a/src/index.ts b/src/index.ts index 715ba37..bc9f61b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export { type IEvent, type IEventHandler, type INotifyPropertiesChanged, type INotifyCollectionChanged, type ICollectionChange, EventDispatcher, DispatchEvent } from './events'; -export { ViewModel } from './view-model'; +export { ViewModel, isViewModel } from './view-model'; export { type IReadOnlyObservableCollection, type IObservableCollection, ReadOnlyObservableCollection, ObservableCollection } from './observable-collection'; @@ -10,10 +10,10 @@ export { type IFormFieldViewModel, FormFieldViewModel } from './form-field-view- export { type FormFieldSet, FormFieldCollectionViewModel, DynamicFormFieldCollectionViewModel } from './form-field-collection-view-model'; export { type EventHandler, useEvent, watchEvent } from './hooks/use-event'; -export { type ViewModelType, type ViewModelFactory, useViewModel, isViewModel, watchViewModel } from './hooks/use-view-model'; +export { type ViewModelType, useViewModel, watchViewModel } from './hooks/use-view-model'; +export { type ViewModelFactory, useViewModelFactory } from './hooks/use-view-model-memo'; export { useObservableCollection, watchCollection } from './hooks/use-observable-collection'; export { useViewModelType } from './hooks/use-view-model-type'; -export { useViewModelFactory } from './hooks/use-view-model-factory'; export { useValidators } from './hooks/use-validators'; export { useCollectionValidators } from './hooks/use-collection-validators'; diff --git a/src/view-model.ts b/src/view-model.ts index c87ccfa..9c2c7ef 100644 --- a/src/view-model.ts +++ b/src/view-model.ts @@ -1,6 +1,16 @@ import type { IEvent, INotifyPropertiesChanged } from './events'; import { EventDispatcher } from './events'; +/** + * Checkes whether the provided instance is a view model (implements {@link INotifyPropertiesChanged}). + * @template TViewModel The type of view model to check, defaults to {@link INotifyPropertiesChanged}. + * @param maybeViewModel The value to check if is a view model. + * @returns Returns `true` if the provided instance implements {@link INotifyPropertiesChanged}; otherwise `false`. + */ +export function isViewModel(maybeViewModel: any): maybeViewModel is TViewModel { + return maybeViewModel !== undefined && maybeViewModel !== null && !(maybeViewModel instanceof Function) && 'propertiesChanged' in maybeViewModel; +} + /** Represents a base view model class providing core features. */ export abstract class ViewModel implements INotifyPropertiesChanged { private readonly _propertiesChangedEvent: EventDispatcher = new EventDispatcher(); diff --git a/tests/form-field-collection-view-model-tests.ts b/tests/form-field-collection-view-model-tests.ts index b01e4ea..70ffd12 100644 --- a/tests/form-field-collection-view-model-tests.ts +++ b/tests/form-field-collection-view-model-tests.ts @@ -205,13 +205,13 @@ describe('form-field-collection-view-model/FormFieldCollectionViewModel', (): vo it('creating a dynamic form registers all fields', (): void => { const formFieldCollection = FormFieldCollectionViewModel.create({ - field1: new FormFieldViewModel("field1", null), - field2: new FormFieldViewModel("field2", null) + field1: new FormFieldViewModel('field1', null), + field2: new FormFieldViewModel('field2', null) }); expect(formFieldCollection.fields.toArray()) .does.include(formFieldCollection.field1) .and.does.include(formFieldCollection.field2) - .and.property("length").is.equal(2); + .and.property('length').is.equal(2); }); }); \ No newline at end of file diff --git a/tests/hooks/use-view-model-factory-tests.tsx b/tests/hooks/use-view-model-factory-tests.tsx index e2762dc..0e99a26 100644 --- a/tests/hooks/use-view-model-factory-tests.tsx +++ b/tests/hooks/use-view-model-factory-tests.tsx @@ -1,9 +1,8 @@ -import type { ViewModelFactory } from '../../src/hooks/use-view-model'; import React from 'react'; import { act } from 'react-dom/test-utils'; import { render } from '@testing-library/react'; import { expect } from 'chai'; -import { useViewModelFactory } from '../../src/hooks/use-view-model-factory'; +import { type ViewModelFactory, useViewModelFactory } from '../../src/hooks/use-view-model-memo'; import { ViewModel } from '../../src/view-model'; describe('use-view-model-factory/useViewModelFactory', (): void => {