Tuesday, June 2, 2020

Angular Material Design Stepper icon and behavior

Angular material design stepper can be used to create wizard style UI to lead users to finish a task step by step. The behavior of the stepper is decided by the properties of stepper.

1. linear property on stepper 
If linear proper is set to false, then user can move to the next step by clicking the next button or by clicking on any step icon,  no matter the current step is completed or not.
If linear property is set to true, then the completed attribute on the current step must be set to true in order to move to the next step immediately following the current step, user cannot select any arbitrary step by clicking the step icon.
Linear property does not affect the step icon.

2. Completed property on each step
The completed property for each step indicates whether the input in the current step has passed the validation. 
Completed property works with the stepper's linear property mentioned above, if linear property is set to true, then the Completed property must be set to true for for user to move to the next step by clicking the next button.

3. Editable property on each step
The editable property for each step indicates whether user can move back to a step by clicking the previous button or by clicking the step icon.
If the editable property is set to false, then once user move to the next step, they will not be able to move back to this step again. In this case, the next step should not show the previous button to let user moves back.

The step icon is also controlled by the status of each step. By default, all step icons are shown as circled number as below if editable is set to true and completed set to false. The current step is indicated by blue background.

If a step's completed property is set to true, and it is not the current active step, and the step is not the current selected step, then the step icon will show the create icon (pen icon) to indicate this step has been finished and in good state as below
If a step's competed property is set to true and the step's editable property is also set to true, and the setp is not the current selected step, then the step icon will show the done icon (check mark ion) to indicate the step is finished and in good state and user cannot move back to edit or view it.


Developers can also customize the step icon using ngTemplate. The same icon customization will apply to all the steps.
There are three icons that can be customized, the matStepperIcon attribute value is used to indicate which icon should be used for which state:
matStepperIcon="number" - For the regular number icon 
matStepperIcon="edit" - For step of editable and completed
matStepperIcon="done" - For step of not editable and completed


The below code customizes the edit icon to alarm, the done icon to book, and the regular number icon to face. 

<mat-horizontal-stepper [linear]="isLinear" #stepper>
  <ng-template matStepperIcon="edit">
    <mat-icon>alarm</mat-icon>
  </ng-template>

  <ng-template matStepperIcon="done">
    <mat-icon>book</mat-icon>
  </ng-template>

  <!-- Custom icon with a context variable. -->
  <ng-template matStepperIcon="number" >
    <mat-icon>face</mat-icon>
  </ng-template>

The result looks like below


Developer can also customize the number only for the number icon as below
<ng-template matStepperIcon="number" let-index="index">
    {{index + 10}}
  </ng-template>


The related code used in the testing is shown below
Component html file:
<div>
  <button mat-raised-button (click)="isLinear = !isLinear" id="toggle-linear">
    stepper Linear: {{isLinear ? 'Enabled' : 'Disabled'}}
  </button>
</div>
<div>
  <button mat-raised-button (click)="isStep1Editable = !isStep1Editable">
    step 1 editable: {{isStep1Editable ? 'Editable' : 'Not Editable'}}
  </button>
  <button mat-raised-button (click)="isStep1Completed = !isStep1Completed">
    step 1 complete: {{isStep1Completed ? 'Completed' : 'Not Completed'}}
  </button>
</div>
<div>
  <button mat-raised-button (click)="isStep2Editable = !isStep2Editable">
    step 2 editable: {{isStep2Editable ? 'Editable' : 'Not Editable'}}
  </button>
  <button mat-raised-button (click)="isStep2Completed = !isStep2Completed">
    step 2 complete: {{isStep2Completed ? 'Completed' : 'Not Completed'}}
  </button>
</div>
<div>
  <button mat-raised-button (click)="isStep3Editable = !isStep3Editable">
    step 3 editable: {{isStep3Editable ? 'Editable' : 'Not Editable'}}
  </button>
  <button mat-raised-button (click)="isStep3Completed = !isStep3Completed">
    step 3 complete: {{isStep3Completed ? 'Completed' : 'Not Completed'}}
  </button>
</div>
<mat-horizontal-stepper [linear]="isLinear" #stepper>
  <mat-step [editable]="isStep1Editable" [completed]="isStep1Completed">
    <ng-template matStepLabel>Business information</ng-template>
    <mat-form-field>
      <mat-label>Name</mat-label>
      <input matInput placeholder="Last name, First name" [formControl]="firstInput" required>
    </mat-form-field>
    <div>
      <button mat-button matStepperNext>Next</button>
    </div>
  </mat-step>
  <mat-step [editable]="isStep2Editable" [completed]="isStep2Completed">
    <ng-template matStepLabel>Fill out your address</ng-template>
    <mat-form-field>
      <mat-label>Address</mat-label>
      <input matInput [formControl]="secondInput" placeholder="Ex. 1 Main St, New York, NY" required>
    </mat-form-field>
    <div>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button matStepperNext>Next</button>
    </div>
  </mat-step>
  <mat-step [editable]="isStep3Editable" [completed]="isStep3Completed">
    <ng-template matStepLabel>Done</ng-template>
    <p>You are now done.</p>
    <div>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button (click)="stepper.reset()">Reset</button>
    </div>
  </mat-step>
</mat-horizontal-stepper>
Component ts file
export class AppComponent implements OnInit {
  title = 'steppersample';
  isLinear = false;
  isStep1Editable = true;
  isStep1Completed = false;
  isStep2Editable = true;
  isStep2Completed = false;
  isStep3Editable = true;
  isStep3Completed = false;
  firstInput: FormControl;
  secondInput: FormControl;
  ngOnInit(){
    this.firstInput = new FormControl('');
    this.secondInput = new FormControl('');
  }
}

Monday, May 11, 2020

ionic layout related css styles

1. Relative layout attribute 
The first kind of layout css styles for ionic (or html) are used to specify where to put the current element to its parents' space, including:
slot attribute for ion-item element,
float attribute of ion-float-left, ion-float-right, etc

The second kind of layout css styles are used to specify where its children elements and the element itself 's content should be put in its own space, including:
text align attributes of ion-text-start, and ion-text-center, etc
Flex container horizontal attribute of ion-justify-content-start, ion-justify-content-center, etc. (applied to ion-row for grid)
Flex container vertical attribute of ion-align-self-start, ion-align-self-center (applied to ion-row for grid)
ion-padding (applied to ion-grid for grid)
ion-margin (applied to ion-grid for grid )

2. Difference between ion-spinner and LoadingController 
Both ion-spinner and LoadingController can be used to show a busy spinner on html page to indicate the current task is busy. The difference is ion-spinner is just a html element of busy cursor shown on the screen, and it does not affect or block other html element's function. So user can still click a button to execute its handler.
On the contrary, when LoadingController is created and present on the screen, it shows the busy cursor as well as other message on an overlay of the current html page, similar to showing a modal dialog box on native apps, so all other UI elements are disabled by default, and user cannot click a button or link to continue.

Html code:
  <ion-button (click)="onLogin()"> Login
  </ion-button>
  <ion-button (click)="onLogin(true)"> Login Direct
  </ion-button>
  <ion-button (click)="onLoginByController()"> Login By Controller
  </ion-button>
  <div class="ion-text-center">
    <ion-spinner color="primary" *ngIf="isLoading"></ion-spinner>
  </div>

ts code:
  onLogin(b) {
    this.isLoading = true;
    console.log('login clicked');
    if (b) {
      this.isLoading = false;
      this.authService.login();
    } else {
      setTimeout(() => {
        this.isLoading = false;
        this.authService.login();
      }, 10000);
    }
  }

  onLoginByController() {
    this.loadingCtrl.create({keyboardClose: true, message: 'Logging in...'}).then(loadingEl => {
      loadingEl.present();
      setTimeout(() => {
        this.authService.login();
      }, 10000);
    });
  }

Saturday, May 2, 2020

ionic side drawer or hamburger menu support

ion-menu can be used to support side drawer or hamburger menu on ios and android devices. There are some notes to use the ion-menu function.

ion-menu usually are defined in app component html file, and they are identified by menuId attribute. If multiple ion-menu elements are configured, then only the last one is enabled.
 <ion-menu side="start" contentId="main" menuId="menu1" disabled="false">
    <ion-header>
      <ion-toolbar>
        <ion-title>Menu 1</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
       <ion-list lines="none">
      <ion-menu-toggle>
        <ion-item routerLink="/places/search">
          <ion-icon name="business" slot="start"></ion-icon>
          <ion-label>Search Places</ion-label>
        </ion-item>
      </ion-menu-toggle>
      <ion-menu-toggle>
        <ion-item routerLink="/places/offers">
          <ion-icon name="checkbox-outline" slot="start">
          </ion-icon>
          <ion-label>Your Booking</ion-label>
        </ion-item>
      </ion-menu-toggle>
      <ion-menu-toggle>
        <ion-item (click)="onExit()" button>
          <ion-icon name="exit" slot="start">
          </ion-icon>
          <ion-label>Logout</ion-label>
        </ion-item>
      </ion-menu-toggle>
      </ion-list>
    </ion-content>
  </ion-menu>

In ion-pages, it can use ion-menu-button to show a side drawer or hamburger menu icon using ion-menu-button element as below. The menu attribute indicates which menu element (menu2 in this case) will show when user clicks the side drawer menu icon
<ion-header>
  <ion-toolbar>
    <ion-title>My Offers</ion-title>
    <ion-buttons slot="start">
      <ion-menu-button menu="menu2"></ion-menu-button>
    </ion-buttons>
    <ion-buttons slot="primary">
      <ion-button routerLink="/places/offers/new">
          <ion-icon name="add" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

However, in order to show the side drawer or hamburger menu icon on the page toolbar, the specified menu element must be enabled. An easy way to do so it enable it by calling the MenuController's enabled method as below. When enabling a menu element, all other menu elements will be automatically disabled.


  constructor(private menuCtrl: MenuController) { }


  ngOnInit() {
    this.menuCtrl.enable(true, 'menu2');
  }

In case needed, you can also use the below method to query all available menu element's status. 
      const m  = await this.menuCtrl.getMenus();
      m.forEach(el => {
        console.log(el.menuId);
        el.isOpen().then(d => {console.log('isOpen in loop: ', d); });
        console.log( 'isDisabled in loop: ', el.disabled);
      });

Note javascript await cannot be used inside forEach method, so then method is used to log the async method isOpen's result. Otherwise, using await is a better option to log the result synchronously when handling promise as return value as below
      console.log('menu 1 is Open', await this.menuCtrl.isOpen(el.menuId));
      console.log('menu1 is disabled', await this.menuCtrl.isEnabled(el.menuId));

When a menu item is clicked and handled, the menu will not close automatically. To close the menu automatically, the ion-item needs to be wrapped inside an ion-menu-toggle element. The ionic document is not very helpful in many cases.

Sunday, April 26, 2020

Angular children route configuration

For angular project, route configuration is used to decide which component should be loaded for the current path.

For app-routing.module.ts, the logic is quite simple, if the current url matches an item in routes configuration, then that component will be loaded in the <router-outlet></router-outlet> element specified in app.component.html.

Component can be either lazy loaded or normal loaded. In the below sample, component Com1 is lazy loaded, so the app-routing configuration only indicates which module (Com1Module) should be loaded, once the module is loaded, the module's route configuration will decide which component defined in the module will be loaded. On other hand, if a component is normal loaded, like Comp2Component for path com2, then the route configuration directly indicate which component should be loaded.

app-routign.module.ts

const routes: Routes = [  {    path: 'com1',    loadChildren: () => import('./com1/com1.module').then( m => m.Com1Module)  },  {    path: 'com2', component: Com2Component,    children: [      {        path: 'sub1',        component: Com2sub1Component      },      {        path: 'sub2',        component: Com2sub2Component      }    ]  },
];

In case the matched component's html file contains another

If the loaded component's html file contains a <router-outlet></router-outlet> element, then the route item can use children property to specify which a sub component path to be loaded into this new <router-outlet> element, this works in the same way for both lazy load and normal load. The below shows the com1 components' html and route module file.

Com1.component.html

<p>com1 works!</p><div><a routerLink="sub1" routerLinkActive="active">com1 sub1</a><br><a routerLink="sub2" routerLinkActive="active">com1 sub2</a></div><router-outlet></router-outlet>


Com1-routing.module.ts

const routes: Routes = [
  {
    path: '',
    component: Com1Component,
    children: [
      {
        path: 'sub2',
        loadChildren: () => import('../com1sub2/com1sub2.module').then(m => m.Com1sub2Module)
      }
    ]
  },
  {
    path:'sub1',
    loadChildren: () => import('../com1sub1/com1sub1.module').then( m =>m.Com1sub1Module)
  }
];


Friday, March 20, 2020

CSS style loading order by Angular

Angular provides several ways to specify css styles, as the css style loaded later takes priority over css style loaded before, so it is important to load the css style in proper order.

The test shows the css styles loaded by angular following the below orders:
1. css styles specified in index.html's header section
2. css styles specified in angular.json's styles section
3. css styles specified in component's css file