| name | Create Entity ViewModel |
| description | Create a new entity ViewModel with property ViewModels following Phoenix MVVM patterns. Use when adding new data models, creating entity wrappers, or scaffolding ViewModels for entities. Handles property VM creation, label converters, base class selection, and test generation. |
| allowed-tools | Read, Write, Edit, Grep, Glob |
Create Entity ViewModel
This skill scaffolds a complete entity ViewModel following Phoenix framework patterns.
When to Use
- Creating a new entity ViewModel for a data model
- Wrapping an existing model with UI logic
- Adding property ViewModels for entity properties
- Setting up label converters for enum types
Prerequisites
- Data model must exist in
/model/*.model/src/ and be generated
- Run
npm run generate-model if model was just created
- Entity should be defined with proper property types (ValueProperty, CommandedProperty, etc.)
Process
Step 1: Understand the Data Model
Read the source model to understand:
- Property types (ValueProperty, CommandedProperty, RangedCommandedProperty)
- Enum types that need label converters
- Geographic properties (lat/lon/alt)
- Parent class (Entity, GeoEntity, Platform, etc.)
Reference: ENTITY_ARCHITECTURE.md
Step 2: Choose the Right Base Class
Determine appropriate base class:
- EntityViewModel - Standard entities (settings, configs)
- GeoEntityBaseVM - Entities with location data (platforms, vehicles)
- GeoPointBaseVM - Simple geographic points (waypoints, markers)
- Custom abstract base - Multiple related entities share functionality
Reference: ENTITY_ARCHITECTURE.md - Best Practices
Step 3: Create Property ViewModels
For each property on the model, create appropriate property VM:
Property Type Mapping:
ValueProperty<string> → StringViewModel
CommandedProperty<string> → CommandedStringViewModel
ValueProperty<number> → NumberViewModel
RangedCommandedProperty<number> → RangedCommandedNumberViewModel
CommandedProperty<boolean> → CommandedBooleanViewModel
CommandedProperty<EnumType> → CommandedEnumViewModel<EnumType>
ValueProperty<string[]> → ArrayViewModel<string>
CommandedProperty<string[]> → CommandedArrayViewModel<string>
CRITICAL Patterns:
- Use
CommandedArrayViewModel, NOT ArrayViewModel for commanded arrays
- Use
RangedCommandedNumberViewModel for numbers with min/max, NOT CommandedNumberViewModel
- Always add type hints for labelConverter:
(value: number | null | undefined) => string
- Return
'---' for null/undefined values in labelConverter
@computed Decorator Usage:
Use @computed when getter:
- Includes configuration (labelConverter, defaultValue, etc.) - prevents re-running configure
- Derives/computes values from observables
- Filters or transforms data
@computed
get modeVM(): ICommandedVM<ModeType, IEnumFormatOptions<ModeType>> {
const vm = this.createPropertyVM('mode', CommandedEnumViewModel<ModeType>);
vm.configure({
labelConverter: ModeTypeLabel,
defaultValue: ModeType.DEFAULT
});
return vm;
}
get nameVM(): IPropertyVM<string, IStringFormatOptions> {
return this.createPropertyVM('name', CommandedStringViewModel);
}
@computed
get activeItems(): EntityViewModel[] {
return this.items.filter(item => item.isActive);
}
Reference: ENTITY_ARCHITECTURE.md - Property ViewModel Patterns
Step 4: Create Label Converters for Enums
For each enum type, create a label converter in adapters/types.ts:
export const YourEnumTypeLabel: Record<YourEnumType, string> = {
[YourEnumType.VALUE1]: 'Display Label 1',
[YourEnumType.VALUE2]: 'Display Label 2',
};
Reference: ENTITY_ARCHITECTURE.md - Label Converter Creation
Step 5: Implement Required Methods
All entity ViewModels must implement:
getEntityClassName() - Returns ModelClass.class
getEntityCtr() - Returns the model constructor
Reference: COOKBOOK_PATTERNS_ENHANCED.md - Complete Entity ViewModel
Standard Import Pattern:
import { IEntityConstructor, IFrameworkServices, IPropertyVM, ICommandedVM, IEnumFormatOptions, IStringFormatOptions, INumberFormatOptions } from '@tektonux/framework-api';
import { EntityViewModel, CommandedStringViewModel, CommandedEnumViewModel, RangedCommandedNumberViewModel, CommandedArrayViewModel } from '@tektonux/phoenix-core';
import { computed, makeObservable } from 'mobx';
import { MyEntity } from '@tektonux/your-model-package';
import { MyEnumType } from '@tektonux/your-model-package';
import { MyEnumTypeLabel } from '../adapters/types';
Step 6: Add MobX Support
CRITICAL: Call makeObservable(this) in constructor!
constructor(services: IFrameworkServices) {
super(services);
makeObservable(this);
}
Reference: MOBX_ESSENTIALS.md - Constructor Pattern
Step 7: Update Barrel Exports
Add to {lib}.core/src/index.ts:
export * from './lib/viewModels/yourEntityViewModel';
Add label converters to {lib}.core/src/lib/adapters/index.ts:
export * from './types';
Step 8: Create Tests
Generate unit tests following patterns:
- Mock IFrameworkServices
- Test property VM creation
- Test getEntityClassName/getEntityCtr
- Test label converters
Reference: TESTING_GUIDE.md
Common Pitfalls to Avoid
Reference: COMMON_PITFALLS.md
- ❌ Don't use
BaseEntityViewModel - Use EntityViewModel from phoenix-core
- ❌ Don't use
ArrayViewModel for commanded arrays - Use CommandedArrayViewModel
- ❌ Don't forget
makeObservable(this) in constructor
- ❌ Don't use
CommandedNumberViewModel for constrained numbers - Use RangedCommandedNumberViewModel
- ❌ Don't hardcode labels - Create label converters in adapters
- ❌ Don't forget type hints on labelConverter functions
- ❌ Don't return 'N/A' for null - Use
'---'
- ❌ Don't forget
@computed on property VMs with configuration or derived values
Complete Template
Reference: COOKBOOK_PATTERNS_ENHANCED.md - Complete Entity ViewModel
File Locations
- Entity ViewModels:
{lib}.core/src/lib/viewModels/
- Label Converters:
{lib}.core/src/lib/adapters/types.ts
- Tests:
{lib}.core/src/lib/viewModels/__tests__/
Verification Steps
After creation:
- Run
./tools/build-helpers/count-client-errors.sh - Should be 0
- Run
npm test - New tests should pass
- Verify exports in index.ts
- Check label converters work in UI components
Ask User If Unclear
- Which library to create the VM in (faad.core, alpha.core, etc.)
- Whether this is a geographic entity (needs GeoEntityBaseVM)
- Default values for enum properties
- Whether to create abstract base class (if multiple similar entities)