npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

mvvm-core

v0.5.0

Published

A minimal MVVM framework for building reactive web applications.

Readme

MVVM Web Library

A framework-agnostic web library for building robust client-side applications using the Model-View-ViewModel (MVVM) pattern. This library leverages the power of RxJS for reactive data flow and Zod for strong data validation, aiming to simplify state management and API interactions across various frontend frameworks like React, Angular, and Vue.


Key Features

  • MVVM Core: Provides BaseModel and BaseViewModel for structured application development.
  • Reactive Data Flow: Built entirely on RxJS, ensuring all data, loading states, and errors are reactive observables.
  • Strong Data Validation: Integrates Zod schemas for compile-time and runtime data validation.
  • RESTful API Management: RestfulApiModel simplifies CRUD operations with optimistic updates, acting as a local data store, managing loading states, and handling errors automatically.
  • Command Pattern: Offers a Command utility for encapsulating UI actions, including canExecute and isExecuting states, for clean UI-ViewModel separation. Implements IDisposable.
  • Observable Collections: ObservableCollection provides reactive list management, notifying views of granular changes (add, remove, update) for efficient rendering.
  • Resource Management: Core components like BaseModel and Command implement IDisposable for proper resource cleanup (e.g., completing RxJS Subjects), helping prevent memory leaks.
  • Framework Agnostic: Designed with no direct UI framework dependencies, allowing seamless integration with React, Angular, Vue, and others.
  • Client-Heavy App Focused: Ideal for building complex dashboards, forms, and data-intensive single-page applications.

Getting Started

Installation

To install the library, you'll need npm or yarn.

npm install your-library-name rxjs zod
# or
yarn add your-library-name rxjs zod

You'll also need TypeScript configured in your project.

Basic Usage

  1. Defining a Model with Zod
// src/models/user.model.ts
import { BaseModel } from 'mvvm-core/BaseModel'; // Adjust import path
import { z } from 'zod';

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(3),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

export type User = z.infer<typeof UserSchema>;

export class UserModel extends BaseModel<User, typeof UserSchema> {
  constructor(initialData?: User) {
    super({ initialData: initialData || null, schema: UserSchema });
  }
}
  1. Creating a ViewModel
// src/viewmodels/user.viewmodel.ts
import { BaseViewModel } from 'mvvm-core/BaseViewModel'; // Adjust import path
import { UserModel } from '../models/user.model';

export class UserViewModel extends BaseViewModel<UserModel> {
  constructor(model: UserModel) {
    super(model);
  }

  // You can add computed properties (RxJS operators) or methods here
  get displayName$() {
    return this.data$.pipe(map((user) => (user ? `User: ${user.name}` : 'No user selected')));
  }
}
  1. Using RestfulApiModel for CRUD

The RestfulApiModel constructor takes an input object which, in addition to baseUrl, endpoint, fetcher, schema, and initialData, also accepts an optional validateSchema boolean (defaults to true). If set to false, Zod schema validation of the API response will be skipped.

// src/models/user.api.model.ts
import { RestfulApiModel, Fetcher } from 'mvvm-core/RestfulApiModel'; // Adjust import path
import { User, UserSchema } from './user.model';

// Example fetcher (can be window.fetch, axios, etc.)
const myCustomFetcher: Fetcher = async (url, options) => {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response; // Return response object, RestfulApiModel will parse JSON
};

export class UserApiModels extends RestfulApiModel<User[], typeof UserSchema> {
  constructor() {
    // Assuming your API returns an array of users for the base endpoint
    super({
      baseUrl: 'https://api.yourapp.com',
      endpoint: 'users',
      fetcher: myCustomFetcher,
      schema: z.array(UserSchema), // The Zod schema for data validation (e.g., for an array of Users)
      initialData: null, // Optional initial data for the model
      validateSchema: true, // Optional: defaults to true. Set to false to skip schema validation.
    });
  }
}
// Example usage in a component/service
async function loadUsers() {
  const userApi = new UserApiModels();
  userApi.data$.subscribe((users) => {
    console.log('Current users:', users);
  });
  userApi.isLoading$.subscribe((loading) => {
    console.log('Loading users:', loading);
  });
  userApi.error$.subscribe((error) => {
    if (error) console.error('Error loading users:', error);
  });

  try {
    await userApi.fetch(); // Fetches all users

    // Create example
    const newUserPayload = { name: 'New User', email: '[email protected]' }; // No ID needed if server generates
    const createdUser = await userApi.create(newUserPayload);
    if (createdUser) {
      console.log('Created User:', createdUser); // Has server-assigned ID

      // Update example
      const updatedUser = await userApi.update(createdUser.id, { name: 'Updated User Name' });
      console.log('Updated User:', updatedUser);

      // Delete example
      if (updatedUser) {
        await userApi.delete(updatedUser.id);
        console.log('User deleted successfully.');
      }
    }
  } catch (e) {
    // Errors from create, update, delete are re-thrown after setting model.error$
    // and reverting optimistic updates.
    console.error('API operation failed:', e, userApi.error$.getValue());
  } finally {
    // It's good practice to dispose of models/commands when they are no longer needed,
    // especially if they are long-lived and manage subscriptions.
    // userApi.dispose();
  }
}
  1. Implementing Commands
// src/viewmodels/auth.viewmodel.ts
import { Command } from 'mvvm-core/Command'; // Adjust import path
import { BehaviorSubject } from 'rxjs';

export class AuthViewModel {
  private _isLoggedIn = new BehaviorSubject(false);
  public isLoggedIn$ = this._isLoggedIn.asObservable();

  public loginCommand: Command<string, boolean>; // param: password, result: success boolean

  constructor() {
    this.loginCommand = new Command(
      async (password: string) => {
        console.log(`Attempting login with password: ${password}`);
        // Simulate API call
        return new Promise((resolve) => {
          setTimeout(() => {
            const success = password === 'secret';
            this._isLoggedIn.next(success);
            resolve(success);
          }, 1000);
        });
      },
      // canExecute$ Observable - login is only possible if not already logged in
      this.isLoggedIn$.pipe(map((loggedIn) => !loggedIn)),
    );
  }

  // In a React/Vue/Angular component:
  // <button
  //   onClick={() => authViewModel.loginCommand.execute('myPassword')}
  //   disabled={!(await authViewModel.loginCommand.canExecute$.pipe(first()).toPromise()) || (await authViewModel.loginCommand.isExecuting$.pipe(first()).toPromise())}
  // >
  //   { (await authViewModel.loginCommand.isExecuting$.pipe(first()).toPromise()) ? 'Logging in...' : 'Login' }
  // </button>
}

5. Integrating RestfulApiViewModel with React

This example demonstrates how to use RestfulApiModel and RestfulApiViewModel to fetch a list of items from a fake API endpoint and display them in a simple React functional component.

a. Define Item Schema and Type (e.g., src/models/todo.types.ts)

// src/models/todo.types.ts
import { z } from 'zod';

export const TodoSchema = z.object({
  id: z.string(),
  userId: z.number(),
  title: z.string(),
  completed: z.boolean(),
});

export type Todo = z.infer<typeof TodoSchema>;

b. Create the RestfulApiModel (e.g., src/models/todo.api.model.ts)

// src/models/todo.api.model.ts
import { RestfulApiModel, Fetcher } from 'your-library-name/models/RestfulApiModel'; // Adjust import path
import { z } from 'zod';
import { TodoSchema, Todo } from './todo.types';

// A mock fetcher for demonstration purposes
const mockTodoFetcher: Fetcher = async (url, options) => {
  console.log(`Mock fetcher called: ${options?.method || 'GET'} ${url}`);
  // Simulate API delay
  await new Promise((resolve) => setTimeout(resolve, 500));

  if (url.endsWith('/todos') && options?.method === 'GET') {
    const mockTodos: Todo[] = [
      { id: '1', userId: 1, title: 'Fake Todo 1 from API', completed: false },
      { id: '2', userId: 1, title: 'Fake Todo 2 from API', completed: true },
    ];
    return new Response(JSON.stringify(mockTodos), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });
  }
  // Fallback for other unhandled requests by the mock
  return new Response(JSON.stringify({ message: 'Mocked endpoint not found' }), {
    status: 404,
    headers: { 'Content-Type': 'application/json' },
  });
};

export class TodoListModel extends RestfulApiModel<Todo[], z.ZodArray<typeof TodoSchema>> {
  constructor() {
    super({
      baseUrl: 'https://jsonplaceholder.typicode.com', // Using a real base URL for structure
      endpoint: 'todos', // The specific endpoint
      fetcher: mockTodoFetcher, // Our mock fetcher
      schema: z.array(TodoSchema), // Expect an array of Todos
      initialData: null, // No initial data
    });
  }
}

c. Create the RestfulApiViewModel (e.g., src/viewmodels/todo.viewmodel.ts)

// src/viewmodels/todo.viewmodel.ts
import { RestfulApiViewModel } from 'your-library-name/viewmodels/RestfulApiViewModel'; // Adjust import path
import { TodoListModel } from '../models/todo.api.model';
import { Todo, TodoSchema } from '../models/todo.types';
import { z } from 'zod';

export class TodoViewModel extends RestfulApiViewModel<Todo[], z.ZodArray<typeof TodoSchema>> {
  constructor() {
    // Create an instance of the model
    const todoListModel = new TodoListModel();
    super(todoListModel);
  }

  // You can add specific methods or computed observables related to Todos here if needed
  // For example, a command to fetch only completed todos (would require model changes)
  // Or an observable that filters/maps the data$
}

d. Create a custom hook for using RxJS Observables in React (e.g., src/hooks/useObservable.ts)

This is a common pattern to bridge RxJS with React's state.

// src/hooks/useObservable.ts
import { useState, useEffect } from 'react';
import { Observable } from 'rxjs';

export function useObservable<T>(observable: Observable<T>, initialValue: T): T {
  const [value, setValue] = useState<T>(initialValue);

  useEffect(() => {
    const subscription = observable.subscribe(setValue);
    return () => subscription.unsubscribe();
  }, [observable]);

  return value;
}

Note: For more complex scenarios or production apps, consider robust libraries like rxjs-hooks.

e. Create the React Component (e.g., src/components/TodoList.tsx)

// src/components/TodoList.tsx
import React, { useMemo, useEffect } from 'react';
import { TodoViewModel } from '../viewmodels/todo.viewmodel'; // Adjust path
import { useObservable } from '../hooks/useObservable'; // Adjust path
import { Todo } from '../models/todo.types'; // Adjust path

const TodoListComponent: React.FC = () => {
  // Instantiate the ViewModel. In a real app, you might use a context or DI.
  const todoViewModel = useMemo(() => new TodoViewModel(), []);

  // Subscribe to the ViewModel's observables
  const todos = useObservable(todoViewModel.data$, null);
  const isLoading = useObservable(todoViewModel.isLoading$, false);
  const error = useObservable(todoViewModel.error$, null);

  // Clean up the ViewModel when the component unmounts
  useEffect(() => {
    return () => {
      todoViewModel.dispose();
    };
  }, [todoViewModel]);

  const handleFetchTodos = () => {
    todoViewModel.fetchCommand.execute();
  };

  return (
    <div>
      <h2>Todo List (React Example)</h2>
      <button onClick={handleFetchTodos} disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Fetch Todos'}
      </button>

      {error && <p style={{ color: 'red' }}>Error: {error.message || 'Failed to fetch'}</p>}

      {isLoading && !todos && <p>Loading todos...</p>}

      {todos && todos.length > 0 && (
        <ul>
          {todos.map((todo: Todo) => (
            <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.title} (User ID: {todo.userId})
            </li>
          ))}
        </ul>
      )}

      {!isLoading && todos && todos.length === 0 && <p>No todos found.</p>}
    </div>
  );
};

export default TodoListComponent;

This example provides a complete, albeit simplified, flow from defining data structures and models to consuming them in a React UI. Remember to adjust import paths (your-library-name) as per your actual library's name and structure.

  1. Using ObservableCollection
// src/viewmodels/todos.viewmodel.ts
import { ObservableCollection } from 'mvvm-cores/ObservableCollection'; // Adjust import path
import { map } from 'rxjs/operators';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export class TodosViewModel {
  public todos: ObservableCollection<Todo>;

  constructor() {
    this.todos = new ObservableCollection([
      { id: '1', text: 'Learn MVVM', completed: false },
      { id: '2', text: 'Build awesome app', completed: true },
    ]);
  }

  addTodo(text: string) {
    const newTodo: Todo = { id: Date.now().toString(), text, completed: false };
    this.todos.add(newTodo);
  }

  toggleTodo(id: string) {
    this.todos.update((todo) => todo.id === id, {
      ...this.todos.toArray().find((t) => t.id === id)!,
      completed: !this.todos.toArray().find((t) => t.id === id)!.completed,
    });
  }

  removeCompleted() {
    this.todos.remove((todo) => todo.completed);
  }

  // In a React/Vue/Angular component:
  // <ul *ngIf="todos.items$ | async as todoList">
  //   <li *ngFor="let todo of todoList">
  //     {{ todo.text }} ({{ todo.completed ? 'Completed' : 'Pending' }})
  //   </li>
  // </ul>
}