-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backbone and ES6 Classes #3560
Comments
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 |
Great blog post. I'd been wonder how ES6 and Backbone classes would play together. As for you solutions:
Even class properties come after the |
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. |
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. |
ES7 will have correct class properties, I guess https://gist.github.com/jeffmo/054df782c05639da2adb |
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) |
@jridgewell I was referring to this part of @benmccormick's post:
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 _.instOrStaticVar = function(instance, property) {
if (instance == null) return void 0;
var value = instance[property] || instance.constructor[property];
return _.isFunction(value) ? value.call(instance) : value;
} |
Yup, but:
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.
Can you explain how the decorators would apply?
👍. 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.
That could be interesting... |
You could do: class MyView extends Backbone.View {
constructor() {
super({ tagName: 'h1' });
this.el.textContent = 'Hello World';
}
} |
@thejameskyle That's the Pass all properties as default options to the superclass constructor option. 😛 |
Instead of relying on 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() {
//...
}
} |
@milesj hmm? That will error out immediately with the final ES6 class spec
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 |
@milesj: The thing is, you have to call |
@milesj that's still not allowed when you are subclassing. @jridgewell Oh sorry, I missed that. It does seem like the most natural option. I spoke to jeffmo and sebmck about this. To give you guys some backstory, the reasoning is because in order to support extending native types (i.e. Array) |
@jridgewell @thejameskyle Then simply call super() first (updated example). I really don't see the issue here as I've done the same thing in my ES6 classes. Just move the views constructor logic to the |
@milesj did you read the original blog post? Running super first means the properties aren't processed. See here for a full in depth explanation: http://benmccormick.org/2015/04/07/es6-classes-and-backbone-js/ |
Yes, I've read it, and I'm still curious why this is not a solution. Everyone keeps talking about the views constructor needing to be called, but that isn't necessarily the case. Why isn't something like the following not a solution (albeit a bit contrived)? var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
// extend()ing options is no longer needed if properties are set directly
};
View.prototype.setup = function() {
this._ensureElement();
this.initialize.call(this, arguments);
};
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.setup(...arguments);
}
} I'm guessing because of backwards compatibility with non-ES6? |
Then the default |
That's an issue that all ES6 classes have to deal with, not just Backbone. I personally solved it by using the Babel ES7 class properties spec. |
@milesj As stated before ES7 class properties do not solve this issue as they aren't instantiated until the end of the constructor. I spoke to jeffmo and sebmck about doing this: class Root {
rootProp = 'root';
constructor() {
console.log('Root', this.rootProp);
console.log('Root', this.derivedProp);
}
}
class Derived extends Root {
derivedProp = 'derived';
constructor() {
super();
console.log('Derived', this.rootProp);
console.log('Derived', this.derivedProp);
}
} Desugaring to: function Root() {
this.rootProp = 'root';
console.log('Root', this.rootProp);
console.log('Root', this.derivedProp);
}
function Derived() {
super();
this.derivedProp = 'derived';
console.log('Derived', this.rootProp);
console.log('Derived', this.derivedProp);
} But that still doesn't fix the issue here and leads to inconsistency: new Derived();
// >> 'Root' 'root'
// >> 'Root' undefined
// >> 'Derived' 'root'
// >> 'Derived' 'derived' |
Hm?
You're gonna have a lot of DIV elements with no |
I see. In that case, I'd suggest going with the "Pass all properties as default options to the superclass constructor" option, or the last line about creating a "properties" method (which doesn't touch the constructor). class DocumentRow extends Backbone.View {
loadProperties() {
return {
tagName: 'li',
className: 'document-row',
events: {
"click .icon": "open",
"click .button.edit": "openEditDialog",
"click .button.delete": "destroy"
},
foo: 'bar'
};
}
}
// Contrived example
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, this.loadProperties(), _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
}; I did something similar in Toolkit, which can be seen here: titon/toolkit#107 |
Hi. If I understand correctly the discussion here - the Backbone developers are discussing workarounds and best practice, but have no intention of actually making changes to the BB core to deal with this issue? (I'm not suggesting they should, nor would I have any idea what those changes could be). In other words, is the suggestion to use either all properties as functions or getters the final word on the topic? Thanks. |
@gotofritz We're discussing workarounds because ES6's solution of forcing all properties to live on instances doesn't scale. Backbone's class system is doing the right thing here. There's some discussion about adding static prototype properties to ES7 classes but so far nothing concrete. In the meantime I'd say stick with Backbone's |
Thanks. I'll try ES6 classes for a little longer... :-) For the benefit of anyone else stumbling upon this, in practice I find the "Pass all properties as default options to the superclass constructor" better - for example, our app has dynamic (localized) routes that need to be passed in at instantiation time, and having a routes() method just doesn't work. Whereas the following does
|
I've just had a look at this, and I think that both workarounds don't work for the |
Excellent catch, I'll work on a PR addressing this. In the meantime, you can use getter notation to supply custom class Model extends Backbone.Model {
get idAttribute() {
return '_id';
}
get cidPrefix() {
return '__c';
}
} |
awesome blog post @benmccormick !! going to use those decorators in my project |
@benmccormick, I whipped up with another way to declare classes with default properties, take a look: https://github.com/epicmiller/es2015-default-class-properties It runs normally in any environment that natively supports classes, transpiles well, and looks far nicer than defining them in the constructor or after the declaration. With proposals for decorators and class properties coming down the pipeline for ES2016/ES2017 this may be more of an academic exercise than the long term solution for Backbone, but something like this is definitely a viable option if 2-3 years is too long of a wait. |
Well, the thing is that Class Properties is still at stage 1 in the Ecmascript proposal stage system. I have no idea why, since it seems like a gimme in terms of "what the user gets". Of course, I have no idea what sort of things it might break under the hood both syntactically and in terms of reference implementations. https://github.com/tc39/ecma262 |
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... |
@t-beckmann that's a quite nice solution - looks readable and requires minimal changes. Thanks! |
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 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:
Just wanted to drop it here in case it helps anyone else, or anyone has any thoughts about it. Thanks! |
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({...})? |
@enzious definitely seems possible. Whether it is worth it is up to you :) |
@t-beckmann's solution seems the most straightforward. should we integrate that into backbone itself? |
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. |
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: |
@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 { x.Example = Example; x.Example.prototype.idAttribute = 'customIdAttr'; |
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. |
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. |
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. |
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:
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:
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:
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.
The text was updated successfully, but these errors were encountered: