Mind is Software

Ying’s thoughts about software and business

Advanced Ionic Notes

This is a study note of Ionic framework based on Elite Ionice Course that covers testing, performance, and user experience.

1 Performance

There are a few steps for a browser to render a frame of an application: processing JavaScript code that changes a page, applying styles, calculating layout, paiting a frame, compositing layers of painting (such as opacity change). To improve the app performance, you should avoid as many of the steps as possbile and make the necessary steps as efficient as possible. For example, adding contain: layout; to a modal, the layout calculating is improved significantly.

Using ‘–prod’ option to build an app to enable Ahead-of-Time compilation and optimization such as tree shaking and minimization. You should debug a production build. To see a production build in browser, use npm run ionic:build --prod.

Reduce resource size is important. TinyPng optimizes images. Add 3rd party packages using npm.

It is preferred to manage all CSS files in build process. Copy node_module/@ionic/app-scripts/config/sass.config.js to your config folder. Edit the file to include the existing CSS files and new paths. Then edit package.json file to include "config": {"ionic_sass": "./config/sass.config.js"}. Then add your css file to src/theme/variables.scss as @import "your-css-file";.

2 Components and Directives

2.1 Basic Concepts

A component can have multiple selectors separated by a comma. A selector can be selected by an element name or an atrribute. The @Component decorator tags a class as an Angular component that has change detection and propagation.

A component receives data via input binding @Input() someInput: someType. A component can trigger event via @output() someEvent = new EventEmitter(). A component can recieve data or send out data via service injection.

The primary role of a directive is to attach behaviour to the DOM elements. A directive can be added as an attribute to an elment or can exist on its own. For example, ion-list is a directive but not a component.

2.2 Component Reference and Projected Content

You can grab a reference to the host element of a component or a directive by injecting ElementRef in its constructor. Though you can access the underlying natvie element via nativeElement property, you shouldn’t use it directly for portability. InjectRenderer and use its methods are the recommended way to manipulate host element.

The content supplied between the component tags will be projected into the the ng-content tag. Angular supports multiple content projections. The ng-content can have a select attribute that matches the projected content.

Use @ViewChild(selector) ref: RefType to get a reference to a component. To get its native element, use @ViewChild(selector, {read: ElementRef}) ref: ElementRef. The selector can be a class name of the component or an element reference defined using #myRef. Depending on the attached element of the element reference, the return type could be an element type or ElementRef if it is a normal HTML element. Use @ViewChild or @ViewChildren to grab elements added to the component by yourself. Use @ContentChild or @ContentChildren to grap elements projected by ng-content.

2.3 Listen for Events and Bind to Properties

Use @HostListener('eventName', ['eventArg',...] handler(arg) {...} to listen for host component events and pass event parameters.

Use HostBinding to add/remove classes or attributes to/from the host component. @HostBinding('class.my-class') applyTheClass = true; add the my-class to host component when applyTheClass is true. @HostBinding('attr.someAttribute') attributeToApply = 'whatever'; add someAttribute=whatever to the host component. @HostBinding('id') idToApply = 'whatever'; set host component id to whatever.

An alternative method is to use host metadata to define event listener and property binders.

3 Test

3.1 TDD

TDD is about using automated tests to drive the development. Following the TDD philosophy, the recommended development process is:

  • write an e2e test for a specific requirement.
  • the e2e test fails
  • decide what functionality needs pass the e2e test.
  • write a unit test for the functionality.
  • the unit test fails.
  • implement the code to satisfy the unit test.
  • pass the unit test, revise the code if it fails.
  • pass the e2e test, add/revise functionality if it fails.

The failure steps of both e2e and unit test are necessary to make sure two things: 1) the test is writting correctly, and 2) force to work within the parameters of the test.

To start a project, first come with a list of requirements and corresponding e2e tests. Then prioritize the requirements and e2e tests to make a minimum viable product (MVP) ASAP.

The key difference between a unit test and an E2E test is that a unit test will test code, whereas an E2E will test behaviour. An e2e test tests the page contents that include the current page and the navigated pages. A unit test only checks the logic of the current page. Therefore starting from the homepage, every e2e test tests the current page content and navigation to child pages. Each page’s unit test tests the component logic such as componentent creation and setting member variables from the navigator’s parameters.

3.2 Testing Utilities

3.2.1 Setup e2e Test

Use a help page object to setup navigation and retrieve content.

// in an page object file
import { browser, element, by } from 'protractor';
export class HomePageObject {
    browseToPage() {
        browser.get('');
    }

    getPageContent() {
        return element.all(by.css('selector'));
    }
}

// in an e2e test file
import { LessonSelectPageObject } from './page-objects/lesson-select.po'
import { HomePageObject } from './page-objects/home.po'

describe('Home', () => {
    let homePage: HomePageObject
    let lsPage: LessonSelectPageObject

    beforeEach(() => {
        homePage = new HomePageObject()
        lsPage = new LessonSelectPageObject()
        homePage.browseToPage()
    })

    it('should be able to view a list of modules', () => {
        expect(homePage.getPageContent()...)
    })
})

3.2.2 Setup Unit Test

Use TestBed to compile and create a component used in a test. If a component uses an external template, you need to compile it asynchronously. The sample code is as the following:

let de: DebugElement;
let comp: MyPage;
let fixture: ComponentFixture<MyPage>;

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [MyPage],
    imports: [IonicModule.forRoot(MyPage)],
    providers: [
      { provide: DeepLinker, useClass: DeepLinkerMock },
      { provide: NavController, useClass: NavMock }
    ]
  }).compileComponents();
}));

beforeEach(() => {
  fixture = TestBed.createComponent(MyPage);
  comp = fixture.componentInstance;
  de = fixture.debugElement;
});

In the above code, the DeepLinker is required for the component to compile properly when using lazy loding (To Be Confirmed).

3.2.3 Test Utitlities

When Ionic navigates from one page to another page, it will activate an overlay that prevents clicks for a short duration. Where there are multiple click events, it could be an issue. Use the following code to wait till it is ready:

import { protractor, browser, element, by } from "protractor";

export class AppPageObject {
  waitForClickBlock() {
    let clickBlockElement = element(by.css(".click-block-active"));
    browser.wait(protractor.ExpectedConditions.stalenessOf(clickBlockElement));
  }
}

// in other test after click event
app = new AppPageObject();

// in the brwoseToPage method
this.parent.getItem().click();
app.waitForClickBlock();

Inside a test case, use const navCtrl = fixture.debugElement.injector.get(NavController) to get injected objects. If a fixture is not avaialbe (not testing a component), use inject in a test case to explicitly inject service providers.

Use navParams.get = jasmine.createSpy('get').and.returnValue(...) to add testing data to navParams.

3.2.4 Async Test

Run async test cases in fakeAsync() that catches and controls all asynchrounous operations. Call flashMicrotasks(), tick() or tick(timeout) to clear microtasks (Promise and Observable). Call tick(timeout) to advance timer for a macrotask (setTimeout()).


Share