Skip to content

Backbone and ES6 Classes #3560

@benmccormick

Description

@benmccormick
Contributor

With the final changes to the ES6 class spec (details here), it's no longer possible to use ES6 classes with Backbone without making significant compromises in terms of syntax. I've written a full description of the situation here (make sure to click through to the comments at the bottom for an additional mitigating option), but essentially there is no way to add properties to an instance of a subclass prior to the subclasses parents constructor being run.

So this:

class DocumentRow extends Backbone.View {

    constructor() {
        this.tagName =  "li";
        this.className = "document-row";
        this.events = {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
        super();
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

is no longer valid in the final ES6 spec. Instead you effectively have 3 (not very appealing) options if you want to try to make this work:

Attach all properties as functions

Backbone allows this, but it feels dumb to write something like this:

class DocumentRow extends Backbone.View {

    tagName() { return "li"; }

    className() { return "document-row";}

    events() {
        return {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

compared to the current extends syntax

Run the constructor twice

I don't view this as a real option due to the issues it would cause running initialize a second time with different cids, etc.

Pass all properties as default options to the superclass constructor

This was suggested by a commenter on my blog and is probably the most practical current option. It looks something like this:

class MyView extends Backbone.View {
  constructor(options) {
    _.defaults(options, {
      // These options are assigned to the instance by Backbone
      tagName: 'li',
      className: 'document-row',
      events: {
        "click .icon": "open",
        "click .button.edit": "openEditDialog",
        "click .button.delete": "destroy"
      },
      // This option I'll have to assign to the instance myself
      foo: 'bar'
    });


    super(options);


    this.foo = options.foo;
  }
}

Since all of these current options involve clear compromises relative to the current Backbone extends syntax, it would be wonderful if a better solution could be developed. I'm not totally sure what this should look like, but one idea that came to mind while I did the writeup for my blog was the addition of a "properties" function that would output a hash of properties. The constructor could then run that function and add them to the instance prior to the other processing done by the constructor.

Activity

akre54

akre54 commented on Apr 7, 2015

@akre54
Collaborator

Yeah this is definitely a bummer. Thanks for doing the legwork.

I guess moral of the story is don't use ES6 classes with Backbone, at least until static property support lands. Of the fallback options you proposed my preferred solution is defining the strings / objects as return values. A key part of Backbone's API design is in these prototype-shared strings and objects, and it would dirty up the API to require devs to assign each property to the instance in the constructor (not to mention being memory wasteful).

Aside from consistency, is there any reason to use the class keyword with Backbone over extend?

jridgewell

jridgewell commented on Apr 7, 2015

@jridgewell
Collaborator

Great blog post. I'd been wonder how ES6 and Backbone classes would play together. As for you solutions:

  1. Attach all properties as functions: I'm not super opposed to this. It's not as clean as setting the object directly on the prototype, but I've seen a ton of code trip up on mutating prototype objects. This way is immune, which is why I think ES6 chose not to include class properties.
  2. Pass all properties as default options: Isn't this how you'd do something in a more classical language? I feel like this is a even less clean solution than the above.
  3. Run the constructor twice: Ick.

I guess moral of the story is don't use ES6 classes with Backbone, at least until static property support lands.

Even class properties come after the super() call. 😞

benmccormick

benmccormick commented on Apr 7, 2015

@benmccormick
ContributorAuthor

Aside from consistency, is there any reason to use the class keyword with Backbone over extend?

I addressed this in the blog post. Practically? No. In theory it would allow Backbone in the long term to reduce code and additional concepts, but realistically its going to be at least a few years before ES6 classes are widely supported on all relevant browsers without transpiling, and the code reduction would be next to nothing.

But don't underrate the consistency aspect. If this becomes "the way" of doing Object Oriented programming in JavaScript (seems likely given the standardization on this from Ember/Angular/React/Typescript/Aurelia etc), Backbone not using it will be an added learning curve for the library relative to other options. Especially for Junior developers. I'm not sure that necessarily merits a change. But it's not just for pedantic "hobgoblin of small minds" consistency.

lukeasrodgers

lukeasrodgers commented on Apr 8, 2015

@lukeasrodgers
Contributor

I agree with @akre54 and @jridgewell that the "attach all properties as functions" approach is probably the best of the proposed options. FWIW, I remember that when I was originally learning backbone as a relative js newcomer, I was a bit confused by these "static" properties and how they should be used.

A

A commented on Apr 10, 2015

@A

ES7 will have correct class properties, I guess https://gist.github.com/jeffmo/054df782c05639da2adb

benmccormick

benmccormick commented on Apr 10, 2015

@benmccormick
ContributorAuthor

The ES7 proposal is just that, a very early community driven proposal. Not at all clear it will actually ever be part of an official spec. Current implementations cause properties to be added to the instance AFTER the constructor runs, so it doesn't help with Backbone. (see jridgewell's link above or try it yourself with Babel 5.0.0)

akre54

akre54 commented on Apr 10, 2015

@akre54
Collaborator

@jridgewell I was referring to this part of @benmccormick's post:

React Developers have noted the same issues with property initializers that Backbone users encounter. As part of version 0.13 of React, they're supporting a special property initialization syntax for classes, which may eventually be standardized. There's more info on that in this ESDiscuss thread. This standard is still being worked out but an experimental support version is available in Babel 5.0.0. Unfortunately that version defines class properties as being instantiated after the superclass constructor is run, so this doesn't solve Backbone's issues here.

See for example wycats' js-decorators strawman or the original (superseded) harmony classes proposal.

I might suggest that we use getters with class properties:

class Row extends Backbone.View {
  get tagName() { return 'li'; }
}

As an absolute last resort, we could check for instance or static props with a helper a la _.result:

_.instOrStaticVar = function(instance, property) {
  if (instance == null) return void 0;
  var value = instance[property] || instance.constructor[property];
  return _.isFunction(value) ? value.call(instance) : value;
}
jridgewell

jridgewell commented on Apr 10, 2015

@jridgewell
Collaborator

Yup, but:

Unfortunately that version defines class properties as being instantiated after the superclass constructor is run, so this doesn't solve Backbone's issues here.

So, ES5'd:

// ES6
class View extends Backbone.View {
  tagName = 'li';

  constructor() {
    // Do anything that doesn't touch `this`
    super();
    // Do anything that touches `this`
  }
}

// ES5
function View() {
  // Do anything that doesn't touch `this`
  Backbone.View.apply(this, arguments);

  // Add class properties
  this.tagName = 'li';

  // Do anything that touches `this`
}
View.prototype = _.create(Backbone.View.prototype, {
  constructor: View
});

Our element would still be constructed before we got a change to set the instance variable.

See for example wycats' js-decorators strawman...

Can you explain how the decorators would apply?

I might suggest that we use getters with class properties:

👍. I see that as the same boat as attach all properties as functions. Not as clean as what we currently have, but perfectly acceptable and mutation proof.

As an absolute last resort, we could check for instance or static props with a helper a la _.result:

That could be interesting...

jamiebuilds

jamiebuilds commented on May 4, 2015

@jamiebuilds

You could do:

class MyView extends Backbone.View {
  constructor() {
    super({ tagName: 'h1' });
    this.el.textContent = 'Hello World';
  }
}
jridgewell

jridgewell commented on May 4, 2015

@jridgewell
Collaborator

@thejameskyle That's the Pass all properties as default options to the superclass constructor option. 😛

milesj

milesj commented on May 4, 2015

@milesj

Instead of relying on super() to setup the class, you could simply have an init() function or something.

class DocumentRow extends Backbone.View {

    constructor() {
        super();
        this.tagName =  "li";
        this.className = "document-row";
        this.events = {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
        this.init();
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}
benmccormick

benmccormick commented on May 4, 2015

@benmccormick
ContributorAuthor

@milesj hmm? That will error out immediately with the final ES6 class spec

In a derived class, you must call super() before you can use this

Even if it did work you're never actually calling the Backbone constructor and will not get its initialization code.

See this link from my first post: http://www.2ality.com/2015/02/es6-classes-final.html

jridgewell

jridgewell commented on May 4, 2015

@jridgewell
Collaborator

@milesj: The thing is, you have to call super() before setting this.tagName or the like. And, since we ensure an element in the View's constructor, we've already created an element before we'll ever set this.tagName.

55 remaining items

alexsasharegan

alexsasharegan commented on Mar 10, 2018

@alexsasharegan

The story here is still very unclear for refreshing legacy Backbone applications to utilize modern tooling and language features. It's especially disappointing to see things like Symbol.iterator implemented and not available in a production release.

For those still looking for clearer answers to this question, I'm adding TypeScript to a backbone app and found the solution from this comment most helpful.

So far it's working nice enough, with the drawback of having to explicitly annotate properties passed through the decorator rather than having nicer inference.

export function Props<T extends Function>(props: { [x:string]: any }) {
  return function decorator(ctor: T) {
    Object.assign(ctor.prototype, props);
  };
}

@Props({
  routes: {
    home: "home",
    about: "about",
    dashboard: "dashboard",
    blog: "blog",
    products: "products",
    accountSettings: "accountSettings",
    signOut: "signOut",
  },
})
export class Router extends Backbone.Router {
  home() {}
  about() {}
  // ...
}

@Props({
  model: CategoryModel,
  comparator: (item: CategoryModel) => item.display_value,
})
export class CategoryCollection extends Backbone.Collection<CategoryModel> {}

Example of explicity property annotation:

image

kamsci

kamsci commented on Jun 14, 2018

@kamsci

@raffomania, @jridgewell & Co., for what it's worth, my team got around this problem by adding idAttribute to the prototype outside of the class.

class Example extends ParentExample {
// Class methods etc here
}

x.Example = Example;

x.Example.prototype.idAttribute = 'customIdAttr';

blikblum

blikblum commented on Jun 14, 2018

@blikblum

@kamsci i did the same in this branch where i converted Backbone to ES6 classes

bptremblay

bptremblay commented on Sep 13, 2018

@bptremblay

Backbone uses configuration to the point of the config objects being declarative. This is nice but it's never going to to play nice with inheritence. (Clone the class, then configure it. That's not inheritence.)

If we're going to write new code using backbone, It's okay to to think differently. Cutting and pasting ES5 code and then making it look like ES6 doesn't work. So what?

I don't have any problem at all passing in everything through a config object. How we expose the contents of that config, or make it easier to read/work with, is a problem to solve, not to cry about.

Nobody want to run a constructor twice. That's silly. But, the pattern of

Foo = BackboneThing.extend({LONG DECLARATIVE OBJECT LITERAL}) is mother-loving ugly, too. You all have just been doing it so long you don't see how ugly it is.

maparent

maparent commented on Mar 18, 2019

@maparent

FYI: I have a large Marionette project, and wanted to use ES6 syntax. I created a jscodeshift transformer that translates Backbone extends declarations into ES6 classes. It makes many simplifying assumptions, but may still be useful for some of you, if only as a starting point. It follows the syntax proposed by @t-beckmann as I ran into issues with decorators.
https://gist.github.com/maparent/83dfd65a37aaaabc4072b30b67d5a05d

oliverfoster

oliverfoster commented on Mar 6, 2020

@oliverfoster

To me there seems a weird misnomer in this thread. 'static properties' to ES6 are properties on the constructor which exist on the Class without instantiation (Class.extend for example). In this thread 'static properties' seems to refer to named attributes on the prototype with a 'static' value (not getters or functions). Have I got that right?

For prototype properties with a static value, declaring the Backbone pre initialise values as function return values is quite a straightforward transition and works well as _.result performs as expected for defaults, className, id etc. Other instance properties seem to be fine declared at the top of the initialise function as normal. This problem seems only to arise as in ES6 classes you can't define prototype properties with a static value at present, only getters, setters and functions.

Either way, constructor/class static properties (Class.extend) aren't inherited in backbone as they are in ES6. Backbone copies class static properties to the new class/constructor each time when performing the extend function rather than having these properties inherit as ES6 does. I have made a pr to fix that here #4235

I would appreciate some comments / feedback, I'm not sure if it'll break anything, I've tested it out quite a bit and it seems to work well. Backbone classes inherit Class.extend afterwards rather than copying a reference to each new constructor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @jashkenas@jridgewell@milesj@gautamborad@tbranyen

        Issue actions

          Backbone and ES6 Classes · Issue #3560 · jashkenas/backbone