Building Ember Apps with Duplos®
Background: wallpaperscraft
jonkilroy jkusa
Photos: AIatariel, polarstein, Lego
Web Apps
Web Services
Data Pipelines
2007
Photo: Lego Ideas
Data Systems
How We Build Apps
Photo: Lego Ideas
Data Systems
Data Systems 101
-
Table: Collection of related data
-
📐 Metric: Measurement/Fact
-
Dimension: Categorizes Metric
Part Num | Brick Type | Brick Color | Brick Count |
---|---|---|---|
3003 | 2x2 | Red | 13 |
3003 | 2x2 | Blue | 21 |
3001 | 4x2 | Red | 34 |
Dimension
Metric
Dimension
Dimension
{ "date":"2019-10-17", "event": brickPurchase ... },
{ "date":"2019-10-17", "event": brickPurchase ... }
{ "date":"2019-10-17", "brickCount": 2 ... }
{ "brickId": 3003, "type": "2x2" ... }
{ "brickId": 3003, "brickCount": 2 ... }
1x4 ▉▉▉▉▉▉▉▉▉▉ 2x2 ▉▉▉▉▉▉ 4x2 ▉▉▉▉ 1x2 ▉▉
Photo: Jon Kilroy
Ember Has Been There
For Us
Photo: Lego
ember
1.2.0
ember
3.
12.0
2015
When You Build
A Lot of Apps
Photo: Tom Nagy
You See The Patterns
Credit: Lego
Credit: Lego Snazzy
Credit: Richard Süselbeck
Credit: Sarah von Innerebner
Photo: Lego
Large Patterns
Photo: https://i.ebayimg.com/images/g/~b4AAOSwcBJdJkkp/s-l1600.png
Photo: Lego
!==
!==
-
This Talk Is Not Endorsed By Lego®
-
This Talk Is Not Sponsored By Lego®
-
I Am Not An Employee of Lego®
-
I Have No Connections With Lego®
-
All Lego Images Are Owned By Lego®
-
I Am Not Being Paid In Legos®
Photo: Lego
Lego === 'play well'
Is Like Building With Legos
Just Like Real Legos
Ember Addons
Work Well Together
Legos + Denmark + EmberFest === 'It All Fits Together'
ember-power-select
ember-data
ember-cp-validations
ember-cli-clipboard
ember-ember-modal-dialog
ember-composable-helpers
ember-cli-flash
ember-concurrency
ember-tooltips
ember-c3
ember-font-awesome
ember-animated
ember-tooltips
vertical-collection
ember-gridstack
Photo: CC0 1.0
Using Small Composable Addons To Build Features Provides A Lift
Development Is Costly
Photo: Lego
-
🧮 UX/Usability
-
🕹 Interactions/Wiring
-
🖼 Styles
-
🧪 Testing
-
♿︎ Accessibility
-
🇩🇰 l10n & i18n
-
🎧 Customer Feedback
Extract Large Patterns Into Larger Pieces
Duplos®
Photo: Maxx 3001
With Ember We Can Build
Large Composable Addons
That Encapsulate Large
Amounts Functionality
And Prevent Choking Hazards
models
routes
components
services
controllers
templates
Addon
Encapsulating Large Swaths Of Functionality Can
-
💰 Save On Development Costs
-
👨🏻💻 Collaborative Development
-
🏋️♀️ Reduce Repetitive Work
Is this Ember Engines?
Photo: Jon Kilroy
Photo: Lego
The Short Answer Is
Not Really
Ember App
Photo: Jon Kilroy
Engine A
Engine B
Engine C
Engine D
Team 1
Team 2
Photo: Jon Kilroy
Engine A
Engine B
Engine C
Engine D
Ember App
Photo: Jon Kilroy
Duplo A
Duplo B
Duplo C
Photo: Jon Kilroy
Duplo A
Duplo B
Duplo C
Duplo A
Duplo B
Duplo D
App One
Experiences
App Two
Experiences
Photo: Jon Kilroy
App One
App Two
Customizations
Duplo A
Duplo B
Duplo C
Duplo A
Duplo B
Duplo D
Photo: Jon Kilroy
Photo: Lego
Each App Is A Snowflake
Each With Its Own
-
📐 Requirements
-
🥩 Stakeholders
-
🍶 Special sauce
What Does This Look Like?
Photo: letsbuilditagain.com
One Pattern We've Observed
Photo: Loozrboy
Photo: Lego
Made with: mecabricks.com
Self Serve
Photo: The Brick Zombie
Our Duplos
Core Addons
Reports
Dashboards
Directory
Alerts
Admin
navi
Custom Data App
Navi Reports
Navi Dashboards
Navi Core
Custom App Views & Experiences
Custom Experience A
Custom Experience B
Custom Data App
Navi Reports
Navi Dashboards
Navi Core
Custom App Views & Experiences
Custom Experience A
Custom Experience B
Navi Features
Scheduled Reports
CSV Export
Dashboard Filters
PDF Export
Threshold Alerting
Report Visualization
Save Reports
Ad-hoc Reports
Share URL
API Query URL
Clone Report
Dashboards
Asset Organization
Scheduled Dashboards
Bulk Import
Report Templates
Print Views
Cardinality Aware Lookups
Metric Descriptions
Admin Controls
Report Conversion
-
⏰ < Week To Get To Prod
-
🍶 Focus on Special Sauce
-
👨👧👦 Collaborative Development
-
🧮 Used In 8 Product Areas
Photo: CC0 1.0
How Do We Build Reusable Duplos®?
Photo: Jon Kilroy
Abstraction
Flexibility
size
<NaviReportBuilder
@subComponent1="customSubComponent1"
@subComponent2="customSubComponent2"
@subComponent3="customSubComponent3"
@subComponent4="customSubComponent4"
@subComponent5="customSubComponent5"
@subComponent6="customSubComponent6"
@subComponent7="customSubComponent7"
@subComponent8="customSubComponent8"
@subComponent9="customSubComponent9"
@subComponent10="customSubComponent10"
@subComponent11="customSubComponent11"
@subComponent12="customSubComponent12"
@subComponent13="customSubComponent13"
@subComponent14="customSubComponent14"
@subComponent15="customSubComponent15"
...
onAction1={{action 'action1'}}
onAction2={{action 'action2'}}
onAction3={{action 'action3'}}
onAction4={{action 'action4'}}
onAction5={{action 'action5'}}
onAction6={{action 'action6'}}
onAction7={{action 'action7'}}
onAction8={{action 'action8'}}
onAction9={{action 'action9'}}
onAction10={{action 'action10'}}
...
as | yield1 yield2 yield3 yield4 yield5 yield6 yield7 yield8 |
>
...
</NaviReportBuilder>
Photo: Jon Kilroy
Photo: Michael Brennand-Wood
How Ember Wires Your App
Build
Config
Route
Component
Services
Made with: mecabricks.com
bricks.io/bricks
bricks
route
bricks
controller
bricks
template
component tree
Made with: mecabricks.com
How Does Ember Do It?
Magical
Photo: Lego
Build
Config
Route
Component
Services
Dependency Injection
Made with: mecabricks.com
-Ember Guides
Ember applications utilize the dependency injection ("DI") design pattern to declare and instantiate classes of objects and dependencies between them.
Which Supports...
Photo: Big Partnership/PA Wire
Photo: LitFilmFest
Dependency Inversion Principle
Photo: The Lego Movie
Depend On Abstractions
Not Concretions
Made with: mecabricks.com
1x2 Interface
2x2 Interface
Made with: mecabricks.com
Made with: mecabricks.com
Deeper Look
Photo: Lego
Module Lookup
Photo: Lego
//goodbricks/app/routes/bricks.js
import Route from '@ember/routing/route';
export default class Bricks extends Route {
model() {
return [1,2,3];
}
}
ES6 Modules
Asynchronous Module Definitions
goodbricks/
|-- app
|-- routes
| |-- bricks.js
| |-- sets.js
|-- controllers
| |-- bricks.js
| |-- sets.js
--> goodbricks/routes/bricks
--> goodbricks/routes/sets
--> goodbricks/controllers/bricks
--> goodbricks/controllers/sets
https://github.com/amdjs/amdjs-api/wiki/AMD
Module ID
AMD Modules
Container
owner.lookup('route:bricks')
resolve('route:bricks')
hasRegistration('route:bricks')
Registry
Resolver
goodbricks/routes/bricks
Supports Addons
goodbricks
|-- app
|-- components
| |-- brick-card.js
| |-- brick-details.js
|-- helpers
|-- brick-name.js
ember-cli-clipboard
|-- app
|-- components
| |-- copy-button.js
goodbricks
|-- app
|-- components
| |-- brick-card.js --> goodbricks/components/brick-card
| |-- brick-details.js
| |-- copy-button.js
|-- helpers
|-- format-brick-name.js
--> goodbricks/components/brick-card
--> goodbricks/components/brick-details
--> goodbricks/components/copy-button
--> goodbricks/helpers/format-brick-name
APP ADDON
Module ID
MERGED
AMD Modules
Container
owner.lookup('component:copy-button')
Registry
Resolver
goodbricks/components/copy-button
Photo: twoclevermoms.com
Single Module Registry
ember-power-select
ember-data
ember-cp-validations
ember-cli-clipboard
ember-ember-modal-dialog
ember-composable-helpers
ember-cli-flash
ember-concurrency
ember-tooltips
ember-c3
ember-font-awesome
ember-animated
ember-tooltips
vertical-collection
ember-gridstack
WHAT IF I TOLD YOU
YOU CAN BRING YOUR
APP INTO YOUR ADDON
Photo: Lego
Ember Apps Can Inject Customization Into Addons
Patterns For Customizable Duplos®
🎛 Configuration
🧩 Providers
🔌 Plugins
♟ Extend & Replace
Photo: CC0 1.0
create-data-app
Part Num | Brick Type | Brick Color | Brick Count |
---|---|---|---|
3003 | 2x2 | Red | 13 |
3003 | 2x2 | Blue | 21 |
3001 | 4x2 | Red | 34 |
$ ember new goodbricks
$ ember install navi-reports
//goodbricks/app/router.js
import EmberRouter from '@ember/routing/router';
import config from './config/environment';
import { reportRoutes } from 'navi-reports/router';
const Router = EmberRouter.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('my-sets');
reportRoutes(this, {/* options */});
});
export default Router;
goodbricks
|-- app
|-- routes
|-- bricks.js
|-- sets.js
navi-reports
|-- app
|-- routes
| |-- navi-reports
|-- new.js
|-- report
|-- clone.js
|-- save-as.js
...
APP ROUTES ADDON ROUTES
goodbricks
|-- app
|-- routes
|-- bricks.js
|-- sets.js
|-- navi-reports
|-- new.js
|-- report
|-- clone.js
|-- save-as.js
...
Configuration
Photo: fllcasts
App Config
'use strict';
module.exports = function(environment) {
let ENV = {
modulePrefix: 'goodbricks',
environment,
rootURL: '/',
locationType: 'auto',
EmberENV: {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true
},
EXTEND_PROTOTYPES: {
// Prevent Ember Data from overriding Date.parse.
Date: false
}
},
APP: {
// Here you can pass flags/options to your application instance
// when it is created
},
};
...
return ENV;
};
//goodbricks/config/environment.js
Addon Configuration
getOwner(this).resolveRegistration('config:environment');
import config from 'ember-get-config'; //via addon
Using Configuration
import config from 'goodbricks/config/environment';
Within Addon Tree
module.exports = function(/* environment, appConfig */) {
return {
navi: {
dataSources: [/* {
name: 'example',
uri: 'https://data.naviapp.io/v1',
type: 'fili',
timeout: 60000
}*/],
predefinedIntervalRanges: {
day: ['P1D', 'P7D', 'P14D'],
month: ['current/next', 'P1M', 'P3M'],
year: ['current/next', 'P1Y', 'P2Y']
},
FEATURES: {
enableScheduling: false,
enablePDFExport: false,
enabledNotifyIfData: false
}
}
};
};
Core Settings
Refinements
Feature Flags
//navi-reports/config/environment.js
Navi Default Config
module.exports = function(/* environment, appConfig */) {
return {
...
navi: {
dataEpoch: '2019-01-01',
dataSources: [{
name: 'brickData',
uri: 'https://data.goodbricks.io/v1',
type: 'fili'
}],
FEATURES: {
enableScheduleReports: true
...
}
}
};
};
//goodbricks/config/environment.js
Host App Override Config
import config from 'ember-get-config';
const dataSources = config.navi.dataSources;
export default class NaviDataAdapter {
...
_buildURLPath(request, options) {
const { uri } = dataSources.find(d => d.name = options.name);
...
return `${uri}/${namespace}/${table}/${timeGrain}/${dimensions}`;
}
...
}
{{#if (feature-flag "enableScheduleReports")}}
<NaviExportReport>
</NaviIcon name="clock-o"> Schedule
</NaviExportReport>
{{/if}}
Get Data Source URI
Check Feature Flag
Run Time
Photo: Cyril Byrne
Configuration Can Be A Beast
Photo: Lego
📐 1000+ Metrics
Photo: Rebecca Alvy
Get The API Team To Do It
Navi
API
What tables, metrics, dimensions do you have?
{
tables: [{
name: "bricks",
metrics: [...]
}],
metrics: [... ]
dimension: [...]
}
Dimensions
Metrics
Photo: Lego
Providers
Allows An Application To Provide One Of Multiple Implementations
Made with: mecabricks.com
2x2
Interface
Made with: mecabricks.com
LEGO 3943v2
Made with: mecabricks.com
LEGO 4591
Made with: mecabricks.com
Services
Photo: La Petite Brique
Notification Service
...
@service naviNofications;
@action
async saveReport(report) {
await report.save();
this.naviNotifications.add({
message: 'Report was successfully saved!',
type: 'success'
});
}
...
goodbricks/services/navi-notifications
Provider Usage
Interface
import Service from '@ember/service';
import { assert } from '@ember/debug';
export default class extends Service {
add(/* options */) {
assert("NaviNotifications must implement `add`");
}
clearMessages() {
assert("NaviNotifications must implement `clear`");
}
}
//navi-core/services/navi-base-notifications.js
Provider Interface
goodbricks
|-- app
|-- services
|-- navi-notifications.js
$ ember g service navi-notifications
goodbricks/services/navi-notifications
ember-cli-flash
ember-cli-notifications
...
import Notification from 'navi-core/services/navi-base-notifications';
export default class FlashMessageService extends Notification {
@service
notificationMessages; //ember-cli-notifications
add(options = {}) {
const clearDuration = TIMEOUTS[options.timeout];
const { message, type } = options;
return this.notificationMessages[type](message, {
autoClear: true,
clearDuration
});
}
clearMessages() {
this.notificationMessages.clearAll();
}
}
//goodbricks/app/services/navi-notifications.js
Concrete Provider
Components
Photo: Lego
<NaviIcon />
<span>
</NaviIcon "download"> Export
</span>
goodbricks/components/navi-icon
Provider Usage
goodbricks
|-- app
|-- components
| |-- navi-icon.js
|-- templates
|-- components
|-- navi-icon.hbs
$ ember g component navi-icon
goodbricks/components/navi-icon
import NaviIcon from 'navi-core/components/navi-icon';
const ICON_MAP = {
download: 'cloud-download'
...
};
export default class extends NaviIcon {
get normalizedName() {
return ICON_MAP[this.name];
}
}
{{!-- goodbricks/app/templates/components/navi-icon.hbs --}}
<i aria-hidden="true" class="d-icons d-{{this.normalizedName}}"></i>
//goodbricks/app/components/navi-icon.js
Concrete Provider
denali icons
font awesome
Can Be Sourced From
App or Addon
Photo: 1LittleCraftyCorner
Plugins
Plugins Allow You To Add Additional Functionality To Your Application
Visualizations
display
configure
select
Registration & Discovery
AMD Modules
Container
owner.lookup('route:bricks')
resolve('route:bricks')
hasRegistration('route:bricks')
Registry
Resolver
goodbricks/routes/bricks
Knows All The Things
Module Registry
/* global require */
...
const TYPE = 'navi-visualization-manifest';
const REGEX = new RegExp(`^${modulePrefix}/${TYPE}/([a-z-]*)$`);
export default NaviVisualizationService extends Service {
...
all() {
const modules = Object.keys(require.entries); //all modules
const owner = getOwner(this);
return modules.filter(module => REGEX.test(module))
.map(vis => REGEX.exec(vis)[1])
.map(name => owner.lookup(`${TYPE}:${name}`));
}
});
Custom Visualization
$ember g navi-visualization lego-bar-chart
installing navi-visualization
create app/components/navi-visualizations/lego-bar-chart.js
create app/components/navi-visualization-config/lego-bar-chart.js
create app/navi-visualization-manifests/lego-bar-chart.js
create app/models/lego-bar-chart.js
create app/templates/components/navi-visualizations/lego-bar-chart.hbs
create app/templates/components/navi-visualization-config/lego-bar-chart.hbs
installing navi-visualization-test
create tests/integration/components/navi-visualizations/lego-bar-chart-test.js
create tests/integration/components/navi-visualization-config/lego-bar-chart-test.js
Blueprint
Extend & Replace
Replace Default Functionality With App Specific Logic
Made with: mecabricks.com
WHO WINS?
myaddon
|-- app
|-- components
|-- brick-card.js
goodbricks
|-- app
|-- components
|-- brick-card.js
ADDON APP
LET THE
WOOKIE HOST APP WIN
Photo: Lego Ideas
Part Numbers
Brick Counts
goodbricks/components/navi-cell-renderers/dimension
goodbricks
|-- app
|-- components
|-- navi-cell-renderers
|-- dimension.js
$ ember g component navi-cell-renderers/dimension
goodbricks/components/navi-cell-renderers/dimension
{{#if (eq name "partNumber")}}
<div class="navi-cell-renderer--lego-dim__container">
<div>
<img src="{{legoImgBaseUrl}}/{{value}}.png" alt="brick">
</div>
<span>{{value}}</span>
</div>
{{else}}
<span>{{value}}</span>
{{/if}}
import CellComponent from 'navi-core/components/cell-renderers/dimension';
export default class extends CellComponent {
classNames = ['navi-cell-renderer--lego-dim'];
}
// goodbricks/app/components/navi-cell-renderers/dimension.js
{{!-- goodbricks/app/templates/components/cell-renderers/dimension.hbs --}}
Escape Pod Hatch
Photo: Lego
-
🧪 Allows For Experimentation
-
👷♂️ Abstractions Are Hard
-
⏰ Patterns Take Time
-
🎧 Feedback For Formalization
Photo: dailytelegraph
Looking Towards The Future
Photo: Lego
Addon Ecosystem Has Been a Big Win
Duplo Ecosystem
Photo: Lego
Photo: brickmania
Photo: Lego
Photo: Sarah Goldschadt
Photo: Andy Baird
-
Standardized App Structure
-
Canonical Way To Style & Theme
-
Formalized Registration & Injection
-
Common State Management
-
Type Safety
Abstractions
time
Photo: theprisonerandthepenguin.com
Citizen Developer
Photo: Jon Kilroy
Thanks
Photo: Jon Kilroy
Questions
Photo: CC0 1.0
What about testing Duplos?
What about namespacing?
What about Embroider?
Photo: Jon Kilroy
Photo: Lego
-
Acceptance Test Duplo Integrations
-
Integration Test Custom Provider
-
Functional Test API Integrations
navi-${name}.js
<Navi::Component>
Build Time DI
Photo: sugarbeecrafts.com
Building Ember Apps with Duplos®
By jkusa
Building Ember Apps with Duplos®
- 1,772