Skip to content

Commit

Permalink
Added useViewModelMemo for factory alternative
Browse files Browse the repository at this point in the history
It is difficult to differentiate between classes and functions in JS.
  • Loading branch information
Andrei15193 committed Sep 14, 2023
1 parent 3e1656d commit e4910a3
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 74 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@ A library for developing React applications using Model-View-ViewModel inspired
* [ICollectionChange\<TItem\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ICollectionChange)
* [ItemRemovedCallback\<TItem\>](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\<TItem\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IReadOnlyObservableCollection)
* [IObservableCollection\<TItem\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IObservableCollection)
* [ReadOnlyObservableCollection\<TItem\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ReadOnlyObservableCollection)
* [ObservableCollection\<TItem\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ObservableCollection)
* **ViewModels**
* [ViewModel](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ViewModel)
* [isViewModel\<TViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/isViewModel)
* **Forms**
* [IFormFieldViewModel\<TValue\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IFormFieldViewModel)
* [IFormFieldViewModelConfig\<TValue\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/IFormFieldViewModelConfig)
* [FormFieldViewModel\<TValue\>](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\<TFormFieldViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldCollectionViewModel)
* [FormFieldSet\<TFormFieldViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/FormFieldSet)
* [DynamicFormFieldCollectionViewModel\<TFormFieldViewModel, TFormFields\>](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)
Expand All @@ -53,7 +54,8 @@ A library for developing React applications using Model-View-ViewModel inspired
* [useEvent\<TEventArgs\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useEvent)
* [ViewModelType\<TViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ViewModelType)
* [ViewModelFactory\<TViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/ViewModelFactory)
* [useViewModel\<TViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useViewModel)
* [useViewModel\<TViewModel, TConstructorArgs\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useViewModel)
* [useViewModelMemo\<TViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useViewModelMemo)
* [useObservableCollection\<TItem\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useObservableCollection)
* [useValidators\<TValidatableViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useValidators)
* [useCollectionValidators\<TItem, TValidatableViewModel\>](https://github.com/Andrei15193/react-model-view-viewmodel/wiki/useCollectionValidators)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/form-field-view-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface IFormFieldViewModelConfig<TValue> {
/** 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<IValidationConfig<FormFieldViewModel<TValue>>, "target">;
readonly validationConfig?: Omit<IValidationConfig<FormFieldViewModel<TValue>>, 'target'>;
/** Optional, a set of validators for the field. */
readonly validators?: readonly ValidatorCallback<FormFieldViewModel<TValue>>[];
}
Expand Down
12 changes: 0 additions & 12 deletions src/hooks/use-view-model-factory.ts

This file was deleted.

31 changes: 31 additions & 0 deletions src/hooks/use-view-model-memo.ts
Original file line number Diff line number Diff line change
@@ -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 extends INotifyPropertiesChanged> = () => 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<TViewModel extends INotifyPropertiesChanged>(viewModelFactory: ViewModelFactory<TViewModel>, 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<TViewModel extends INotifyPropertiesChanged>(viewModelFactory: ViewModelFactory<TViewModel>, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel {
return useViewModelMemo(viewModelFactory, [], watchedProperties);
}
57 changes: 10 additions & 47 deletions src/hooks/use-view-model.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -9,11 +10,6 @@ export type ViewModelType<TViewModel extends INotifyPropertiesChanged, TConstruc
new(...constructorArgs: TConstructorArgs): TViewModel;
};

/** Represents a view model factory callback.
* @template TViewModel The type of view model to create.
*/
export type ViewModelFactory<TViewModel extends INotifyPropertiesChanged> = () => TViewModel;

/**
* Watches a view model for property changes.
* @template TViewModel The type of view model to watch.
Expand All @@ -31,60 +27,27 @@ export function useViewModel<TViewModel extends INotifyPropertiesChanged>(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<TViewModel extends INotifyPropertiesChanged, TConstructorArgs extends readonly any[]>(viewModelType: ViewModelType<TViewModel, TConstructorArgs>, constructorArgs?: ConstructorParameters<ViewModelType<TViewModel, TConstructorArgs>>, 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<TViewModel extends INotifyPropertiesChanged>(viewModelFactory: ViewModelFactory<TViewModel>, deps: DependencyList, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel;
export function useViewModel<TViewModel extends INotifyPropertiesChanged, TConstructorArgs extends readonly any[]>(viewModelType: ViewModelType<TViewModel, TConstructorArgs>, constructorArgs: ConstructorParameters<ViewModelType<TViewModel, TConstructorArgs>>, watchedProperties?: readonly (keyof TViewModel)[]): TViewModel;

export function useViewModel<TViewModel extends INotifyPropertiesChanged, TConstructorArgs extends readonly any[]>(typeViewModelOrFactory: TViewModel | ViewModelType<TViewModel, TConstructorArgs> | ViewModelFactory<TViewModel>, constructorArgsOrDepsOrWatchedProperties?: ConstructorParameters<ViewModelType<TViewModel, TConstructorArgs>> | DependencyList | readonly (keyof TViewModel)[], watchedProperties?: readonly (keyof TViewModel)[]): void | TViewModel {
const isViewModelCase = isViewModel<TViewModel>(typeViewModelOrFactory);
const constructorArgsOrDeps: TConstructorArgs | DependencyList = isViewModelCase
? [typeViewModelOrFactory]
: (constructorArgsOrDepsOrWatchedProperties || []);
export function useViewModel<TViewModel extends INotifyPropertiesChanged, TConstructorArgs extends readonly any[]>(viewModelOrViewModelType: TViewModel | ViewModelType<TViewModel, TConstructorArgs>, constructorArgsOrWatchedProperties?: ConstructorParameters<ViewModelType<TViewModel, TConstructorArgs>> | readonly (keyof TViewModel)[], watchedProperties?: readonly (keyof TViewModel)[]): void | TViewModel {
const isViewModelCase = isViewModel<TViewModel>(viewModelOrViewModelType);
const dependencies: DependencyList = isViewModelCase
? [isViewModelCase, viewModelOrViewModelType]
: [isViewModelCase, ...(constructorArgsOrWatchedProperties || [])];

const viewModel = useMemo(
() => {
if (isViewModelCase)
return typeViewModelOrFactory;

try {
const viewModelType = typeViewModelOrFactory as ViewModelType<TViewModel, TConstructorArgs>;
const constructorArgsAndDeps = constructorArgsOrDeps as TConstructorArgs;
return new viewModelType(...constructorArgsAndDeps);
}
catch {
const viewModelFactory = typeViewModelOrFactory as ViewModelFactory<TViewModel>;
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<TViewModel extends INotifyPropertiesChanged = INotifyPropertiesChanged>(maybeViewModel: any): maybeViewModel is TViewModel {
return maybeViewModel !== undefined && maybeViewModel !== null && !(maybeViewModel instanceof Function) && 'propertiesChanged' in maybeViewModel;
}

type Destructor = () => void;
type EffectResult = void | Destructor;

Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';
Expand Down
10 changes: 10 additions & 0 deletions src/view-model.ts
Original file line number Diff line number Diff line change
@@ -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<TViewModel extends INotifyPropertiesChanged = INotifyPropertiesChanged>(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<readonly string[]> = new EventDispatcher<readonly string[]>();
Expand Down
6 changes: 3 additions & 3 deletions tests/form-field-collection-view-model-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
3 changes: 1 addition & 2 deletions tests/hooks/use-view-model-factory-tests.tsx
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down

0 comments on commit e4910a3

Please sign in to comment.