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 🙏

© 2026 – Pkg Stats / Ryan Hefner

mobx-model-ui

v0.4.1

Published

Data models and UI interactions based on MobX

Readme

MobX-Model-UI

Library for data models and UI interactions built on MobX. successor of @Andrey-Omelyanuk/MobX-ORM:2.1.7

Data and UI models based on MobX Inspired by ember-data and js-data. This library is another opinion (my personal) about what models should be on the front-end and how to work with them.

Using with AI coding agents:

The package ships machine-readable docs so an AI assistant in your project knows how to use the library:

  • SKILL.md — quick start / recipes (also a valid Claude Code Skill, see below)
  • AGENTS.md — deep reference (architecture, API, conventions)
  • dist/mobx-model-ui.d.ts + src/ (incl. *.spec.ts tests) — the API surface and executable examples the agent can read directly

Pick one of these to wire it into a consumer project (AI agents do not auto-read docs from node_modules, so you must point them at it):

1. Claude Code plugin (one command, recommended for Claude Code)

/plugin marketplace add Andrey-Omelyanuk/mobx-model-ui
/plugin install mobx-model-ui@omelyanuk-plugins

The Skill then auto-activates whenever you work on models/queries/forms.

2. Copy the Skill into your project (any Claude Code project)

mkdir -p .claude/skills/mobx-model-ui
cp node_modules/mobx-model-ui/SKILL.md .claude/skills/mobx-model-ui/SKILL.md

Or into ~/.claude/skills/mobx-model-ui/ to enable it for all your projects.

3. Pointer in your root AGENTS.md / CLAUDE.md (works with any tool: Cursor, opencode, …)

## mobx-model-ui
This project uses `mobx-model-ui`. Before writing models, queries, filters,
forms or adapters, read `./node_modules/mobx-model-ui/SKILL.md` (quick start)
or `./node_modules/mobx-model-ui/AGENTS.md` (deep reference).

Note: the library uses TypeScript decorators. The consumer tsconfig.json needs "experimentalDecorators": true and "useDefineForClassFields": true (see AGENTS.md → Tech Stack for the full list).

Introduction:

The library contains these entities:

Model, Field, Repository, Adapter
Query, Filter
Input, Form

A quick example:

    // This example is not tested yet, it shows how I want it to work.
    import { Model, model, id, field, foreign, many } from 'mobx-model-ui'

    @model class User extends Model {
        @id    id   : number  // it can be a string for UUID for example
        @field name : string

        authorship: Author[]
    }

    @model class Book extends Model {
        @id    id   : number
        @field title: string

        authorship: Author[]
    }

    @model class Author extends Model {
        @id    id       : number 
        @field user_id  : string
        @field book_id  : string

        @foreign(User)  user: User 
        @foreign(Book)  book: Book 
    }
    many(Author)(User, 'authorship') 
    many(Author)(Book, 'authorship') 

    let user_a = new User({id: 1, name: 'User A'})
    let user_b = new User({id: 2, name: 'User B'})

    let book_a = new Book({id: 1, title: 'Book A'})
    let book_b = new Book({id: 2, title: 'Book B'})
    let book_c = new Book({id: 3, title: 'Book C'})

    new Author({id: 1, user_id: 1, book_id: 1})
    new Author({id: 2, user_id: 1, book_id: 2})
    new Author({id: 3, user_id: 1, book_id: 3})
    new Author({id: 4, user_id: 2, book_id: 3})

    for (const authorship of user_a.authorship) console.log(authorship.book.title)
    // Book A
    // Book B
    // Book C
    for (const authorship of user_b.authorship) console.log(authorship.book.title)
    // Book C
    for (const authorship of book_a.authorship) console.log(authorship.user.name)
    // User A
    for (const authorship of book_b.authorship) console.log(authorship.user.name)
    // User A
    for (const authorship of book_c.authorship) console.log(authorship.user.name)
    // User A
    // User B

More examples you can find in the ./e2e tests.

Architecture:

Main Entities

┌─────────────────────────────────────────────────────────────────────┐
│                        DEFINITION LAYER                             │
│                                                                     │
│  @model ──► ModelDescriptor ──► models (global registry)            │
│  @id    ──► registers model, injects/ejects from Cache              │
│  @field ──► observable field with type validation                   │
│  @foreign──► many-to-one relation (auto-resolves from Cache)        │
│  one()  ──► one-to-one reverse relation                             │
│  many() ──► one-to-many reverse relation                            │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                        DATA LAYER                                   │
│                                                                     │
│  Model (abstract) ──────── Cache<ID, Model>                         │
│  ├ rawData / rawObj       ├ inject() / eject() / clear()            │
│  ├ is_changed             └ Identity Map: one instance per ID       │
│  ├ updateFromRaw()                                                  │
│  └ cancelLocalChanges()                                             │
│                                                                     │
│  Repository<M> ────────── Adapter<M> (abstract)                     │
│  ├ create()                ├ create() / update() / delete()         │
│  ├ update() / save()       ├ get() / find() / load()                │
│  ├ delete() / get()        ├ action() / modelAction()               │
│  ├ find() / load()         ├ getTotalCount() / getDistinct()        │
│  ├ action() / modelAction() └───────────────────────────────────────┘
│  └ getQuery / getQueryPage / getQueryStream / getQueryCacheSync     │
│                                    │
│                          ┌─────────┼──────────────┐
│                          ▼         ▼              ▼
│                    LocalAdapter  ReadOnlyAdapter  ConstantAdapter
│                    (in-memory)   (no writes)      (static data)
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                        QUERY & FILTER LAYER                         │
│                                                                     │
│  Query<M> (abstract) ────── Filter (abstract)                       │
│  ├ filter / orderBy        ├ URLSearchParams (for API)              │
│  ├ offset / limit          ├ isMatch(obj) (client-side)             │
│  ├ isLoading / isReady     └────────────────────────────────────────┘
│  ├ autoupdate (reaction)                                           │
│  └ items / timestamp                                               │
│       │
│       ├─ QueryPage<M>     (pagination: setPage, goToNextPage...)    │
│       ├─ QueryStream<M>   (infinite scroll: append items)           │
│       ├─ QueryCacheSync<M> (live cache observation, no refetch)     │
│       ├─ QueryRaw<M>      (raw objects, no Model conversion)        │
│       ├─ QueryRawPage<M>  (raw + pagination)                        │
│       └─ QueryDistinct    (distinct field values)                   │
│                                                                     │
│  SingleFilter ── EQ, NOT_EQ, GT, LT, LIKE, ILIKE, IN ...           │
│  AND_Filter   ── combines multiple filters                          │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                        INPUT & FORM LAYER                           │
│                                                                     │
│  Variable<T>               ObjectInput<M>                           │
│  ├ value (observable)      ├ extends Variable<ID>                   │
│  ├ validation (TypeDesc)   ├ holds Query<M> for options             │
│  ├ debounce                └ obj getter (resolves from Cache)       │
│  ├ disable / error                                                  │
│  ├ sync: URL / localStorage / cookie                                │
│  └ isReady                                                          │
│                                                                     │
│  Form (abstract)           TypeDescriptor<T> (abstract)             │
│  ├ inputs: Map<Variable>   ├ toString() / fromString()              │
│  ├ isLoading / isError     ├ validate() / default()                 │
│  ├ isReady                 └ required / null                        │
│  ├ submit() → apply()                                          │
│  └ onSuccess / onCancel                                        │
│       │
│       ├─ ActionForm<M>        (repository.modelAction)              │
│       ├─ ObjectForm<M>        (bound to specific model instance)    │
│       │   ├─ SaveObjectForm<M>    (create/update model)             │
│       │   ├─ ActionObjectForm<M>  (run action on model)             │
│       │   └─ DeleteObjectForm<M>  (delete model)                    │
│                                                                     │
│  StringDescriptor / NumberDescriptor / BooleanDescriptor            │
│  DateDescriptor / DateTimeDescriptor / ArrayDescriptor              │
│  OrderByDescriptor ([field, ASC|DESC])                              │
└─────────────────────────────────────────────────────────────────────┘

Class Relationships (Mermaid)

classDiagram
    class Model {
        <<abstract>>
        +ID
        +rawData
        +rawObj
        +is_changed
        +updateFromRaw()
        +cancelLocalChanges()
        +create()
        +save()
        +delete()
        +refresh()
        +destroy()
    }

    class Cache {
        +store
        +inject()
        +eject()
        +clear()
    }

    class ModelDescriptor {
        +cls
        +id
        +fields
        +relations
        +cache
        +updateCachedObject()
    }

    class Repository {
        +adapter
        +descriptor
        +create()
        +update()
        +save()
        +delete()
        +get()
        +find()
        +load()
        +action()
        +modelAction()
        +getQuery()
        +getQueryPage()
        +getQueryStream()
        +getQueryCacheSync()
    }

    class Adapter {
        <<abstract>>
        +create()
        +update()
        +delete()
        +get()
        +find()
        +load()
        +action()
        +modelAction()
        +getTotalCount()
        +getDistinct()
    }

    class LocalAdapter
    class ReadOnlyAdapter
    class ConstantAdapter

    class Query {
        <<abstract>>
        +filter
        +orderBy
        +offset
        +limit
        +isLoading
        +isReady
        +isNeedToUpdate
        +error
        +items
        +timestamp
        +load()
        +shadowLoad()
    }

    class QueryPage {
        +setPage()
        +setPageSize()
        +goToFirstPage()
        +goToNextPage()
        +goToPrevPage()
        +goToLastPage()
    }

    class QueryStream {
        +goToFirstPage()
        +goToNextPage()
    }

    class QueryCacheSync
    class QueryRaw
    class QueryRawPage
    class QueryDistinct

    class Filter {
        <<abstract>>
        +URLSearchParams
        +isMatch()
        +isReady
    }

    class SingleFilter {
        +EQ()
        +NOT_EQ()
        +GT()
        +GTE()
        +LT()
        +LTE()
        +LIKE()
        +ILIKE()
        +IN()
    }

    class AND_Filter {
        +add()
    }

    class Variable {
        +value
        +type
        +isReady
        +isError
        +error
        +debounce
        +disabled
        +validate()
        +syncURL()
        +syncLocalStorage()
        +syncCookie()
    }

    class ObjectInput {
        +query
        +obj
    }

    class Form {
        <<abstract>>
        +inputs
        +isLoading
        +isReady
        +isError
        +errors
        +submit()
        +apply()
    }

    class ActionForm
    class ObjectForm {
        <<abstract>>
        +obj
    }
    class SaveObjectForm
    class ActionObjectForm
    class DeleteObjectForm

    class TypeDescriptor {
        <<abstract>>
        +required
        +null
        +toString()
        +fromString()
        +validate()
        +default()
    }

    Model --> Cache : uses
    Model --> ModelDescriptor : metadata
    ModelDescriptor --> Cache : owns
    Repository --> Adapter : delegates
    Repository --> ModelDescriptor : uses
    Adapter <|-- LocalAdapter
    Adapter <|-- ReadOnlyAdapter
    Adapter <|-- ConstantAdapter
    Repository --> Query : creates
    Query --> Filter : uses
    Query --> Repository : loads via
    Query <|-- QueryPage
    Query <|-- QueryStream
    Query <|-- QueryCacheSync
    Query <|-- QueryRaw
    Query <|-- QueryRawPage
    Filter <|-- SingleFilter
    Filter <|-- AND_Filter
    Form --> Variable : contains
    Form <|-- ActionForm
    Form <|-- ObjectForm
    ObjectForm <|-- SaveObjectForm
    ObjectForm <|-- ActionObjectForm
    ObjectForm <|-- DeleteObjectForm
    Variable --> TypeDescriptor : validates with
    Variable <|-- ObjectInput

Data Flow

  ┌──────────┐    ┌────────────┐    ┌──────────┐    ┌──────────┐
  │  Filter  │───►│   Query    │───►│Repository│───►│ Adapter  │
  └──────────┘    └────────────┘    └────┬─────┘    └────┬─────┘
                                         │               │
                                         ▼               │
  ┌──────────┐    ┌──────────┐    ┌──────────────┐       │
  │   Cache  │◄───│ModelDesc.│◄───│updateCached  │◄──────┘
  │          │    │          │    │Object(raw)   │
  └────┬─────┘    └──────────┘    └──────────────┘
       │
       │ @foreign / one() / many() observe Cache
       │ QueryCacheSync observes Cache
       ▼
  ┌──────────┐
  │    UI    │  (MobX reactions auto-update)
  └──────────┘

For Developers:

I recommend using Docker for development. See the makefile for how to use it.

If you don't want to use Docker then you can see scripts commands in package.json How to debug:

node_modules/.bin/jest --testMatch='**/src/**/*.spec.ts' --watchAll
node --inspect-brk=0.0.0.0 node_modules/.bin/jest --runInBand --testMatch='**/src/**/model-class.spec.ts'
node --inspect-brk=0.0.0.0 node_modules/.bin/jest --runInBand -t 'raw_obj with one relations'
chrome://inspect/#devices