Frontend Evolution — Rethinking Frontend - part 3 - introduction to jModel
Overview jModel is a prototype library designed for managing client-side models (business logic) in a simple, flexible, and efficient way. It offers built-in tools for dependency injection, state management, and validation. This article explores the core concepts of jModel and its role in building modern web applications. Key Features Minimalist API – Focused on simplicity and ease of use Own Dependency Injection (DI) – Supports lazy initialization, allowing dependencies to be registered only when first used Validation Tools – Built-in utilities to handle data validation (at the model level) State Management – Reactive model management with efficient updates Performance Optimization – Tree-shakable, ensuring only used code is included in the final bundle Framework Agnostic – Can be integrated into different frontend frameworks/libraries through adapters Cross-Platform Compatibility – Works on both browsers and Node.js without relying on browser-specific APIs Data Management – Enables switching between various storage and caching solutions depending on the platform Model Definition Similar to writing traditional services for business logic, the first step is to define a model object. A typical setup includes a token for dependency injection, lifecycle option, providers, and a model function factory. export const userSource = { [TOKEN]: Symbol('USER_SOURCE'), [LIFETIME]: Lifetime.scoped, [PROVIDERS]: { [USER_REPOSITORY]: userRepository, [USER_STORE]: userStore, [USER_MAP]: () => mapUserToString, }, [FACTORY]: userSourceFactory, }; Key elements TOKEN: A unique identifier used for dependency injection LIFETIME: Determines how long the model is retained in the dependency injection container (e.g., scoped, singleton). PROVIDERS: Specifies dependencies, which can be other models or standalone functions like mapUserToString. It uses TOKENS as keys, allowing a single token (e.g., USER_REPOSITORY) to have different implementations across models. FACTORY: Specifies how the model is created. userRepository export const USER_REPOSITORY = Token('USER_REPOSITORY'); export const userRepository = { [TOKEN]: USER_REPOSITORY, [LIFETIME]: Lifetime.scoped, [FACTORY]: () => ({ get: userGet, getUserNames: userNamesGetAll }), }; userSourceFactory export const userSourceFactory = ({ inject }: Context) => ({ setRandomUsername, refs: inject(USER_STORE).refs, }); The model factory function provides access to the execution context, allowing to inject dependencies. Also all methods defined within it, such as setRandomUsername, will execute within the model's context. Execution Context Execution context (Context) has two methods: inject - it allow to inject dependencies available in the same scope or singletones execute - it allow to execute different method in the same context setRandomUsername export async function setRandomUsername(this: Context): Promise { const store = this.inject(USER_STORE); const randomUsername = await this.execute(getRandomUsername) store.setUserName(randomUsername); } export async function getRandomUsername(this: Context): Promise { const repository = this.inject(USER_REPOSITORY); const mapUserDataToString = this.inject(USER_MAP); const userNames = await this.repository.userNamesGetAll(); return mapUserDataToString(userNames[0]); } In the functions above, dependencies are injected from model providers such as USER_STORE, USER_REPOSITORY, and USER_MAP. The execute function is also used to call getRandomUsername within the same context as setRandomUsername Reactive State Management jModel supports reactive state management which can be created by calling createReactiveModel function. userStoreFactory function userStoreFactory() { const model = createReactiveModel({ name: '', age: null }); const name = model.getRef((schema) => schema.name); return { setUserName, refs: { name }, }; } The getRef method allows to select fields that need to be observed for changes. Integration with UI Frameworks jModel can be easily integrated with UI frameworks like Angular by using ngContextBuilder adapter. @Injectable() export class UserComponentContext extends ngContextBuilder({ model: userSource, }) {} UserComponentContext usage export class UserComponent { private readonly model = inject(UserComponentContext).model; userName = refToSignal(this.model.refs.name); setRandomUsername(): void { this.model.setRandomUsername(); } } refToSignal is another adapter which transform jModel reactive field into Angular signal Model Validation jModel also includes built-in validation utilities. export function userUpsertStoreFactory() { const userModel = createReactiveModel({ firstName: $field('', { validators: [required] }), lastName: $field('', { validator

Overview
jModel is a prototype library designed for managing client-side models (business logic) in a simple, flexible, and efficient way. It offers built-in tools for dependency injection, state management, and validation. This article explores the core concepts of jModel and its role in building modern web applications.
Key Features
- Minimalist API – Focused on simplicity and ease of use
- Own Dependency Injection (DI) – Supports lazy initialization, allowing dependencies to be registered only when first used
- Validation Tools – Built-in utilities to handle data validation (at the model level)
- State Management – Reactive model management with efficient updates
- Performance Optimization – Tree-shakable, ensuring only used code is included in the final bundle
- Framework Agnostic – Can be integrated into different frontend frameworks/libraries through adapters
- Cross-Platform Compatibility – Works on both browsers and Node.js without relying on browser-specific APIs
- Data Management – Enables switching between various storage and caching solutions depending on the platform
Model Definition
Similar to writing traditional services for business logic, the first step is to define a model object. A typical setup includes a token for dependency injection, lifecycle option, providers, and a model function factory.
export const userSource = {
[TOKEN]: Symbol('USER_SOURCE'),
[LIFETIME]: Lifetime.scoped,
[PROVIDERS]: {
[USER_REPOSITORY]: userRepository,
[USER_STORE]: userStore,
[USER_MAP]: () => mapUserToString,
},
[FACTORY]: userSourceFactory,
};
Key elements
- TOKEN: A unique identifier used for dependency injection
- LIFETIME: Determines how long the model is retained in the dependency injection container (e.g., scoped, singleton).
- PROVIDERS: Specifies dependencies, which can be other models or standalone functions like
mapUserToString
. It uses TOKENS as keys, allowing a single token (e.g.,USER_REPOSITORY
) to have different implementations across models. - FACTORY: Specifies how the model is created.
userRepository
export const USER_REPOSITORY = Token<UserRepository>('USER_REPOSITORY');
export const userRepository = {
[TOKEN]: USER_REPOSITORY,
[LIFETIME]: Lifetime.scoped,
[FACTORY]: () => ({
get: userGet,
getUserNames: userNamesGetAll
}),
};
userSourceFactory
export const userSourceFactory = ({ inject }: Context) => ({
setRandomUsername,
refs: inject(USER_STORE).refs,
});
The model factory function provides access to the execution context, allowing to inject dependencies. Also all methods defined within it, such as setRandomUsername
, will execute within the model's context.
Execution Context
Execution context (Context) has two methods:
- inject - it allow to inject dependencies available in the same scope or singletones
- execute - it allow to execute different method in the same context
setRandomUsername
export async function setRandomUsername(this: Context): Promise<void> {
const store = this.inject(USER_STORE);
const randomUsername = await this.execute(getRandomUsername)
store.setUserName(randomUsername);
}
export async function getRandomUsername(this: Context): Promise<string> {
const repository = this.inject(USER_REPOSITORY);
const mapUserDataToString = this.inject(USER_MAP);
const userNames = await this.repository.userNamesGetAll();
return mapUserDataToString(userNames[0]);
}
In the functions above, dependencies are injected from model providers such as USER_STORE
, USER_REPOSITORY
, and USER_MAP
. The execute function is also used to call getRandomUsername
within the same context as setRandomUsername
Reactive State Management
jModel supports reactive state management which can be created by calling createReactiveModel
function.
userStoreFactory
function userStoreFactory() {
const model = createReactiveModel({
name: '',
age: null
});
const name = model.getRef((schema) => schema.name);
return {
setUserName,
refs: {
name
},
};
}
The getRef
method allows to select fields that need to be observed for changes.
Integration with UI Frameworks
jModel can be easily integrated with UI frameworks like Angular by using ngContextBuilder
adapter.
@Injectable()
export class UserComponentContext extends ngContextBuilder({
model: userSource,
}) {}
UserComponentContext usage
export class UserComponent {
private readonly model = inject(UserComponentContext).model;
userName = refToSignal(this.model.refs.name);
setRandomUsername(): void {
this.model.setRandomUsername();
}
}
refToSignal
is another adapter which transform jModel reactive field into Angular signal
Model Validation
jModel also includes built-in validation utilities.
export function userUpsertStoreFactory() {
const userModel = createReactiveModel({
firstName: $field('', { validators: [required] }),
lastName: $field('', { validators: [required] }),
});
return { state: userModel.getRefs() };
}
export const USER_UPSERT_STORE =
Token<ReturnType<typeof userUpsertStoreFactory>>('USER_UPSERT_STORE');
export const userUpsertStore = {
[TOKEN]: USER_UPSERT_STORE,
[LIFETIME]: Lifetime.scoped,
[FACTORY]: userUpsertStoreFactory,
};
The $field
method allow to add validators to model fields.
The getRefs
method transforms all fields into observable fields, enabling them to be tracked for changes.
Model usage (with two way data binding)
@Injectable()
export class UserUpsertComponentContext extends ngContextBuilder({
model: userUpsertSource,
}) {}
export class UserUpsertPageComponent {
ctx = inject(UserUpsertComponentContext);
form = this.ctx.model.state;
// jModel library provides special utilities such as:
// isFirstChange, isValid, disable, isDisabled etc.
isTouched(ref) {
return isFirstChange(ref);
}
}
Model usage in HTML
type="text"
[(ngModel)]="form.firstName.$value"
name="firstName"
/>
Value: {{ form.firstName.$value }}
*ngIf="isTouched(form.firstName) && form.firstName.$errors">
Errors: {{ form.firstName.$errors | json }}
Every ref field has $value
field which allow to get current value or set new one and $errors
field which allow to get all validation errors.
This is a brief introduction to the jModel library. Below, you'll find links to the project repository and an example application.
Project link: https://github.com/x-model/jmodel
App example: https://github.com/x-model/jmodel/tree/develop/apps/swapp
Would love to hear your thoughts!