ember-tribe
v2.5.2
Published
The default blueprint for using Tribe API and Junction within EmberJS.
Keywords
Readme
ember-tribe
An addon that connects EmberJS to Tribe API. Tribe is a collaborative project management framework - https://github.com/tribe-framework/tribe
https://junction.express provides an interface to build a Tribe compatibale no-code backend in minutes.
Compatibility
- Ember.js v6.0 or above
- Ember CLI v6.0 or above
Installation
- Install
ember install ember-tribe- Configure
- Enter TRIBE_API_URL and TRIBE_API_KEY in .env file, copy of .env.sample
Ember-Tribe Documentation
Overview
ember-tribe is a powerful Ember.js addon that bridges the gap between backend CMS data structures and frontend application development. It helps you make an Ember app based on storylang.json and simplified-types.json files, if and when these files are availble.
Purpose
Ember-tribe enables rapid application development by:
- Providing pre-configured addon ecosystem for common functionality
- Implementing standardized patterns for Tribe applications
- Creating responsive, Bootstrap-based interfaces with minimal configuration
- Supporting automatic model generation from backend track definitions in simplified-types.json
Installation and Setup
Prerequisites
- Ember.js 6.x
- Node.js (latest LTS)
- Junction backend
Installation
ember install ember-tribeThe addon automatically configures following essential packages:
Ember Addons:
ember-cli-dotenv- Environment configurationember-cli-sass- SCSS supportember-modifier- DOM manipulation helpersember-composable-helpers- Template utilitiesember-truth-helpers- Boolean logic helpersember-file-upload- File handlingember-power-select- Advanced select componentsember-table- Data tablesember-animated- Smooth animations
NPM Packages:
bootstrap- UI framework@popperjs/core- Bootstrap dependencyanimate.css- CSS animationsvideo.js- Video playerswiper- Touch slidershowler- Audio management
Core Architecture
Think in this order
ember-tribe follows a specific order that includes our learnings and Ember.js best practices, and must be followed exactly:
- router.js - Application routing structure
- app/routes - Route handlers and data loading
- app/templates & controllers - UI templates and minimal controllers
- app/components - Reusable UI components with component specific logic
- app/services - Application-wide logic and state
This order ensures proper dependency resolution and optimal code organization.
Package Philosophy
Try using npm packages over ember addons because ember addons are sometimes outdated. Ember-tribe prioritizes npm packages for better compatibility and maintenance.
File Structure
app/
├── routes/
├── templates/
├── controllers/
├── components/
├── helpers/
├── modifiers/
├── services/
├── styles/app.scss
└── router.js
installer.shRequired File Outputs
Make separate, complete code files for each category:
installer.sh
ember g route <name>;
ember g controller <name>;
ember g component <name> -gc;
ember g helper <name>;
ember g modifier <name>;
ember g service <name>;app/styles/app.scss
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
$font-family-sans-serif: 'IBM Plex Mono', serif !default;
$display-font-family: 'IBM Plex Mono', serif !default;
$primary: #000000 !default;
$secondary: #cccccc !default;
$success: #00ff00 !default;
$info: #0000ff !default;
$warning: #ffff00 !default;
$danger: #ff0000 !default;
$light: #eeeeee !default;
$dark: #333333 !default;
$enable-rounded: false !default;
$enable-negative-margins: true !default;
$enable-cssgrid: true !default;
$spacer: 1rem !default;
$spacers: (
0: 0,
1: $spacer * 0.25,
2: $spacer * 0.5,
3: $spacer,
4: $spacer * 1.5,
5: $spacer * 3,
6: $spacer * 4.5,
7: $spacer * 6,
8: $spacer * 7.5,
9: $spacer * 9,
10: $spacer * 12,
) !default;
@import 'node_modules/bootstrap/scss/bootstrap';
@import 'node_modules/animate.css/animate';app/templates/application.hbs
{{page-title 'Your Application Name'}}
{{outlet}}
<BasicDropdownWormhole />Application.hbs Extension Guidelines:
- Extend when adding global navigation components
- Include shared modals or dropdowns
- Add application-wide notification systems
- Insert global loading states or overlays
EmberData Integration
Automatic Model Generation
Ember-tribe automatically generates models from backend track definitions through the types service:
// app/routes/application.js
export default class ApplicationRoute extends Route {
@service types;
async beforeModel() {
// Auto-generates models from backend
await this.types.fetchAgain();
}
}Data Access Patterns
All backend data uses the modules key for field access:
// Accessing track data
let post = await this.store.findRecord('post', 1);
console.log(post.id); // Direct property
console.log(post.slug); // Direct property
console.log(post.modules.title); // Field access
console.log(post.modules.content_privacy); // Universal fieldQuery Operations
// Complex queries
this.store.query('post', {
modules: { status: 'published' }, // AND conditions
filter: { category: 'tech' }, // OR conditions
sort: 'title,-created_date', // Sort (- for desc)
page: { offset: 0, limit: 10 }, // Pagination
show_public_objects_only: false, // Include drafts
});Styling System
Default Configuration (app.scss)
// Typography
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono...');
$font-family-sans-serif: 'IBM Plex Mono', serif !default;
// Color Palette
$primary: #000000 !default;
$secondary: #cccccc !default;
$success: #00ff00 !default;
// ... additional colors
// Bootstrap Configuration
$enable-rounded: false !default;
$enable-negative-margins: true !default;
$enable-cssgrid: true !default;
@import 'node_modules/bootstrap/scss/bootstrap';
@import 'node_modules/animate.css/animate';Design Principles
- Minimal Controllers: Logic should reside in components and services
- Bootstrap 5.x Foundation: Responsive, accessible design system
- Subtle Animations: animate.css for enhanced user experience
- FontAwesome 6.x: Comprehensive icon library
Component Architecture
Component Types
Based on storylang.json component definitions:
Layout Components:
table,figure,accordion,card,list-groupnavbar,nav,tab,breadcrumb
Interactive Components:
button,button-group,dropdown,modalinput-field,select,file-uploaderpagination,popover,tooltip
Component Structure
// Component class
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { service } from '@ember/service';
export default class FileCardComponent extends Component {
@service store;
@tracked isSelected = false;
@action
toggleSelection() {
this.isSelected = !this.isSelected;
}
}Template Patterns
<div
class='card {{if this.isSelected "border-primary"}}'
{{on 'click' this.toggleSelection}}
>
<div class='card-body'>
<h5 class='card-title'>{{@file.modules.title}}</h5>
<p class='card-text'>{{@file.modules.description}}</p>
</div>
</div>Services Integration
Built-in Services
store- EmberData for CRUD operationsrouter- Navigation and route managementtypes- Automatic model generationbootstrap- Bootstrap component initialization
Custom Services
Make services based on storylang.json service definitions:
// app/services/visualization-builder.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
export default class VisualizationBuilderService extends Service {
@service store;
@tracked supportedTypes = ['network', 'tree', 'sankey'];
buildVisualization(files, type, config) {
// Service logic implementation
}
}Route Generation
Route Creation
Make routes based on storylang.json route definitions:
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class FilesRoute extends Route {
@service store;
queryParams = {
page: { refreshModel: true },
search: { refreshModel: true },
};
model(params) {
return this.store.query('json_file', {
page: { offset: (params.page - 1) * 10, limit: 10 },
modules: params.search ? { title: params.search } : {},
});
}
}Helper System
Global Helpers
Make helpers based on storylang.json helper requirements:
// app/helpers/format-date.js
import { helper } from '@ember/component/helper';
export default helper(function formatDate([date, format = 'short']) {
return new Intl.DateTimeFormat('en-US', {
dateStyle: format,
}).format(new Date(date));
});Template Usage
<span class='text-muted'>
{{format-date @post.modules.created_date 'medium'}}
</span>Modifier System
DOM Interaction Modifiers
Make modifiers for specific DOM manipulation needs:
// app/modifiers/tooltip.js
import { modifier } from 'ember-modifier';
import { Tooltip } from 'bootstrap';
export default modifier((element, [content]) => {
const tooltip = new bootstrap.Tooltip(element, {
title: content,
});
return () => tooltip.dispose();
});Ember.js Reference Guide
EmberData Patterns
Smart use of EmberData can significantly reduce size of the codebase. Make sure you take advantage of that.
EmberData operations always use a "modules" key for field access, except for .id and .slug properties. All field names from backend storage use underscore notation: modules.any_field.
Universal Default Module:
All objects include: "content_privacy": "string | public, private, pending, draft"
Single Record Operations:
// Find by ID or slug
this.store.findRecord('track', 30);
this.store.findRecord('track', 'some-slug-here');
// Access without network request (if already in store)
let post = this.store.peekRecord('post', 1);
// Usage pattern
this.store.findRecord('post', 1).then((post) => {
// Access: post.id, post.slug, post.modules.<field_name>
});Multiple Records:
this.store
.query('person', {
modules: { name: 'Peter', location: 'delhi' }, //results with AND
/*
filter: { name: 'Peter', location: 'delhi' } //results with OR
sort: "location,-age,name", //minus for descending order of that field, default is -id
page: { offset:0, limit:-1 }, //for pagination or smart uses, -1 means everything
ignore_ids: [10,14] //excludes these IDs from results
show_public_objects_only: false, //default = true, if set false results include content_privacy = drafts, private or pending
*/
})
.then((results) => {
// Process results
});Prefer using backend (this.store.query) for search, filter and sort, over front-end JS functions to achieve the same thing. Avoid using this.store.findAll altogether, use this.store.query with page: { offset:0, limit:-1 } instead.
CRUD Operations:
// Update
let post = await this.store.findRecord('post', 1);
post.modules.title = 'A new title';
await post.save(); // => PATCH request
// Delete
let post = this.store.peekRecord('post', 2);
post.destroyRecord(); // => DELETE requestHelpers
Helper functions are JavaScript functions callable from Ember templates that perform computations or operations beyond basic template syntax, keeping templates clean while adding dynamic functionality.
Local Helpers:
- Defined as methods within component classes
- Scoped to specific component
- Called with
this.prefix in templates
// app/components/user-card.js
export default class UserCard extends Component {
formatName = (firstName, lastName) =>
`${firstName} ${lastName}`.toUpperCase();
}<!-- app/components/user-card.hbs -->
<h2>{{this.formatName @user.modules.first_name @user.modules.last_name}}</h2>Global Helpers:
- Defined in
app/helpers/folder as separate files - Available across all application templates
- Called directly by function name
// app/helpers/format-currency.js
export default function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(amount);
}<span>{{format-currency @item.modules.price 'EUR'}}</span>Helper Features:
- Support positional arguments:
{{helper arg1 arg2}} - Support named arguments:
{{helper arg1 key=value}} - Can be nested:
{{outer-helper (inner-helper @value)}} - Built-in helpers available:
{{get}},{{concat}},{{let}},{{array}},{{hash}}
Usage Guidelines:
- Local: Component-specific logic, simple transformations
- Global: Reusable functionality across multiple components (formatting, calculations)
Component Architecture & Principle of Substitution
Ember components should be thought of as templates that re-execute from scratch whenever data changes. Write templates that produce correct output for any given input; Ember efficiently updates only what has changed.
Template Patterns:
<article title='{{@article.modules.title}}'>
<header><h1>{{@article.modules.title}}</h1></header>
<section>{{@article.modules.body}}</section>
</article>Dynamic Updates:
- Text and Attributes: Use
{{}}syntax for automatic DOM updates - Conditional Logic: Use helpers for conditional attributes
<div class={{if @user.modules.is_admin 'superuser' 'standard'}}>
Welcome to my app.
</div>Event Handling:
Use {{on}} element modifier for event handlers:
<button type='button' {{on 'click' this.increment}}>+</button>import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class CounterComponent extends Component {
@tracked count = 0;
@action
increment() {
this.count++;
}
}Component Communication & Modifiers
Design Pattern:
- Component manages state
- Modifiers handle DOM interactions
- Separation enables better reusability and testing
Complex Interaction Example:
<audio src={{@song.modules.src_url}} {{play-when this.isPlaying}} />
<button type='button' {{on 'click' this.play}}>Play</button>
<button type='button' {{on 'click' this.pause}}>Pause</button>// Component manages state
@tracked isPlaying = false;
@action
play() {
this.isPlaying = true;
}// Modifier handles DOM interaction
import { modifier } from 'ember-modifier';
export default modifier((element, [isPlaying]) => {
if (isPlaying) {
element.play();
} else {
element.pause();
}
});Modifier Forwarding:
Modifiers applied to components pass through via ...attributes:
<Tooltip {{custom-modifier}} />
<!-- Forwards to: -->
<div ...attributes>...</div>Services
Services are Ember objects that persist for the entire application duration, providing shared state or persistent connections across different parts of your app.
Service Definition:
// app/services/shopping-cart.js
import { TrackedArray } from 'tracked-built-ins';
import Service from '@ember/service';
export default class ShoppingCartService extends Service {
items = new TrackedArray([]);
add(item) {
this.items.push(item);
}
remove(item) {
this.items.splice(this.items.indexOf(item), 1);
}
empty() {
this.items.splice(0, this.items.length);
}
}Service Access:
import Component from '@glimmer/component';
import { service } from '@ember/service';
export default class CartContentsComponent extends Component {
// Loads service from: app/services/shopping-cart.js
@service shoppingCart;
}Usage Guidelines:
- Use for application-wide state management
- Share functionality across multiple routes/components
- Maintain data that survives route transitions
Code Generation Process
File Upload Javascript Example
import ENV from '<your-application-name>/config/environment';
@action
async uploadFile(file) {
try {
const response = await file.upload(ENV.TribeENV.API_URL + '/uploads.php');
response.json().then(async (data) => {
if (data.status == 'success') {
//data.file.name
//data.file.mime
//data.file.url
//if mime type is an image, following converted sizes are also available
//data.file.xl.url
//data.file.lg.url
//data.file.md.url
//data.file.sm.url
//data.file.xs.url
} else if (data.status == 'error') {
alert(data.error_message);
}
});
} catch (error) {
file.state = 'aborted';
}
}Installation Commands
# Write all installer.sh commands
ember g route files
ember g controller files
ember g component file-card -gc
ember g helper format-date
ember g modifier tooltip
ember g service visualization-builderPre-approval Process
Before code generation, ember-tribe requests approval for additional packages:
# Example approval request
npm i chart.js
npm i lodash
ember install ember-tableApplication Structure
Application Template
{{page-title 'Your Application Name'}}
{{outlet}}
<BasicDropdownWormhole />Best Practices
Controller Minimization
- Keep controllers minimal - prefer component logic
- Use controllers only for query params and route-level actions
- Move business logic to services
Animation Guidelines
- Use animate.css for enhanced UX
- Prefer subtle animations (fadeIn, slideIn)
- Avoid overwhelming users with excessive animation
Data Flow
- Routes: Fetch and prepare data
- Components: Display and interact with data
- Services: Handle business logic and state
- Helpers: Transform data for display
Performance Considerations
- Leverage EmberData caching with
peekRecord - Use backend filtering over frontend array manipulation
- Implement pagination for large datasets
- Minimize controller file count
Integration Examples
With Junction CMS
// Automatic sync with Junction tracks
async beforeModel() {
await this.types.fetchAgain(); // Syncs with backend types
}With External APIs
// Service for external integrations
@service externalApi;
async model() {
const localData = await this.store.query('post', {});
const externalData = await this.externalApi.fetch('/posts');
return { local: localData, external: externalData };
}Troubleshooting
Common Issues
- Model Not Found: Ensure
types.fetchAgain()completes in application route - Module Access: Remember to use
modules.field_namefor backend fields - Bootstrap Components: Initialize through modifier or service
- Animation Conflicts: Check animate.css class conflicts
Debug Commands
// Check loaded models
this.store.peekAll('your-model-name');
// Verify service registration
this.owner.lookup('service:your-service');
// Route debugging
console.log(this.router.currentRouteName);License
This project is licensed under the GNU GPL v3 License.
