{"id":20525975,"url":"https://github.com/codeyellowbv/mobx-spine","last_synced_at":"2025-04-14T04:09:06.348Z","repository":{"id":16678901,"uuid":"80426034","full_name":"CodeYellowBV/mobx-spine","owner":"CodeYellowBV","description":"MobX with support for models, relations and an external API.","archived":false,"fork":false,"pushed_at":"2024-02-26T14:30:14.000Z","size":996,"stargazers_count":31,"open_issues_count":33,"forks_count":14,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-04-14T04:09:00.215Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CodeYellowBV.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-01-30T13:58:23.000Z","updated_at":"2024-04-09T17:55:54.000Z","dependencies_parsed_at":"2024-06-21T16:41:41.999Z","dependency_job_id":"166d1aaf-403e-4485-9b0c-ab11dbaf90fd","html_url":"https://github.com/CodeYellowBV/mobx-spine","commit_stats":null,"previous_names":[],"tags_count":83,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CodeYellowBV%2Fmobx-spine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CodeYellowBV%2Fmobx-spine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CodeYellowBV%2Fmobx-spine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CodeYellowBV%2Fmobx-spine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CodeYellowBV","download_url":"https://codeload.github.com/CodeYellowBV/mobx-spine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248819405,"owners_count":21166477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-15T23:11:38.247Z","updated_at":"2025-04-14T04:09:06.314Z","avatar_url":"https://github.com/CodeYellowBV.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mobx-spine\n\n[![Build Status](https://travis-ci.org/CodeYellowBV/mobx-spine.svg?branch=master)](https://travis-ci.org/CodeYellowBV/mobx-spine)\n[![codecov](https://codecov.io/gh/CodeYellowBV/mobx-spine/branch/master/graph/badge.svg)](https://codecov.io/gh/CodeYellowBV/mobx-spine)\n\nA frontend package built upon [MobX](https://mobx.js.org/) to add models and collections. It has first-class support for relations and can communicate to a backend.\n\nBy default it comes with a \"communication layer\" for [Django Binder](https://github.com/CodeYellowBV/django-binder), which is Code Yellow's Python backend framework. It is easy to add support for another backend.\n\n```shell\nyarn add mobx-spine lodash mobx moment\nnpm install mobx-spine lodash mobx moment\n```\n\n**Work In Progress.**\n\nmobx-spine is highly inspired by Backbone and by the package we built on top of Backbone, [Backbone Relation](https://github.com/CodeYellowBV/backbone-relation).\n\n## Design differences with Backbone\n\nSince mobx-spine uses MobX, it does not need to have an event system like Backbone has. This means that there are no `this.listenTo()`'s. If you need something like that, look for [`autorun()`](https://mobx.js.org/refguide/autorun.html) or add a [`@computed`](https://mobx.js.org/refguide/computed-decorator.html) property.\n\nAnother difference is that in mobx-spine, all properties of a model must be defined beforehand. So if a model has the props `id` and `name` defined, it's not possible to suddenly add a `slug` property unless you define it on the model itself. Not allowing this helps with keeping overview of the props there are.\n\nmobx-spine has support for relations and pagination built-in, in contrast to Backbone.\n\nA model or collection can only do requests to an API if you add an `api` instance to it. This allows for easy mocking of the API, and makes mobx-spine not coupled to Binder, our Python framework. It would be easy to make a package or just a separate file with a custom backend.\n\n\n## Model\n\nA model is a data container with a set of helper functions. Models should extend `Model` from mobx-spine, and the very least define some properties. \n\nThe `Model` contructor takes 2 arguments: \n- `data`: An object with default values for a model.\n- `options`: An object with options.\n\n### Constructor: data\n\nLet's for example define a class `Animal` with 2 properties `id` and `name`.\n\n```js\nimport { observable } from 'mobx';\nimport { Model } from 'mobx-spine';\n\n// Define a class Animal, with 2 observed properties `id` and `name`.\nclass Animal extends Model {\n    @observable id = null; // Default value is null.\n    @observable name = ''; // Default value is ''.\n}\n```\n\nIf we instantiate a new animal without arguments it will create an empty animal using defaults defined on the model:\n\n```js\n// Create an empty instance of an Animal.\nconst lion = new Animal();\n\nconsole.log(lion.id); // null\nconsole.log(lion.name); // ''\n```\n\nYou can also supply data when creating a new instance:\n\n```js\n// Create an instance of an Animal with existing data.\nconst cat = new Animal({ id: 1, name: 'Cat' });\n\nconsole.log(cat.name); // Cat\n```\n\nWhen data is supplied in the constructor, these can be reset by calling `clear`:\n\n```js\nconst cat = new Animal({ id: 1, name: 'Cat' });\n\ncat.name = '';\nconsole.log(cat.name); // ''\n\ncat.clear();\nconsole.log(cat.name); // 'Cat'\n```\n\nWhen an undefined property key is supplied, it will be ignored:\n\n```js\nconst cat = new Animal({ id: 1, name: 'Cat', undefinedProperty: 'will be ignored' });\n\ncat.name = '';\nconsole.log(cat.undefinedProperty); // undefined\n```\n\n### Constructor: options\n\n|key|default| | |\n|---|---|---|---|\n|relations|undefined|Relations to be instantiated when instantiating this model as well. Should be an array of strings.| `['location', 'owner.parents']`\n\n\n### Properties\n\nIn its basic form, a model holds a few properties. These properties are normally observables and default values are defined on the property as well. This will define a basic animal model:\n\n```js\nimport { observable } from 'mobx';\nimport { Model } from 'mobx-spine';\n\n// Define a class Animal, with 2 observed properties `id` and `name`.\nclass Animal extends Model {\n    @observable id = null; // Default value is null.\n    @observable name = ''; // Default value is ''.\n    @observable color; // Default value is undefined.\n}\n```\n\nYou can also define frontend only fields, which will be excluded when performing for example a `save`. These properties start with a underscore:\n\n```js\nclass Animal extends Model {\n    @observable id = null;\n    @observable name = '';\n\n    /**\n     * Fields starts with underscore, so excluded from saving to \n     * backend because `toBackend` filters them out.\n     **/ \n    @observable _notSavedToBackend = true;\n}\n```\n\n#### Forbidden properties\n\nThere are some forbidden property names. Currently these are:\n\n- url\n- urlRoot\n- api\n- isNew\n- isLoading\n- parse\n- save\n- clear\n\n### Backend request\n\nA model can communicate with the backend using a few functions:\n\n- fetch\n- save\n- delete\n\nThese functions go through the api, and by default the BinderApi is shipped with mobx-spine.\n\n#### Backend request: fetch\n\nFetching data can be done by calling `fetch`. Lets look at an example and assume the backend returns with `name` `Garfield`:\n\n```js\nconst api = new BinderApi();\n\nclass Animal extends Model {\n    api = api;\n    \n    // Supply either a backendResourceName (Model will calculate urlRoot) or a urlRoot.\n    static backendResourceName = 'animal';\n    // urlRoot = '/api/animal/';\n\n    @observable id = null;\n    @observable name = '';\n}\n\nconst animal = new Animal({ id: 2 });\n\n// Performs a GET request: /api/animal/2/\nanimal.fetch().then(() =\u003e {\n    console.log(animal.name); // Garfield\n});\n\n\n```\nYou can also cancel the previous request by passing `{ cancelPreviousFetch: true }` to fetch\n\n```js\nanimal.fetch(); // request cancelled\nanimal.fetch({cancelPreviousFetch: true});\n```\n\n#### Backend request: save\n\nSaving data can be done by calling `save`. Lets look at creating a new model and saving that in the database:\n\n```js\nconst api = new BinderApi();\n\nclass Animal extends Model {\n    api = api;\n    static backendResourceName = 'animal';\n\n    @observable id = null;\n    @observable name = '';\n}\n\nclass animal = new Animal();\n\n// Performs a POST request: /api/animal/\nanimal.save().then(() =\u003e {\n    console.log(animal.id); // 1\n});\n```\n\nAn existing model in the database can be updated as follows:\n\n```js\nclass animal = new Animal({ id: 1 });\n\n// Performs a PUT request: /api/animal/\nanimal.save().then(() =\u003e {\n    console.log(animal.id); // 1\n});\n```\n\nThe `save` function accepts a few paramaters as an `options` object:\n\n|key|default| | |\n|---|---|---|---|\n|onlyChanges|false|When true, only changes made with `setInput` are saved.| `animal.save({ onlyChanges: true })`\n|url|undefined|When set, use specified url for the request.| `animal.save({ url: '/api/animal/special/url' })`\n|data|undefined|When set, append `data` to result. Existing keys from `toBackend` will be overwritten by data, while new keys will be added. | `animal.save({ data: { id: 1, some_other_field: 'will be added' } })`\n|mapData|undefined|You can change the data which will be used for the request send by supplying a function. First argument is the formatted data ready for sending a request. Called at the very last of data formatting operations.| `animal.save({ mapData: data =\u003e (...data, some_other_field: 'will be added' } ) } })`\n|forceFields|undefined|When `onlyChanges` is given, you can force fields to be included despite of having no changes.| `animal.save({ onlyChanges: true, forceFields: ['name'] } ) } })`\n|relations|undefined|Relations to save when saving this model as well. Note that its not needed to include relations here so that they will be linked, only to save the models themselves. Should be an array of strings.| `animal.save({ relations: ['location', 'owner.parents'] })`\n\n#### Backend request: delete\n\nDeleting a model can be done by calling `model.delete()`. Lets look at an example:\n\n```js\nconst api = new BinderApi();\n\nclass Animal extends Model {\n    api = api;\n    static backendResourceName = 'animal';\n\n    @observable id = null;\n    @observable name = '';\n}\n\nclass animal = new Animal({ id: 2 });\n\n// Performs a DELETE request: /api/animal/2/\nanimal.delete();\n\n\nAn example with a Store (called a Collection in Backbone).\n```\n\n### Relations\n\nModels can have relations to other models / stores. These relations are defined as follows:\n\n```js\nclass Breed extends Model {\n    @observable id = null;\n    @observable name = '';\n}\n\nclass AnimalStore extends Store {\n    Model = Animal;\n}\n\nclass Animal extends Model {\n    @observable id = null;\n    @observable name = '';\n    \n    @observable breed = this.relation(Bread);\n    @observable relatives = this.relation(AnimalStore);\n\n}\n```\n\nNote that it is really import for relations to be observable, otherwise the parsing of the model data will fail. \n\n\nYou can now instantiate the animal with it's breed \u0026 relatives relation recursively:\n\n```js\nclass animal = new Animal(\n    { \n        id: 2, \n        name: 'Rova', \n        breed: { id: 3, name: 'Main Coon' }, \n        relatives: [\n            { id: 5, name: 'Gizmo', breed: { id: 3, name: 'Main Coon' } },\n            { id: 7, name: 'Chiggy', breed: { id: 5, name: 'Mixed' } },\n        ],\n    }, { \n        relations: ['breed', 'relatives.breed'] \n    }\n);\n\nconsole.log(animal.name); // Rova\nconsole.log(animal.breed.name); // Main Coon\nconsole.log(animal.relatives.get(5).name); // Gizmo\nconsole.log(animal.relatives.get(5).breed.name); // Main Coon\nconsole.log(animal.relatives.get(7).name); // Chiggy\nconsole.log(animal.relatives.get(7).breed.name); // Mixed\n```\n\nYou can now instantiate the animal without it's breed relation and try to access it, it will throw an error:\n\n```js\nclass animal = new Animal({ id: 2, name: 'Rova', breed: { id: 3, name: 'Main Coon' } });\n\nconsole.log(animal.breed.name); // Throws cannot read property name from undefined.\n```\n\n### Alternative legacy relation definition\n\nAlternatively, relations can be defined by overriding the relations(). This is used in old models. Note that this way of defining models has two disadvantages:\n\n- It doesn't allow inheriting relations\n- It doesn't allow for easy type hinting in typescript\n\n```js\nclass Breed extends Model {\n    @observable id = null;\n    @observable name = '';\n}\n\nclass AnimalStore extends Store {\n    Model = Animal;\n}\n\nclass Animal extends Model {\n    @observable id = null;\n    @observable name = '';\n\n    relations() {\n        return {\n            breed: Breed, // Define a breed relation to Breed.\n            relatives: AnimalStore, // Define a relatives relation to AnimalStore.\n        };\n    }\n}\n```\n\n\n### Pick fields\n\nYou can pick fields by either defining a static `pickFields` variable or a `pickFields` function. Keep in mind that `id` is mandatory, so it will always be included.\n\n#### As a static field\n```js\nclass Animal extends Model {\n    static pickFields = ['name'];\n\n    @observable id = null;\n    @observable name = '';\n    @observable color = '';\n}\n\nconst animal = new Animal({ id: 1, name: 'King', color: 'orange' });\nanimal.toBackend(); // { id: 1, name: 'King' }\n```\n\n#### As a function\n```js\nclass Animal extends Model {\n    pickFields() {\n        return ['name];\n    }\n\n    @observable id = null;\n    @observable name = '';\n    @observable color = '';\n}\n\nconst animal = new Animal({ id: 1, name: 'King', color: 'orange' });\nanimal.toBackend(); // { id: 1, name: 'King' }\n```\n\n### Omit fields\n\nYou can omit fields by either defining a static `omitFields` variable or a `omitFields` function. Keep in mind that `id` is mandatory, so it will always be included.\n\n#### As a static field\n```js\nclass Animal extends Model {\n    static omitFields = ['color'];\n\n    @observable id = null;\n    @observable name = '';\n    @observable color = '';\n}\n\nconst animal = new Animal({ id: 1, name: 'King', color: 'orange' });\nanimal.toBackend(); // { id: 1, name: 'King' }\n```\n\n#### As a function\n```js\nclass Animal extends Model {\n    omitFields() {\n        return ['color];\n    }\n\n    @observable id = null;\n    @observable name = '';\n    @observable color = '';\n}\n\nconst animal = new Animal({ id: 1, name: 'King', color: 'orange' });\nanimal.toBackend(); // { id: 1, name: 'King' }\n```\n\n### Update properties\n\nThere are 2 ways to update properties:\n\n- Direct assignment\n- Using `setInput`\n\n```js\nlion.name = 'Lion'; // Direct assignment, doesn't register a change on the `name` property.\nlion.setInput('name',  'Lion'); // Use `setInput` which registers a change on the `name` property.\n```\n\nWhen using `setInput`, a `model.save({ onlyChanges: true })` will only submit fields to the backend which have been changed using `setInput`.\n\n## Store\n\nA Store (Collection in Backbone) is holds multiple instances of models and have several helper functions.\n\n### Constructor: options\n\n|key|default| | |\n|---|---|---|---|\n|relations|undefined|Relations to be instantiated when new models are instantiated using `add()`. Should be an array of strings.| `animalStore = new AnimalStore({ relations: ['location', 'owner.parents'] })`\n|limit|25|Page size per fetch, also able to set using `setLimit()`. By default a limit is always set, but there are occations where you want to fetch everything. In this case, set limit to false. | `animalStore = new AnimalStore({ limit: false })`\n|comparator|undefined| The models in the store will be sorted by comparator. When it's a string, the models will be sorted by that property name. If it's a function, the models will be sorted using the [default array sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). | `animalStore = new AnimalStore({ comparator: 'name' })`\n|params|undefined| All params will be converted to GET params. This is used for quering the server to fill the store with models. | `animalStore = new AnimalStore({ params: { 'search': 'Gizmo' } })`\n\n### Adding models\n\nAdding models to a store can be done using `store.add()`. You can supply either an object or an array of objects:\n\n```js\nimport { Model } from 'mobx-spine';\n\nclass AnimalStore extends Store {\n    Model = Animal;\n}\n\nconst animalStore = new AnimalStore();\n\nanimalStore.add({ id: 1, name: 'Rova' });\n\nconsole.log(animalStore.length) // 1\nconsole.log(animalStore.at(0).name) // Rova\n\nanimalStore.add([\n    { id: 2, name: 'Gizmo' },\n    { id: 3, name: 'Diva' },\n]);\n\nconsole.log(animalStore.length) // 3\nconsole.log(animalStore.at(0).name) // Rova\nconsole.log(animalStore.at(1).name) // Gizmo\nconsole.log(animalStore.at(2).name) // Diva\n```\n\n### Getting models\n\nThere are a few ways to get a specific model:\n\n- `get`: Use models id.\n- `at`: Use model index.\n- `find`: Use callback.\n- `store.models`: Get the mobx array that holds the models.\n\nSome examples:\n\n```js\nanimalStore.add([\n    { id: 1, name: 'Rova' },\n    { id: 2, name: 'Gizmo' },\n    { id: 3, name: 'Diva' },\n]);\n\nconsole.log(animalStore.at(0).name) // Rova\nconsole.log(animalStore.get(1).name) // Rova\nconsole.log(animalStore.find(animal =\u003e animal.name === 'Rova').name) // Rova\nconsole.log(animalStore.models.find(animal =\u003e animal.name === 'Rova').name) // Rova\n```\n\n### Backend request\n\nA store can communicate with the backend using a few functions:\n\n- fetch\n\nThese functions go through the api, and by default the BinderApi is shipped with mobx-spine.\n\n### Backend request: fetch\n\nFetching data can be done by calling `fetch`. Lets look at an example and assume the backend returns 1 model with `name` `Garfield`:\n\n```js\nconst api = new BinderApi();\n\nclass AnimalStore extends Store {\n    Model = Animal\n    api = api;\n    \n    // Supply either a backendResourceName (Model will calculate url) or a url.\n    static backendResourceName = 'animal';\n    // url = '/api/animal/';\n}\n\nconst animalStore = new AnimalStore();\n\n// Performs a GET request: /api/animal/?limit=25\nanimalStore.fetch().then(() =\u003e {\n    console.log(animalStore.at(0).name); // Garfield\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodeyellowbv%2Fmobx-spine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodeyellowbv%2Fmobx-spine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodeyellowbv%2Fmobx-spine/lists"}