Challenges I face(d) with Ember.js

Sarbbottam Bandyopadhyay
May 3 · 9 min read

Ember.js is one of the popular JavaScript front-end frameworks. I have been using Ember.js since the mid of 2017. However, I still get puzzled with it. ember-cli the primary command line tool for any Ember.js project has been of great help but it also created a lot of magical experiences.

It takes a lot of dedicated effort and intelligence from the community to create and maintain such a framework and tooling. Often time Ember.js is compared with magic. There is a lot of intelligence behind every magic, and Ember.js is no exception.

Magic is great, I guess we all love magic, but it is an illusion and not a reality.

As a commoner, I get overwhelmed rather perplexed in this magical land of Ember.js, quite often.

In this post, I would like to share my experience, the challenges that I face(d) with Ember.js. This post is not to criticize the effort that the community has put forward.

Thank you all the Ember.js community for your contribution. 🙏

The challenges

The challenges that I primarily face(d) are related to:

  • Ember Object Model
  • Handlebars
  • Convention over Configuration

Ember Object Model

I found and Ember Object Model extremely difficult to get my head around.

Grammar/syntax related to Ember Object Model

For example, consider the following code snippet.

filteredCollection: computed(
'someObject.someCollection.@each.{someProperty,someOtherProperty}',
function() {
const someCollection = this.get('someObject.someCollection');
let filteredCollection;
// some computation to update filteredCollection
return filteredCollection;
}
)

When I noticed 'someObject.someCollection.@each.{someProperty,someOtherProperty}' for the first time, I had no clue how to decipher it.
I also could not understand why I needed const someCollection = this.get(‘someObject.someCollection’); when I could do const someCollection = this.someObject && this.someObject.someCollection;.

Similarly, I did not understand why I neededthis.set('someObject', someObject) when I could do this.someObject = someObject.

I still get confused with these syntactic sugars, which are the fundamentals of Ember Object Model, to observe any changes in the data and update the UI accordingly. In my humble opinion, these look more like a Domain Specific Language.

As of Ember 3.1 it is possible to use this.someObject instead of this.get('someObject'), however, for setting a value, one still needs to use this.set('someObject', someObject). Refer Ember.js 3.1 release blog post for more information.

Ember object and prototype chain

The understanding of object creation in Ember ecosystem can be challenging too. All the properties passed as the object hash to the extend method, gets associated with the prototype and will be shared across every instance.

Consider the following example:

const MyObject = Ember.Object.extend({ 
actions: {
foo() {
console.log('hi');
}
}
});
const one = MyObject.create();
const two = MyObject.create();
one.hasOwnProperty('actions') // false
Object.getPrototypeOf(one).hasOwnProperty('actions') // true
two.hasOwnProperty('actions') // false
Object.getPrototypeOf(two).hasOwnProperty('actions') // true
one.actions.foo(); // hi

Updating the action.foo on any instance will impact every other instance.

one.actions.foo(); // hitwo.actions.foo = function() { console.log(‘hello’) }one.actions.foo(); // hello

Similarly, if the actions of an instance of any component is modified it will cascade to all the instances.

Consider the following example:

//some-component-test.jsconst SomeComponent = this.owner.lookup('component:some-component');SomeComponent.actions.someAction = function() { 
/**
* Now action.someAction for every SomeComponent
* will refer to the new implementation
*/
}

Now action.someAction for every SomeComponent will refer to the new implementation. Ignorance about this fundamental can create leakage in tests.

Handlebars

I find handlebars extremely challenging. There is a lot to learn before one can get productive in Ember.js. Handlebars is a Domain Specific Language on its own and its lisp-like syntax gets in my way every now and then.

I have seen the use of handlebar helpers getting abused, business logic which should be part of the .js files, being pushed to the applications' handlebar helpers. One could argue that the handlebars helper is nothing but a .js file. But I fail to understand why to invoke a JavaScript function from a .hbs file using a lisp-like syntax when the same could have been done in the corresponding .js file.

If there was a way to write inline JavaScript in .hbs file like .jsx, then I would not have to write helpers for simple iteration and logical expression.

Let’s consider the following scenario, where I would like to loop for a desired number of times, to repeat the same markup fragment:

Ember.js

//https://github.com/DockYard/ember-composable-helpers/blob/master/addon/helpers/repeat.jsimport { helper } from '@ember/component/helper';
import { typeOf } from '@ember/utils';
export function repeat([length, value]) {
if (typeOf(length) !== 'number') {
return [value];
}
return Array.apply(null, { length }).map(() => value); // eslint-disable-line
}
export default helper(repeat);//components/progress-bar.jsimport Component from '@ember/component';
import { computed } from '@ember/object';
import progressBar from './app/components/user-success/progress-bar';
export default Component.extend({
slots: computed('numberOfSlots', function() {
const numberOfSlots = this.get('numberOfSlots');
return numberOfSlots || 5;
}),
});
//templates/components/progress-bar.hbs<div class="progress-bar">
{{#each (repeat slots)}}
<div class="slot"></div>
{{/each}}
</div>
// Usage:
// in some-where-else.hbs
{{progress-bar numberOfSlots=5}}

React

// ProgressBar.jsimport React from 'react';function ProgressBar(props) {
const numberOfSlots = props.numberOfSlots || 5;
const slots = []
for (let i = 0; i < numberOfSlots; i++) {
slots.push(<div class='slot'></div>);
}
return (
<div class="progress-bar">
{slots}
</div>
)
}
// Usage:
// in SomeWhereElse.js
<ProgressBar numberOfSlots={5} />

Native JavaScript

function progressBar(numberOfSlots = 5) {

const slots = [];
for (let i = 0; i < numberOfSlots; i++) {
slots.push(`<div class='slot'></div>`);
}

const progressBar = document.createElement('div');
progressBar.innerHTML = slots.join('');
return progressBar;
}
// Usage:
// in SomeWhereElse.js
progressBar(5);

The usage in the Ember.js, React and Native JavaScript looks almost the same other than the syntax.

However, the implementation in the Ember.js is a little complicated in my opinion. Someone with knowledge of JavaScript could understand the React code easily than the Ember.js one. Also, the React example looks very similar to the Native JavaScript implementation.

In the React example, I used for loop, similar to the Native JavaScript implementation. However, in the Ember.js example, I needed to understand how helper works and use two helpers to create the desired iteration. There is no loop available in handlebars, the only way to achieve a looping is to create an array of the desired length and iterate over it using the each helper.

Also, the React example and the Native JavaScript implementation example, needed a single file. However, the Ember.js example implementation spans across three files. The concerns of spreading across multiple files is not a huge deal during the initial implementation, but it gets challenging during maintenance, to keep track of the implementation across several locations.

Tracking the source of the implementation

By looking at the .hbs file it is not evident if the repeat helper is from the application code or from an addon.

If it is from an addon, then which addon?

The only way I find it is by running a find . -name repeat.js | grep 'helpers/repeat.js'.

This is true for any code being consumed from an addon. I don’t know if there is a better way to do that.

Handlebar’s grammar and syntax

Things get more confusing with the as |alias|syntax, primarily at contextual components template syntax. Understanding and getting used to this syntax takes time. Also to keep track of how things are being passed I have to keep switching between multiple .js and .hbs files. I guess I will face similar situations when contextual-helpers becomes mainstream.

A deep understanding of Handlebar’s vast grammar and syntaxes is very much needed to get productive in Ember.js.

One could say that JSX is also a DSL. For sure it is, but with very limited grammar and most of it looks quite similar to native HTML construct.

I wish I could avoid handlebar helpers with regular utils or inline JavaScript and avoid the burden of learning yet another Domain Specific Language.

Is it a helper or a component?

Often time just by looking at the code it is not clear if it is a component or a helper usage.

For example, link-to. https://api.emberjs.com states about link-to in the Ember.Templates.helpers section but describes it as a component. I often find myself lost in this ambiguity.

Origin of the property/attribute

Identifying the origin of a property/attribute is not possible just by looking at the .hbs file. It gets more complicated when it involves multiple levels of components. To identify the origin of the information, I have to continuously search for the desired property/attribute in the corresponding .js and .hbs files and traverse up the chain.

I wish there was a way for me to identify which property is part of the current component and which has been passed own by looking at the corresponding .js and .hbs file. In React this.props and this.state makes it easy to identify.

Moreover, the .hbs and the corresponding .js files are far apart in the filesystem which makes this tracking more difficult.

Fuzzy search in the editor helps but I wish I didn’t need a separate .hbs file and could have the rendering code as part of the .js file, then I could avoid the constant context switch between .js and .hbs files.

Convention over Configuration

When files are created in an Ember.js project using ember-cli, they are generated in the corresponding directory with respect to the functionality.

For example, when a service named some-service is created using ember-cli it gets created in the app/service directory. ember-cli also generates the corresponding some-service-test.js file, in this case, a unit test.

$ ember g service some-serviceinstalling service
create app/services/some-service.js
installing service-test
create tests/unit/services/some-service-test.js

If a component was created, ember-cli will create the corresponding .js, .hbs and -test.js file, in this case, an integration test.

ember g component some-componentinstalling component
create app/components/some-component.js
create app/templates/components/some-component.hbs
installing component-test
create tests/integration/components/some-component-test.js

This is great! 🎉

As a developer, I don’t have to think about where the files need to be created. ember-cli has done it for me. This is primarily possible due to the convention over configuration. ember-cli also scaffolds the bare metal structure of the file’s content. This extremely helpful during initial development.

Magical filename resolution

Ember.js runtime is based on dependency injection which also relies on the same convention. For example, to consume a service named some-service I can just inject it using the name and Ember.js will figure out how to make it available.

import { inject as service } from '@ember/service';...
someService: service('some-service'),
...

Note that I didn’t have to specify the full path of the some-service.js file. Ember.js would automatically inject some-service.

I could have also omitted the some-service.

someService: service(),

Ember.js will translate someService to some-service and will inject it automatically.

This initially threw me off the cliff when I was modifying existing code. I had no idea what was going on.

This still creates an unpleasant workflow for me. My editor will not autocomplete the methods available at this service. I have to manually look up the file and refer it. This concern is valid for any file in an Ember.js project.

Due to this convention and implicit linking/bindings, things become more complicated if the implementation is from an addon.

At times it is not obvious what is going on.

Black magic

For example, when I was working on an existing route I came across the following code:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
export default Route.extend({xhr: service('xhr'),. . .prefetch() {
return hash({
completionMeterData: this._getData(),
});
},
_getData() {
return this.get('xhr').fetch(API_ENDPOINT);
},
});

I had no clue how the prefetch method was being processed. Ember.js Route API documentation has no mention about it.

Eventually, I tracked it down to ember-prefetch addon using find command. Searching for prefetch.js in the project’s node_modules directory I found a couple of prefetch.js to be part of ember-prefetch addon. This is pretty much the workflow if the fuzzy file lookup in my editor fails. The node-modules directory is excluded from my editor’s file lookup and thus if the implementation is not part of the application’s code my editor does not display any match.

Looking at the ember-prefetch implementation, I guess the files need not be named prefetch.js but it was helpful for me to track it down. However, it was quite a time-consuming process.

This is just one example, there are several such instances when the code is part of an addon.

By looking at the application’s codes it is extremely difficult to identify how things come together, due to this implicit or rather magical bindings, partly at the build time and partly at the runtime.

I wish the files were explicitly referenced, then I could easily identify the origin of any code using the reference created by my editor.

Conclusions

I see a lot of improvement with respect to developer ergonomics happening in Ember.js, however, it still requires a steep learning curve. I wish someday Ember.js will be less magical and more accessible to any JavaScript developer.

If you are an Ember.js Pro/Core contributor and happen to read this post, please feel free to correct me if I have mistaken. I would be more than happy to update the post with any corrections. However, if you happen to find my concerns valid, please do the needful to address them.

Once again, thank you all the Ember.js contributors for your hard work and time. 🙏