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.
Here are a bunch of references I've been using: