Table of Contents

Introductions

Welcome! Below you’ll find all slides and instructions for the hands-on activities being held throughout the day. The site will remain up well after the event so you can refer back to the material at any time.

Prerequisites

All attendees must have the NativeScript CLI installed, and all system requirements in place to develop for either iOS or Android. If you haven’t already, complete the installation instructions using the link below.

TIP: If you run into issues completing the setup instructions, try joining the NativeScript Slack and asking questions in the #getting-started channel.

Additionally, please also download and install Visual Studio Code, and then install the editor’s NativeScript extension. You’ll be using NativeScript’s Visual Studio Code extension as you complete this guide.

What is NativeScript?

Below please find slides describing NativeScript:

The NativeScript CLI

Now that you have the basics down, let’s look at how to actually build NativeScript apps.

Exercise 1: Creating an app

Navigate to a folder you’d like to store apps in as you go through this workshop, and then run the following two commands.

tns create HelloWorld --ng
cd HelloWorld

Exercise 2: Running your app

You use the tns run command to run NativeScript apps. If you’d like to complete this workshop on iOS, run the following command.

tns run ios

And if you’d like to complete this workshop on Android, run the following command instead.

tns run android

Exercise 3: Making some changes

Open your app/item/items.component.html file and change the title attribute of the <ActionBar>.

Debugging NativeScript apps

Together we’ll go through the instructions to debug NativeScript apps in Visual Studio Code.

Project setup

The files for this part of the workshop are in the /warmup folder. Clone the repo and navigate to its /warmup folder by running the following commands.

git clone https://github.com/NativeScript/workshop.git
cd workshop/warmup

Next, run the project in an emulator using one of the command below.

tns run ios

Or

tns run android

Lesson 1 - UI

In this lesson we are going to familiarize ourselves with some of the most commonly used UI components in NativeScript.

For this exercise we will use the contents of the app/profile folder, which already contains some pieces of the app that we need.

If you are using Playground then you should head to: https://play.nativescript.org/?template=nsday-profile`

If you open profile.component.ts you will notice that our component has an attribute profile with some populated values. In the next few steps we will create a screen that will allow us to display and edit these values.

Here are some examples of UI components:

<Label text="name"></Label>
<TextField hint="your name here" text="Jack"></TextField>
<Switch checked="true"></Switch>
<DatePicker [day]="2" [month]="2" [year]="2002"></DatePicker>
<Slider [minValue]="0" [maxValue]="10" [value]="3"></Slider>
<Button text="Do Something" (tap)="clear()"></Button>

Attribute binding

The above example uses hardcoded values, but you can also use one way binding (with [] around the attribute you want to bind to) to display the values in the profile.

For example:

<Label [text]="profile.name"></Label>
<TextField [text]="profile.name" hint="name"></TextField>

Events

UI Components include events to handle common user interactions. A component can handle events such as: tap, doubleTap, pinch, pan, swipe, rotation, longPress, touch. To leverage them, use the (eventName) notation.

For example:

<StackLayout (longPress)="clearForm()">
  <Label text="Action:" (swipe)="printDirection($event)"></Label>
  <Button text="Do Something" (tap)="doSomething()"></Button>
</StackLayout>

Exercise: Use UI components

Exercise: Use Label, TextField, Switch, DatePicker, Slider, Button

Recreate the below UI and bind the input components to the profile attributes. Additionally, make sure that the two buttons call save() and clear() respectively.

HINT To make a TextField password friendly use secure="true".

Recreate UI

Edit profile.component.html and have fun.

NOTE: Your goal at this point is just to get the initial values of the profile property to show up. If you change the values of the form fields and you don’t see those changes when you hit Save don’t worry; we’ll tackle that in the next section.

<ActionBar title="Profile" class="action-bar">
</ActionBar>

<TabView selectedIndex="0" >
  <ScrollView *tabItem="{title:'UI Components'}">
    <StackLayout>
      <Label text="Name:"></Label>
      <TextField
        [text]="profile.name"
        hint="name">
      </TextField>

      <Label text="Password:"></Label>
      <TextField
        [text]="profile.password"
        hint="password"
        secure="true">
      </TextField>

      <Label text="Angular Pro:"></Label>
      <Switch
        [checked]="profile.angularPro"
        class="switch"
        horizontalAlignment="left">
      </Switch>

      <Label text="Date of Birth:"></Label>
      <DatePicker 
        [day]="profile.dob.getDate()"
        [month]="profile.dob.getMonth()+1"
        [year]="profile.dob.getYear() + 1900">
      </DatePicker>
      
      <Label text="Coding power:"></Label>
      <Slider
        [value]="profile.codingPower"
        [minValue]="0"
        [maxValue]="10"
        class="slider">
      </Slider>

      <Button text="Save" (tap)="save()"></Button>
      <Button text="Clear" (tap)="clear()"></Button>
    </StackLayout>
  </ScrollView>

  <GridLayout *tabItem="{title:'DataForm'}" rows="*, auto, auto">
    <!-- http://docs.telerik.com/devtools/nativescript-ui/Controls/Angular/DataForm/Editors/dataform-editors-list -->
    <RadDataForm [source]="profile" row="0">

    </RadDataForm>
    <Button text="Save" (tap)="save()" class="btn btn-primary" row="1"></Button>
    <Button text="Clear" (tap)="clearForm()" class="btn btn-primary" row="2"></Button>
  </GridLayout>
</TabView>

Two-way binding

One-way binding is not particularly useful for input forms. This is where [(ngModel)] comes in handy.

Each of the input components you used a moment ago allows you to use [(ngModel)] to configure two-way binding:

<TextField [(ngModel)]="email" hint="your name here"></TextField>
<Switch [(ngModel)]="optIn"></Switch>
<DatePicker [(ngModel)]="dob"></DatePicker>
<Slider [(ngModel)]="size" [minValue]="0" [maxValue]="10"></Slider>

Exercise: Two way binding

Exercise: Two-way binding

Before you use [(ngModel)] in your app, you need to add NativeScriptFormsModule to @NgModule => imports, so let’s do that now.

Step 1

Open app.module.ts and uncomment the line that imports NativeScriptFormsModule.

import { NativeScriptFormsModule } from 'nativescript-angular/forms';

Then uncomment the line that adds NativeScriptFormsModule to the @NgModule imports array:

imports: [
  NativeScriptModule,
  AppRoutingModule,
  NativeScriptUIDataFormModule,
  NativeScriptHttpModule,
  NativeScriptFormsModule
],

Step 2

Update all input components to use two-way binding. Test it by clicking the Clear and Save buttons and see what happens.

NOTE: To keep an eye on the slider value, print it in the label above it:

<Label [text]="'Coding power:' + profile.codingPower"></Label>
<ActionBar title="Profile" class="action-bar">
</ActionBar>
<TabView selectedIndex="0" >
  <ScrollView *tabItem="{title:'UI Components'}">
    <StackLayout>
      <Label text="Name:"></Label>
      <TextField
        [(ngModel)]="profile.name"
        hint="name">
      </TextField>

      <Label text="Password:"></Label>
      <TextField
        [text]="profile.password"
        hint="password"
        secure="true">
      </TextField>

      <Label [text]="'Angular Pro: ' + ((profile.angularPro) ? 'Yes': 'No')"></Label>
      <Switch
        [(ngModel)]="profile.angularPro"
        class="switch"
        horizontalAlignment="left">
      </Switch>

      <Label [text]="'Date of Birth: ' + profile.dob.toLocaleDateString()"></Label>
      <DatePicker
        [(ngModel)]="profile.dob">
      </DatePicker>
      
      <Label [text]="'Coding power:' + profile.codingPower"></Label>
      <Slider
        [(ngModel)]="profile.codingPower"
        [minValue]="0"
        [maxValue]="10">
      </Slider>
      
      <Button text="Save" (tap)="save()"></Button>
      <Button text="Clear" (tap)="clear()"></Button>
    </StackLayout>
  </ScrollView>

  <GridLayout *tabItem="{title:'DataForm'}" rows="*, auto, auto">
    <!-- http://docs.telerik.com/devtools/nativescript-ui/Controls/Angular/DataForm/Editors/dataform-editors-list -->
    <RadDataForm [source]="profile" row="0">

    </RadDataForm>
    <Button text="Save" (tap)="save()" class="btn btn-primary" row="1"></Button>
    <Button text="Clear" (tap)="clearForm()" class="btn btn-primary" row="2"></Button>
    
  </GridLayout>
</TabView>

Theme

Now that we have the profile page doing something sort of useful, let’s make it look a little bit better.

The good news is that NativeScript comes with many built in themes.

Color Schemes

Most of the standard UI components come with styles that you can use for quick styling improvements.

Text-based components can use:

  • text-primary, text-muted, text-danger to change the text color,
  • text-center, text-left, text-right to change the alignment of the text,
  • text-lowercase, text-uppercase, text-capitalize to apply text transformation

For example:

<Label text="Name" class="text-primary text-right"></Label>
<Label text="Email" class="text-danger"></Label>

Buttons can use:

  • btn, btn-primary, btn-outline, btn-active - to change the general style
  • btn-rounded-sm and btn-rounded-lg - to make the buttons rounded
  • btn-blue, btn-brown, btn-forest, btn-grey, btn-lemon, btn-lime, btn-ruby, btn-sky - to change the primary color (this only works in conjunction with btn-primary)

For example:

<Button text="Primary" class="btn btn-primary"></Button>
<Button text="Outline" class="btn btn-outline"></Button>
<Button text="Orange" class="btn btn-primary btn-ornage"></Button>
<Button text="Rounded Grey" class="btn btn-primary btn-grey btn-rounded-sm"></Button>

Recreate UI

Other components can use

  • action-bar - for the default <ActionBar> styling
  • switch - for the default <Switch> styling
  • slider - for the default <Slider> styling

Margins and padding

You can use predefined styles for margins and padding. Use m for margin and p for padding. Then optionaly add:

  • -t: top
  • -b: bottom
  • -l: left
  • -r: right
  • -x: horizontal (i.e. both left and right)
  • -y: vertical (i.e. both top and bottom)

Finally add the size: 0, 2, 4, 5, 8, 10, 12, 15, 16, 20, 24, 25, 28, 30

For example:

<StackLayout class="m-x-10 p-5">
  <Label text="name" class="m-l-10"></Label>
  <Button text="Go" class="btn btn-primary p-20"></Button>
</StackLayout>

Note: To read more about themes, go to the NativeScript theme docs.

Exercise: Theme

Exercise: NativeScript Theme

Step 1

Update the UI to make it look more like the one in the picture below.

Recreate UI

HINT 1 You may need to update the margin on the StackLayout, so that the UI components don’t stay too close to the edge of the screen.

HINT 2 If you are working with a small screen, you may need to add <ScrollView> around the <StackLayout> to allow the user to scroll in the screen. Like this:

<ScrollView>
  <StackLayout>
    ...
  <StackLayout>
</ScrollView>
<ActionBar title="Profile" class="action-bar">
</ActionBar>

<TabView selectedIndex="0" >
  <ScrollView *tabItem="{title:'UI Components'}">
    <StackLayout class="form m-l-5">
      <Label text="Name:" class="text-primary"></Label>
      <TextField
        [(ngModel)]="profile.name"
        hint="name">
      </TextField>

      <Label text="Password:" class="text-primary"></Label>
      <TextField
        [(ngModel)]="profile.password"
        hint="password"
        secure="true">
      </TextField>

      <Label [text]="'Angular Pro: ' + ((profile.angularPro) ? 'Yes': 'No')" class="text-primary"></Label>
      <Switch
        [(ngModel)]="profile.angularPro"
        class="switch"
        horizontalAlignment="left">
      </Switch>

      <Label [text]="'Date of Birth: ' + profile.dob.toLocaleDateString()" class="text-primary"></Label>
      <DatePicker
        [(ngModel)]="profile.dob">
      </DatePicker>
      
      <Label [text]="'Coding power:' + profile.codingPower" class="text-primary"></Label>
      <Slider
        [(ngModel)]="profile.codingPower"
        [minValue]="0"
        [maxValue]="10">
      </Slider>

      <Button text="Save" (tap)="save()" class="btn btn-primary"></Button>
      <Button text="Clear" (tap)="clear()" class="btn btn-outline"></Button>
    </StackLayout>  
  </ScrollView>

  <GridLayout *tabItem="{title:'DataForm'}" rows="*, auto, auto">
    <!-- http://docs.telerik.com/devtools/nativescript-ui/Controls/Angular/DataForm/Editors/dataform-editors-list -->
    <RadDataForm [source]="profile" row="0">

    </RadDataForm>
    <Button text="Save" (tap)="save()" class="btn btn-primary" row="1"></Button>
    <Button text="Clear" (tap)="clearForm()" class="btn btn-primary" row="2"></Button>
    
  </GridLayout>
</TabView>

Step 2

Open app.css and change the imported style to each of the values below to see which one you like the most:

  • aqua.css
  • blue.css
  • brown.css
  • core.dark.css
  • core.light.css
  • forest.css
  • grey.css
  • lemon.css
  • lime.css
  • orange.css
  • purple.css
  • ruby.css
  • sky.css

(Bonus) - Use the NativeScript Themebuilder tool to create your own theme

Build your own theme using the NativeScript theme builder. This cool tool lets you view changes in a web browser, download a file, and style your app with a custom CSS file. Try building a patriotic theme with your flag’s colors, then download it to the root folder of your app. To see your theme, edit app.css to use the core theme and your new custom theme, like this:

@import 'nativescript-theme-core/css/core.light.css';
@import '~/custom.css';

Make something beautiful!

Animations

Adding animation to your app can really enhance its attraction and usefulness. There are several ways to add animation:

  1. Leveraging Angular-style animation
  2. Use NativeScript’s built-in animation library
  3. Use Animate.css
  4. Use CSS keyframe animation techniques
  5. Use Lottie (AirBnB’s library) or Keyframes (by Facebook) animations
  6. Use a community-generated animation plugin

Let’s work with keyframe animations to give you a feel for how animations work in NativeScript apps.

Exercise: Animations

Exercise: Animations

Enhance the slider so that when you slide it to a value greater than 7, its color changes and the label above it expands. To do this, you need to leverage the Angular bindings we learned about above.

Give the slider a class so that we can style it in the css, and bind the class.danger-slider property to the value profile.codingPower > 7:

class="slider"
    [class.danger-slider]="profile.codingPower > 7"

Then, edit the Label above the slider to expand when the slider value is more than 7 by giving it a class name that is bound to that value:

[class.zoom]="profile.codingPower > 7"

Take a look at profile.component.css to see how the keyframe animation is invoked.

<Label [text]="'Coding power:' + profile.codingPower" class="text-primary" [class.zoom]="profile.codingPower > 7"></Label>
<Slider
  [(ngModel)]="profile.codingPower"
  [minValue]="0"
  [maxValue]="10"
  class="slider"
  [class.danger-slider]="profile.codingPower > 7">
</Slider>
.danger-slider {
  background-color: red;
}

.zoom {
  animation-name: zoom;
  animation-duration: 2s;
}

@keyframes zoom {
  from { transform: scale(0.5, 0.5) }
  40% { transform: scale(1.6, 1.6) }
  to {  transform: scale(1.0,1.0) }
}

(Bonus) - Change the animation to spin, instead of zoom.

Instead of zooming, make the label spin around, just for practice. Hint, both profile.component.html and profile.component.css need to be edited.

<Label [text]="'Coding power:' + profile.codingPower" class="text-primary" [class.spin]="profile.codingPower > 7"></Label>
<Slider
  [(ngModel)]="profile.codingPower"
  [minValue]="0"
  [maxValue]="10"
  class="slider"
  [class.danger-slider]="profile.codingPower > 7">
</Slider>
.danger-slider {
  background-color: red;
}

.spin {
  animation-name: spin;
  animation-duration: 2s;
}

@keyframes spin {
  from { transform: rotate(-30) }
  40% { transform: rotate(420) }
  to {  transform: rotate(0)}
}

NativeScript UI

NativeScript comes with an additional FREE UI library called NativeScript UI it comes with a set of great components like: ListView, SideDrawer, Calendar, DataForm, Gauges and AutoComplete.

You can find the Angular Docs for these components here

Adding NativeScript UI to a project

To install it, we only need to run: tns plugin add nativescript-pro-ui

Then we need to add the necessary Modules to @NgModule. For example:

import { NativeScriptUICalendarModule } from 'nativescript-pro-ui/dataform/angular';
...

@NgModule({
  imports: [
    NativeScriptUICalendarModule,
    ...
  ],
  ...
})

And then we are ready to use the component in the html.

DataForm

For today we will focus on one of the components DataForm.

It allows you to construct nice looking entry forms with all the styling and eye pleasing UX purely through configuration.

Let’s say we want to create a feedback form with the following structure. Please note that for component we will use the components array as a source of values.

  public feedback = {
    title: 'Amazing Results',
    score: 5,
    date: new Date(),
    component: 'DataForm',
    note: `This looks really great, 
I was really amazed how little effort it took to implement it.
I can't wait to see other components`,
    test: false
  }

  public components: string[] = [
    'DataForm',
    'SideDrawer',
    'Calendar',
    'ListView',
    'Gauge',
    'AutoComplete'
  ];

To do that we need to use RadDataForm.

First we need to add it to the @NgModule:

import { NativeScriptUICalendarModule } from 'nativescript-pro-ui/dataform/angular';
...

@NgModule({
  imports: [
    NativeScriptUIDataFormModule,
    ...
  ],
  ...
})

Then we can add the RadDataForm (with source bound to feedback) to the html:

<RadDataForm [source]="feedback">
</RadDataForm>

This will already produce a nice DataForm, however the form doesn’t necessarily know in which order to arrange the fields or what sort of input editors we want to use.

Configuring Properties

For a simple field we need to use TKEntityProperty. We need to provide

  • name - the name of the property,
  • displayName - the label for the field,
  • index - used to define the order in which the fields should be displayed on the form, 1 means first
<RadDataForm [source]="feedback">
  <TKEntityProperty tkDataFormProperty name="name" displayName="displayName" index="1"></TKEntityProperty>
</RadDataForm>

Configuring Editors

Then we can additionally specify an editor type. For the full list see the documentation

For example, to match to a date we can use TKPropertyEditor with type:"DatePicker":

<RadDataForm [source]="feedback">
  <TKEntityProperty tkDataFormProperty name="date" displayName="Date" index="3">
    <TKPropertyEditor tkEntityPropertyEditor type="DatePicker"></TKPropertyEditor>
  </TKEntityProperty>
</RadDataForm>

Configuring List based Editors

Also to provide a list of possible values we can use TKPropertyEditor with type:"DatePicker" and [valuesProvider]:

<RadDataForm [source]="feedback">
  <TKEntityProperty tkDataFormProperty name="component" displayName="Component" index="4" [valuesProvider]="components">
    <TKPropertyEditor tkEntityPropertyEditor type="Picker"></TKPropertyEditor>
  </TKEntityProperty>
</RadDataForm>

Final result

Recreate UI

Here is the full implementation of the feedback form:

<RadDataForm [source]="feedback">
  <TKEntityProperty tkDataFormProperty name="name" displayName="displayName" index="1"></TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="score" displayName="Score" index="2">
    <TKPropertyEditor tkEntityPropertyEditor type="Stepper"></TKPropertyEditor>
  </TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="date" displayName="Date" index="3">
    <TKPropertyEditor tkEntityPropertyEditor type="DatePicker"></TKPropertyEditor>
  </TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="component" displayName="Component" index="4" [valuesProvider]="components">
    <TKPropertyEditor tkEntityPropertyEditor type="Picker"></TKPropertyEditor>
  </TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="note" displayName="Note" index="5">
    <TKPropertyEditor tkEntityPropertyEditor type="MultilineText"></TKPropertyEditor>
  </TKEntityProperty>
</RadDataForm>

This is just scratching the surface of what RadDataForm can do. We can also add input validation, add field grouping add images and more.

Exercise: NativeScript UI

Exercise: NativeScript UI

Change selectedIndex="1" on the TabView component, so that each time the app refreshes we will start on the second tab.

In this exercise you need to populate Entity Properties to match each item of the profile. There is already a RadDataForm on the second tab.

<RadDataForm [source]="profile" row="0">

</RadDataForm>

You can find the list of editors in the documentation

Here is how your DataForm should look like:

<RadDataForm [source]="profile" row="0">
  <TKEntityProperty tkDataFormProperty name="name" displayName="Name" index="0"></TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="password" displayName="Password" index="1">
    <TKPropertyEditor tkEntityPropertyEditor type="Password"></TKPropertyEditor>
  </TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="dob" displayName="DOB" index="2">
    <TKPropertyEditor tkEntityPropertyEditor type="DatePicker"></TKPropertyEditor>
  </TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="codingPower" displayName="Coding Power" index="3">
    <TKPropertyEditor tkEntityPropertyEditor type="Stepper"></TKPropertyEditor>
  </TKEntityProperty>
  <TKEntityProperty tkDataFormProperty name="angularPro" displayName="Angular Pro" index="3">
    <TKPropertyEditor tkEntityPropertyEditor type="Switch"></TKPropertyEditor>
  </TKEntityProperty>
</RadDataForm>

Lesson 2 - Navigation

Intro

In this lesson we are going to familiarize ourselves with navigation techniques.

Routing configuration

The Angular Router enables navigation from one view to the next as users perform application tasks.

A routed Angular application has one singleton instance of the Router service. When the app’s URL changes, that router looks for a corresponding Route from which it can determine the component to display.

When you create a brand new {N} app, you will straight away get a sample Routes configuration, which should look like this:

const routes: Routes = [
  { path: "", redirectTo: "/items", pathMatch: "full" },
  { path: "items", component: ItemsComponent },
  { path: "item/:id", component: ItemDetailComponent },
];

This tells us 3 things:

  • When the app starts, it should automatically redirect to items path,
  • If you navigate to 'items', you will be provided with ItemsComponent,
  • If you navigate to 'items/somevalue' you will be provided with ItemDetailComponent, which additionally will receive somevalue as id

As your application grows, so will your list of routes. One way to manage them is to group them into related parent<->children groups like this:

const routes: Routes = [
  { path: '', redirectTo: '/articles', pathMatch: 'full' },
  { path: 'items', children: [
    { path: '', component: ItemsComponent },
    { path: ':id', component: ItemDetailComponent },
  ]},
  { path: 'articles', children: [
    { path: '', component: ArticlesComponent },
    { path: 'read/:id', component: ArticleComponent },
    { path: 'edit/:id', component: EditArticleComponent },
    { path: 'search/:tech/:keyword', component: ArticleSearchResultsComponent },
  ]},
];

This time:

  • The default path is for articles,
  • items and items/:id are grouped together, which means that would could change items to something else in just one place,
  • we can also navigate to articles, articles/read/5, articles/edit/5 and articles/search/angular/navigation (this will translate to tech='angular' and keyword='navigation')

There is a lot more you can do in here, which is out of scope for this workshop. See Angular docs for more info on the subject.

Exercise: Routing configuration

Exercise: Routing configuration

Step 1

For this exercise we will use the contents of the app/color folder, which already contains some pieces of the app that we need.

Open app.routing.ts and change the redirectTo of the default route to '/color'

{ path: '', redirectTo: '/color', pathMatch: 'full' },

If you are using Playground then you should head to: https://play.nativescript.org/?template=nsday-color`

Step 2

Now it is time to add routes for the Red and RGB components. Update the children of the color route, so that:

  • 'color/red' path will navigate to RedComponent - you can see how this is done for the blue example,
  • 'color/rgb' + rgb (as a parameter) path will navigate to RGBComponent while passing the rgb parameter
{ path: 'color', children: [
  { path: '', component: ColorComponent },
  { path: 'blue', component: BlueComponent },
  { path: 'red', component: RedComponent },
  { path: 'rgb/:rgb', component: RGBComponent },
]},

One way to add navigation in markup is with the nsRouterLink directive. It is similar to routerLink (which is used in the web), but works with NativeScript navigation.

nsRouterLink expects an array of parameters, which can be matched to one of the routes defined in app.routes.ts.

For example, if we want to create a label that should navigate to a “reading” page and pass it a value, such as “5”, we can achieve this by providing an absolute path. That would look something like this:

<Label text="Angular Navigation" [nsRouterLink]="['/articles/read', '5']"></Label>

Relative Paths

We can also use relative paths. Where you provide the path based on the page you are currently at.

Children

If you are in the articles page (path '/articles') and want to navigate to the same pages as in the previous example. You can use './name_of_child_path' or 'name_of_child_path', like this:

<!--With an embeded param-->
<Label text="Angular Navigation" [nsRouterLink]="['./read', '5']"></Label>
<!--with a param provided separately-->
<Label text="Angular Navigation" [nsRouterLink]="['./read', navigationId]"></Label>

OR

<Label text="Angular Navigation" [nsRouterLink]="['read', '5']"></Label>
<Label text="Angular Navigation" [nsRouterLink]="['read', navigationId]"></Label>

Parent

If you are in the 'articles/read/5' route and you want to provide a relative path back to the parent, you can use '..', like this:

<Label text="Articles" [nsRouterLink]="['..']"></Label>

Siblings

If you are in the 'articles/read/5' route and you want to provide a relative path to the edit page or search page, you can use '../name_of_sibling_path', like this:

<Label text="Articles" [nsRouterLink]="['../edit', 5]"></Label>
<Label text="Articles" [nsRouterLink]="['../search', 'angular', 'navigation']"></Label>

Cheat sheet

[nsRouterLink]="['/absolute']"
<!--Navigate to parent-->
[nsRouterLink]="['..']"

[nsRouterLink]="['../sibling']"

[nsRouterLink]="['./child']" // or
[nsRouterLink]="['child']" 

Clear History

Also if you add a clearHistory flag, you can clear the navigation stack. Which means that there won’t be a back button displayed on iOS, or pressing back on Android will not take you back to this page again.

<Label text="Back to Articles" [nsRouterLink]="['..']" clearHistory="true"></Label>

Exercise: Navigation with nsRouterLink

Open color.component.html and update [nsRouterLink] for each button, so that:

  • Blue button navigates to the BlueComponent
  • Red button navigates to the RedComponent
  • Pink button navigates to the RGBComponent with '#ff0088' as the parameter
  • Gray button navigates to the RGBComponent with 'gray' as the parameter
  • Lavender button navigates to the RGBComponent with '#bad' as the parameter

NOTE: The parameter you pass to the “rgb” route won’t have an effect on that page—yet. Later in this section you’ll utilize that data to change the colors on the “rgb” component.

Here is the configuration for each:

Blue

[nsRouterLink]="['/color/blue']" OR [nsRouterLink]="['blue']"

Red

[nsRouterLink]="['/color/red']" OR [nsRouterLink]="['red']"

Pink

[nsRouterLink]="['/color/rgb', '#ff0088']" OR [nsRouterLink]="['rgb', '#ff0088']"

Gray

[nsRouterLink]="['/color/rgb', 'gray']" OR [nsRouterLink]="['rgb', 'gray']"

Lavender

[nsRouterLink]="['/color/rgb', '#bad']" OR [nsRouterLink]="['rgb', '#bad']"

Navigation can also be done with JavaScript.

For that you will either need the standard Router from @angular/router, or RouterExtensions from nativescript-angular/router, which comes with additional functionality: to clearHistory, choose a page transition or navigate back.

Note: If you are working on a project where you need to share the code between web and mobile, then you might want to use the standard Angular Router. However if your project is mobile only, then you should stick with RouterExtensions.

Once you choose which Router to use, navigation is really easy:

  • import the router you need,
  • inject the router in the constructor,
  • call navigate - just like you did with nsRouterLink
import { Router } from '@angular/router';
// or
import { RouterExtensions } from 'nativescript-angular';

@Component({
  selector: 'my-articles',
  templateUrl: './articles/articles.component.html',
})
export class ArticlesComponent {
  constructor(private router: RouterExtensions) {
  }

  readArticle(id: number) {
    this.router.navigate(['/articles/read', id]);
  }
}

Relative path

To use a relative path you need to:

  • import ActivatedRoute, which can be used as the relative point,
  • inject it in the constructor
  • provide it as a parameter for navigate, as relativeTo
import { RouterExtensions } from 'nativescript-angular';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'my-articles',
  templateUrl: './articles/articles.component.html',
})
export class ArticlesComponent {
  constructor(
    private router: RouterExtensions,
    private route: ActivatedRoute) {
  }

  readArticle(id: number) {
    this.router.navigate(['./read', id], { relativeTo: this.route });
  }
}

Cheat sheet

this.router.navigate(['/absolute/path']);
//navigate to parent
this.router.navigate(['..'],         {relativeTo: this.route});

this.router.navigate(['../sibling'], {relativeTo: this.route});

this.router.navigate(['./child'],    {relativeTo: this.route}); // or
this.router.navigate(['child'],      {relativeTo: this.route});

Clear History

To clear history just provide clearHistory into navigate, like this:

this.router.navigate(['/articles', { clearHistory: true }]);

Please note that you must use RouterExtensions for this to work. Also, clearHistory works only with page-router-outlet; this doesn’t work with router-outlet.

To navigate back, you can use RouterExtensions functionality to call either:

  • this.router.back() - always takes us to back the previous view from the navigation stack,
  • this.router.backToPreviousPage() - always takes us back to the previous page from the navigation stack, skipping navigation stack items on the same page.

What is the difference?

Let’s imagine you navigated through a number of paths in this order.

  • navigate /articles
  • navigate /articles/read/1
  • navigate /articles/read/2
  • navigate /articles/edit/3

So now we are at /articles/edit/3.

Calling back or backToPreviousPage, will both result in navigating to: /articles/read/2.

Now calling back would take us to /articles/read/1, which is another article in the same page. However calling backToPreviousPage from /articles/read/2, would take us to /articles.

Default iOS and Android back operations

The default back button which appears in the iOS <ActionBar> performs backToPreviousPage, while the Android back button performs back.

Exercise: Navigation with code

Exercise: Navigation with code

In this exercise we will play with the blue component. The blue.component.html already contains four buttons, each calling a different function.

Your task is to implement the empty functions in blue.component.ts, so that:

  • goRed() navigates to the Red page
  • goPink() navigates to the RGB page with this.pink as the parameter
  • goBack() navigates back
  • goHome() navigates home whilst clearing the navigation history

goRed (Absolute Solution)

this.router.navigate(['/color/red']);

goPink

this.router.navigate(['/color/rgb', this.pink]);

goBack

this.router.back();

goHome

this.router.navigate(['/color'], { clearHistory: true });

Receiving parameters

For components that are expected to receive parameters from the route navigation, you need to use ActivatedRoute.

You just have to perform the appropriate imports:

Import

import { ActivatedRoute } from '@angular/router';

Inject

export class ArticleSearchResultsComponent {
  constructor(private route: ActivatedRoute) {
  }
}

Use

Here we have two options. You can take a snapshot, which will be triggered only when we navigate to this page from another page.

ngOnInit() {
  this.tech = this.route.snapshot.params['tech'];
  this.keyword = this.route.snapshot.params['keyword'];

  this.searchArticles(this.tech, this.keyword);
}

Using a snapshot will not work if we try to navigate from the search to itself, but with different parameters. To make this work, we need to use params.forEach instead.

ngOnInit() {
  this.route.params
    .forEach(params => {
      this.tech = this.route.snapshot.params['tech'];
      this.keyword = this.route.snapshot.params['keyword'];

      this.searchArticles(this.tech, this.keyword);
    });
}

Exercise: Receiving parameters

Exercise: Receiving parameters

In this exercise we will play with the rgb component: rgb.component.ts. Currently every time you navigate to rgb the input parameters are getting ignored. Your task is to intercept the ‘rgb’ parameter and update this.rgb.

ngOnInit() {
  this.route.params
    .forEach(params => this.rgb = params['rgb']);
}

Page Transitions

One of the great things about NativeScript is its ability to use native animations and page transitions with very little effort.

Here is a list of all available navigation transitions

Here is a list of all available animation curves

Transition via html

To add pageTransition in html, just add pageTransition with a name of the transition you need:

<Button
  text="Open Path"
  [nsRouterLink]="['/path']"
  pageTransition="slideBottom">
</Button>

Transition via code

To add page transition in JavaScript, just add a transition object to the navigate options. Just like this:

this.router.navigate(['/relative/path'], {
  transition: {
    name: 'slideBottom',
    duration: 500,
    curve: 'linear'
  }
});

Exercise: Page Transitions

Exercise: Page Transitions

In this exercise we will play with the color and red components.

Step 1

Your task is to update the buttons in color.component.html, so that:

  • The Blue button triggers curl transition
  • The Red button triggers the fade transition
  • The Pink, Gray and #bad buttons trigger the flip transition
  • Blue => pageTransition="curl"
  • Red => pageTransition="fade"
  • Pink, Gray and #bad => pageTransition="flip"

Step 2

red.component.html already contains 4 buttons, each calling a different function.

Your task is to implement the empty functions in red.component.ts, so that:

  • goBlue() navigates to the Blue page with page transition slideTop, duration 2 seconds and curve spring
  • goGray() navigates to the RGB page with gray as the parameter and page transition fade and duration 1 second

goBlue

this.router.navigate(['/color/blue'], {
  transition: {
    name: 'slideTop',
    duration: 2000,
    curve: 'spring'
  }
});

goGray

this.router.navigate(['/color/rgb', 'gray'], {
  transition: {
    name: 'fade',
    duration: 1000
  }
});

Lesson 3 - Components and Services

Services

Services are JavaScript functions that are responsible for doing a specific task. Angular services are injected using a Dependency Injection mechanism and include the value, function or feature that is required by the application. There nothing specially related to Services in Angular–there is no ServiceBase class–but still services can be treated as fundamental to Angular applications.

Creating a service

Creating a Service is really simple. You need to import Injectable function and apply it as the @Injectable decorator. Then we need to create a class for our service and export it:

import { Injectable } from '@angular/core';

@Injectable()
export class MyHappyService {
  
  public doSomethingFun() {
    console.log('I am a happy bunny... hop, hop, hop');
  }
}

Naming convention

Following the naming convention in Angular, the above service should be placed in a file called: my-happy.service.ts. This is basically the name of the class in lower case, each word (excluding the word service) separated with - and followed by .service.ts.

The same naming convention applies to all files in an Angular app like: currency.pipe.ts, navigation-menu.component.ts, login.model.ts.

Adding the service to app.modules.ts

In order to make our service available in the app, you need to add to providers in the @NgModule. The global @NgModule is located in app.module.ts.

import { MyHappyService } from './my-happy.service';

@NgModule({
  bootstrap: [
    AppComponent
  ],
  imports: [
    NativeScriptModule,
    AppRoutingModule,
    NativeScriptHttpModule,
    NativeScriptFormsModule
  ],
  declarations: [
    AppComponent,
    ProfileComponent
  ],
  providers: [
    MyHappyService
  ],
  schemas: [
    NO_ERRORS_SCHEMA
  ]
})
export class AppModule { }

Injecting services

In order to use a service in a component, we need to inject it in the component’s constructor.

Note: You can also inject services into other services or pipes.

This is done like this:

constructor(private myHappyService: MyHappyService) {
  //constructor code
}

Here is how you inject and then use a service:

import { MyHappyService } from '../my-happy.service';

@Component({
  selector: 'app-mood',
  templateUrl: './mood/mood.component.html'
})
export class MoodComponent {

  constructor(private myHappyService: MyHappyService) {
  }

  showYourMood() {
    this.myHappyService.doSomethingFun();
  }
}

Http

NativeScript comes with its own implementation of the Http module, which uses Android and iOS native functionality to perform the calls.

This is exposed as NativeScriptHttpModule, which implements the same interface as the web Http module.

Http: adding the module to the app

This means that all you have to do is declare our NativeScript wrapper in the respective module file and Dependency Injection will take care of the rest.

This is done by adding NativeScriptHttpModule to @NgModule imports.

import { NativeScriptHttpModule } from 'nativescript-angular/http';
imports: [
  ...
  NativeScriptHttpModule,
],

From this point onwards the code that uses the Http module is exactly the same as the code you would write for a web application.

This gives us a high level Angular Http module that is capable of performing various request natively for Android, iOS and Web.

Http

Http: Injecting the service

Then you can import and Inject the Http module where you are planning to use it.

import { Http } from '@angular/http';
constructor(private http: Http) {
}

Http: calling the service

The http module has a bunch of useful functions like, get, post, put, delete and others. Each takes a url as a parameter and optional options, and then they return an Observable with a Response.

get(url: string, options?: RequestOptionsArgs): Observable<Response>

Example of how to call get and subscribe to the Observable result: Please note that you should always convert the response to json()

doSomething() {
  this.http.get('http://api.someopendata.org/cities') // make the call
  .map(response => response.json())                   // convert the result to json()
  .map(result => result.cities)                       // map the result to the object we need to return
  .subscribe(                                         // subscribe and do something with the result
    cities => console.log(cities),
    error => console.error('Error: ' + err),
    () => console.log('Completed!')
  )
}

Example of how to call get and convert the Observable to a Promise:

doSomething() {
  this.http.get('http://api.someopendata.org/cities') // make the call
  .map(response => response.json())                   // convert the result to json()
  .map(result => result.cities)                       // map the result to the object we need to return
  .toPromise()                                        // convert the observable to a promise
  .then(                                              // then do something with the result
    cities => console.log(cities),
    error => console.error('Error: ' + err)
  )
}

Http: Adding Headers to http calls

If you need to pass headers into a http call, you can construct it by using Headers class, append data and then add it to options?: RequestOptionsArgs.

import { Http, Headers } from '@angular/http';
let myHeaders = new Headers();
myHeaders.append('key', 'value');

this.http.get('http://api.someopendata.org/cities', 
  { headers: myHeaders })

Http: Constructing URL search params

If you need to pass query parameters (like service?mood=’happy’&face=’round’) into a http call, you can construct it by using URLSearchParams class, append query params and then add it to options?: RequestOptionsArgs.

import { Http, Headers, URLSearchParams } from '@angular/http';
let searchParams: URLSearchParams = new URLSearchParams();
searchParams.set('mood', 'happy');
searchParams.set('face', 'round');

this.http.get('http://api.someopendata.org/cities', 
  { headers: myHeaders, search: searchParams })

Exercise: Football Service

For this exercise we will use ServiceTestComponent located in service-test folder and FootballService, which you can find in football.service.ts.

If you are using Playground then you should head to: https://play.nativescript.org/?template=nsday-football`

ServiceTestComponent has several buttons, each designed to test a function of the FootballService that you will be constructing in this exercise.

The football service is based on football-data.org API

Test Service

Exercise: Injecting football service

Let’s start with changing the default route in app.routing.ts to '/service-test':

{ path: '', redirectTo: '/service-test', pathMatch: 'full' },

If you try to run the application, it will fail with the following error: Error: No provider for FootballService!

You need to add the FootballService to app.module.ts.

HINT Remember that this should be added to the providers.

First import FootballService

import { FootballService } from './football.service';

And then add this inside @NgModule:

providers: [
  FootballService
],

Now the app should be loading without any issues. However none of the buttons will deliver the results we want.

Exercise: Implementing the http calls

For your convenience the http service is already injected into FootballService and the header with apiKEY is already configured.

Step 1 - Make it work

If you open football.service.ts you will notice that getTeams and getLeagueTable are already implemented, which are the functions required to display the data in the TablesComponent.

If you press the Get PL Table button or Get PL Teams, you should get the data in the terminal.

Step 2 - Implement the missing functions

Your job is to implement the remaining functions:

  • getTeam - should make a call to: https://api.football-data.org/v1/teams/{teamId} with the teamId param,
  • getPlayers - should make a call to: https://api.football-data.org/v1/teams/{teamId}/players with the teamId param,
  • getTeamFixtures - should make a call to: https://api.football-data.org/v1/teams/{teamId}/fixtures with the teamId param,
  • getFixtures - should make a call to: https://api.football-data.org/v1/competitions/{competitionId}/fixtures with the competitionId param. Additionally this function should construct URLSearchParams for attributes passed in options.

To implement the first 3 functions, you can follow the getTeams function as the example. To implement getFixtures, see getLeagueTable, which constructs URLSearchParams.

In each function you will need to follow these steps:

  • construct the url - you can use the baseUrl property as the basis
  • use the http service to call get()
  • map and convert the result to json
  • use FootballFactory to convert the Raw output into the expected objects

As you implement each of the functions, you can test them with the buttons in the Service Test. If you get the data in the terminal, then you most likely did it right. But if you get an error message, then you need to keep working :)

getTeam

public getTeam(teamId: number): Observable<Team> {
  const url = `${this.baseUrl}/teams/${teamId}`;

  return this.http.get(url, { headers: this.header })
  .map(result => result.json())
  .map(result => FootballFactory.teamFromRaw(result));
}

getPlayers

public getPlayers(teamId: number): Observable<Player[]> {
  const url = `${this.baseUrl}/teams/${teamId}/players`;

  return this.http.get(url, { headers: this.header })
  .map(result => result.json())
  .map(result => FootballFactory.playersFromRaw(result));
}

getTeamFixtures

public getTeamFixtures(teamId: number): Observable<Fixture[]> {
  const url = `${this.baseUrl}/teams/${teamId}/fixtures`;

  return this.http.get(url, { headers: this.header })
  .map(result => result.json())
  .map(result => FootballFactory.fixturesFromRaw(result));
}

getFixtures

public getFixtures(competitionId: number, options: FixtureSearchOptions = {}): Observable<Fixture[]> {
  const url = `${this.baseUrl}/competitions/${competitionId}/fixtures`;

  let searchParams: URLSearchParams = new URLSearchParams();
  if (options.matchday) {
    searchParams.set('matchday', options.matchday.toString());
  } else if (options.timeFrame) {
    searchParams.set('timeFrame', options.timeFrame);
  }

  // alternative way
  // let searchParams = this.buildSearchParams(options);

  return this.http.get(url, { headers: this.header, params: options })
  .map(result => result.json())
  .map(result => FootballFactory.fixturesFromRaw(result));
}

Components

The component is a controller class with a template which mainly deals with a view of the application and logic on the page. It is a bit of code can be used throughout an application. The component knows how to render itself and configure dependency injection.

The component contains two important things; one is a view and another contains some logic.

A component is usually made of a @Component decorator, which contains:

  • selector - html tag that should be used for this component
  • templateUrl - a location of the file that contains the html code (view) - you can also use template to provide the html code inline, but this is not a great idea
  • styleUrls - a location of the file that contains the css

Then we need a Component Class that will encapsulate all the logic.

Here is an example:

blue.component.ts

@Component({
  selector: 'my-blue',
  templateUrl: './color/blue.component.html',
  styleUrls: ['./color/color.component.css']
})
export class BlueComponent{
  private pink: string = '#ff0088';

  constructor(private router: RouterExtensions, private route: ActivatedRoute) {
  }

  // other functions
}

blue.component.html

<ActionBar title="BLUE" color="white" backgroundColor="blue">
</ActionBar>

<StackLayout>
  <Button text="Go Red" (tap)="goRed()" class="btn red"></Button>
  <Button text="Go Pink" (tap)="goPink()" class="btn pink"></Button>
  <Button text="Go Home" (tap)="goHome()" class="btn btn-primary"></Button>
  <Button text="Go Back" (tap)="goBack()" class="btn btn-outline"></Button>
</StackLayout>

Adding your component to the app

If you try to use your module straight after creating it, you will get an error like this: Component BlueComponent is not part of any NgModule or the module has not been imported into your module.

Solution:

All components should be added to @NgModule declarations. By default each should be added to app.modules.ts:

declarations: [
  AppComponent,
  ProfileComponent,
  ColorComponent,
  BlueComponent,
  RedComponent,
  RGBComponent,
]

Smart versus Presentation components

Components can be divided into two categories:

  • smart - those contain the business logic of your application. Like a LoginComponent that contains the logic of how to log in and where to redirect after user successfully logs in
  • presentation - those are used to encapsulate something that we want to show on the screen. Like a LogoComponent, which contains the img tag with your logo, which you can paste everywhere you need to display your logo. However when you need to change the logo, you can do it all in one place (the definition of the component).

Components with custom input (one-way binding)

Just like the <Label> component has a text attribute, your components can have their own custom attributes as well.

Adding custom attributes to a component is really easy—just add an @Input() decorator in front of your attribute or property set and you are ready to go.

@Component({
  selector: 'my-calendar',
  templateUrl: './calendar/calendar.component.html'
})
export class CalendarComponent {
  @Input() day: number;
  @Input() month: number;

  private _year: number;
  @Input() set year(year: number) {
    this._year = (year > 100) ? year : year + 2000; 
  }

Now you can use it like this:

<my-calendar day="12" month="11" year="10"></my-calendar>

Exercise: Creating a presentation component with @Input

For this part of the exercise we will be using all components in the football folder.

Change the default route to:

{ path: '', redirectTo: '/football', pathMatch: 'full' },

And run the application. You should get a view displaying a league table and a tab bar for navigation. When you press the View Fixtures button, you will get a list of fixtures.

League Table Fixtures

Your task is to encapsulate the fixture template into a FixtureComponent and use it in CompetitionFixturesComponent instead of the current fixture template.

Exercise: Create FixtureComponent with @Input

Step 1 - Replace current fixture template in Competition Fixtures

The initial structure for FixtureComponent is already in place (see fixture.component.ts) and added to declarations in app.module.ts.

Open competition-fixtures.component.html, comment out the GridLayout and then add <my-fixture [fixture]="fixture"></my-fixture> in its place.

You will notice that my-fixture expects a [fixture] attribute. This will be added in the next exercise.

The template in competition-fixtures.component.html should look like this:

<template let-fixture="item">
  <StackLayout class="list-group-item">
    <!-- Fixture Template -->
    <my-fixture [fixture]="fixture"></my-fixture>
  </StackLayout>
</template>

Now if you reload the app and go to View Fixtures you should get something like this:

Fixtures

Step 2 - Update FixtureComponent and add @Input for fixture

Head to fixture.component.ts.

Currently the FixtureComponent has a fixture attribute, however in this state there is no way to update the value of the fixture from the LeagueFixtures Component.

What we need is to turn the fixture into an @Input type attribute.

Refer to the solution below if you get stuck.

FixtureComponent should look like this:

export class FixtureComponent {
  @Input() fixture: Fixture;

  public fakeDate: Date = new Date();

  public displayScore(): boolean {
    // return this.fixture.status === 'FINISHED' || this.fixture.status === 'IN_PLAY'
    return false;
  }
}

Step 3 - Update displayScore

You should also update displayScore() to use the commented out logic. Basically it is a helper function that is used to define whether we should display the score or the date and time of the game.

public displayScore(): boolean {
  return this.fixture.status === 'FINISHED' || this.fixture.status === 'IN_PLAY'
}

Step 4 - Update HTML

Head to fixture.component.html.

Update all the Labels so that they display the data from the fixture attribute. Make sure you take care of homeTeamName, awayTeamName, result.goalsHomeTeam, result.goalsAwayTeam, and date.

<GridLayout rows="auto" columns="*, auto, *" class="list-group-item">  
  <Label col="0" [text]="fixture.homeTeamName" class="h4 text-right"></Label>
  
  <StackLayout col="1"  horizontalAlignment="center" class="m-x-10 h3">
    <StackLayout *ngIf="displayScore()"  orientation="horizontal">
      <Label [text]="fixture.result.goalsHomeTeam" class="score m-r-5"></Label>
      <Label [text]="fixture.result.goalsAwayTeam" class="score"></Label>
    </StackLayout>

    <StackLayout *ngIf="!displayScore()" class="text-center text-muted h5">
        <Label [text]="fixture.date | date:'H:m'"></Label>
        <Label [text]="fixture.date | date:'dd-MMM'" textWrap="true"></Label>
    </StackLayout>
  </StackLayout>

  <Label col="2" [text]="fixture.awayTeamName" class="h4 text-left"></Label>
</GridLayout>

Reload the app. Now the fixtures should be displayed correctly again.

Components with custom events

Adding a custom event to a component is easy. Let’s have a look at LeagueTableComponent as an example.

To make it work we need first to create an EventEmitter:

@Output() teamSelected: EventEmitter<number> = new EventEmitter<number>();

Note that this is made of 3 parts:

  • @Output - decorator
  • teamSelected - eventName
  • EventEmitter - EventEmitter with the type of output

Then every time we want to trigger the event, we can call emit(value) on this.teamSelected. Just like this:

onTeamSelect(event) {
  const selectedTeamId = this.table.standing[event.index].teamId;
  console.log('::LeagueTableComponent::onTeamSelect::' + selectedTeamId);
  this.teamSelected.emit(selectedTeamId);
}

Obviously there must be something that actually triggers onTeamSelect. In this case this is done by the <ListView>

<ListView [items]="table?.standing" class="list-group" (itemTap)="onTeamSelect($event)">

All this means that everywhere we use <my-league-table> we can now add a handler for teamSelected like this (see tables.component.html):

<my-league-table [competitionId]="PremierLeagueId" (teamSelected)="onTeamTap($event)"></my-league-table>

Note that $event will contain the value passed into emit, in this case this will be a teamId.

Exercise: Creating a presentation component with @Output

In this exercise we need to update the app, so that if the user taps on a team in the league table, the app should navigate to TeamComponent with teamId of that team.

Even though you could make it happen by adding [nsRouterLink] on each team standing. We want the navigation logic to be delegated to the parent component, so it should be the TablesComponent that should trigger the navigation.

So in short: when the user taps on a team, we need the LeagueTableComponent to emit teamSelected with the teamId. And the TablesComponent should intercept the teamSelected event and call onTeamSelected where it should navigate to the TeamComponent.

Exercise: Update LeagueTableComponent with @Output

Step 1 - Add @Output EventEmitter

Add an EventEmitter<number> called teamSelected to your LeagueTableComponent in league-table.component.ts.

@Output() teamSelected: EventEmitter<number> = new EventEmitter<number>();

Step 2 - Emit value

Update the onTeamSelected function, so that it emits the teamSelected event with the teamId

onTeamSelected(event) {
  const selectedTeamId = this.table.standing[event.index].teamId;
  console.log('::LeagueTableComponent::onTeamSelect::' + selectedTeamId);

  this.teamSelected.emit(selectedTeamId);
}

Step 3 - Call OnTeamSelect from the UI

Now we need the ListView in league-table.component.html to call onTeamSelected whenever the user taps on one of the teams. ListView has an event itemTap which does precisely that.

Add (itemTap)="onTeamSelected($event)" to the ListView.

ListView first line

<ListView [items]="table?.standing" class="list-group" (itemTap)="onTeamSelected($event)">

Step 4

Now the LeagueTableComponent is ready to emit a teamId each time user taps on it. We just need to take it and navigate to the TeamComponent.

The onTeamTap function in tables.component.ts already has a logic to navigate to the TeamComponent with a specified teamId.

private onTeamTap(teamId: number) {
  console.log('::TablesComponent::onTeamTap::' + teamId);
  this.router.navigate(['/football/team', teamId]);
}

We just need to update each of the <my-league-table> tag to bind to the (teamSelected) event and call onTeamTap

HINT: Don’t forget to pass $event to teamSelected.

<my-league-table [competitionId]="PremierLeagueId" (teamSelected)="onTeamTap($event)"></my-league-table>

Step 5

Test the app to see if this works.

Now upon tapping on a team in the table you should be redirected to a team view, which should display fixtures for that given team.

Components with custom input (two-way binding)

To create a custom attribute that is capable of both taking data as an input and also updating it, we need to use two-way binding.

To do that we need to combine the power of the @Input and @Output decorators.

Let’s imagine we are working on a ColorPicker component, which should take a color, as an input, but when the user selects a different color, it should provide an updated value.

First we need to create a property @Input color.

Then we need to add a custom event, which is called propertyNameChange. For our example we’ll use @Output() colorChange.

Finally, we need to emit the new value colorChange.emit(newColor);

Here is the full code:

@Component({
  selector: 'color-picker',
  templateUrl: './color-picker/color-picker.component.html'
})
export class ColorPickerComponent {
  @Input color: string;
  @Output() colorChange = new EventEmitter<string>();

  onColorPick(newColor: string) {
    colorChange.emit(newColor);
  }
}

Please note that @Input could be also used with a getter and setter.

Now you can use the ColorPickerComponent like this:

<color-picker [(color)]="selectedColorFromParentClass"></color-picker>

Bonus Component Exercise

Can you implement the missing pieces of the PlayerComponent? Your task is to add an @Input to capture the player object, then update TeamComponent, so that it displayes a list of players instead of fixtures.

Each PlayerComponent should display player details like: name, position, jerseyNumber and nationality.

Lesson 4 - Plugins

In this Lesson you are going to learn how to use a few out of a generous collection of NativeScript plugins.

Most of the plugins you can install either by calling npm install or for those that contain some native iOS and/or Android elements tns plugin add.

Exercise: Setup

In this exercise we will be working on plugins/wizard-profile.component.

Let’s start with changing the default route in app.routing.ts to '/plugins':

{ path: '', redirectTo: '/plugins', pathMatch: 'full' },

Exercise: Camera Plugin

Exercise: Camera Plugin

You can find the camera plugin here.

To install it run:

npm i nativescript-camera --save

Remember, every time you make a change to native bits of your app (including adding/removing plugins) you need to rebuild and redeploy your app with tns run.

Next, open wizard-profile.component.ts and import nativescript-camera using the line of code below.

import * as camera from 'nativescript-camera';

After that, update the WizardProfileComponent’s takeProfilePicture function to take a picture and call this.updateProfilePicture, passing the ImageAsset you got from the camera plugin’s callback function. Try to figure it out based on the info in the documentation. Note that you might need to call camera.requestPermissions(); from ngOnInit.

ngOnInit() {
  // get camera permissions when loading for the first time
  camera.requestPermissions();

  this.reloadPowers();
}

takeProfilePicture() {
  const options: camera.CameraOptions = {
    width: 300,
    height: 300,
    keepAspectRatio: true,
    saveToGallery: false
  };

  camera.takePicture(options)
  .then((imageAsset: ImageAsset) => {
    this.updateProfilePicture(imageAsset);
  }).catch(err => {
    console.log(err.message);
  });
}

Exercise: Social Share Plugin

Exercise: Social Share Plugin

You can find the nativescript-social-share here

To install it run:

tns plugin add nativescript-social-share

Next, open wizard-profile.component.ts and import nativescript-social-share using the code below:

import * as SocialShare from 'nativescript-social-share';

After that, update the share function in WizardProfileComponent to share the existing messageBody variable.

SocialShare.shareText(messageBody);

Finally, update the WizardProfileComponent’s sharePicture function to share the component’s profilePicture property.

SocialShare.shareImage(this.profilePicture);

Exercise: Fancy Alert Plugin

Exercise: Fancy Alert Plugin

You can find the nativescript-fancyalert plugin here.

To install it run:

npm install nativescript-fancyalert --save

Open wizard-profile.component.ts and import nativescript-fancyalert using the line of code below.

import { TNSFancyAlert } from 'nativescript-fancyalert';

Next, replace the 3 alert calls in displayPower with TNSFancyAlert.showNotice, TNSFancyAlert.showInfo, TNSFancyAlert.showWarning. You can pass in power.name and power.description as the parameters.

displayPower(power: Power) {
  if(power.level < 5) {
    TNSFancyAlert.showNotice(power.name, power.description, 'Nice');
  } else if(power.level < 9) {
    TNSFancyAlert.showInfo(power.name, power.description, 'W00000W!!!');
  } else {
    TNSFancyAlert.showWarning(power.name, power.description, 'Be careful');
  }
}

Exercise: Pull To Refresh Plugin

Exercise: Pull To Refresh Plugin

You can find the nativescript-pulltorefresh plugin here.

To install it run:

tns plugin add nativescript-pulltorefresh

This plugin is slightly different, as it needs to be added directly to the UI, which requires you to register it first.

Open main.ts, import element-registry and then call registerElement (Just make sure to add it before platformNativeScriptDynamic().bootstrapModule(AppModule);):

import {registerElement} from "nativescript-angular/element-registry";
registerElement("PullToRefresh", () => require("nativescript-pulltorefresh").PullToRefresh);
import { platformNativeScriptDynamic } from 'nativescript-angular/platform';

import { AppModule } from './app.module';

import {registerElement} from "nativescript-angular/element-registry";
registerElement("PullToRefresh", () => require("nativescript-pulltorefresh").PullToRefresh);

platformNativeScriptDynamic().bootstrapModule(AppModule);

Once that is done, open wizard-profile.component.html and wrap the PullToRefresh around the ListView

<PullToRefresh (refresh)="onPull($event)">
  <ListView 
    ...
  </ListView>
</PullToRefresh>
<PullToRefresh (refresh)="onPull($event)">
  <ListView [items]="powers" class="list-group" (itemTap)="onPowerTap($event)">
    <template let-power="item">
      <StackLayout>
        <Label [text]="power.name + ': ' + power.level" class="list-group-item"></Label>
      </StackLayout>
    </template>
  </ListView>
</PullToRefresh>

App Challenge Part 1 - Build a List

Now that you’ve got the NativeScript basics down, it’s time to put your skills to the test. Your task in the next three chapters is to build an entire NativeScript app from scratch.

What you’re building

So what are you building? The ultimate app for finding pets of all shapes and sizes—FurFriendster! At the end of the day you’ll have an app that looks something like this.

Don’t get too overwhelmed the scope of this app as you’ll be building it one step at a time. Let’s start by building the list page.

Building the list

Let’s get started building by starting a new project.

Exercise: Create FurFriendster

Navigate to a folder where you’d like your new project to live in your file system, and run the following command.

tns create FurFriendster --ng

After that completes, cd into your newly created project.

cd FurFriendster

Your task for the first part of building this app is to create a big list of pets. Specifically, you’ll want your UI to look something like this.

For the most part you will be building this app on your own without any copy-and-paste guidance from us, but we are going to provide a few things.

Exercise: Get the starting files

FurFriendster is driven by the Petfinder API, and we have a pre-configured Angular service and a few model objects you can use to get the data you need. To install it run the following command:

npm i petfinder-angular-service --save

Alternatively (if npm install doesn’t work) you can open the workshop folder you’ve been working in today, and find its child app-challenge-files folder. Next, copy every file and folder in app-challenge-files, and paste them into your new FurFriendster app.

Eventually in this workshop you’ll allow users to filter which types of pets they’d like to see on the list page, but for now you’ll need to hardcode some really basic animal data.

On your list page you’ll need to call your new service’s findPets() method. Here is some data you can pass in for testing.

findPets("10001", {    // 10001 is the US zip code for New York City
  age: "",
  animal: "bird",      // you can replace this with "cat", "dog", etc
  breed: "",
  sex: "",
  size: ""
});

So now it’s time to get building. Here are your requirements for this part of the workshop.

Requirements

  • 1: Show the list of pets returned from findPets using a <ListView> UI component.
  • 2: Each entry in the <ListView> should display the pet’s name and its image.

From there it’s up to you. Feel free to implement the design we show in this section’s screenshots, or to build something unique. If you get stuck here are a few tips you can refer to.

Tips

Tip #1: ListView

The NativeScript ListView documentation is available at https://docs.nativescript.org/angular/ui/list-view.html.

Setup your List

Make sure to start work in app.module.ts. You will need to import the NativeScriptHttpModule:

import { NativeScriptHttpModule } from "nativescript-angular/http";

and include that in imports.

Likewise, import your petfinder service that you installed from npm:

import { PetFinderService } from "petfinder-angular-service";

and include it in your Providers.

Then, start your work in items.component.ts by importing the petfinder service and pet model.

Change items to pets and edit ngOnInit() with the new service call, returning a promise:

this.petService.findPets("10001", {
  age: "",
  animal: "bird",
  breed: "",
  sex: "",
  size: ""
})
.then(pets => this.pets = pets)

Then, get to work on the items.component.html file, editing the ListView to display the pets.

Tip #2: Images

Our service provides a convenience method for accessing the appropriate pet images. You can bind an <Image> tag using the following code.

[src]="item.media.getFirstImage(2, 'res://icon')"

Help loading external images

Note: You may encounter an error when loading images from external sources on iOS. To fix this, add the following code right under the initial <dict> key in App_Resources/iOS/Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
  <dict>
    <key>photos.petfinder.com</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key>
      <true/>
      <key>NSIncludesSubdomains</key>
      <true/>
    </dict>
  </dict>
</dict>

Hint: use a GridLayout within your ListView template to layout the image next to the label.

Tip #3: Styling

The NativeScript core theme has a few CSS class names for displaying thumbnail images. Check out https://docs.nativescript.org/ui/theme#listviews for details.

App Challenge Part 2 - Master-Detail Pages

With your basic list complete, your next test is to turn your list of pets into a functional master-detail interface.

What you’re building

The master-detail user interface pattern is a popular way to design mobile applications. Master-detail interfaces work best when you have a large list of some type of data, and the individual items in the list have details associated with them — details that the user needs to view or modify.

The Petfinder API provides exactly this information, so let’s put this user interface into practice with FurFriendster. After you complete this challenge, readers should be able to tap items on the master list of pets, and navigate to a second screen where they can see details about the pet they’re interested in.

Here’s what your master-detail UI might look like after you complete this section.

Building the page

Your main task for this workshop will be figuring out how to add a new route to act as the details page, and how to pass data to that new page from the master page.

Don’t worry too much if your details page doesn’t look amazing. You main task is to show the appropriate data for each pet and get the navigation working. Here are the specific requirements for this part of the workshop.

Requirements

  • 1. Show the following information for each pet on its details screen.
    • description
    • breeds
    • Two images, which you can access by binding an <Image> component’s src attribute to the following properties of a Pet object: media.getFirstImage(3, 'res://icon') and media.getSecondImage(4, 'res://icon').
  • 2. Add the following two plugins to the details screen.

Tips

Tip #1: clip-path

If you want to try to recreate the diamond shape images we used in our screenshots, give the CSS clip-path property a shot. You can use this tool to define your own custom shape.

Tip #2: Angular configuration

When adding your new route, make sure to add the appropriate entries to both your app.routing.ts and app.module.ts files.

Tip #3: ScrollView

If your UI components no longer fit on a user’s screen, add a <ScrollView> as a top-level UI component.

App Challenge Part 3 - Setting up a Form

With a master/detail interface complete, your app is almost ready to go. Your next step is to remove your hardcoded data, and let users filter and find exactly the pet they’re looking for.

What you’re building

In the third and final part of this challenge you’ll be building a third screen for your app. The app will be the new starting screen of your app, and will look something like the screenshot below.

Building the form

There are many different ways you can implement the filters that help users find specific pets. As a first step, start with a <TextField> for accepting a user’s location, and a <SegmentedBar> for allowing the user to choose between dogs and cats. Your task will then be passing that data from the form to the list page, so that the filters can be sent to the Petfinder API.

Here are the specific things you need to do.

Requirements

  • 1: Build a form that allows the user to input the following data
    • Pet type: “dog” or “cat”
    • Location: Free-form text
  • 2: Pass that data to your list screen, and only show pets that match the user-selected filters.

If you have extra time, there are a lot more filters you might want to implement. Check out the list of values in search-options.ts for a full list of values you can pass to the Petfinder API.

Tips

Tip #1: NativeScriptFormsModule

Since your app now has a form, you need to include the following import in your app.module.ts file.

import { NativeScriptFormsModule } from "nativescript-angular/forms";

And then include NativeScriptFormsModule in the imports array of your main NgModule.

Tip #2: SegmentedBar

The easiest way to implement radio-button-like controls in native apps is with SegmentedBars. Here are the docs on how to use them in NativeScript

App Challenge Part 4 - EXTRA BONUS TIME

Have you completed all requirements of the app challenge and still have time left? Good news! There’s still more fun to be had.

Take whatever time you have left and implement a new feature for FurFriendster. Use your imagination! What feature does the best pet finding app on the market really need?

If you’re having trouble coming up with good ideas, we have a few suggestions.

  • Adding favorites
    • Allow users to heart or start their favorite pets, and show those favorites in a new screen in your app.
  • A new theme
    • Try a different color scheme to give your app a whole new look.
  • Random pet
    • Give the user a way of seeing for a random pet.

Where to go from here?

Congratulations! You’ve completed the NativeScript workshop 🎉

Regardless of what you choose to do with NativeScript, joining the NativeScript community is a great way to keep up with the latest and greatest in the NativeScript world. Here are some ways you can get involved: