Add Spina, a modern TypeScript/ES6-supported specialization of Backbone.

Review Request #12788 — Created Jan. 19, 2023 and submitted

Information

Spina
master

Reviewers

We make heavy use of Backbone.js, but Backbone has little support for
modern tooling for JavaScript, such as TypeScript or ES6 classes. The
former is available through a community-driven typing package, and the
latter sort of works but requires some tradeoffs in how classes are
structured.

Those tradeoffs are due to a conflict in the way that Backbone
initializes classes and the way ES6 classes are initialized. Backbone's
base objects (Model, View, etc.) will call preinitialize(),
initialize(), and other methods on the class being constructed, which
is pretty reasonable. However, ES6 class construction is not reasonable.
Each class in the hierarchy must call the parent class's constructor
first, and until that finishes, the class does not have a prototype
(meaning this does not work). That prevents parent classes from
calling or accessing properties on a derived class.

We've addressed much of this, and are introducing the result as a new
library called Spina.

Spina provides some tooling for delayed class initialization, through
the use of an intermediary class wrapper that's meant to sit in front of
a base class like Backbone.Model and through decorators that decorate
each chain in a hierarchy. These help control the initialization order,
ensuring classes are set up correctly.

We have Spina versions of most of the Backbone classes:

  • Spina.BaseCollection
  • Spina.BaseModel
  • Spina.BaseRouter
  • Spina.BaseView
  • Spina.Collection
  • Spina.Router

Unlike Backbone's versions, our base classes are truly base classes, and
cannot be instantiated directly. Subclasses are required.

Every subclass in a hierarchy must be decorated with the @spina
decorator. This sets up front-end wrapper class that triggers the proper
initialization order and then passes an instance of the actual class.
For most classes (those inheriting directly from Spina.BaseModel, for
example), there's minimal waste. For deeper hierarchies, there will be
small constructor-only classes that end up in the prototype chain.

This also bundles a fork of @types/backbone along with it. This
customizes types in the following ways:

  • Avoids some of the hard-coded method-only types found in places like
    Backbone.Model.defaults. While that may have been generally
    necessary for Backbone, it was ultimately a hack and it's not
    something we'll be needing to deal with as we move to Spina.

  • Adds support for interfaces to customize options passed to views.

Our version of the types are opt-in, requiring the following in
tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "Backbone": [
        "node_modules/@beanbag/spina/lib/@types/backbone"
      ]
    }
  }
}

Spina does not embed Backbone, so consumers will need to ensure that's
loaded before loading Spina.

And finally, there are improvements to views:

  • New show(), hide(), and renderInto() methods simplify some
    common patterns in our codebase.

  • A modelEvents attribute makes it easy to connect model events to the
    view on the first render.

  • A new onRender() method replaces render() without the
    return this requirement, simplifying code.

  • A new onInitialRender() method allows for logic to execute only on
    the first render.

The render behavior was discussed and debated separately, but I've
ultimately added it because we do use this pattern a fair amount (and
has been a pet peeve of mine for years), and the state logic is required
anyway in order to support model events.

Successfully built this and ran through a series of tests in Review Board,
involving new base classes and various levels of subclasses.

Verified that initialization orders and resulting prototype states were
always correct.

Verified that the types for the various attributes formerly typed as methods
can now be methods or values.

This is NOT 100% tested at this point. There's a lot of real-world
testing required still.

Summary ID
Add Spina, a modern TypeScript/ES6-supported specialization of Backbone.
We make heavy use of Backbone.js, but Backbone has little support for modern tooling for JavaScript, such as TypeScript or ES6 classes. The former is available through a community-driven typing package, and the latter sort of works but requires some tradeoffs in how classes are structured. Those tradeoffs are due to a conflict in the way that Backbone initializes classes and the way ES6 classes are initialized. Backbone's base objects (`Model`, `View`, etc.) will call `preinitialize()`, `initialize()`, and other methods on the class being constructed, which is pretty reasonable. However, ES6 class construction is not reasonable. Each class in the hierarchy must call the parent class's constructor first, and until that finishes, the class does not have a prototype (meaning `this` does not work). That prevents parent classes from calling or accessing properties on a derived class. We've addressed much of this, and are introducing the result as a new library called Spina. Spina provides some tooling for delayed class initialization, through the use of an intermediary class wrapper that's meant to sit in front of a base class like `Backbone.Model` and through decorators that decorate each chain in a hierarchy. These help control the initialization order, ensuring classes are set up correctly. We have Spina versions of most of the Backbone classes: * `Spina.BaseCollection` * `Spina.BaseModel` * `Spina.BaseRouter` * `Spina.BaseView` * `Spina.Collection` * `Spina.Router` Unlike Backbone's versions, our base classes are truly base classes, and cannot be instantiated directly. Subclasses are required. Every subclass in a hierarchy must be decorated with the `@spina` decorator. This sets up front-end wrapper class that triggers the proper initialization order and then passes an instance of the actual class. For most classes (those inheriting directly from `Spina.BaseModel`, for example), there's minimal waste. For deeper hierarchies, there will be small constructor-only classes that end up in the prototype chain. This also bundles a fork of `@types/backbone` along with it. This customizes types in the following ways: * Avoids some of the hard-coded method-only types found in places like `Backbone.Model.defaults`. While that may have been generally necessary for Backbone, it was ultimately a hack and it's not something we'll be needing to deal with as we move to Spina. * Adds support for `interface`s to customize options passed to views. Our version of the types are opt-in, requiring the following in `tsconfig.json`: ```json { "compilerOptions": { "paths": { "Backbone": [ "node_modules/@beanbag/spina/lib/@types/backbone" ] } } } ``` Spina does not embed Backbone, so consumers will need to ensure that's loaded before loading Spina. And finally, there are improvements to views: * New `show()`, `hide()`, and `renderInto()` methods simplify some common patterns in our codebase. * A `modelEvents` attribute makes it easy to connect model events to the view on the first render. * A new `onRender()` method replaces `render()` without the `return this` requirement, simplifying code. * A new `onInitialRender()` method allows for logic to execute only on the first render. The render behavior was discussed and debated separately, but I've ultimately added it because we do use this pattern a fair amount (and has been a pet peeve of mine for years), and the state logic is required anyway in order to support model events.
215a31164fb738886d18ba75f4738fa9bb048887
Description From Last Updated

Typo: "Subclasss" -> "Subclasses"

maubinmaubin

Typo: "Subclasss" -> "Subclasses"

maubinmaubin
chipx86
david
  1. Ship It!
  2. 
      
maubin
  1. Awesome.

  2. src/view.ts (Diff revision 2)
     
     
    Show all issues

    Typo: "Subclasss" -> "Subclasses"

  3. src/view.ts (Diff revision 2)
     
     
    Show all issues

    Typo: "Subclasss" -> "Subclasses"

  4. 
      
chipx86
Review request changed
Status:
Completed