Expert Ember.js Development
Write modern, performant Ember.js applications following Ember Octane conventions and current best practices.
Critical First Step: Use context7 MCP
ALWAYS use context7 MCP before writing or editing ANY Ember code - Before generating, modifying, or reviewing any Ember code, you MUST use the context7 MCP to check for relevant documentation. Context7 provides project-specific conventions, architectural patterns, coding standards, and technical decisions that override these general guidelines.
When to Use context7
Use context7 MCP in these situations:
- Before writing any new Ember component, route, service, or model
- Before modifying existing Ember code
- When implementing features that might have project-specific patterns
- When unsure about architectural decisions
- Before suggesting refactors or improvements
How to Use context7
javascript1// Example: Check for Ember component patterns before creating a component 2// Use the context7 MCP to search for relevant documentation 3// Query examples: 4// - "ember component patterns" 5// - "ember routing conventions" 6// - "ember data models" 7// - "ember testing standards" 8// - "glimmer component lifecycle" 9 10// Apply the documentation from context7 to your implementation
If context7 returns relevant documentation, follow it EXACTLY even if it conflicts with this skill's general guidance. Project-specific conventions always take precedence.
Core Principles
- Embrace Ember Octane - Use Glimmer components, tracked properties, and native classes
- Convention over configuration - Follow Ember's resolver patterns and file structure
- Use the platform - Prefer native JavaScript features over framework-specific abstractions when possible
- Composition through services and modifiers - Extract reusable logic into services and UI behavior into modifiers
- Data down, actions up (DDAU) - Maintain clear data flow patterns
Context7 MCP Workflow
Before implementing any Ember code, follow this workflow:
-
Query context7 - Search for relevant documentation using specific terms:
- Component type (e.g., "ember glimmer component")
- Feature area (e.g., "ember routing", "ember data")
- Specific pattern (e.g., "form validation", "authentication")
-
Review results - Read any returned documentation carefully
- Note project-specific naming conventions
- Identify required patterns or abstractions
- Check for mandatory testing requirements
- Look for deprecated approaches to avoid
-
Apply documentation - Implement code following context7 guidance
- Use project-specific utilities and helpers
- Follow established architectural patterns
- Match existing code style and structure
- Include required metadata or annotations
-
Fall back to general patterns - If context7 has no relevant docs, use this skill's patterns
- Apply standard Ember Octane conventions
- Follow community best practices
- Use examples from this skill as templates
Remember: context7 documentation always overrides this skill's general guidance.
Context7 Query Examples
When implementing different types of Ember code, use these query patterns:
For Components:
- "ember component patterns"
- "ember component architecture"
- "glimmer component conventions"
- "[specific component type]" (e.g., "button component", "form component")
- "component props" or "component arguments"
- "component lifecycle"
- "component testing"
For Routes:
- "ember routing patterns"
- "ember route conventions"
- "route data loading"
- "route guards" or "route authentication"
- "nested routes"
- "query parameters"
For Ember Data:
- "ember data models"
- "ember data adapters"
- "ember data serializers"
- "api integration"
- "data relationships"
- "data layer patterns"
For Services:
- "ember services"
- "service patterns"
- "shared state management"
- "[specific service]" (e.g., "authentication service", "api service")
For Testing:
- "ember testing"
- "component testing"
- "integration tests"
- "acceptance tests"
- "test patterns"
- "test data" or "fixtures"
For Styling:
- "ember styling"
- "css conventions"
- "component styles"
- "tailwind" or "[css framework]"
When Editing Existing Code:
- Search for the specific feature: "user profile", "login form", etc.
- Search for the pattern you're implementing: "form validation", "dropdown menu"
- Search for utilities you might need: "validation utilities", "date helpers"
Modern Ember Patterns (Octane+)
Glimmer Components
javascript1// app/components/user-profile.js 2import Component from '@glimmer/component'; 3import { tracked } from '@glimmer/tracking'; 4import { action } from '@ember/object'; 5import { inject as service } from '@ember/service'; 6 7export default class UserProfileComponent extends Component { 8 @service currentUser; 9 @service router; 10 11 @tracked isEditing = false; 12 @tracked formData = null; 13 14 constructor(owner, args) { 15 super(owner, args); 16 // Use constructor for one-time setup 17 this.formData = { ...this.args.user }; 18 } 19 20 @action 21 toggleEdit() { 22 this.isEditing = !this.isEditing; 23 if (this.isEditing) { 24 this.formData = { ...this.args.user }; 25 } 26 } 27 28 @action 29 async saveUser(event) { 30 event.preventDefault(); 31 32 try { 33 await this.args.onSave(this.formData); 34 this.isEditing = false; 35 } catch (error) { 36 // Handle error 37 console.error('Save failed:', error); 38 } 39 } 40 41 @action 42 updateField(field, event) { 43 this.formData = { 44 ...this.formData, 45 [field]: event.target.value 46 }; 47 } 48}
handlebars1{{! app/components/user-profile.hbs }} 2<div class="user-profile"> 3 {{#if this.isEditing}} 4 <form {{on "submit" this.saveUser}}> 5 <label> 6 Name: 7 <input 8 type="text" 9 value={{this.formData.name}} 10 {{on "input" (fn this.updateField "name")}} 11 /> 12 </label> 13 14 <label> 15 Email: 16 <input 17 type="email" 18 value={{this.formData.email}} 19 {{on "input" (fn this.updateField "email")}} 20 /> 21 </label> 22 23 <button type="submit">Save</button> 24 <button type="button" {{on "click" this.toggleEdit}}>Cancel</button> 25 </form> 26 {{else}} 27 <div class="profile-display"> 28 <h2>{{@user.name}}</h2> 29 <p>{{@user.email}}</p> 30 31 {{#if this.currentUser.canEdit}} 32 <button {{on "click" this.toggleEdit}}>Edit Profile</button> 33 {{/if}} 34 </div> 35 {{/if}} 36</div>
Tracked Properties and Getters
javascript1import Component from '@glimmer/component'; 2import { tracked } from '@glimmer/tracking'; 3import { cached } from '@glimmer/tracking'; 4 5export default class DataGridComponent extends Component { 6 @tracked sortColumn = 'name'; 7 @tracked sortDirection = 'asc'; 8 @tracked filterText = ''; 9 10 // Use @cached for expensive computations that depend on tracked properties 11 @cached 12 get filteredData() { 13 const { filterText } = this; 14 if (!filterText) return this.args.data; 15 16 const lower = filterText.toLowerCase(); 17 return this.args.data.filter(item => 18 item.name.toLowerCase().includes(lower) || 19 item.email.toLowerCase().includes(lower) 20 ); 21 } 22 23 @cached 24 get sortedData() { 25 const data = [...this.filteredData]; 26 const { sortColumn, sortDirection } = this; 27 28 return data.sort((a, b) => { 29 const aVal = a[sortColumn]; 30 const bVal = b[sortColumn]; 31 const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; 32 return sortDirection === 'asc' ? result : -result; 33 }); 34 } 35}
Services
javascript1// app/services/notification.js 2import Service from '@ember/service'; 3import { tracked } from '@glimmer/tracking'; 4import { action } from '@ember/object'; 5 6export default class NotificationService extends Service { 7 @tracked notifications = []; 8 9 @action 10 add(message, type = 'info', duration = 5000) { 11 const id = Date.now() + Math.random(); 12 const notification = { id, message, type }; 13 14 this.notifications = [...this.notifications, notification]; 15 16 if (duration > 0) { 17 setTimeout(() => this.remove(id), duration); 18 } 19 20 return id; 21 } 22 23 @action 24 remove(id) { 25 this.notifications = this.notifications.filter(n => n.id !== id); 26 } 27 28 @action 29 success(message, duration) { 30 return this.add(message, 'success', duration); 31 } 32 33 @action 34 error(message, duration = 10000) { 35 return this.add(message, 'error', duration); 36 } 37 38 @action 39 clear() { 40 this.notifications = []; 41 } 42}
Custom Modifiers
javascript1// app/modifiers/click-outside.js 2import { modifier } from 'ember-modifier'; 3 4export default modifier((element, [callback]) => { 5 function handleClick(event) { 6 if (!element.contains(event.target)) { 7 callback(event); 8 } 9 } 10 11 document.addEventListener('click', handleClick, true); 12 13 return () => { 14 document.removeEventListener('click', handleClick, true); 15 }; 16});
handlebars1{{! Usage }} 2<div {{click-outside this.closeDropdown}} class="dropdown"> 3 {{! dropdown content }} 4</div>
Routing
Route Definitions
javascript1// app/router.js 2import EmberRouter from '@ember/routing/router'; 3import config from 'my-app/config/environment'; 4 5export default class Router extends EmberRouter { 6 location = config.locationType; 7 rootURL = config.rootURL; 8} 9 10Router.map(function () { 11 this.route('dashboard', { path: '/' }); 12 13 this.route('users', function () { 14 this.route('index', { path: '/' }); 15 this.route('new'); 16 this.route('user', { path: '/:user_id' }, function () { 17 this.route('edit'); 18 this.route('settings'); 19 }); 20 }); 21 22 this.route('not-found', { path: '/*path' }); 23});
Route Class
javascript1// app/routes/users/user.js 2import Route from '@ember/routing/route'; 3import { inject as service } from '@ember/service'; 4 5export default class UsersUserRoute extends Route { 6 @service store; 7 @service router; 8 9 async model(params) { 10 try { 11 return await this.store.findRecord('user', params.user_id, { 12 include: 'profile,settings' 13 }); 14 } catch (error) { 15 if (error.errors?.[0]?.status === '404') { 16 this.router.transitionTo('not-found'); 17 } 18 throw error; 19 } 20 } 21 22 // Redirect if user doesn't have permission 23 afterModel(model) { 24 if (!this.currentUser.canViewUser(model)) { 25 this.router.transitionTo('dashboard'); 26 } 27 } 28 29 // Reset controller state on exit 30 resetController(controller, isExiting) { 31 if (isExiting) { 32 controller.setProperties({ 33 queryParams: {}, 34 isEditing: false 35 }); 36 } 37 } 38}
Loading and Error States
javascript1// app/routes/users/user/loading.js 2import Route from '@ember/routing/route'; 3 4export default class UsersUserLoadingRoute extends Route {}
handlebars1{{! app/templates/users/user/loading.hbs }} 2<div class="loading-spinner"> 3 <p>Loading user...</p> 4</div>
Ember Data
Models
javascript1// app/models/user.js 2import Model, { attr, hasMany, belongsTo } from '@ember-data/model'; 3 4export default class UserModel extends Model { 5 @attr('string') name; 6 @attr('string') email; 7 @attr('date') createdAt; 8 @attr('boolean', { defaultValue: true }) isActive; 9 @attr('number') loginCount; 10 11 @belongsTo('profile', { async: true, inverse: 'user' }) profile; 12 @hasMany('post', { async: true, inverse: 'author' }) posts; 13 14 // Computed properties still work but use native getters 15 get displayName() { 16 return this.name || this.email?.split('@')[0] || 'Anonymous'; 17 } 18 19 get isNewUser() { 20 const daysSinceCreation = (Date.now() - this.createdAt) / (1000 * 60 * 60 * 24); 21 return daysSinceCreation < 7; 22 } 23}
Custom Adapters
javascript1// app/adapters/application.js 2import JSONAPIAdapter from '@ember-data/adapter/json-api'; 3import { inject as service } from '@ember/service'; 4 5export default class ApplicationAdapter extends JSONAPIAdapter { 6 @service session; 7 8 host = 'https://api.example.com'; 9 namespace = 'v1'; 10 11 get headers() { 12 const headers = {}; 13 14 if (this.session.isAuthenticated) { 15 headers['Authorization'] = `Bearer ${this.session.data.authenticated.token}`; 16 } 17 18 return headers; 19 } 20 21 handleResponse(status, headers, payload, requestData) { 22 if (status === 401) { 23 this.session.invalidate(); 24 } 25 26 return super.handleResponse(status, headers, payload, requestData); 27 } 28}
Custom Serializers
javascript1// app/serializers/application.js 2import JSONAPISerializer from '@ember-data/serializer/json-api'; 3 4export default class ApplicationSerializer extends JSONAPISerializer { 5 // Normalize date strings to Date objects 6 normalizeDateFields(hash) { 7 const dateFields = ['createdAt', 'updatedAt', 'publishedAt']; 8 9 dateFields.forEach(field => { 10 if (hash[field]) { 11 hash[field] = new Date(hash[field]); 12 } 13 }); 14 15 return hash; 16 } 17 18 normalize(modelClass, resourceHash) { 19 this.normalizeDateFields(resourceHash.attributes || {}); 20 return super.normalize(modelClass, resourceHash); 21 } 22}
Testing
Component Integration Tests
javascript1// tests/integration/components/user-profile-test.js 2import { module, test } from 'qunit'; 3import { setupRenderingTest } from 'ember-qunit'; 4import { render, click, fillIn } from '@ember/test-helpers'; 5import { hbs } from 'ember-cli-htmlbars'; 6 7module('Integration | Component | user-profile', function (hooks) { 8 setupRenderingTest(hooks); 9 10 test('it displays user information', async function (assert) { 11 this.set('user', { 12 name: 'Jane Doe', 13 email: 'jane@example.com' 14 }); 15 16 await render(hbs`<UserProfile @user={{this.user}} />`); 17 18 assert.dom('h2').hasText('Jane Doe'); 19 assert.dom('p').hasText('jane@example.com'); 20 }); 21 22 test('it allows editing when canEdit is true', async function (assert) { 23 this.owner.lookup('service:current-user').canEdit = true; 24 this.set('user', { 25 name: 'Jane Doe', 26 email: 'jane@example.com' 27 }); 28 this.set('onSave', () => {}); 29 30 await render(hbs` 31 <UserProfile @user={{this.user}} @onSave={{this.onSave}} /> 32 `); 33 34 await click('button:contains("Edit Profile")'); 35 36 assert.dom('form').exists(); 37 assert.dom('input[type="text"]').hasValue('Jane Doe'); 38 39 await fillIn('input[type="text"]', 'Jane Smith'); 40 await click('button[type="submit"]'); 41 42 assert.dom('form').doesNotExist(); 43 }); 44});
Route/Acceptance Tests
javascript1// tests/acceptance/user-flow-test.js 2import { module, test } from 'qunit'; 3import { visit, currentURL, click, fillIn } from '@ember/test-helpers'; 4import { setupApplicationTest } from 'ember-qunit'; 5import { setupMirage } from 'ember-cli-mirage/test-support'; 6 7module('Acceptance | user flow', function (hooks) { 8 setupApplicationTest(hooks); 9 setupMirage(hooks); 10 11 test('visiting /users and creating a new user', async function (assert) { 12 await visit('/users'); 13 14 assert.strictEqual(currentURL(), '/users'); 15 assert.dom('h1').hasText('Users'); 16 17 await click('a:contains("New User")'); 18 assert.strictEqual(currentURL(), '/users/new'); 19 20 await fillIn('[data-test-name-input]', 'John Doe'); 21 await fillIn('[data-test-email-input]', 'john@example.com'); 22 await click('[data-test-submit]'); 23 24 assert.strictEqual(currentURL(), '/users/1'); 25 assert.dom('[data-test-user-name]').hasText('John Doe'); 26 }); 27});
Performance Optimization
See references/performance.md for comprehensive optimization strategies.
Quick Reference
| Problem | Solution |
|---|---|
| Unnecessary component re-renders | Use @cached for expensive getters |
| Large lists | Use ember-collection or virtual scrolling |
| Slow Ember Data queries | Optimize includes, use custom serializers |
| Bundle size | Use route-based code splitting, lazy engines |
| Memory leaks | Properly clean up in willDestroy, cancel timers |
Critical Anti-patterns
javascript1// ❌ Mutating tracked properties directly 2this.items.push(newItem); // Won't trigger reactivity 3 4// ✅ Replace the entire array 5this.items = [...this.items, newItem]; 6 7// ❌ Creating new functions in templates 8{{on "click" (fn this.handleClick item)}} 9 10// ✅ Use actions or stable references 11@action handleItemClick(item) { /* ... */ } 12{{on "click" (fn this.handleItemClick item)}} 13 14// ❌ Not using @cached for expensive computations 15get expensiveComputation() { 16 return this.data.filter(/* complex logic */); 17} 18 19// ✅ Use @cached 20@cached 21get expensiveComputation() { 22 return this.data.filter(/* complex logic */); 23}
Project Structure
app/
├── components/ # Glimmer components
│ └── user-profile/
│ ├── component.js
│ ├── index.hbs
│ └── styles.css
├── controllers/ # Controllers (use sparingly in Octane)
├── helpers/ # Template helpers
├── modifiers/ # Custom modifiers
├── models/ # Ember Data models
├── routes/ # Route classes
├── services/ # Services
├── templates/ # Route templates
├── adapters/ # Ember Data adapters
├── serializers/ # Ember Data serializers
├── styles/ # Global styles
└── app.js
tests/
├── integration/ # Component tests
├── unit/ # Unit tests (models, services, etc.)
└── acceptance/ # Full application tests
Tooling Recommendations
| Category | Tool | Notes |
|---|---|---|
| CLI | ember-cli | Official tooling |
| Testing | QUnit + ember-qunit | Built-in, well integrated |
| Linting | ESLint + ember-template-lint | Catch template issues |
| Formatting | Prettier | Use with ember-template-lint |
| Mocking | ember-cli-mirage | API mocking for tests |
| State management | Services + tracked | Built-in, no extra deps |
| HTTP | fetch or ember-fetch | Native or polyfilled |
Common Patterns
Form Handling with Validation
javascript1// app/components/registration-form.js 2import Component from '@glimmer/component'; 3import { tracked } from '@glimmer/tracking'; 4import { action } from '@ember/object'; 5import { inject as service } from '@ember/service'; 6 7export default class RegistrationFormComponent extends Component { 8 @service notification; 9 10 @tracked email = ''; 11 @tracked password = ''; 12 @tracked confirmPassword = ''; 13 @tracked errors = {}; 14 @tracked isSubmitting = false; 15 16 get isValid() { 17 return ( 18 this.email && 19 this.password.length >= 8 && 20 this.password === this.confirmPassword && 21 Object.keys(this.errors).length === 0 22 ); 23 } 24 25 @action 26 updateEmail(event) { 27 this.email = event.target.value; 28 this.validateEmail(); 29 } 30 31 @action 32 updatePassword(event) { 33 this.password = event.target.value; 34 this.validatePassword(); 35 } 36 37 @action 38 updateConfirmPassword(event) { 39 this.confirmPassword = event.target.value; 40 this.validateConfirmPassword(); 41 } 42 43 validateEmail() { 44 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 45 46 if (!this.email) { 47 this.errors = { ...this.errors, email: 'Email is required' }; 48 } else if (!emailRegex.test(this.email)) { 49 this.errors = { ...this.errors, email: 'Invalid email format' }; 50 } else { 51 const { email, ...rest } = this.errors; 52 this.errors = rest; 53 } 54 } 55 56 validatePassword() { 57 if (this.password.length < 8) { 58 this.errors = { ...this.errors, password: 'Password must be at least 8 characters' }; 59 } else { 60 const { password, ...rest } = this.errors; 61 this.errors = rest; 62 } 63 64 // Re-validate confirm password if it's filled 65 if (this.confirmPassword) { 66 this.validateConfirmPassword(); 67 } 68 } 69 70 validateConfirmPassword() { 71 if (this.password !== this.confirmPassword) { 72 this.errors = { ...this.errors, confirmPassword: 'Passwords do not match' }; 73 } else { 74 const { confirmPassword, ...rest } = this.errors; 75 this.errors = rest; 76 } 77 } 78 79 @action 80 async submit(event) { 81 event.preventDefault(); 82 83 if (!this.isValid) return; 84 85 this.isSubmitting = true; 86 87 try { 88 await this.args.onSubmit({ 89 email: this.email, 90 password: this.password 91 }); 92 93 this.notification.success('Registration successful!'); 94 } catch (error) { 95 this.notification.error(error.message || 'Registration failed'); 96 } finally { 97 this.isSubmitting = false; 98 } 99 } 100}
Infinite Scroll with Modifier
javascript1// app/modifiers/infinite-scroll.js 2import { modifier } from 'ember-modifier'; 3 4export default modifier((element, [callback], { threshold = 200 }) => { 5 let isLoading = false; 6 7 function handleScroll() { 8 if (isLoading) return; 9 10 const { scrollTop, scrollHeight, clientHeight } = element; 11 const distanceFromBottom = scrollHeight - (scrollTop + clientHeight); 12 13 if (distanceFromBottom < threshold) { 14 isLoading = true; 15 callback().finally(() => { 16 isLoading = false; 17 }); 18 } 19 } 20 21 element.addEventListener('scroll', handleScroll, { passive: true }); 22 23 return () => { 24 element.removeEventListener('scroll', handleScroll); 25 }; 26});
TypeScript Support
Ember has strong TypeScript support. Enable it with:
bash1ember install ember-cli-typescript
typescript1// app/components/user-profile.ts 2import Component from '@glimmer/component'; 3import { tracked } from '@glimmer/tracking'; 4import { action } from '@ember/object'; 5import type { TOC } from '@ember/component/template-only'; 6 7interface UserProfileArgs { 8 user: { 9 name: string; 10 email: string; 11 avatarUrl?: string; 12 }; 13 onSave: (data: UserData) => Promise<void>; 14 canEdit?: boolean; 15} 16 17interface UserData { 18 name: string; 19 email: string; 20} 21 22export default class UserProfileComponent extends Component<UserProfileArgs> { 23 @tracked isEditing = false; 24 @tracked formData: UserData | null = null; 25 26 @action 27 async saveUser(event: SubmitEvent): Promise<void> { 28 event.preventDefault(); 29 30 if (!this.formData) return; 31 32 await this.args.onSave(this.formData); 33 this.isEditing = false; 34 } 35} 36 37// Template-only component signature 38export interface GreetingSignature { 39 Element: HTMLDivElement; 40 Args: { 41 name: string; 42 }; 43} 44 45const Greeting: TOC<GreetingSignature> = <template> 46 <div ...attributes>Hello {{@name}}!</div> 47</template>; 48 49export default Greeting;
Remember: Always Use context7 MCP First!
Before implementing any Ember code, query the context7 MCP for relevant project documentation. Context7 provides project-specific guidelines that always supersede these general best practices.
Quick context7 Query Guide
Before writing components:
- Query: "ember component patterns", "glimmer component", "component architecture"
Before routing work:
- Query: "ember routing", "route patterns", "navigation"
Before Ember Data:
- Query: "ember data models", "api integration", "data layer"
Before tests:
- Query: "ember testing", "test patterns", "test requirements"
When editing existing code:
- Query the specific feature or pattern you're working with
The context7 MCP is your source of truth for this project's Ember conventions.