{"id":19642071,"url":"https://github.com/beanbaginc/spina","last_synced_at":"2025-04-28T12:31:46.041Z","repository":{"id":147098028,"uuid":"591203792","full_name":"beanbaginc/spina","owner":"beanbaginc","description":"A modern TypeScript/ES6 JavaScript foundation built around Backbone.js","archived":false,"fork":false,"pushed_at":"2024-05-21T15:11:08.000Z","size":637,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-05T08:35:08.286Z","etag":null,"topics":["backbonejs","javascript","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@beanbag/spina","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beanbaginc.png","metadata":{"files":{"readme":"README.md","changelog":"NEWS.md","contributing":null,"funding":null,"license":"LICENSE","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":"2023-01-20T06:59:47.000Z","updated_at":"2024-12-13T12:36:47.000Z","dependencies_parsed_at":"2024-03-13T06:29:58.056Z","dependency_job_id":"7a639356-f3ff-4124-9f35-22ce4633be02","html_url":"https://github.com/beanbaginc/spina","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beanbaginc%2Fspina","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beanbaginc%2Fspina/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beanbaginc%2Fspina/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beanbaginc%2Fspina/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beanbaginc","download_url":"https://codeload.github.com/beanbaginc/spina/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251313694,"owners_count":21569472,"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":["backbonejs","javascript","typescript"],"created_at":"2024-11-11T14:11:26.623Z","updated_at":"2025-04-28T12:31:45.615Z","avatar_url":"https://github.com/beanbaginc.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spina, a modern Backbone\n\nAt [Beanbag](https://www.beanbaginc.com), we make heavy of\n[Backbone.js](https://backbonejs.org). We like it for the ability to cleanly\nseparate how we store state and process logic from how we display it.\n\nUnfortunately, there are problems with Backbone when writing modern-day\nJavaScript. So we set out to solve some of these. The result is Spina.\n\n\n## How does Spina improve Backbone?\n\nSpina wraps Backbone, rather than replacing it, and makes it more suitable\nfor modern JavaScript development.\n\nIt introduces the following:\n\n1. Restored ability to define `defaults`, `url`, etc. as attributes in\n   ES6-based Backbone subclasses (using `class ... extends ...`)\n\n2. A fixed order of initialization for subclasses when using ES6 classes.\n\n3. Improvements to views:\n\n   1. Easier model event registration (similar to DOM event registration)\n\n   2. Better control over render behavior.\n\n4. Improved typing for TypeScript.\n\n5. Mixins for classes.\n\n6. Full compatibility with code already using Backbone.\n\nIf you want to learn more about the initialization problem of typing issues,\nread the Deep Dives below.\n\n\n## Installing Spina\n\nTo install, run:\n\n```\nnpm install --save @beanbag/spina\n```\n\n\n### Enabling TypeScript Support\n\nIf you're using TypeScript, you'll then want to enable experimental decorators\nand add our Backbone types. You can do this by placing the following in\n`tsconfig.json`:\n\n```json\n{\n    \"compilerOptions\": {\n        \"experimentalDecorators\": true,\n        \"moduleResolution\": \"node\",\n        \"paths\": {\n            \"Backbone\": [\"node_modules/@beanbag/spina/lib/@types/backbone\"]\n        }\n    }\n}\n```\n\n\n## Usage\n\nSpina provides new base classes for several Backbone classes:\n\n* `Spina.BaseCollection` replaces `Backbone.Collection`\n* `Spina.BaseModel` replaces `Backbone.Model`\n* `Spina.BaseRouter` replaces `Backbone.Router`\n* `Spina.BaseView` replaces `Backbone.View`\n\nPlus generic classes that can be instantiated:\n\n* `Spina.Collection`\n* `Spina.Router`\n\nThe `Base*` classes are abstract base classes, and must be subclassed before\nuse.\n\nTo subclass any of these classes, you need to use our `@spina` decorator.\nThis fixes up the object initialization order, letting you set attributes\nwithout defining them as methods on your class. You'll see examples below.\n\n\n### Spina.BaseCollection\n\nThis replaces `Backbone.Collection`, and is used as a base for new\nsubclasses:\n\n```typescript\nimport { BaseCollection, spina } from '@beanbag/spina';\n\n@spina\nclass MyCollection extends BaseCollection {\n    static model = MyModel;\n\n    ...\n}\n\n// Or:\nconst MyCollection = spina(class MyCollection extends BaseCollection {\n    ...\n});\n```\n\nIf using TypeScript, it can optionally take the model type:\n\n```typescript\nimport { BaseCollection, spina } from '@beanbag/spina';\n\n@spina\nclass MyCollection extends BaseCollection\u003cMyModel\u003e {\n    static model = MyModel;\n\n    ...\n}\n```\n\n\n#### Utility Accessors\n\nThere's a useful utility accessor method available for collections:\n\n* `getURL()` (accesses `url`).\n\nThis will return the value of the corresponding attribute, whether that\nattribute is set to a value or a function returning the value.\n\n\n### Spina.BaseModel\n\nModels can be defined using attributes for `defaults`, `url`, etc.\n\nFor example:\n\n```typescript\nimport { BaseModel, spina } from '@beanbag/spina';\n\n@spina\nclass MyModel extends BaseModel {\n    static defaults = {\n        attr1: 'foo',\n        attr2: 42,\n    };\n\n    static url = '/api/mymodels';\n\n    initialize() {\n        ...\n    }\n}\n\n// Or:\nconst MyModel = spina(class MyModel extends BaseModel {\n    ...\n});\n```\n\nIf using TypeScript, it can optionally take an `interface` describing the\nattributes, as well as an `interface` for additional options to pass to the\nconstructor. For example:\n\nExample:\n\n```typescript\ninterface MyModelAttrs {\n    attr1: string;\n    attr2: number;\n}\n\ninterface MyModelOptions {\n    option1: string;\n    option2: boolean;\n}\n\n@spina\nclass MyModel extends BaseModel\u003cMyModelAttrs, MyModelOptions\u003e {\n    ...\n}\n```\n\n(If you're using this same support in `Backbone.Model` today, we've\nswapped the 2nd and 3rd values for the Generics. This makes it easier to\ndefine custom options.)\n\nIf you need to return dynamic attributes, you can define a static method.\nThis will be transformed into a method on the prototype, allowing Spina to\ncall it with ``this`` set to the instance. For example, using TypeScript:\n\n```typescript\nimport { BaseModel, spina } from '@beanbag/spina';\n\n@spina\nclass MyModel extends BaseModel {\n    static defaults(this: MyModel) {\n        return {\n            attr1: 'foo',\n            attr2: 42,\n            attr3: this.someValue,\n        };\n    };\n\n    someValue: string = 'test';\n}\n```\n\nBackbone and Spina allow many attributes to be defined as methods.\n\n\n#### Utility Accessors\n\nThere are a handful of useful utility accessor methods available for models:\n\n* `getDefaultAttrs()` (accesses `defaults`)\n* `getURL()` (accesses `url`)\n* `getURLRoot()` (accesses `urlRoot`)\n\nEach of these will return the value of the corresponding attribute, whether\nthat attribute is set to a value or a function returning the value.\n\n\n### Spina.BaseRouter\n\nThis replaces `Backbone.Router`, and is used as a base for new subclasses:\n\n```typescript\nimport { BaseRouter, spina } from '@beanbag/spina';\n\n@spina\nclass MyRouter extends BaseRouter {\n    ...\n};\n```\n\n\n### Spina.BaseView\n\n#### Event Registration\n\nViews handle event registration the same way they do in Backbone.\n\nSpina views don't require `events` to be a function. Instead, they're as simple\nas:\n\n\n```typescript\nimport { BaseView, spina } from '@beanbag/spina';\n\n@spina\nclass MyView extends BaseView {\n    static events = {\n        'click': '_onClick',\n    };\n\n    _onClick(evt) {\n        ...\n    }\n}\n```\n\n**Note:** Due to limitations with ES6 classes, you can't use private methods\nin the form of `#myHandler`, since it's not possible for the event handlers\nto look up the right function. If you're using TypeScript, you may want to\nprefix your handler method with `private` or `protected`.\n\n\n##### Automatic Merging of Events\n\nIf subclassing a view with `events`, the parent's event handlers are\nautomatically registered. This means there's no need to use `_.defaults(...)`\nor `_.extend(...)` to pass in the parent's `events`.\n\nTo disable that, do:\n\n```typescript\n@spina({\n    skipParentAutomergeAttrs: ['events'],\n})\nclass MyView extends BaseView {\n    static events = {\n        ...\n    };\n}\n```\n\n\n#### Model Event Registration\n\nViews now support automatic registration of model events on the first\nrender (if you haven't overridden `render()`):\n\n```typescript\nimport { BaseView, spina } from '@beanbag/spina';\n\n@spina\nclass MyView extends BaseView {\n    static modelEvents = {\n        'change:attr1': '_onAttr1Changed',\n    };\n\n    _onAttr1Changed(model, evt) {\n        ...\n    }\n}\n\n// Or:\nconst MyView = spina(class MyView extends BaseView {\n    ...\n});\n```\n\nIf using TypeScript, it can optionally take a model type and HTML element type:\n\n```typescript\nimport { BaseView, spina } from '@beanbag/spina';\n\n@spina\nclass MyView extends BaseView\u003cMyModel, HTMLDivElement\u003e {\n    ...\n}\n```\n\n\n##### Automatic Merging of Events\n\nIf subclassing a view with `modelEvents`, the parent's event handlers are\nautomatically registered. This means there's no need to use `_.defaults(...)`\nor `_.extend(...)` to pass in the parent's `modelEvents`.\n\n```typescript\n@spina({\n    skipParentAutomergeAttrs: ['modelEvents'],\n})\nclass MyView extends BaseView {\n    static modelEvents = {\n        ...\n    };\n}\n```\n\n\n#### Render Helpers\n\nViews gained a new method, `renderInto()`, which helps to render a view\nand then append it (or prepend it) to an element. For example:\n\n```typescript\n// Append to a parent.\nmyView.renderInto(parentEl);\n\n// Prepend to a parent.\nmyView.renderInto(parentEl, {prepend: true});\n\n// Empty the parent first.\nmyView.renderInto(parentEl, {empty: true});\n```\n\n\nRenders are also better managed. This is partly to enable model event\nregistration, and partly to solidify some patterns we often use.\n\nInstead of overriding `render()`, you can now override `onInitialRender()` to\nrender only the first time `render()` is called, and/or override `onRender()`\nto render each time `render()` is called.\n\nBonus: No need to return `this`.\n\n```typescript\n@spina\nclass MyView extends BaseView {\n    protected onInitialRender() {\n        // Do this only the first time render() is called.\n    }\n\n    protected onRender() {\n        // Do this every time render() is called.\n    }\n}\n```\n\nBoth are optional.\n\nAlong with that, `render()` now triggers two events:\n\n* `rendering`: Called before anything is rendered.\n* `rendered`: Called after rendering is complete.\n\n\n#### Improved View Removal\n\n(Added in Spina 3.1)\n\nInstead of carefully overriding `remove()` and being sure to call the parent\nmethod in the right order and returning the right value0, subclasses can\nsimply override `onRemove()`.\n\n```typescript\n@spina\nclass MyView extends BaseView {\n    protected onRemove() {\n        // Perform any removal logic.\n    }\n}\n```\n\n`remove()` will call this automatically at the right time.\n\nAlong with that, `remove()` now triggers two events:\n\n* `removing`: Called before anything is removed.\n* `removed`: Called after removal is complete.\n\n\n#### Show/Hide\n\nViews can now be shown using `view.show()` or hidden using `view.hide()`:\n\n```typescript\n// Hide the view.\nview.hide();\n\n// Now show it again.\nview.show();\n```\n\n\n#### Utility Accessors\n\nThere are a handful of useful utility accessor methods available for views:\n\n* `getAttributes()` (accesses `attributes`)\n* `getClassName()` (accesses `className`)\n* `getID()` (accesses `id`)\n* `getTagName()` (accesses `tagName`)\n\nEach of these will return the value of the corresponding attribute, whether\nthat attribute is set to a value or a function returning the value.\n\n\n### Spina.Collection\n\nThis is a generic implementation of `Spina.BaseCollection`. It can be\ninstantiated and used without subclassing.\n\nFor example:\n\n```typescript\nimport { Collection } from '@beanbag/spina';\n\nconst myCollection = new Collection({\n    model: MyModel,\n});\n```\n\nIf using TypeScript, you can constrain this to a model type:\n\n```typescript\nimport { Collection } from '@beanbag/spina';\n\nconst myCollection = new Collection\u003cMyModel\u003e({\n    model: MyModel,\n});\n```\n\n\n### Spina.Router\n\nThis is a generic implementation of `Spina.BaseRouter`. It can be instantiated\nand used without subclassing.\n\n```typescript\nimport { Router } from '@beanbag/spina';\n\nconst myRouter = new Router(...);\n```\n\n\n## Defining Spina Subclasses\n\nAll subclasses in a Spina hierarchy must use the `@spina` decorator. This\nsets up the class to be initialized correctly, and also provides a handful\nof other benefits.\n\nThe following options can be passed to the `@spina` decorator:\n\n* `automergeAttrs`\n* `mixins`\n* `name`\n* `prototypeAttrs`\n* `skipParentAutomergeAttrs`\n\n\n### automergeAttrs\n\nSpina classes can automatically merge in static attributes for key/value\nobjects into any subclasses. This is useful for things like `events` on views\nor `defaults` on models, but may also be useful for your own classes.\n\nThis option is automatically inherited by all descendant classes.\n\nFor example:\n\n```typescript\n@spina({\n    automergeAttrs: ['itemSerializers'],\n});\nclass BaseSerializer extends BaseModel {\n    static itemSerializers = {\n        'string': StringSerializer,\n        'int': IntSerializer,\n    };\n}\n\n\n@spina\nclass MySerializer extends BaseSerializer {\n    // This will automatically contain BaseSerializer.itemSerializer entries.\n    static itemSerializers = {\n        'date': DateSerializer,\n    };\n}\n```\n\n\n### mixins\n\nThis option makes it easy to mix in plain objects, prototypes, or ES6 classes\ninto your class.\n\nFor example:\n\n```typescript\n@spina({\n    mixins: [\n        // A class mixin.\n        class {\n            static mixedInAttr1 = 'attr1';\n            mixedInFunc1() {\n                return true;\n            }\n        },\n\n        // A prototype mixin.\n        Backbone.Model.extend({\n            mixedInAttr2: 'attr2',\n            mixedInFunc2: function() {\n                return 'test';\n            },\n        }),\n\n        // A simple object mixin.\n        {\n            mixedInAttr3: 'attr3',\n            mixedInFunc3() {\n                return 123;\n            }\n        },\n    ]\n})\nclass MyClass extends BaseModel {\n    ...\n}\n```\n\nThis would be roughly equivalent to:\n\n```typescript\n@spina\nclass MyClass extends BaseModel {\n    static mixedInAttr1 = 'attr1';\n\n    mixedInFunc1() {\n        return true;\n    }\n\n    mixedInFunc2() {\n        return 'test';\n    }\n\n    mixedInFunc3() {\n        return 123;\n    }\n}\nMyClass.prototype.mixedInAttr2 = 'attr2';\nMyClass.prototype.mixedInAttr3 = 'attr3';\n```\n\n\n### name\n\nIf you're dynamically creating classes, or have some special requirements, you\ncan use `name` to set the resulting name of your class. For example:\n\n```typescript\nconst MyClass = spina({\n    name: 'MyName',\n}, class extends BaseModel {\n    ...\n});\n```\n\n\n### prototypeAttrs\n\nES6 classes don't have a way of defining attributes on the prototype. You can\nonly define instance variables or static variables.\n\nSpina addresses this by letting you define static variables and promoting them\nto the prototype. This allows them to be accessed using `this`.\n\nStatic methods can also be listed, and will work with `this`.\n\nThis option is automatically inherited by all descendant classes.\n\nFor example:\n\n```typescript\n@spina({\n    prototypeAttrs: ['registrationID', 'category'],\n})\nclass RegisteredModel extends BaseModel {\n    static registrationID = null;\n    static category = null;\n    static options = {};\n\n    initialize() {\n        someRegistry.registerInstance({\n            id: this.registrationID,\n            category: this.category,\n            options: _.result(this, 'options'),\n        });\n    }\n}\n\n@spina\nclass MyEntry extends RegisteredModel {\n    static registrationID = 'my-id';\n    static category = 'my-category';\n    static options() {\n        return generateCommonOptions();\n    }\n}\n```\n\n\n### skipParentAutomergeAttrs\n\n`automergeAttrs` is a useful option, but sometimes you want to avoid merging\nin some attributes.\n\n`skipParentAutomergeAttrs` can be set to a list of attribute names (previously\ndefined in `automergeAttrs`) to skip. Or it can be set to `true` to skip all\nattributes.\n\nFor example:\n\n```typescript\n@spina({\n    automergeAttrs: ['itemSerializers'],\n});\nclass BaseSerializer extends BaseModel {\n    static itemSerializers = {\n        'string': StringSerializer,\n        'int': IntSerializer,\n    };\n}\n\n\n@spina({\n    skipParentAutomergeAttrs: ['itemSerializers'],\n})\nclass MySerializer extends BaseSerializer {\n    // This will only contain a 'date' key.\n    static itemSerializers = {\n        'date': DateSerializer,\n    };\n}\n```\n\n\n## Deep Diving into the Backbone Problems\n\n### The ES6 Class Initialization Problem\n\nThere are trade-offs when using ES6 classes with Backbone classes. The\ntop-level Backbone classes (like `Backbone.Model`) want to help by controlling\ninitialization of your subclass for you, calling methods like `initialize()`\nand getting data from attributes like `Model.defaults`.\n\nBut they can't do this when using ES6 classes.\n\nWhen constructing an object using ES6 classes, your subclass's instance doesn't\nreally exist until the parent constructor finishes. This means that when\nconstruction gets to the Backbone object, there's no way for it to look up any\nattributes on your subclass.\n\nTo work around this, you have to implement every attribute as a method, which\nis fine in ES6 class land. But that comes with its own trade-offs. Not to\nmention, those functions still can't access attributes.\n\nSo, by no real fault of Backbone's, it's a mess to use ES6 classes with\nBackbone objects. And we weren't satisfied by the workarounds. So we solved\nit... with new workarounds.\n\n\n### TypeScript + Backbone\n\nSome wonderful volunteers have worked hard on adding TypeScript support for\nBackbone. This is available in the\n[@types/backbone](https://www.npmjs.com/package/@types/backbone) package.\n\nThose types try to enforce the method-only approach to attributes when using\nES6 classes. We've solved that in Spina, meaning those workarounds were no\nlonger needed.\n\nSpina bundles a fork of the Backbone types that restore attribute access,\nand additional support such as custom view option types.\n\nThis support must be explicitly enabled, and is recommended if you're using\nSpina with TypeScript.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeanbaginc%2Fspina","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeanbaginc%2Fspina","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeanbaginc%2Fspina/lists"}