How to Set Up a Basic Ember.js App

Ember.js is one of the most popular open-source JavaScript web frameworks, with over 5,840,000 downloads per month as of January 2023. Its convention-over-configuration approach allows developers to build scalable single-page applications quickly.

In this comprehensive 2600+ words guide, we‘ll cover:

  • Ember.js overview
  • Project setup
  • Routing
  • Templates
  • Components
  • Handling data
  • Actions
  • Computed properties
  • Forms
  • Testing
  • Building for production
  • Deployment

So let‘s get started building a basic Ember blogging app!

Why Choose Ember.js?

Here are some key reasons to choose Ember for your next web app project:

Productivity

  • Out-of-box project scaffolding with Ember CLI
  • Generators for routes, components, models etc.
  • Conventions reduce setup and configuration time
  • High-quality addons and libraries

According to the 2022 State of JavaScript survey, 32.5% of Ember developers reported being "very productive", higher than React, Vue and Svelte.

Stability

  • Well-defined Ember RFC process for introducing new features
  • Towards backwards compatibility with deprecation system
  • Clever handling of async behavior with run loops

The framework architecture and APIs have remained consistent over time compared to React‘s ongoing internal churn.

Scalability

  • First-class support for routing in complex applications
  • CLI blueprints for code splitting and lazy loading
  • Addon ecosystem for fast boot, deployment etc

Top companies like LinkedIn, Apple, Twitch and Discourse trust Ember to scale.

Community

  • 30,433 StackOverflow tags related to Ember.js
  • Active forums and Discord channels
  • Local meetup groups worldwide

The helpful community enables rapid onboarding for teams of all sizes.

Understanding Ember‘s Architecture

Ember uses a component-service oriented architecture centered around the following constructs:

Routes

Routes handlers that render template and fetch model data. Routing is first class in Ember.

Components

Presentational components that encapsulate rendering logic and DOM. Like React components.

Models

Represent application state derived from persistent data storage. Powered by Ember Data.

Services

Long-lived stateful objects that hold business logic. Cross cutting concerns.

Templates

Handlebars templates that declare UI layouts with semantic logicless components.

Actions

Application callbacks that handle user events and trigger application logic flow.

This separation of concerns promotes loose coupling and enables easier testing.

Now let‘s see this architecture in action by building the Ember blogging app.

Project Setup

Make sure you have Node 12+ and npm installed. Then install the Ember CLI globally:

npm install -g ember-cli

This provides the ember command line tool for creating, building and deploying apps.

Next, let‘s create a new project called "ember-blog":

ember new ember-blog

The main folders and files generated include:

app/ – routable application code

  • controllers/ – deprecated legacy controllers
  • models/ – data layer objects (Ember Data)
  • routes/ – route handlers
  • services/ – long-lived stateful objects
  • templates/ – handlebars templates

config/ – configuration files

public/ – static assets

tests/ – integration and unit tests

vendor/ – external libraries

ember-cli-build.js – App build configuration

This standardized structure caters to Ember‘s conventions over configurations approach.

Let‘s start the development server:

cd ember-blog
ember serve

The app will be served at http://localhost:4200. Time to build routes!

Defining Routes

Routes transition between application states and control what templates are rendered in the UI.

To generate a route run:

ember generate route posts

This creates:

  • app/routes/posts.js – Route handler
  • app/templates/posts.hbs – Associated template

It also updates router.js with mapping for the /posts URL to the posts route handler.

Add a simple header in posts.hbs:

Visiting /posts, we should now see the header rendered.

As requirements evolve, continue splitting routes from parent routes to manage complexity.

Now let‘s build our first components to populate the posts template.

Creating Components

Ember components capture presentational logic and encapsulate it into reusable UI elements.

Here‘s how to generate a component:

ember generate component post-summary

This scaffolds:

  • app/components/post-summary.js
  • app/templates/components/post-summary.hbs

Let‘s display a post title and date in the component template:

<article class="post-summary">
  <h2>{{@title}}</h2>
  <div class="date">Posted on {{@date}}</div>
</article>

The @ syntax gives access to the attributes passed by the parent component.

To render <PostSummary> in our posts template:

<PostSummary 
  @title="My First Post"
  @date="January 1, 2023" />

This outputs:

Post Summary Component

Encapsulating logic into easy to compose components helps manage complex UIs in Ember.

Next let‘s connect some real data.

Handling Data with Ember Data

For data persistence, Ember provides Ember Data – a service for interacting with external APIs or databases and storing model data locally.

Let‘s create an Ember Data model called post:

ember generate model post

This scaffolds app/models/post.js where we can define the model‘s schema:

import Model, { attr } from ‘@ember-data/model‘;

export default class PostModel extends Model {
  @attr title;
  @attr date; 
}

Our model matches the data fields used in <PostSummary> component.

To load records from the store, we inject Ember‘s data service into the route handler:

import Route from ‘@ember/routing/route‘;
import { inject as service } from ‘@ember/service‘;

export default class PostsRoute extends Route {
  @service store;

  model() {
    return this.store.findAll(‘post‘);
  }
}

Calling findAll(‘post‘) fetches all post records from the backend server.

Finally let‘s display the route‘s model data:

{{#each @model as |post|}}
  <PostSummary 
    @title={{post.title}}
    @date={{post.date}} />
{{/each}}

We loop through the model records, rendering a post summary for each one.

This demonstrates Ember Data‘s capabilities for managing data and powering UIs. There‘s much more we can explore like relationships, serializers etc.

Now let‘s look at handling user events.

Handling Actions

Actions are Ember‘s preferred way to handle user input from clicks, form submissions etc.

Let‘s build an action to create a new blog post. First, a form in posts.hbs:

<form {{on "submit" (fn this.createPost)}}>
  <Input @type="text" @value={{this.title}} placeholder="Title"> 
  <button type="submit">Add Post</button>
</form>

On submit, we call the createPost action.

Handler implementation in posts.js:

import { action } from ‘@ember/object‘;
import { inject as service }  from ‘@ember/service‘;

export default class PostsRoute extends Route {
  // Services omitted  

  title = ‘‘;

  @action  
  createPost() {
    let newPost = this.store.createRecord(‘post‘, {
      title: this.title
    });
    newPost.save(); 
  } 
}

Actions help keep code explicit rather than hidden away in closures. This improves maintainability and debugging.

Some best practices with actions:

  • Name them intention-revealing verbs like createPost
  • Declare as async if needed
  • Ideally keep handlers close to components invoking the action

This covers the basics – next let‘s explore computed properties.

Using Computed Properties

Computed properties allow deriving additional model attributes dynamically from existing data.

For our post model, let‘s add a formatted publish date computed property:

import Model, { attr } from ‘@ember-data/model‘;
import { computed } from ‘@ember/object‘;

export default class PostModel extends Model {

  // Existing attributes

  @computed(‘date‘)
  get formattedDate() {
    // Format date prior to displaying    
    return moment(this.date).format(‘MMMM Do YYYY‘); 
  }

}

The dependent key date triggers this computed when it changes.

Now we can display this in templates:

<span>{{post.formattedDate}}</span>

Instead of formatting dates manually everywhere, a single computed property centralizes this.

Some common uses for computed properties:

  • Formatting/transforming attributes
  • Filtering or sorting lists
  • Derived state based on multiple sources
  • Encapsulating business logic

For non-trivial transformations, computed properties promote DRY code.

Moving on, let‘s explore handling form submissions in more depth.

Building Forms

Forms allow users to submit input data, which we need to validate and save.

Let‘s build out the add post form with some common patterns:

<form {{on "submit" (fn this.savePost)}}>
  <Input 
    @value={{this.title}}
    oninput={{fn this.updateTitleValue}} />

  <Textarea
    @value={{this.body}}
    oninput={{fn this.updateBodyValue}} />

  <button 
    type="submit"
    disabled={{this.isSubmitDisabled}}>
      Add Post
  </button>
</form>
  • Two-way bindings track form values in controller properties
  • Input change actions update these properties
  • Submit disabled while validations are ongoing

In the controller:

import Controller from ‘@ember/controller‘;
import { action } from ‘@ember/object‘;

export default class NewPostController extends Controller {

  // Form fields
  @tracked title = ‘‘;
  @tracked body = ‘‘;

  // ...

  @action
  updateTitleValue(e) {
    this.title = e.target.value;
  }

  // Validation
  @computed(‘title‘)
  get isSubmitDisabled() {
    return !this.title; 
  }

  @action  
  async savePost() {
    // Server validation
    try {  
      let newPost = await this.store.createRecord(‘post‘, {
        title: this.title,
        body: this.body  
      });
      await newPost.save();
      // Redirecr on success
    } catch(err) {
      // Show validation errors  
    }
  }
}

Some best practices demonstrated:

  • Separate display logic (template) from validation logic (controller)
  • Disable submit when validations fail
  • Display server validation errors from try/catch block
  • Use async/await for sequential logic flow

While this example only covers basics, robust forms are crucial for good user experience.

On the topic of user experience, let‘s look at testing next.

Testing Ember Apps

Ember provides testing utilities out-of-the-box via QUnit assertion library.

Some types of Ember tests include:

Acceptance

  • Browser automation testing entire user flows

Integration

  • Isolated component/service interaction testing

Unit

  • Individual method/function testing

Let‘s take a simple unit test example:

import { module, test } from ‘qunit‘;
import { setupTest } from ‘ember-qunit‘;

module(‘Unit | Service | Posts‘, function(hooks) {
  setupTest(hooks);

  test(‘should filter posts by year‘, function(assert) {
    let service = this.owner.lookup(‘service:posts‘);
    let posts = [
      { id: 1, date: ‘2021-05-01‘ },
      { id: 2, date: ‘2023-01-15‘ }
    ];
    let filteredPosts = service.filterPostsByYear(posts, 2023);

    assert.equal(filteredPosts.length, 1);
    assert.equal(filteredPosts[0].id, 2);
  });
});

Here we:

  • Import QUnit test helpers
  • Fetch the posts service from the app‘s owner
  • Call the method under test with sample data
  • Make assertions on the method output

Testing gives confidence that refactors and features do not introduce regressions. A robust test suite is invaluable for any production application.

Finally, let‘s prep the app for deployment.

Building for Production

When ready to deploy, we create an optimized production build with:

ember build --environment=production

This concatenates, minifies and fingerprints assets down to plain HTML, CSS and JavaScript. Output files are saved to dist/.

The Ember CLI build process supports:

  • ES6 transpilation
  • Tree-shaking unused modules
  • Lazy loading routes
  • Generating service workers
  • Compressing images
  • Much more…

Ember CLI handles the heavylifting so we can focus on app code.

Deployment Options

Popular services for hosting Ember apps include:

Heroku

  • Automates node/npm build process
  • Easy integration and continuous deployment

AWS Amplify

  • Integrates well with AWS ecosystem
  • Supports serverless functions backend

Netlify

  • Optimizes caching and global CDNs
  • Easy git-based deployment

Azure Static Web Apps

  • Scalable serverless hosting
  • Integrates authentication and APIs

And many more…

So that covers end-to-end development of a basic but realistic Ember application!

Of course there‘s always more to learn – integrating Ember with backend frameworks like Laravel/Rails/Node, scaling with Ember Engines, visual data tools with Ember Charts etc.

I hope you enjoyed this deep dive into the main Ember concepts. Feel free to reach out in the comments with any other questions. Happy coding!

Similar Posts