Table of Contents

Using a View Model

Every React functional component can have a view model. This is accomplished using the withViewModel() method.

import { withViewModel } from '@cratis/arc.react.mvvm';

export const Counter = withViewModel(CounterViewModel, ({viewModel}) => {
    return (
        <>
            Counter is : {viewModel.counter}
            <button onClick={() => viewModel.increaseCounter()}>Increase counter</button>
        </>
    )
});

The component uses the withViewModel and passes the type CounterViewModel to be created for the view. As arguments you can then have the viewModel, this instance survives re-renders and can be stateful.

export class CounterViewModel {
    counter: number;

    increaseCounter() {
        this.counter++;
    }
}

The viewModel is automatically observable, which means that all properties on it will notify the view if there are any changes to them. This means that the increaseCounter() method can just go ahead and increase the counter and the view will automatically re-render.

Props

Components can have props associated with them. The withViewModel supports specifying props type and ability to take the props in as a parameter on the render function:

import { withViewModel } from '@cratis/arc.react.mvvm';

export interface CounterProps {
    initialValue: number;
}

export const Counter = withViewModel<CounterViewModel, CounterProps>(CounterViewModel, ({viewModel, props}) => {
    return (
        <>
            Counter is : {viewModel.counter}
            <button onClick={() => viewModel.increaseCounter()}>Increase counter</button>
        </>
    )
});

The withViewModel has 2 generic arguments that can be passed:

  • ViewModel type
  • Props type

When you don't have a props type, you don't need to specify the ViewModel type arguments, as TypeScript will automatically infer its type from the first argument that specifies the type to use.

Note: The reason we provide the type of view model to use as a parameter to the withViewModel and not just rely on the ViewModel type argument, is that we need a proper token / type to be used to be able to create an instance of it. Generic information is only available at compile-time in TypeScript and when transpiled to JavaScript this information is gone.

Injecting Props as dependency

If you're component only gets loaded once and the props typically don't change without the component being unloaded and then loaded again, you can simply inject it as part of the constructor.

The @props decorator wraps tsyringe's @inject and also captures the parameter type:

import { props } from '@cratis/arc.react.mvvm';
import { injectable } from 'tsyringe';

@injectable()
export class MyViewModel {
    constructor(@props componentProps: Props) {
    }
}

Using @inject manually

You can also use tsyringe's @inject directly with WellKnownBindings.props:

import { WellKnownBindings } from '@cratis/arc.react.mvvm';
import { inject, injectable } from 'tsyringe';

@injectable()
export class MyViewModel {
    constructor(@inject(WellKnownBindings.props) props: Props) {
    }
}

Handling Props

For the scenario were the props are changing by a consumer of your component, you need to implement the IHandleProps<>. It is a generic interface, but the generic argument is optional and is defaulted to object if not specified.

import { IHandleProps } from '@cratis/arc.react.mvvm';

export class MyViewModel implements IHandleProps<Props>  {
    handleProps(props: Props): void {
        // Do things based on props
    }
}

Note: The handleProps method will be called both on initial load and for any subsequent changes.

Params

Params defined as part of routes using react-router can easily be accessed by a view model in a couple of ways.

Injecting Params as dependency

If you're component only gets loaded once and the parameters can't change without the component being unloaded and then loaded again, you can simply inject it as part of the constructor.

The @params decorator wraps tsyringe's @inject and also captures the parameter type. When a typed class is provided, withViewModel will use JsonSerializer to deserialize the string-based URL parameters into the correct types. Properties on the params class must be annotated with @field from @cratis/fundamentals so the serializer knows how to convert each value — for example a number property will be converted from the URL string "42" to the integer 42. Without @field, the property values remain as strings:

import { field } from '@cratis/fundamentals';
import { params } from '@cratis/arc.react.mvvm';
import { injectable } from 'tsyringe';

class RouteParams {
    @field id!: string;
    @field count!: number;
}

@injectable()
export class MyViewModel {
    constructor(@params routeParams: RouteParams) {
        // routeParams.count is a proper number, not the URL string "42"
    }
}

Using @inject manually

You can also use tsyringe's @inject directly with WellKnownBindings.params. In this case the parameters are injected as raw strings as they appear in the URL:

import { WellKnownBindings } from '@cratis/arc.react.mvvm';
import { inject, injectable } from 'tsyringe';

@injectable()
export class MyViewModel {
    constructor(@inject(WellKnownBindings.params) params: Params) {
    }
}

The downside of this approach is that if a param changes and the component is not unloaded, you won't get the change.

Handling Params

For the scenario were the params are changing while the component is not unloaded, implementing the IHandleParams<> interface is a better option. It is a generic interface, but the generic argument is optional and is defaulted to object if not specified.

import { IHandleParams } from '@cratis/arc.react.mvvm';

export class MyViewModel implements IHandleParams<Params>  {
    handleParams(params: Params): void {
        // Do things based on params
    }
}

Note: The handleParams method will be called both on initial load and for any subsequent changes.

QueryParams

QueryParams defined as part of routes using react-router can easily be accessed by a view model in a couple of ways.

Injecting Query Params as dependency

If you're component only gets loaded once and the query parameters can't change without the component being unloaded and then loaded again, you can simply inject it as part of the constructor.

The @queryParams decorator wraps tsyringe's @inject and also captures the parameter type. When a typed class is provided, withViewModel will use JsonSerializer to deserialize the string-based URL query parameters into the correct types. Properties on the query params class must be annotated with @field from @cratis/fundamentals so the serializer knows how to convert each value — for example a number property will be converted from the URL string "1" to the integer 1. Without @field, the property values remain as strings:

import { field } from '@cratis/fundamentals';
import { queryParams } from '@cratis/arc.react.mvvm';
import { injectable } from 'tsyringe';

class SearchParams {
    @field filter!: string;
    @field page!: number;
}

@injectable()
export class MyViewModel {
    constructor(@queryParams searchParams: SearchParams) {
        // searchParams.page is a proper number, not the URL string "1"
    }
}

Using @inject manually

You can also use tsyringe's @inject directly with WellKnownBindings.queryParams. In this case the parameters are injected as raw strings as they appear in the URL:

import { WellKnownBindings } from '@cratis/arc.react.mvvm';
import { inject, injectable } from 'tsyringe';

@injectable()
export class MyViewModel {
    constructor(@inject(WellKnownBindings.queryParams) queryParams: Params) {
    }
}

The downside of this approach is that if a query param changes and the component is not unloaded, you won't get the change.

Handling Query Params

For the scenario were the query params are changing while the component is not unloaded, implementing the IHandleParams<> interface is a better option. It is a generic interface, but the generic argument is optional and is defaulted to object if not specified.

import { IHandleQueryParams } from '@cratis/arc.react.mvvm';

export class MyViewModel implements IHandleQueryParams<Params>  {
    handleQueryParams(queryParams: QueryParams): void {
        // Do things based on query params
    }
}

Note: The handleQueryParams method will be called both on initial load and for any subsequent changes.

View Model lifecycle

Detaches

You can get notified when a view model is detached from its view, typically as a consequence of the view being removed from the DOM. This is achieved by implementing the IViewModelDetached interface on your view model:

import { IViewModelDetached } from '@cratis/arc.react.mvvm';

export class MyViewModel implements IViewModelDetached {
    detached() {
        // Clean up...
    }
}