Nick Schot
Co-founder @ Webhub
CS Software Technology @ University of Twente
nickschot
@nickschot
nickschot.nl
An experimental approach
ember init emberconf2018
ember install ember-cli-sass
ember install ember-bootstrap
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
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
{{#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}}
scrollable
viewport
fixed bar
fixed bar
Full height
Scrollable
Mask which closes the menu
Gestures
Pan from edge to open
Pan on menu to close
Swipe
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
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
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
Tab component with pan support
Panning between resource routes
Best we can do here is query params
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
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}}
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();
}
While opening our side menu, the pane also activates!
<html>
<body>
<div>
<button value="Click me!" />
</div>
</body>
</html>
Phases
1. capture
3. bubble
Ember works after the bubble phase...
ember-gestures doesn't support event capturing...
Touch event based pan recognition
Lock support through service
Optional capture phase
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
Correct locking makes parent pane take over when first/last nested pane is reached
Also note the navigation keeping the active item in view
When used correctly transitions...
Mobile transitions follow route hierarchy/context in a predictable way
We can abstract this into components
Router.map(function() {
this.route('posts', function(){
this.route('post', { path: ':post_id' }, function(){
this.route('edit');
})
});
});
/posts
/posts/1
/posts/1/edit
Component which...
{{#mobile-page route='index' isRoot=true}}
...
{{/mobile-page}}
Currently supports
In the near future
Similar to mobile-page
Mobile scroll state should...
We use document based scrolling
But that won't work with the transitions we have
Manages scroll based on hierarchy and route transition
Manages horizontal scroll state
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}}
Settings menu
Not what we'd like on desktop!
beforeModel(){
if(!this.get('media.isXs')){
this.transitionTo('settings.general');
}
}
Mobile transitions don't work well on desktop
Use ember-responsive to disable or modify the transitions
transitionsEnabled: computed.reads('media.isXs')
{{#if transitionsEnabled}}
{{#animated-value}}
{{yield}}
{{/animated-value}}
{{else}}
{{yield}}
{{/if}}
Progressive Web App enhancements
Hybrid apps
async model hooks
svelte list rendering
vertical-collection
Try it yourself!