Skip to content

Instantly share code, notes, and snippets.

@bfitch
Created May 4, 2018 18:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bfitch/4320fd7f7218b616a076978c52e2f36e to your computer and use it in GitHub Desktop.
Save bfitch/4320fd7f7218b616a076978c52e2f36e to your computer and use it in GitHub Desktop.
#EmberJS2018: Embrace the Javascript Ecosystem

#EmberJS2018: Embrace the Javascript Ecosystem

Adopting a convention usually means eliminating inconsequential choices or, in the event that there are two or more viable options, picking one as the blessed path. Ember was launched in a much different environment than today and it made some excellent choices by:

  1. Building missing language and platform features like the Ember object/class model, Ember.Enumerable and utils, RSVP, Backburner.js, and even ember addons

  2. Rallying around one choice in a crowded and chaotic ecosystem, like broccoli.js, testem/qunit, JSON-API, and ember-cli.

But, as the JS ecosystem has matured, many of those initial choices are simply not needed or the community has embraced other solutions.

Tooling

When jumping into an Ember app it can often feel as though you're stepping into an alternate ecosystem from the rest of the Javascript community. I'll even go so far as to say that, in 2018, any JS framework that does not support npm as a first class citizen is a non-starter. I don't only mean first class in the sense that importing npm modules "just works", but also the overall philosophy of composing large, complex software from independently useful, distributed modules.

There are good, historical reasons why Ember isn't "there yet". For one, when Ember started npm was not nearly as popular as it is now, the tooling was really rough, there were no module bundlers or talk of using CJS modules in the browser, and ES modules were far off. There is a lot of excellent work underway to get there, and Glimmer.js has floated the idea of "npm installing your way to Ember", but if Ember wants to keep pace and win over new developers it needs to fully embrace the wider ecosystem with tight integration with npm at a tooling and philosophical level.

Consider Broccoli. Broccoli has served Ember really well over the years but, as far as I can tell, it has not been embraced by the rest of the community. Ember itself is currently running on a fork of the project and it is a bit of a black box, even to experienced developers. New Ember developers have almost certainly been exposed to newer module bundlers like webpack or Rollup and very little of their previous knowledge is transferable. That knowledge can span something as simple as importing an npm module to more complex features like code splitting and tree-shaking.

Thankfully most of the pain is abstracted by ember-cli (and better docs can definitely help), but new Ember developers are placing a much bigger bet, as they're not only opting into a view library or router or state management solution, but a suite of unfamiliar tools. React (create-react-app), Angular, vuejs, and even Rails (Webpacker) have embraced webpack. Unfortunately, Ember has been isolated by tackling the problem first and betting on tech that the rest of the community has not embraced. One of Ember's strongest assets is a commitment to shared solutions, but those shared solutions need to extend beyond the Ember community to the wider JS community. Sometimes Ember needs to lead the way, but occasionally Ember needs to aggressively adapt to where the wider community is. Getting off of in-house, legacy build tooling should be high on the priority list for Ember 4. That probably doesn't mean ditching broccoli, but making progress on the new Packager API and exposing webpack (or Rollup) config the same way ember-cli exposes eslint or .babelrc: sensible defaults with hooks to customize the underlying tool.

The Programming Model

Aside from embracing tooling and ecosystem conventions, Ember could really benefit from clarifying conventions at the API and programming model level. That means "making good" on the shift to components that happened in the 2.x series. Specifically, figuring out a way to render components with model data from router transitions (routable components). Ember is a component based framework and its API's should reflect that. I would love if the mental model for getting JSON on the screen could be reduced to: URL --> Route --> Component Tree, with the component tree having no concept of {{outlet}}s or intervening templates– just an <Application/> component {{yield}}ing to child components synced with URL changes.

Another way to solidify the programming model would be to deprecate two way bindings and fully support DDAU. That could mean making computed properties read-only by default or explicit support for immutable objects or more guidance about where to handle actions that update data– maybe all of the above! But, my impression is that one way data flow still isn't conventional (or the natural default) and some API help is needed.

Always Bet on JS™

API's and ecosystem/interop can be improved by embracing "just Javascript" wherever possible. To me, that means:

  • ES6 classes and decorators

  • Full async/await support at the framework level, i.e. no runloop interop problems in tests and native Promise support (bye bye RSVP)

  • No prototype extensions. Move all Enumerable methods and other utils (isPresent(), etc) to addons.

  • This may be controversial, but maybe even removing Ember's custom DI/injection system with plain ES module import/export:

    export default Component.extend({
      flash: service()
    });

    with:

    import flash from 'app/services/flash';
    
    export default Component.extend({
      flash
    });

Details aside, the general idea is to make Ember feel like "native" Javascript as much as possible, building on knowledge developers already have and making the places where it diverges, divergent for good reason.

Conclusion

The exciting thing is there's already incredible progress on everything mentioned above. But, these are suggestions for the 2018 Roadmap and roadmaps are about prioritization and focus. If Ember were to fully embrace the current Javascript ecosystem it would bring tremendous benefits for happy, longtime users and also entice potential new users to give it another, well deserved look.

@pzuraq
Copy link

pzuraq commented May 5, 2018

Love all the points here, I completely 100% agree that we need to get on NPM integration and integration with other build tools, and I've been pushing hard for native class support - ditching the object model would really help to simplify the learning curve for Ember. The one exception is the one you said might be controversial (shocking I know 😛)

This may be controversial, but maybe even removing Ember's custom DI/injection system with plain ES module import/export

There are some good reasons for having DI, the main one being switching implementations in different environments (Web vs Fastboot/Server vs Unit Tests, etc). In fact if you look at the wider ecosystem, most frameworks do have some form of DI - Angular has a very similar system, React-Redux has a "provider" system which is essentially DI, but not formalized/generalized, etc.

Services are Ember's general solution to long-term state (e.g. what happens when my components are torn down? How do I store their state somewhere?), and in theory you can build whatever you want on top of them. This is how Ember's component layer can hook up to the wider Javascript ecosystem - its essentially the interop layer.

@bfitch
Copy link
Author

bfitch commented May 5, 2018

Thanks for your comment @pzuraq. I'm glad most of the points resonated with you!

Re: services. I definitely agree that services are great and needed; I even did an integration with a state management solution (ember-cerebral) that leverages them heavily. My hangup is, in light of ES modules and import/export, I'm not seeing why Ember's current API inject() w/ container lookup is necessary. To me it looks like plain JS imports can be the DI mechanism. I'm sure there's something inject is doing that I don't understand yet, so I don't wanna push too hard on it. Like I said: it's controversial. 😛

Aside from the services thing (which is a minor nit pick), I'm glad there's a lot of enthusiasm for integrating with the wider community. I think it'll be great for Ember.

@oligriffiths
Copy link

Great article. Couple of points to clarify

I’m working on removing the broccoli fork, and moving to broccoli 1.0, as well as documentation and examples for a new broccoli website https://t.co/8qNzbJ41P3

The advantage of services is that it allows you to stub them in tests, which is much harder to do with an import as that create a rigid binding that you have to do backflips to get around in tests.

Otherwise this is a great article.

@knownasilya
Copy link

I like to write my service injection like so: storeService: service('store'). I think it's a lot less magical and clearly shows what is happening.

@knownasilya
Copy link

@bfitch I have also played around with services and state management a bit https://ember-twiddle.com/228b63aeb8f8e6464960ba019d7ce720?openFiles=templates.application.hbs%2C

@tschoartschi
Copy link

I like the article as well :) one thing I wanted to point out: DI and import/export are not doing the same thing. As others mentioned DI helps you to change the implementation. This is useful for testing etc. In my opinion, the differences between DI and import/export as well as the usecases and benefits of DI are more clearly in a statically typed language. We created our own service injection for a Glimmer.js project since there is no "official" way of creating services at the moment. Our code looks the following way (it's written in TypeScript):

import inject from '../utils/inject';
import EventHandler from './event-handler';

export default class Api {
  @inject
  private _eventHandler: EventHandler;
}

and then in testing we can do the following thing:

import ContainerHelper from '../../utils/test-helpers/container-helper';
import EventHandlerMock from '../../utils/test-helpers/event-handler-mock';
module('Testing the API Surface', {

    beforeEach() {
        CONTAINER = new ContainerHelper();
        CONTAINER.injectService('event-handler', EventHandlerMock);
    }
}, function () {

// .... TESTS ....//

});

I think the current syntax of inject is quite ok. I also agree with @knownasilya that it looks like "magic" for new developers but I think it's fine. Maybe a decorator would be more elegant. It's also interesting to see how the ember-cli-typescript project handles injections.

@bfitch
Copy link
Author

bfitch commented May 7, 2018

Thanks for commenting @knownasilya and @tschoartschi! I really like what @wycats sketched out for a potential inject API here: https://twitter.com/wycats/status/992496715142914048

@bfitch
Copy link
Author

bfitch commented May 8, 2018

@chilicoder very cool! Thanks for the link.

@rtablada
Copy link

rtablada commented May 9, 2018

@bfitch I think import/export and DI can be used together.

Instead the two ideas go hand in hand.
Using import to import singletons (as is used by many JS libs) is a fairly bad pattern and with async modules it can become more of an issue.
In React, many devs are actually using the context API to create an application container and even some devs use this with a DI container to do lookup.

That said, because of legacy best patterns there are definitely places in Ember Addons or even Ember Core where import/export could be used instead of DI.

@bfitch
Copy link
Author

bfitch commented May 9, 2018

@rtablada thanks for commenting. I'm curious to hear what pain you've encountered from import-ing singleton objects. I've never ran into problems while working on react stuff. That said, I'm excited to see how import/export can be used more and also how the DI concept can be refined for use cases where runtime lookup or the container is really necessary.

@jamesarosen
Copy link

Along exactly these lines, I'd also suggest CustomElement. The custom element API is sufficient for many small components like buttons, switches, modals.

Without a richer templating and binding layer, it's not a complete replacement for HTMLBars / Glimmer, especially for larger (page-scope) components. But if Ember provided out-of-the box support for custom elements, I could write components that work in multiple frameworks -- and with no framework at all, depending on browser support.

My ideal would be

// app/components/my-button.js

export default class MyButton extends HTMLButton {...}

but I'd gladly accept

import { wrapCustomElement } from '@ember/component'
class MyButton extends HTMLButton {...}
export default wrapCustomElement(MyButton)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment