Creating fluid app-like experiences with Ember

Nick Schot

Nick Schot

Co-founder @ Webhub

CS Software Technology @ University of Twente

nickschot

@nickschot

nickschot.nl

Creating the application

An experimental approach

 ember init emberconf2018
 ember install ember-cli-sass
 ember install ember-bootstrap

What do we need?

  • Basic (interactive) UI elements
    • Toolbars
    • Side menu
    • Panes
  • Pan gesture support
  • App-like transitions
  • Scroll state management

What's available?

  • ember-gestures
  • liquid-fire ember-animated
  • sidebar/menu addons
    • ember-burger-menu
    • ember-side-menu

The Bar

Apps

Fixed top/bottom bars

 

Scrollable viewport filling rest of the height

scrollable

viewport

fixed bar

fixed bar

Bars in the context of the page for easier animation

In the browser

Use the body as the scrolling element

The bars must be

position: fixed;

The bars cannot be inside an element which has a CSS transform

So it must be defined high in the route/template hierarchy

The component

  • Wrapper component yielding the bars
{{#mobile-bar-wrapper as |mbw|}}
  {{outlet}}

  {{#mbw.mobile-bar type='top'}}
    ...
  {{/mbw.mobile-bar}}

  {{#mbw.mobile-bar type='bottom'}}
    ...
  {{/mbw.mobile-bar}}
{{/mobile-bar-wrapper}}
  • Wrapper adjusts it's padding to height of bars

scrollable

viewport

fixed bar

fixed bar

{{mobile-bar isLocked=false}}

  • Follows downward touch scroll
  • Comes back into view with upward scroll
  • Also works for bottom bar!
  • Optionally set the collapsible height

The Menu

Menu

 

Full height

Scrollable

Mask which closes the menu

Gestures

Pan from edge to open

Pan on menu to close

Swipe

The Setup

Wrapper component yields sub-components and actions


{{!-- application.hbs --}}


{{#mobile-menu-wrapper as |mmw|}}
    {{#mmw.mobile-menu type='left' as |mm|}}
       {{#mm.link-to 'index'}}Home{{/mm.link-to}}
    {{/mmw.mobile-menu}}

    {{outlet}}
{{/mobile-menu-wrapper}}

Right and a left menus are supported

A link-to component which closes the menu on transition is provided

Edge pan recognition

const { x } = e; // pan event

if(!this.get('activeMenu')){
  if(x < 15 && leftMenu){
    this.set('activeMenu', leftMenu);
    this.set('isDraggingOpen', true);
  } else if(x > getWindowWidth() - 15 && rightMenu){
    this.set('activeMenu', rightMenu);
    this.set('isDraggingOpen', true);
  }
}

Pan start

  • Wrapper defined high in the DOM
     
  • Events trigger on the entire element

What about iOS?

We can't use edge gestures in iOS browsers :(

Conflicts with browser's "pan to go back"

  //Detect if the user is using the app 
  //from a browser on iOS
  
  userAgent: service(),

  _isIOSBrowser(){
    return this.get('userAgent.os.isIOS')
       && !window.navigator.standalone;
  }

We can use edge gestures when the app is "added to home"!

const { x } = e; // pan event

if(!this.get('activeMenu') && !this._isIOSBrowser()){
  if(x < 15 && leftMenu){
    this.set('activeMenu', leftMenu);
    this.set('isDraggingOpen', true);
  } else if(x > getWindowWidth() - 15 && rightMenu){
    this.set('activeMenu', rightMenu);
    this.set('isDraggingOpen', true);
  }
}
window.matchMedia('(display-mode: standalone)').matches

Detect Chrome/Firefox standalone

{{mobile-menu type='left'}}

The Pane

Features

Tab component with pan support

Panning between resource routes

Best we can do here is query params

The components

Pannable tabs with navigation

{{#mobile-pane activeIndex=idx onChange=(action 'changeQP') as |mp|}}

  {{mp.nav}}

  {{#mp.scroller as |mps|}}

    {{#mps.pane title="General"}}
       ...
    {{/mps.pane}}

  {{/mp.scroller}}
{{/mobile-pane}}

{{#mp.infinite-scroller
  previousModel=previousModel
  currentModel=model
  nextModel=nextModel
as |mpis|}}

  Hello world! My name is {{mpis.model.name}}

{{/mp.infinite-scroller}}

Infinite panes

There is another way we can use this!

A carousel is basically a set of panes with limited interaction and timed change

Just need to add a simpler indicator

{{#mobile-pane activeIndex=pane as |mp|}}
  {{#mp.scroller as |mps|}}
      {{#mps.pane}}
        ...
      {{/mps.pane}}
  {{/mp.scroller}}

  {{mp.simple-indicator}}

{{/mobile-pane}}

And some ember-concurrency

  changePane: task(function * (){
    while(true){
      yield timeout(5000);

      const newPane = (this.get('visiblePane') + 1) % this.get('panes.length');
      this.set('visiblePane', newPane);
    }
  }).restartable()
  mouseEnter(){ this.get('changePane').cancelAll(); },
  mouseLeave(){ this.get('changePane').perform(); },

  touchStart(){ this.get('changePane').cancelAll(); },
  touchEnd(){ this.get('changePane').perform(); }
  didInsertElement(){
    this._super(...arguments);
    get(this, 'changePane').perform();
  }

A problem...

While opening our side menu, the pane also activates!

Browser events

<html>

  <body>

    <div>

      <button value="Click me!" />

    </div>

  </body>

</html>

 Phases

  1. Capture
  2. Target
  3. Bubble

1. capture

3. bubble

But...

Ember works after the bubble phase...

ember-gestures doesn't support event capturing...

ember-mobile-core

Pan recognition

Touch event based pan recognition

  • Coordinates
  • Angle
  • Distance
  • Velocity

Lock support through service

Optional capture phase

Back to the side menu

1.  Enable capture events

import RecognizerMixin 
    from 'ember-mobile-core/mixins/pan-recognizer';

export default Component.extend(RecognizerMixin, {
  useCapture: true

  ...
}
const { x } = e; // pan event

if(!this.get('activeMenu')){
  if(x < 15 && leftMenu){
    this.lockPan();
    this.set('activeMenu', leftMenu);
    this.set('isDraggingOpen', true);
  } else if(x > getWindowWidth() - 15 && rightMenu){
    this.lockPan();
    this.set('activeMenu', rightMenu);
    this.set('isDraggingOpen', true);
  }
}

2.  Claim the pan lock on a valid edge pan

If a lock is taken, other pan events using ember-mobile-core won't be triggered

Everything now works as you'd expect

Nested panes

Correct locking makes parent pane take over when first/last nested pane is reached

Also note the navigation keeping the active item in view

Transitions

 with ember-animated 

Why?

 

  • Enhance perceived responsiveness

When used correctly transitions...

  • (Un)consciously inform users of context

Example

  • Title of previous page morphs into back button
  • New page slides over old page

An observation

Mobile transitions follow route hierarchy/context in a predictable way

We can abstract this into components

Route hierarchy

Router.map(function() {
  this.route('posts', function(){
    this.route('post', { path: ':post_id' }, function(){
      this.route('edit');
    })
  });
});
/posts
/posts/1
/posts/1/edit

 {{mobile-page}}

Component which...

  • Is aware of route transition direction
  • Selects the matching transition effect
  • Manages page/viewport style during transition
{{#mobile-page route='index' isRoot=true}}
   ...
{{/mobile-page}}
  • Manages scroll state

 {{mobile-page}}

Currently supports

  • Switching to an out of flow context
  • Transitioning down (slide left)
  • Transitioning up (slide right)

In the near future

  • Horizontal transitions (fade)
  • Switching to a "new" context

 {{top-toolbar}}

Similar to mobile-page

  • Transitions based on same rules as mobile-page
  • Provides ember-elsewhere portals
    • left button
    • title
    • right button
    • second row

Managing scroll-state

Use the hierarchy

Mobile scroll state should...

  • ​​Get restored when going up or sideways in the hierarchy
  • ​​Get reset when going down the route hierarchy

Scroll state

We use document based scrolling

  • Makes it a little harder during transitions
  • The transitioning element is not the scrolling element
  • Document size must be limited to prevent scrollbars
  • During transition scroll is applied as a CSS transform

Normally we'd use...

  • memory-scroll
  • ember-router-scroll

But that won't work with the transitions we have 

Manages scroll based on hierarchy and route transition

 {{mobile-page}}

Manages horizontal scroll state

 {{infinite-scroller}}

Responsiveness

The tools

ember-responsive

// Bootstrap 4.0.0 default breakpoints
export default {
  xs: '(max-width: 575px)',
  sm: '(min-width: 576px) and (max-width: 767px)',
  md: '(min-width: 768px) and (max-width: 991px)',
  lg: '(min-width: 992px) and (max-width: 1199px)',
  xl: '(min-width: 1200px)'
};

CSS media queries

// Bootstrap 4.0.0 media query util
@include media-breakpoint-only(xs){
    // applies only to XS
}


@include media-breakpoint-up(sm){ }
@include media-breakpoint-down(sm){ }
// hide or show element on mobile w/ classes
<div class="d-none d-sm-block"></div>
<div class="d-sm-none"></div>

May cause flicker when using fastboot

{{#if media.isXs}}
  {{!-- only shown on mobile --}}
{{/if}}

Templating example

Settings menu

  • Index route with menu
  • Nested routes with settings pages

Not what we'd like on desktop!

A solution

  • Move the settings menu into a component
  • Render it on:
    • the settings.index route
    • the settings route on desktop
  • Redirect settings.index to the first settings menu on desktop
beforeModel(){
  if(!this.get('media.isXs')){
    this.transitionTo('settings.general');
  }
}

Transitions

Mobile transitions don't work well on desktop

Transitions

Use ember-responsive to disable or modify the transitions


transitionsEnabled: computed.reads('media.isXs')
  {{#if transitionsEnabled}}
    {{#animated-value}}
      {{yield}}
    {{/animated-value}}
  {{else}}
    {{yield}}
  {{/if}}

Last remarks

  • Splash screen & Icon
  • Offline availability
  • Native notifications
  • "Add to home" notification

Progressive Web App enhancements

  • Corber/Cordova

Hybrid apps

Keeping (perceived) performance up

async model hooks

  • model hook blocks rendering
    and as such also the visual transition 
  1. use ember-concurrency to make model hook async
  2. use loader component

Keeping (perceived) performance up

svelte list rendering

vertical-collection

  • rendering a lot takes time

github.com/nickschot/ember-mobile-

menu

bar

pane

core

github.com/nickschot/emberconf2018

nickschot.github.io/emberconf2018

Try it yourself!

Creating fluid app-like experiences with Ember

By Nick Schot

Creating fluid app-like experiences with Ember

Mobile apps have UI patterns and features not often found in (mobile) web applications. We will take an in-depth look at ways to enhance user interaction for responsive Ember applications without having to develop a completely separate application. Topics include application organization, templating concerns, interaction patterns, relevant addons and a display of newly built Ember addons to fill in for missing interaction features.

  • 899