User Experience

Swiper, no swiping!

Recently, I was asked if there was a solution to disable the swipe to reveal action on particular list items in an Ionic List. I initially wasn’t sure about how to do this. The component itself does not have an enabled property that can be set.

After thinking about it some, I wondered if not having the < ion-item-options > included in the template might serve as a proxy solution. As it turns out, that is exactly what will work. If you do not include the < ion-item-options > in the template there is nothing to reveal, hence no swiping!

noswiping

If you built the Ionic2Do app from my book, here are the few changes you need to make to have this functionality.

First, we need to add a new editable property to our Task class:
  export class Task {
title: string;
status: string;
editable: boolean
}

Next, we need to update our sample data set to include this new property:
this.tasks = [
{ title: 'Milk', status: 'open', editable: false },
{ title: 'Eggs', status: 'open', editable: true},
{ title: 'Syrup', status: 'open', editable: true },
{ title: 'Pancake Mix', status: 'open', editable: true }
];

The last change is actually where it all comes together. In the template, we now use the *ngIf to test if the editable property is true or false. If it is true, then it is included in the render. If this property is false, it is not included in the render and the swipe action will not be allowed.

< ion-item-options *ngIf="task.editable" side="right" (ionSwipe)="removeTask(slidingItem, task)" >

And with that, we have selective control over which items can have the swipe to reveal gesture functioning.

Ionic Split Pane – Part 2

In this post, I am going to dig a little deeper into working with the new Ionic Split Pane component. I explored it some in this post. There are three elements I want to explore: a full header design, understanding how to navigate pages, and handling resizing.

Full Header Design

Several readers asked about how you might achieve this design:

full_header

A full header Ionic Split Pane

After playing around with different variations of the component structure and encountering some navigation troubles, I settled on using a basic CSS approach to this design.

The app.html is a standard component (sorry spacing with the brackets, blame WordPress):

This should look very close the documentation from Ionic. All we need to do is update the app.scss file with two changes:

The first change removes the line that divides the two headers. This line actually runs down the entire right side of the pane. So, with the second change, we restore the line back to the default style of a solid 1-pixel line of medium gray.
We still have two headers, but by not including any visible text in the main header, we initially have the illusion of a single header.

Handling Resizing

Here is where I encountered one of my first issues. The design worked fine in desktop sizes, but when I reduced the width below its breakpoint, my pane would disappear and along with it, my title.
The component has an event listener for when a pane state change is triggered. So, I changed my < ion-split-pane > tag to include (ionChange)=”updateTitles()”.
Now, I just needed to manage what the visible state of the pane is, and the broadcast this state to my other pages, so they could replace the empty title with my pane’s title.
Here is my app.componet.ts code:

We define a menuToggleState boolean to hold our pane’s visible state. This variable is set once the platform is ready. We have hard code the breakpoint to match the split panes. If you change that value, you will need to update it here as well.

The key to this solution is using the Ionic Events module to broadcast this value upon a state change triggered by the IonChange event. When that event occurs, we check the new width, and determine of the pane is now showing or not. Then we publish our custom event throughout our Ionic app.

In each page that is shown with the main content, I adjusted a few items. First, I had the title component is bound the title variable.

When we do not want a title visible, we set this variable to hold a space, rather than an empty string. The reason for this is, without content in the node, Ionic did not properly display any header icons. We also include a button that will auto display our hamburger menu icon when the split pane is hidden, giving us access to that content.

In the component’s code, we need to add the event listener for our custom event we broadcast from the resize action. Here is the full code:

This is for the Project Mercury page, each NASA project page has the similar event subscriber included in its constructor. For this sample, I did not rework this into a custom component to properly encapsulate the code as to not need to repeat so much of it across all these pages. The basic structure of the code listens for the event from the master component and depending on the state updates the title variable.

Now when I resize the window smaller than the breakpoint, the title is updated on the main content pages. Here is what it looks like when resized:

panesmall

and the menu being displayed:

pane_sidemenu

Page Navigation

Another tricky part of working with the split pane is understanding how to navigate pages. It was this issue that forced me to abandon several other attempts at a full header design. The heart of updating the main content from the pane is to use the @ViewChild.

Within the class for the app, we define our ViewChild to come from our root NavController using @ViewChild(Nav) nav: Nav;

Now, we can properly reference it in our component, and navigate correctly. Our list of manned NASA projects will call the projectSelected function when clicked and pass along which page to navigate to:

This function tells the nav that we referenced with the @ViewChild to set it’s rootpage to the project component we stored within our array and that is passed into this function. We also use the MenuController, to automatically close the pane if it is being shown like a normal sidemenu.

Navigation within the Main Content

If you want to have navigation with the main content to a new page, the normal navigation methods work just fine. In the sample, if you click on the NASA meatball logo, it will navigate to a new page showing the NASA worm logo. Clicking the worm logo will return you to the meatball page. However, you can now see the UX issue of having the hamburger menu and the back navigation control. But, the purpose of this exploration with just to get the core functionality working, and not worry about the UX issues. That is left to you and whatever your app’s design might be.
worm
Hopefully, this brief look further into the Split Pane component is useful. The sample code can be found in my GitHub repo. If you are wondering why I picked NASA as my sample, you can visit my other blog, https://spacequest.wordpress.com/ to learn why.

Ionic Split Pane Component – Part 1

Update (March 8, 2017)

Ionic has released version 2.2 of the framework! In addition, to support Angular 2.4.8, the split pane component is now included! Details on this upgrade see the release notes.

Update (February 28, 2017)

The component has been renamed from ion-split-panel to ion-split-pane. I will the GitHub repo soon. Also, note you will need to use this build of Ionic: npm install –save ionic-angular@2.1.0-201702281739
Recently the Ionic team released a preview of the split pane component in this blog post, I want to take a look at it in a bit more depth. For those not familiar with this visual design pattern, it is very similar to the side menu layout. The main difference is that menu usually remains onscreen. Gmail is a great example of this layout pattern. As Ionic begins to expand from the mobile space into progressive web apps and even the desktop, this layout is a common pattern.
Since this component is similar to the side menu layout, let’s scaffold our app using that template.
$ ionic start splitPaneDemo sidemenu --v2
Once the project is ready, go ahead and change the working directory to splitPaneDemo. Since the component is still under development, we need to swap out the release version of Ionic for the nightly build.
$ npm install --save ionic-angular
With our copy of Ionic replaced, go ahead and open app.html. There are just a few things we need to do to convert our side menu template to use the split pane layout.
< ion-split-pane >
  < ion-menu [content]="content" when="xs" >
    < ion-header >
      < ion-toolbar >
        < ion-title >Menu< /ion-title >
      < /ion-toolbar >
    < /ion-header >

    < ion-content >
      < ion-list >
        < button menuClose ion-item *ngFor="let p of pages"    
         (click)="openPage(p)" >
        {{p.title}}
      < /button >
      < /ion-list >
    < /ion-content >

  < /ion-menu >

  < ion-nav main [root]="rootPage" #content swipeBackEnabled="false" >< /ion-nav >
< /ion-split-pane >
We need to tell the component when the ‘menu’ should be displayed. The typical UX flow is to have the split pane hide when the screen or viewport become reduced. The component has the following breakpoints defined:
label min-width
xs 0px
sm 576px
md 768px
lg 992px
xl 1200px
never  –
By passing one of these strings to our when attribute we can control when our menu is shown. Another item to note, the min-width our split pane is 270px and it set to be no larger than 28% of the viewport. All these values are defined within the components SASS files.
< ion-menu [content]="content" when="xs" >
The last adjustment we need to denote the ‘main‘ content for the split pane. For this, just include the main directive to
< ion-nav main [root]="rootPage" #content swipeBackEnabled="false" >
 Running $ ionic serve, will produce this:
screencapture-localhost-8100-1487963384735

The sidemenu template being rendered as a split pane layout.

If you include menuToggle on the header of the main pages, the split pane will understand that directive and use it when the split pane is hidden.
Now, this initial sample is not much more than sidemenu with the expose-aside-when value that was available in v1. Let’s explore a more complex sample.
In this sample, we will enable the split pane to have its own navigation stack that it independent of the main content’s navigation stack. This was a design pattern that I was never able to build using Ionic v1. I had several app ideas that would have been a perfect match for it (yes, I am starting to flesh those apps out now).
First, let’s generate a collection of new pages using the Ionic generate command:
$ ionic g page Main
$ ionic g page SideNav
$ ionic g page SideNav2
$ ionic g page View1
 Next, make sure you update app.module.ts to import these new views. In addition, take note of setting both the root and sideRoot variables, as well as setting myApp within the @NgModule.
import { NgModule, ErrorHandler, Component } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MainPage } from '../pages/main/main';
import { SideNavPage } from '../pages/side-nav/side-nav';
import { SideNav2Page } from '../pages/side-nav2/side-nav2';
import { View1Page } from '../pages/view1/view1';

@Component({
 templateUrl: 'app.html'
})
export class myApp {
 root = MainPage;
 sideRoot = SideNavPage;
}

@NgModule({
 declarations: [
 myApp,
 MainPage,
 SideNavPage,
 SideNav2Page,
 View1Page
 ],
 imports: [
 IonicModule.forRoot(myApp)
 ],
 bootstrap: [IonicApp],
 entryComponents: [
 myApp,
 MainPage,
 SideNavPage,
 SideNav2Page,
 View1Page
 ],
 providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}
Next, let’s adjust the app.html file for our new structure. Instead of directly defining the content of the split pane, we are now just including an element and setting its root property to sideRoot. We will use this reference to populate the content.
< ion-split-pane when="sm" >

  < ion-menu [content]="content" >
    < ion-nav [root]="sideRoot" >< /ion-nav >
  < /ion-menu >

  < ion-nav [root]="root" main #content >< /ion-nav >

< /ion-split-pane >

Since both of these containers had their own navigation stack, we can move through our application independently. Let’s give our various pages some content first so we can see all this in action.

Change side-nav.html to:

< ion-header >

< ion-navbar >
< ion-title >Components< /ion-title >
< /ion-navbar >

< /ion-header >


< ion-content >
< ion-list >
< button menuClose ion-item *ngFor="let p of pages" (click)="displaySubNav(p)" >
{{p.title}}
< /button >
< /ion-list >
< /ion-content >

and side-nav.ts to

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { SideNav2Page } from '../side-nav2/side-nav2';

@Component({
 selector: 'page-side-nav',
 templateUrl: 'side-nav.html'
})

export class SideNavPage {
 pages: Array<{ title: string, component: any }>;

 constructor(public navCtrl: NavController, public navParams: NavParams) {
 this.pages = [
{ title: 'Action Sheets', component: SideNav2Page },
{ title: 'Alerts', component: SideNav2Page },
{ title: 'Badges', component: SideNav2Page },
{ title: 'Buttons', component: SideNav2Page },
{ title: 'Cards', component: SideNav2Page },
{ title: 'Checkbox', component: SideNav2Page },
{ title: 'DateTime', component: SideNav2Page },
{ title: 'FABs', component: SideNav2Page },
{ title: 'Gestures', component: SideNav2Page },
{ title: 'Grid', component: SideNav2Page },
{ title: 'Icons', component: SideNav2Page },
{ title: 'Inputs', component: SideNav2Page },
{ title: 'Lists', component: SideNav2Page },
{ title: 'Loading', component: SideNav2Page },
{ title: 'Menus', component: SideNav2Page },
{ title: 'Modals', component: SideNav2Page },
{ title: 'Navigation', component: SideNav2Page },
{ title: 'Popover', component: SideNav2Page },
{ title: 'Radio', component: SideNav2Page },
{ title: 'Range', component: SideNav2Page },
{ title: 'Searchbar', component: SideNav2Page },
{ title: 'Segment', component: SideNav2Page },
{ title: 'Select', component: SideNav2Page },
{ title: 'Slides', component: SideNav2Page },
{ title: 'Tabs', component: SideNav2Page },
{ title: 'Toast', component: SideNav2Page },
{ title: 'Toggle', component: SideNav2Page },
{ title: 'Toolbar', component: SideNav2Page }
];

}

 displaySubNav(thePage:any) {
  this.navCtrl.push(thePage.component);
 }
}

Next, let’s change the side-nav2.html file to be this:

< ion-header >

 < ion-navbar >
  < ion-title >Details< /ion-title >
 < /ion-navbar >

< /ion-header >

< ion-content >
 < p >New content< /p >
< /ion-content >

We don’t need to change the side-nav2.ts for this simple demo. Let’s change our main.html file this:

< ion-header >

 < ion-navbar >
  < ion-title >Main< /ion-title >
 < /ion-navbar >

< /ion-header >

< ion-content padding >
 < button ion-button primary (click)="goNewView()" >Go View 1< /button >
< /ion-content >

and the main.ts to:

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { View1Page } from '../view1/view1';

@Component({
 selector: 'page-main',
 templateUrl: 'main.html'
})
export class MainPage {

constructor(public navCtrl: NavController, public navParams: NavParams) { }

ionViewDidLoad() {
 console.log('ionViewDidLoad MainPage');
 }

goNewView() {
 this.navCtrl.push(View1Page);
 }
}

Save the files, and try it out.

split-panel-demo

Independent Ionic navigation in the split pane layout.

The source code for this demo is available at my GitHub Repo.

StageWebView Loading Improved

Recently, someone asked about trying to improve hiding the white rectangle of the StageWebView. I got to thinking about this and tried a little experiment. Did I have to set the viewPort during the normal configuration, or could I wait?

So, I decided to try it. I moved the setting of the StageWebView’s viewPort until the page had been loaded. We know this via an Event.COMPLETE event. Here is the sample code:

import flash.media.StageWebView;
import flash.events.Event;

var webView:StageWebView = new StageWebView();
webView.stage = this.stage;
//webView.viewPort = new Rectangle(0, 0, 480, 200);
webView.assignFocus();
webView.loadURL("http://aj-software.com/clients/mobile/360Flex/2011/mobile/map/index.html");

webView.addEventListener(Event.COMPLETE,handleLoad);

function handleLoad(evt:Event):void
{
webView.viewPort = new Rectangle(0, 0, 480, 300);
}

stop();

Demo File

Watch your focus

Many of you might know, I enjoy a fine craft brew from time to time, and living in San Diego, one of the best microbrewery towns certainly helps. To keep track of my various tastings, I use the Untappd service (here is my profile). The app follows many of the same ideas as Foursquare; instead of places you check in with a beer. They also have a badge system among other features you would expect from a social media service.

What is interesting about the app is some of it’s history. Originally, this service was only available as a web app. A fact they were quite proud of. I would usually refer to them as the example of going the web app route versus building a native application.

Recently, they made the leap into the native app realm, by wrapping their web app with PhoneGap. This makes good sense given their previous investment. But this blog post is not about this choice, but rather to talk about a UI choice.

untappd iOS home screen

Take a look at the initial screen from the iOS version of the app. What has focus?

If you said ‘Drink Up’ or the larger yellow tab with beer bottles, you would be wrong! Instead, it is the Friends tab. So many times, I go to check in with a beer and begin typing the beer’s name in the search field only to be informed that the beer can not be found in my friends. Then I remember that I need to switch tabs to log my beer.

untappd Drink Up screen

untappd Drink Up screen

Why do I make this error? Well the designers have placed more visual emphasis on the Drink Up tab than then other tabs. By adding this visual weight to the element, a casual glance of the UI would lead the user to assume that the Drink Up tab was selected and the screen’s contents were based on that selection. In addition, except for the hint text in the search field, there is no difference in the top portion (about 20%) of either screen.

Now, logging your beers is the primary task of this app, and hence the reason for the visual emphasis for the tab’s look and feel. But what fails is that this tab is not automatically selected focus. It is an easy fix, programmatically set the default state to the Drink Up tab. One could go a bit further and maybe repeat the icons that are used on the tabs next to the search field. Let’s face it, this is an app you will use with alcohol! 😉

So, as you develop your next great mobile application, take care in what you provide focus on and how this reflected in the code. Now what’s in my ‘fridge to drink…

Understanding StageWebView and the viewport tag

My current solution for displaying dynamic interactive maps in my Android applications is to display a HTML-based Google Map within an instance of StageWebView. For the most part this has worked well, except for the long start up process. This often gives the appearance of the application being frozen. So, I sought a solution to try to improve the user experience.

The first step was to find a way to hide the StageWebView until it had finished its initialization and contacted the server. Since this component is of the ‘Stage’ type, this means that the normal display manipulation will not work. Instead, I created the viewport of the StageWebView offscreen. Now, I just need to know when it has contacted the server and the first page has been loaded. For this, you can listen for the Location Changed event. (Note, there appears to be a bug in AIR for Android 3.0, where this event is only fired upon the first URL that is accessed. This is important later).

Once the page has been loaded into our StageWebView, I reset the viewport to be on-screen. Since I have a loading spinner animating on the stage, this delay is now neatly masked.

Loading Indicator

Loading spinner

However, my screen still shows a white StageWebView. Then the map tiles are displayed, followed by the map controls. So how can I improve this portion of the process?

StageWebView initializing

The white StageWebView

In exploring the Google Map forums, I came across the notion of first serving a static Google Map, then waiting for the “tiles_loaded” event to be fired to swap the static map with the dynamic one. This was as simple as adding an <img> tag for the static map, and an event listener. In my web browser, this solution worked flawlessly.

Since there is a Location_Changing event that StageWebView is supposed to fire when the URL is changing, I thought I could tie into this by changing the page’s URL with an # attribute. But I then learned that this event does not currently properly fire on Android devices. I had planned to keep the StageWebView off-screen until the dynamic version of the map was ready before changing its viewport location.

Instead, I was forced to display the static map, then allow the loading of tiles and the map controls to occur visibly to the user. I soon ran into another problem. I had defined the width of the static map to be 480 pixels wide (I would have custom versions of the html page per device width), but once the html file was loaded into the StageWebView, I was able to scroll the image horizontally. What was going on!? Everything was working fine in my desktop browsers.

Map with scroll bars

Note the scroll bars

So, I began looking into the various attributes of the viewport meta tag, to see if there was something I could do to adjust the viewport. If you are not familiar with this tag, the Android documentation has a great reference on it. If you read through their documentation, you will learn that web pages on high-density devices (which is required to run AIR for Android) are automatically scaled by 150%. Eureka! Now I had confirmed why the static image became scrollable, now to find the solution. Reading further, there is another interesting attribute that can be set: target-densitydpi. To quote the documentation, “You can change the target screen density for your web page using the target-densitydpi viewport property.” This property has five values; device-dpi, high-dpi, medium-dpi, low-dpi, or a value. The one of interest for us is the device-dpi. If the target-densitydpi is set to device-dpi, the page will use the device’s native dpi as the target dpi and default scaling never occurs. And with this one addition to our viewport tag, our html file will now renders correctly on-device. Here is a sample apk and the source files.