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 handlerapp/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:
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!