This is a brain dump of my experience trying to get something going with Ember.js. My goal was to get to know the ins and outs of the framework by completing a pretty well defined task that I had lots of domain knowledge about. In this case reproducing a simple Yammer feed. As of this time, I have not been able to complete that task. So this is a subjective rundown of the things I think make it difficult to get a handle on Ember. NOTE: My comments are addressing the Ember team and giving suggestions on what they could do to improve the situation.
The new guides have pretty good explanation of the various parts of the framework; routers, models, templates, views. But it's not clear how they all get strapped together to make something that works. There are snippets of examples all over the place like:
App.Router.map(function() {
match('/home').to('home');
});
But they're out of context. What is App.Router? Is it an instance of a Ember.Router
or a subclass? In trying to set up my App, I got tripped up for hours on where it was appropriate to do extend()
or create()
. I thought I had an understanding of the difference between these (though I don't think you can count on that from all your users). But I found the behavior of these in Ember thoroughly confusing. I'll come back to that later. Suffice it to say it wasn't clear to me which one the framework required at any given time.
So App needs to be an instance, Ember.Application.create()
. But then you need to set properties on that instance directly, App.ApplicationController = Ember.ObjectController.extend();
. And that needs to use extend()
. And you can't pass in an object to set the properties of the App instance.
// Doesn't work, even though it's in some of the docs
var App = Ember.Application.create({
ApplicationController: Ember.ObjectController.extend()
});
I lost a lot of time trying to figure that out.
You need to talk about how to set up an Ember Application and what is actually happening. Here are some rough things I pieced together. You can see how I did based on how wrong and incomplete they are.
- You must create an instance of an application.
- It functions as a namespace, so if you don't title case it (app vs. App), you'll get a weird warning. It won't stop you from continuing, but the warning is disconcerting and unhelpful.
- You must create Classes and assign to properties on the App instance
- You need to use
extend()
on pretty much all of these. - I believe these will be "registered" with the App automatically. If they are not placed on the App object, things don't work. It's not clear if you can manually register Classes with the App.
- Near as I can tell, Ember will look these up by naming convention and create instances of them at "the right time". It's not clear when that is or if the instances get reused.
- You need to use
- The App will initialize and auto-start after an asynchronous turn. Some docs have an explicit call to App.initialize(), that doesn't seem to be necessary anymore. Definitely unexpected. Not sure if auto-start can be disabled.
- You MUST create App.ApplicationController, App.ApplicationView, and App.Router. If you don't, you get more unhelpful errors.
Even with all of these things, I'm not sure how to get an Application that does nothing and doesn't error. It's difficult to start from a working no-op App, and then add functionality incrementally so you can learn.
The Object Model section of the guide talks about Class and Instances. Classes are created using extend()
and instances are created using create()
. This seems fine, but once you get these objects, the javascript rules we've come to expect start breaking down.
var O = Ember.Object.extend({
classProp: 'classProp'
, classMethod: function classMethod() {}
});
// good
typeof O // "function"
O.prototype.constructor === O // true
// WAT
O.toString() // "(unknown mixin)".
// Also displays this in the chrome console instead of
// letting you inspect the Object or printing the
// function body.
O.prototype.classProp // undefined
O.prototype.classMethod // undefined
var o = O.create({
prop: "prop"
, method: function method() {}
})
// this seems to behave normally
o instanceof O // true
o.prop // "prop"
typeof o.method // "function"
o.classProp // "classProp"
o.__proto__.classProp // "classProp"
// Except when you evaluate `o` in the Chrome console,
// it prints an object named "Class"
// WAT. now the constructor somehow behaves too
O.prototype.classProp // "classProp"
I can understand doing some trickery with your type system if it adds value. But you don't explain what that value is. And this weirdness is another thing that makes it hard to figure out what's going on by inspecting objects.
I have no idea how the router is supposed to work. Early on I was using the old router api and I understand you guys aren't proud of that one. But I didn't find any good tutorials on the api. Even others who are publishing working examples say in their docs that they don't really understand the magic encantation for the router. They just put it in and things worked.
It seems intuitive that connectOutlets
allows you to fill the places that say {outlet}
in your template. But the api for this method is thoroughly confusing. I kept finding slightly different examples around the web. Probably due to evolving apis. But some questions that still stick out:
- What is the object context in this method? What does
this
refer to? - The first argument seems to be
router
. Is that an instance of router or the same as App.Router? Is it the same instance for the whole app? - Are there other arguments? If so, when are they passed and what's their significance?
router.get('applicationController')
. How exactly do I know I can do this? What's the convention for what you canget
out of the router?- applicationController.connectOutlet. Again, what's the method signature here? It seems to take the name of a template (or is it the name of a View?). The other argument seems to be a model or a controller? Still confused about what's happening there.
After a while of things kind of working, but being frustrated, I was fine discarding that for the new API. So I'm currently running master so I can use the new Router. I've read and watched videos about it. But it just spits errors for me. And there doesn't seem to be enough documentation on it yet to figure out what's going on. Would love to drop back to the old router which at least let me continue without really understanding. But that doesn't seem to be an option. I'm all about progress, but it feels like the api is currently in a gap of uncertainty between old Router and new.
I've been intrigued by ember data since first learning about it. It's an attempt to create an abstract data layer that can represent models and relationships on the client. Essentially allowing to create a sensible data domain for client-side apps. We have a custom solution for this at Yammer, and it was really interesting drawing parallels. That said, I had a really hard time doing that and never actually got things working.
My exploratory project was a little app to display a simplified Yammer feed using test yammer data. We have custom api urls that don't follow rails conventions, and a custom data payload format. I was interested in attempting a really simple custom adapter/serializer combo that could load our data. I read lots about adapters and serializers and looked at your youtube videos explaining them. I even dove into the code and read the extensive comments there. But when it comes down to it, the architecture is just too opaque. There are so many open questions to answer in order to go from a call like app.Feed.find()
, to an ajax call with the correct url and data, to a valid Model that will load data successfully. Here are a few:
When filling in YamAdapter#find, what's the significance of this stuff that I pulled from the built-in adapters?
Ember.run(this, function(){
this.didFindRecord(store, type, json);
});
I know about the Ember run loop, and I'm assuming didFindRecord
passes things off to the serializer among other things. But there's a lot to put together here.
The adapter methods don't return anything. I vaguely understand that they get called via the Store. But when you consider that the Model methods return those promise-like records, it's very unclear how these adapter methods connect back to those.
The adapter uses methods like rootForType()
and buildURL()
to figure out what ajax calls to make. I found that sometimes I didn't have enough context to build the url I needed. The only data that gets passed in for find()
is a type and an id. If you need to put more parameters onto the url, the adapter has to get those from somewhere. I ended up adding a 4th data
argument, but that means I'm firmly outside of the realm of compatibility.
Once I made it into the serializer, things got even more hairy. I read a lot about extracting vs. materialization. But I couldn't figure out the right combination of things to create my models and wire up the relationships properly. It looks like the core of extraction is the method loadValue
which creates a Model instance in the store. I got that far, but then I don't really know how to hook up relationshiops. I see things like "prematerialized" and I'm not sure how that factors in. My promise-models never loaded and I got bogged down in the weeds.
I tried to take a step back. Because of my background with the Yammer paradigm, I understand what's supposed to be happening at a high level. I want to end up with a set of models created from the data payload and have their relationships hooked up. Nevermind the events that need to be fired for now. I tried to figure out how to just process the payload manually because I know exactly what's in it. But I don't know the right methods to do so. I'm thinking of something like the following:
var messages = json.messages.map(function(msg) {
var message = App.Message.createRecord(msg)
, thread
, user;
// look up references
_.each(json.references, function(ref) {
switch(ref.type) {
case 'thread':
if(ref.id == message.thread_id) {
thread = ref;
} else { return; }
break;
case 'user':
if(ref.id == message.sender_id) {
user = ref;
} else { return; }
break;
}
});
if(thread) {
// set the thread relationship on the message
thread = App.Thread.createRecord(thread);
message.setRelationship('thread', thread);
console.log(message.get('thread'));
}
if(user) {
// set the user relationship on the message
user = App.Thread.createRecord(user);
message.setRelationship('sender', user);
console.log(message.get('sender'));
}
});
All this is just to do a simple find()
. There were a few other things I wanted to get going. I still don't exactly get how sinceQuery
or extractMeta
fit in. I think I get it conceptually, but I don't think it's flexible enough to fit my needs. There's a gap between looking at the RESTadapter/RESTSerializer, which are geared towards rails conventions, and looking at the base adapter/serializer which are pretty low level and don't give you much help in filling in gaps.
I still think ember data is an impressive undertaking. In it's current state, it's gonna be pretty difficult to take advantage of if you have non-trivial api requirements.
Where are the tests? I know they exists. There's a tests folder, but there aren't any readable tests there. Things are obfuscated, probably for the test harness. But this is a mistake. When trying to learn a system, tests are a great resource to see examples of usage and get a better understanding of components. But that avenue isn't available here. It also hampers contributions from folks who want to help improve things.
Also I can't run the tests without phantomjs? That's no good. Ember is for the browser first and foremost right? IMO, tests for browser javascript should always be available by just hitting a url. Chrome dev tools is the preferred tool for debugging. You can add options for running in a headless browser or node. But browser should be the default.
I know there are no good standards here yet. But I think you get a lot of mileage out of these suggestions.
My foray into Ember land was extremely enlightening. Make no mistake, I could write a document this size about the stuff I like as well. But the barriers to entry are really significant unless you have a rails background. It's okay to lean on familiarity with rails if that's the audience you want. But if you want to capture more of the larger js community, you've gotta pull back the covers a bit more. Or at least you need to give us a thorough introduction to Convention over Configuration in the context of Ember, because not all of us are going to learn rails just to learn Ember.
I hope this has been helpful, and I'd be happy to talk to you in more depth about any of it. I should be clear that this exercise was for my own education, and I was not evaluating Ember for use at Yammer. Yammer's just a great example for getting a non-trivial overview of what frameworks can do.
Hey Marco,
Thanks for the feedback. This kind of stuff is literally the most valuable thing you can provide to library authors; if you're struggling with an open source project, things like this are a fantastic way to help make it easier to use. So, thank you.
The first thing I'd like to say is that I think a lot of the problems you hit were due to coming into the framework at a bit of a transition period. We were in the midst of making many changes that addressed most of the on-ramp problems that you had. Unfortunately, we needed to do a better job of communicating that on the website. If you had started with a copy of Ember built from
master
, I think a lot of your frustration would have been eliminated. We'll be doing apre3
release shortly that incorporates the fixes to many of the problems that you hit.The TL;DR here is that
App
is a single instance, so youcreate()
it. When in doubt, everything else should be a class. Views, controllers, routes, etc. should all beextend()
ed. Ember.js is responsible for creating the instances for you and wiring them up, which makes it much easier to test pieces of your application in isolation. (Some people refer to this as dependency injection, but we find that that has too much Java flavor. :P)You do need to set them as properties of the
Ember.Application
instance, yep. We are working on a coherent story for people that want to use modules. I am a fan of using modules to load dependencies, but treating each file in your application as a separate module introduces more complexity than it's worth, IMO. Still, we know people want to do this and we're working on it.The current system uses a "container" under the hood that can be hooked into by any kind of loader. We even wrote it as a microlibrary: https://github.com/emberjs/ember.js/blob/master/packages/container/lib/main.js
Inside the application, we lookup classes and instances via this container API (https://github.com/emberjs/ember.js/blob/master/packages/ember-application/lib/system/application.js#L524). Currently this just roundtrips back to the
Application
instance, but in the future, you can imagine this plugging into an AMD loader. Like I said, this is still a work in progress, but the pieces are there, and people like Ryan Florence and Tim Branyen have been extremely helpful with assisting us in thinking through this problem.I'm not sure what your objection to auto-starting the application is, but it's definitely the behavior that most people want. If you want to defer the application starting, just call
App.deferReadiness()
. When you want to release your "lock" on the application starting, callApp.advanceReadiness()
.Again, this is fixed on
master
, and will be fixed in the forthcomingpre3
release.We document all of the value of the Ember object model at http://emberjs.com/guides/object-model/classes-and-instances/. Classes, instances, mixins, observers, computed properties, bindings, etc. I'm not sure what we could do to explain the value other than to make the documentation more "sales-y." Perhaps we need to do that for people who are primarily JavaScript developers, but people coming from other dynamic languages see the value immediately.
You're expecting to be able to use introspection tools from JavaScript with Ember.js objects, which obviously won't work. Ember objects are composed using those JavaScript primitives. We would be handicapping ourselves pretty severely if all we could to improve the development experience was do things that maintained JavaScript semantics precisely.
Ironically, the main introspection tool that you attempted to use (inspecting the prototype) does work, but for performance reasons, we wait until the first instance of a class is created to flatten the prototype. If you had done this after creating an instance, it would have worked as expected.
In terms of debugging, we provide lots of introspection tools into the object model. We should document those better, and we're always open to suggestions for improvement.
The old router is dead. Conceptually there are similarities between the old router and new, but the API is completely different. I'm surprised you had trouble with the new router. The documentation on the website's routing section is up-to-date, and people have been reporting tremendous success compared to the old router. I'd be interested in knowing which errors you hit. We already fixed many of the exceptions that people were hitting, so we should figure out if you were hitting a different error, or if this has been fixed already.
The more conventional your API, the better. If you have an extremely non-standard API, it may be easier to just write imperative code, rather than trying to override the hooks built-in to the default adapter. But, we did design the adapter API to be layered, so if it's easier to write imperative code, just override the top-level hooks (
serialize
,extract
,materialize
) and do what you need to do. Admittedly, we need to document this better, and explain precisely what is going on under the hood so that adapter authors have the right mental model. I hope that my explanation via IM the other night helped at least nudge you in the right direction.Ultimately, we expect the majority of Ember.js developers to not be adapter authors. Most people will pick a backend, stick to those conventions, and find an adapter that works with those conventions.
Not sure what you mean by this. Ember.js is architected as a series of "microlibraries" that are composed together at build time. Each package has its own set of tests. I don't believe they are obfuscated in any way. For example, all of the routing tests are here: https://github.com/emberjs/ember.js/tree/master/packages/ember-routing/tests
You can definitely run the tests without phantomjs. Just run
bin/rackup
in the Ember directory, then visithttp://localhost:9292/tests/index.html?package=all
.You make a very good point. We definitely assume a worldview where our target developers accept that convention-over-configuration is good. We've tried to take the DHH ethos and recontextualize it in JavaScript. That's not really the current culture in JS (unfortunately, IMO).
I think focus in a project is important. For now, we are okay with focusing on meeting the needs of developers who already believe in convention-over-configuration. Once we've saturated that market, perhaps we can turn to convincing the rest of the JS community that some of these ideas are good ones. :)
Thanks again for your feedback. As I said at the beginning, stuff like this is absolutely invaluable for library developers.