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

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: 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

Made with Slides.com