jashkenas / backbone

Give your JS App some Backbone with Models, Views, Collections, and Events
http://backbonejs.org
MIT License
28.1k stars 5.38k forks source link

Backbone and ES6 Classes #3560

Open benmccormick opened 9 years ago

benmccormick commented 9 years ago

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.

t-beckmann commented 8 years ago

Reading through this I find https://github.com/epicmiller/es2015-default-class-properties a good approach. When trying I realized Backbone having build-in support for this. For example:

class MyModel extends Backbone.Model.extend({
   idAttribute: 'id'
}) {
   // ...
};

The above code will set the MyModel.prototype.idAttribute properly. Notice, for TypeScript the declaration file needs to be adjusted slightly to return a constructor function interface, but that's a detail irrelevant to ES6 users...

ttaranov commented 8 years ago

@t-beckmann that's a quite nice solution - looks readable and requires minimal changes. Thanks!

joshlasdin commented 7 years ago

I realize this thread is going on 2 years now, but it's still one of the top (and only) results when searching for Backbone & ES6 Classes, and I thought I'd share a potential solution making use of class properties mentioned several times here.

Now that class properties are in Stage 2 and widely available with the babel preset, I thought I'd give it another look. As stated, the issue with instance/member properties is that they don't get applied to the prototype until after constructor(), but many of the properties needing to be set are used within the constructor. Static properties are applied immediately, but (by design) are not copied to instances of the class.

The following shim copies static properties from the constructor onto the instance before running the constructor (effectively creating a new constructor, applying the properties, and then executing the original constructor). While it's definitely a hack, I'm pretty pleased with the result:

The shim:

export default function StaticShim(Ctor) {
    const NewCtor = function shim(...args) {
       Object.keys(Ctor).forEach((key) => {
            if (this[key] === undefined) {
                this[key] = toApply[key];
            }
        });

        Object.assign(this, this.constructor);

        Ctor.apply(this, args);
    };

    NewCtor.prototype = Object.create(Ctor.prototype);
    NewCtor.prototype.constructor = NewCtor;

    Object.keys(Ctor).forEach((key) => {
        if (NewCtor[key] === undefined) {
            NewCtor[key] = Ctor[key];
        }
    });

    return NewCtor;
}

And then in usage:

class TestModel extends StaticShim(Backbone.Model) {
    static idAttribute = '_id';
    static urlRoot = '/posts';

    initialize() {
        console.log(this.url()); // Correctly logs "/posts/{id}"
    }
}

Just wanted to drop it here in case it helps anyone else, or anyone has any thoughts about it. Thanks!

enzious commented 7 years ago

Obligatory sorry for reviving an old issue.

Would it be possible or worth it to write a babel plugin that transforms an ES6 class declaration to use Backbone.*.extend({...})?

benmccormick commented 7 years ago

@enzious definitely seems possible. Whether it is worth it is up to you :)

dreamalligator commented 7 years ago

@t-beckmann's solution seems the most straightforward. should we integrate that into backbone itself?

ianwijma commented 6 years ago

For me it looks not properly, wouldn't it be more proper to have a method that sets the idAttribute?

Additionally, it would be amazing if there are Promise support. which is a more native approach than using jquery Deferred, which I personally would love to see deprecated within Backbone.

alexsasharegan commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

bptremblay commented 6 years ago

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 commented 5 years ago

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 commented 4 years ago

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 https://github.com/jashkenas/backbone/pull/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.