Skip to content

Vue ViewModel Layer

The ViewModel layer, generated as viewmodels.g.ts, exports a ViewModel class for each API-backed type in your data model (Entity Models, Custom DTOs, and Services). It also exports a ListViewModel type for Entity Models and Custom DTOs.

These classes provide a wide array of functionality that is useful when interacting with your data model through a user interface. The generated ViewModels are the primary way that Coalesce is used when developing a Vue application.

ViewModels

The following members can be found on the generated Entity and Custom DTO ViewModels, exported from viewmodels.g.ts as <TypeName>ViewModel.

Model Data Properties

Each ViewModel class implements the corresponding interface from the Model Layer, meaning that the ViewModel has a data property for each Property on the model. Object-typed properties will be typed as the corresponding generated ViewModel.

Changing the value of a property will automatically flag that property as dirty. See Auto-save & Dirty Flags below for information on how property dirty flags are used.

There are a few special behaviors when assigning to different kinds of data properties on View Models as well:

Model Object Properties

  • If the object being assigned to the property is not a ViewModel instance, a new instance will be created automatically and used instead of the incoming object.
  • If the model property is a reference navigation, the corresponding foreign key property will automatically be set to the primary key of that object. If the incoming value was null, the foreign key will be set to null.
  • If deep auto-saves are enabled on the instance being assigned to, auto-save will be spread to the incoming object, and to all other objects reachable from that object.

Model Collection Properties

  • When assigning an entire array, any items in the array that are not a ViewModel instance will have an instance created for them.
  • The same rule goes for pushing items into the existing array for a model collection - a new ViewModel instance will be created and be used instead of the object(s) being pushed.

Foreign Key Properties

If the corresponding navigation property contains an object, and that object's primary key doesn't match the new foreign key value being assigned, the navigation property will be set to null.

Other Data Properties & Functions

readonly $metadata: ModelType

The metadata object from the Metadata Layer layer for the type represented by the ViewModel.

readonly $stableId: number

An immutable number that is unique among all ViewModel instances, regardless of type.

Useful for uniquely identifying instances with :key="vm.$stableId" in a Vue component, especially for instances that lack a primary key.

$primaryKey: string | number

A getter/setter property that wraps the primary key of the model. Used to interact with the primary key of any ViewModel in a polymorphic way.

$display(prop?: string | Property): string

Returns a string representation of the object, or one of its properties if specified, suitable for display.

$addChild(prop: string | ModelCollectionNavigationProperty, initialDirtyData?: {})

Creates a new instance of an item for the specified child model collection, adds it to that collection, and returns the item. If initialDirtyData is provided, it will be loaded into the new instance with $loadDirtyData().

Loading & Parameters

$load: ItemApiState;
$load(id?: TKey) => ItemResultPromise<TModel>;

An API Caller for the /get endpoint. Accepts an optional id argument - if not provided, the ViewModel's $primaryKey is used instead. Uses the instance's $params object for the Standard Parameters.

$params: DataSourceParameters

An object containing the Standard Parameters to be used for the $load, $save, $bulkSave, and $delete API callers.

$dataSource: DataSource

Getter/setter wrapper around $params.dataSource. Takes an instance of a Data Source class generated in the Model Layer.

$includes: string | null

Getter/setter wrapper around $params.includes. See Includes String for more information.

$loadCleanData(source: {} | TModel, purgeUnsaved = false)

Loads data from the provided model into the current ViewModel, and then clears all dirty flags.

Data is loaded recursively into all related ViewModel instances, preserving existing instances whose primary keys match the incoming data.

If auto-save is enabled, only non-dirty properties are updated. This prevents user input that is pending a save from being overwritten by the response from an auto-save /save request.

If purgeUnsaved is true, items without a primary key will be dropped from collection navigation properties. This is used by the $load caller in order to fully reset the object graph with the state from the server.

$loadDirtyData(source: {} | TModel)

Same as $loadCleanData, but does not clear any existing dirty flags, nor does it clear any dirty flags that will be set while mutating the data properties of any ViewModel instance that gets loaded.

constructor(initialDirtyData?: {} | TModel | null)

Create a new instance of the ViewModel, loading the given initial data with $loadDirtyData() if provided.

Saving and Deleting

$save: ItemApiState;
$save(overrideProps?: Partial<TModel>) => ItemResultPromise<TModel>;

An API Caller for the /save endpoint. Uses the instance's $params object for the Standard Parameters. A save operation saves only properties on the model it is called on - for deep/bulk saves, see $bulkSave.

This caller is used for both manually-triggered saves in custom code and for auto-saves. If the Rules/Validation report any errors when the caller is invoked, an error will be thrown.

overrideProps can provide properties to save that override the data properties on the ViewModel instance. This allows for manually saving a change to a property without setting the property on the ViewModel instance into a dirty state. This makes it easier to handle some scenarios where changing the value of the property may put the UI into a logically inconsistent state until the save response has been returned from the server - for example, if a change to one property affects the computed value of other properties.

When a save creates a new record and a new primary key is returned from the server, any entities attached to the current ViewModel via a collection navigation property will have their foreign keys set to the new primary key. This behavior, combined with the usage of deep auto-saves, allows for complex object graphs to be constructed even before any model in the graph has been created.

When a save is in progress, the names of properties being saved are in contained in $savingProps.

Saving behavior can be further customized with $loadResponseFromSaves and $saveMode, listed below.

$delete: ItemApiState;
$delete() => ItemResultPromise<TModel>;

An API Caller for the /delete endpoint. Uses the instance's $params object for the Standard Parameters.

If the object was loaded as a child of a collection, it will be removed from that collection upon being deleted. Note that ViewModels currently only support tracking of a single parent collection, so if an object is programmatically added to additional collections, it will only be removed from one of them upon delete.

$loadResponseFromSaves: boolean

Default true - controls if a ViewModel will be loaded with the data from the model returned by the /save endpoint when saved with the $save API caller. There is seldom any reason to disable this.

$savingProps: ReadonlySet<string>

When $save.isLoading == true, contains the properties of the model currently being saved by $save (including auto-saves). Does not include non-dirty properties even if $saveMode == 'whole'.

This can be used to make per-property UI state changes during saves - for example, displaying progress indicators on/near individual inputs, or disabling input controls.

$saveMode: 'surgical' | 'whole'

Configures which properties of the model are sent to the server during a save or bulk save.

"surgical" (default)

By default, only dirty properties (and always the primary key) are sent to the server when performing a save.

This improves the handling of concurrent changes being made by multiple users against different fields of the same entity at the same time - specifically, it prevents a user with a stale value of some field X from overwriting a more recent value of X in the database when the user is only making changes to some other property Y and has no intention of changing X.

Save mode "surgical" doesn't help when multiple users are editing field X at the same time - if such a scenario is applicable to your application, you must implement more advanced handling of concurrency conflicts.

WARNING

Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the x-www-form-urlencoded body that is sent to the server.

The Generated C# DTOs implement the necessary logic for this; however, any Custom DTOs must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the Generated C# DTOs, or do not use surgical saves with Custom DTOs.

"whole"

All serializable properties of the object are sent back to the server with every save.

$getPropDirty(propName: string): boolean

Returns true if the given property is flagged as dirty.

$setPropDirty(propName: string, dirty: boolean = true, triggerAutoSave = true)

Manually set the dirty flag of the given property to the desired state. This seldom needs to be done explicitly, as mutating a property will automatically flag it as dirty.

If dirty is true and triggerAutoSave is false, auto-save (if enabled) will not be immediately triggered for this specific flag change. Note that a future change to any other property's dirty flag will still trigger a save of all dirty properties.

$isDirty: boolean

Getter/setter that summarizes the model's property-level dirty flags. Returns true if any properties are dirty.

When set to false, all property dirty flags are cleared. When set to true, all properties are marked as dirty.

Auto-save

// Vue Options API
$startAutoSave(vue: Vue, options: AutoSaveOptions<this> = {})
 
// Vue Composition API
$useAutoSave(options: AutoSaveOptions<this> = {})

Starts auto-saving of the instance when its savable data properties become dirty. Saves are performed with the $save API Caller (documented above) and will not be performed if the ViewModel has any validation errors - see Rules/Validation below.

ts
type AutoSaveOptions<TThis> = 
{ 
    /** Time, in milliseconds, to debounce saves for.  */
    wait?: number;
    
    /** If true, auto-saving will also be enabled for all view models that are
        reachable from the navigation properties & collections of the current view model. */
    deep?: boolean;

    /** Additional options to pass to the third parameter of lodash's `debounce` function. */
    debounce?: DebounceSettings;

    /** A function that will be called before autosaving that can return false to prevent a save. 
        Only allowed if not using deep auto-saves.
    */
    predicate?: (viewModel: TThis) => boolean;
}

$stopAutoSave(): void

Turns off auto-saving of the instance. Does not recursively disable auto-saves on related instances if deep was used when auto-save was enabled.

readonly $isAutoSaveEnabled: boolean

Returns true if auto-save is currently active on the instance.

Bulk saves

$bulkSave: ItemApiState;
$bulkSave(options: BulkSaveOptions) => ItemResultPromise<TModel>;

Bulk saves save all changes to an object graph in one API call and one database transaction. This includes creation, updates, and deletions of entities.

To use bulk saves, you can work with your ViewModel instances on the client much in the same way you would on the server with Entity Framework. Assign objects to reference navigation properties and modify scalar values to perform creates and updates. To perform deletions, you must call model.$remove() on the ViewModel you want to remove, similar how you would call DbSet<>.Remove(model) on the server.

If the client-side Rules/Validation report any errors for any of the models being saved in the operation, an error will be thrown.

On the server, each affected entity is handled through the same standard mechanisms as are used by individual saves or deletes (Behaviors, Data Sources, and Security Attributes), but with a bit of sugar on top:

  • All operations are wrapped in a single database transaction that is rolled back if any individual operation fails.
  • Foreign keys will be fixed up as new items are created, allowing a parent and child record to be created at the same time even when the client has no foreign key to link the two together.

For the response to a bulk save, the server will load and return the root ViewModel that $bulkSave was called upon, using the instance's $params object for the Standard Parameters.

ts
export interface BulkSaveOptions {
  /** A predicate that will be applied to each modified model
   * to determine if it should be included in the bulk save operation.
   *
   * The predicate is applied before validation (`$hasError`), allowing
   * it to be used to skip over entities that have client validation errors
   * that would otherwise cause the entire bulk save operation to fail.
   * */
  predicate?: (viewModel: ViewModel, action: "save" | "delete") => boolean;
}

$remove(): void

Removes the item from its parent collection (if it is in a collection), and marks the item for deletion in the next bulk save.

readonly $isRemoved: boolean

Returns true if the instance was previously removed by calling $remove().

Rules/Validation

$addRule(prop: string | Property, identifier: string, rule: (val: any) => true | string)

Add a custom validation rule to the ViewModel for the specified property. identifier should be a short, unique slug that describes the rule; it is not displayed in the UI, but is used if you wish to later remove the rule with $removeRule().

The function you provide should take a single argument that contains the current value of the property, and should either return true to indicate that the validation rule has succeeded, or a string that will be displayed as an error message to the user.

Any failing validation rules on a ViewModel will prevent that ViewModel's $save caller from being invoked.

$removeRule(prop: string | Property, identifier: string)

Remove a validation rule from the ViewModel for the specified property and rule identifier.

This can be used to remove either a rule that was provided by the generated Metadata Layer, or a custom rule that was added by $addRule. Reference your generated metadata file metadata.g.ts to see any generated rules and the identifiers they use.

$getRules(prop: string | Property): ((val: any) => string | true)[]

Returns an array of active rule functions for the specified property, or undefined if the property has no active validation rules.

$getErrors(prop?: string | Property): Generator<string>

Returns a generator that provides all error messages for either a specific property (if provided) or the entire model (if no prop argument is provided).

TIP

You can obtain an array from a generator with Array.from(vm.$getErrors()) or [...vm.$getErrors()]

readonly $hasError: boolean

Indicates if any properties have validation errors.

Generated Members

API Callers

For each of the instance Methods of the type, an API Caller will be generated.

addTo*() Functions

For each collection navigation property, a method is generated that will create a new instance of the ViewModel for the collected type, add it to the collection, and then return the new object.

Many-to-many helper collections

For each collection navigation property annotated with [ManyToMany], a getter-only property is generated that returns a collection of the object on the far side of the many-to-many relationship. Nulls are filtered from this collection.

ListViewModels

The following members can be found on the generated ListViewModels, exported from viewmodels.g.ts as *TypeName*ListViewModel.

Data Properties

readonly $items: T[]

Collection holding the results of the last successful invocation of the $load API Caller.

Parameters & API Callers

$params: ListParameters

An object containing the Standard Parameters to be used for the $load and $count API callers.

$dataSource: DataSource

Getter/setter wrapper around $params.dataSource. Takes an instance of a Data Source class generated in the Model Layer.

$includes: string | null

Getter/setter wrapper around $params.includes. See Includes String for more information.

$load: ListApiState;
$load() => ListResultPromise<TModel>

An API Caller for the /list endpoint. Uses the instance's $params object for the Standard Parameters.

Results are available in the $items property. The result property of the $load API Caller contains the raw results and is not recommended for use in general development - $items should always be preferred.

$count: ItemApiState;
$count() => ItemResultPromise<number>

An API Caller for the /count endpoint. Uses the instance's $params object for the Standard Parameters.

The result is available in $count.result - this API Caller does not interact with other properties on the ListViewModel like $pageSize or $pageCount.

readonly $hasPreviousPage: boolean 
readonly $hasNextPage: boolean

Properties which indicate if $page can be decremented or incremented, respectively. $pageCount and $page are used to make this determination.

$previousPage(): void 
$nextPage(): void

Methods that will decrement or increment $page, respectively. Each does nothing if there is no previous or next page as returned by $hasPreviousPage and $hasNextPage.

$page: number

Getter/setter wrapper for $params.page. Controls the page that will be requested on the next invocation of $load.

$pageSize: number

Getter/setter wrapper for $params.pageSize. Controls the page that will be requested on the next invocation of $load.

readonly $pageCount: number

Shorthand for $load.pageCount - returns the page count reported by the last successful invocation of $load.

Auto-Load

// Vue Options API
$startAutoLoad(vue: Vue, options: AutoLoadOptions<this> = {})
 
// Vue Composition API
$useAutoLoad(options: AutoLoadOptions<this> = {})

Starts auto-loading of the list as changes to its parameters occur. Loads are performed with the $load API Caller.

ts
type AutoLoadOptions<TThis> =
{ 
    /** Time, in milliseconds, to debounce loads for.  */
    wait?: number;

    /** Additional options to pass to the third parameter of lodash's `debounce` function. */
    debounce?: DebounceSettings;

    /** A function that will be called before loading that can return false to prevent a load. */
    predicate?: (viewModel: TThis) => boolean;
}

$stopAutoLoad()

Manually turns off auto-loading of the instance.

Auto-save

// Vue Options API
$startAutoSave(vue: Vue, options: AutoSaveOptions<this> = {})
 
// Vue Composition API
$useAutoSave(options: AutoSaveOptions<this> = {})

Enables auto-save for the items in the list, propagating to new items as they're added or loaded. See ViewModel auto-save documentation for more details.

$stopAutoSave(): void

Turns off auto-saving of the items in the list, and turns of propagation of auto-save to any future items if auto-save was previously turned on for the list. Only affects items that are currently in the list's $items.

readonly $isAutoSaveEnabled: boolean

Returns true if auto-save is currently active on the instance.

Generated Members

API Callers

For each of the static Methods on the type, an API Caller will be created.

Service ViewModels

The following members can be found on the generated Service ViewModels, exported from viewmodels.g.ts as <ServiceName>ViewModel.

Generated Members

API Callers

For each method of the Service, an API Caller will be created.


Coalesce is a free and open-source framework created by IntelliTect to fill our desire to create better apps, faster. IntelliTect is a high-end software architecture and development consulting firm based in Spokane, Washington.

If you're looking for help with your software project, whether it be a Coalesce application, other technologies, or even just an idea, reach out to us at info@intellitect.com — we'd love to start a conversation! Our clients range from Fortune 100 companies to local small businesses and non-profits.