This tutorial introduces you to the essentials of a code-sharing project, which allows you to build both web and native mobile apps β for Android and iOS β from a single project.
The native mobile part is achieved with the help of NativeScript.
NativeScript is an open source framework for building truly native mobile apps with JavaScript.
Use web skills like: TypeScript, Angular and CSS and get native UI & performance on iOS and Android.
The objective is to share as much code as possible, and break the platform-specific code into separate files.
This usually means that we can share the code for:
- the Routes for the navigation
- the Services for the common business logic
- the Component Class definition for the common behaviour of each component
While, separating the code for:
- the UI Layer (stylesheets and html)βββas you need to use different user interface components in web and NativeScript-built native apps
- the NgModulesβββso that we can import platform-specific modules, without creating conflicts (i.e. Angular Material Designβββwhich is web only) between web and mobile
Hereβs a diagram to show you what that looks like at a high level.
As part of this tutorial, we will look at NativeScript in the context of a code-sharing project.
However, you can also use NativeScript to build mobile apps without the web.
Check out this tutorial on how to build mobile apps with NativeScript.
As the basis of this tutorial, you will use a completed version of the Getting Started Tutorial for Angular.
If you don't have your version around, you can clone it from https://github.com/sebawita/angular-getting-started/.
git clone https://github.com/sebawita/angular-getting-started.git
Also, if you're not familiar with it, you can launch the web application via:
ng serve -o
The idea is to add NativeScript to the Getting Started app, and then step by step convert all web components to also work in NativeScript.
To take advantage of the automated migration commands for NativeScript Angular you will need to install the Angular CLI. Run the following command:
npm install --global @angular/cli
To run mobile apps with NativeScript you will need to install the NativeScript CLI. Execute the following command:
npm install --global nativescript
The NativeScript CLI performs only the build of your Angular code while skipping the Android/iOS build, then it deploys your code to NativeScript Preview (a companion app hosting your app code).
To use preview, you need to install two companion apps on your Android/iOS device(s):
- NativeScript Playground (Android, iOS) β used to scan a QR code provided by the NativeScript CLI
- NativeScript Preview (Android, iOS) β used to host display your app
- Migrate the project to a code-sharing structure
- Update the {N} AppModule
- Update the first component
- Update the rest of the components
- Etc.
First you need to convert the project to a code-sharing structure.
Run the following ng add
command from the root of your project:
ng add @nativescript/schematics@next
This command adds the NativeScript-specific:
- npm modules
- AppModule definition
- AppComponent definition
- TypeScript configuration
And as a result it allows you to build a NativeScript app from the same project.
However, this does not convert your components to work in a mobile app, as HTML doesn't map very well to Native Mobile UI components.
Before you can start sharing code, you need to know how to separate the web code from the mobile code. This is important, so that you could easily create platform-specific code without creating conflicts.
To do that you can use a simple naming convention. By adding a .tns before the file extension, you can indicate that this file is NativeScript-specific, while the same file without the .tns extension is marked as web-specific. If we have just one file without the .tns extension, then this makes it a shared file.
The most common scenario is a component code. Usually we would have:
- name.component.tsβββa shared file for the Component Class definition
- name.component.htmlβββa web-specific template
- name.component.tns.htmlβββa mobile-specific template
- name.component.cssβββa web-specific stylesheet
- name.component.tns.cssβββa mobile-specific stylesheet
Now is the best moment to test the new mobile app. This is done with the NativeScript CLI preview command.
From the root of the project run the following command:
tns preview
After a short moment, the CLI will present you with a QR Code. Scan it with the NativeScript Playground app, which will connect your project with the NativeScript Preview app.
As soon as you scan the QR Code, the CLI will bundle the TypeScript code from your project and push it to the NativeScript Preview app.
If you have the local build configured, then you can perform the build locally with the following commands:
Android:
tns run android
iOS:
tns run ios
Your app should look something like this:
You may notice that a big button with a text saying "auto-generated works!" is not what you had in your web project. Don't worry, this is just a demo component to show you that the mobile app works.
While the tns preview
or tns run [android/ios]
commands are active, any changes to the project will get automatically picked up by the NativeScript CLI and an update will be pushed to the app.
It is a good time to test it out.
Find the src/app/auto-generated/auto-generated.component.tns.html file and change the text
value to text="update now π"
.
This means that you don't need to re-run the build manually, as most of the times this will happen automatically. Just what you would expect from the
ng serve
command.
Before you start converting all components to NativeScript, you need to make sure that the NativeScript AppModule (app.module.tns.ts) imports all the required modules, which are used by the services in the project.
When you check cart.service.ts you will find that it Injects the HttpClient:
constructor(
private http: HttpClient
) {}
In the web version of the AppModule, HttpClient is imported via HttpClientModule.
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
HttpClientModule,
...
],
NativeScript provides a mobile specific implementation of the HttpClient, which is provided via NativeScriptHttpClientModule.
Open app.module.tns.ts where you will find a commented import for NativeScriptHttpClientModule class (line 13). Uncomment it, and add NativeScriptHttpClientModule to @NgModule imports, like this:
import { NativeScriptHttpClientModule } from 'nativescript-angular/http-client';
@NgModule({
...
imports: [
NativeScriptModule,
AppRoutingModule,
NativeScriptHttpClientModule
],
This is enough to make the CartService to work for both Web and Mobile, without any change required to the service itself.
Here is a high-level visualisation of how the right implementation of the HttpClient is provided to the service at build time.
Next you should update the global stylesheet (app.css) for the mobile part of the project, as it will provide you with a common styling classes that you will use across the app.
app.css
@import '~nativescript-theme-core/css/core.light.css';
/* .fa {
font-family: "Font Awesome 5 Free", "fa-solid-900";
} */
ActionBar {
background-color: #1976d2;
color: white;
}
.action-bar-item {
color: white;
padding-right: 5;
}
.big-btn, .btn-red, .btn-green, .btn-blue, .btn-blue-outline {
font-weight: bold;
font-size: 18;
padding: 10 30;
border-radius: 25;
margin: 20 10 0 10;
width: 90%;
}
.btn-red {
color: white;
background-color: #ED4472;
}
.btn-green {
color: white;
background-color: #30CE91;
}
.btn-blue {
color: white;
background-color: #1976d2;
}
.btn-blue-outline {
color: #1976d2;
border-color: #1976d2;
border-width: 2;
}
.title {
color: #1976d2;
font-size: 32;
text-align: center;
margin-bottom: 8;
}
.cart-item, .shipping-item {
padding: 2 4;
margin: 4;
border-radius: 16;
/* border-width: 2; */
background-color: #EEEEEE;
}
.form {
margin-top: 20px;
width: 90%;
}
.form Label {
color: #5a78e3;
font-size: 20;
font-weight: bold;
margin: 5;
position: top;
}
.form TextField {
background-color: white;
padding-left: 5;
border-color: #5a78e3;
border-width: 2;
border-radius: 5;
}
Next, you need to start converting each component, by providing the UI implementation for each.
Converting a web component to a shared component usually involves:
- Adding a
.tns.html
and a.tns.css
files - Adding the component to the {N} AppModule declarations
- Update navigation configuration to display the component
- Providing template and styling code
The steps (1) and (2) can be easily automated with the little help from the Angular CLI and NativeScript Schematics:
ng generate migrate-component --name=cmp-name
or in short:
ng g mc --name=cpm-name
The first component that you should start with is the Product List component, as this is the first component that gets loaded when the app starts. Run the following command:
ng generate migrate-component --name=product-list
This will:
- generate product-list.component.tns.html with the commented out html from product-list.component.html
- generate an empty product-list.component.tns.css
- add the ProductListComponent to the Declarations of app.module.tns.ts
Like this:
src
βββ app
βββ product-list
| βββ product-list.component.html
| βββ product-list.component.tns.html <= create
| βββ product-list.component.ts
| βββ product-list.component.tns.css <= create
| βββ product-list.component.css
βββ app.module.tns.ts <= update
βββ app.module.ts
At this stage the Router Configuration for the web and mobile apps are kept separately. This is especially useful during the migration process, as at the beginning most of your components will be web only, so your mobile app won't be able to navigate to the components that have not been converted yet.
The Product List component can be added to the NativeScript Routes.
Open app-routing.module.tns.ts and replace the Routes array with:
import { ProductListComponent } from '@src/app/product-list/product-list.component';
export const routes: Routes = [
{ path: '', component: ProductListComponent },
];
After you save, your project should update and look something like this:
If that is the case, then you are on the right way to move forward.
Now, you need to update the {N} UI template, to match the content from the web app.
This tutorial won't dive too deep into building the UI using NativeScript components. You can check out the documentation to learn more about UI Widgets and NativeScript Layouts. There is also an interactive tutorial for {N} layouts.
Let's go quickly through the web template and convert it to {N}.
product-list.component.html
<h2>Products</h2>
<div *ngFor="let product of products; index as productId">
<h3>
<a [title]="product.name + ' details'" [routerLink]="['/products', productId]">
{{ product.name }}
</a>
</h3>
<p *ngIf="product.description">
Description: {{ product.description }}
</p>
<button (click)="share()">
Share
</button>
<app-product-alerts [product]="product" (notify)="onNotify()">
</app-product-alerts>
</div>
Step #1
First, you have:
<h2>Products</h2>
This is clearly the title of the page. An ActionBar
is the best component to serve as a title.
Add the following to product-list.component.tns.html at the top.
<ActionBar title="Products">
</ActionBar>
Below you have a StackLayout
container, which is used to hold the contents of the page. Remove the three labels that are inside, like this:
<ActionBar title="Products">
</ActionBar>
<StackLayout>
<!-- Page content goes here -->
</StackLayout>
Please note that besides the ActionBar, a NativeScript template (the same rule also applies to ng-template) can only contain one component. This is why you need a Layout component, which contains all the other components.
Step #2
Then you have a div
that is repeated with *ngFor
for each product in the products array.
<div *ngFor="let product of products; index as productId">
...
</div>
Although, you can use *ngFor
in a mobile app, in most of the cases this is best handled by a ListView
component, which helps with scrolling through the items and optimises GPU and memory handling for large data sources.
Add a list ListView
component inside the StackLayout
, like this:
<StackLayout>
<!-- Page content goes here -->
<ListView [items]="products" class="list-group" height="100%">
<ng-template let-product="item" let-productId="index">
</ng-template>
</ListView>
</StackLayout>
Note the following:
-
[items]
β is used to provide a data source, it can also be used with anasync
pipe, like this:<ListView [items]="data | async">
-
let-product="item"
β is used to provide a name for each item in the array, in this case the name for each item is product, -
`let-productId="index" β is used to provide a name for index of the item, in this case the name of the index is productId
Step 3
Next, you need to reproduce the div
and all of its contents:
product-list.component.html
<div *ngFor="let product of products; index as productId">
<h3>
<a [title]="product.name + ' details'" [routerLink]="['/products', productId]">
{{ product.name }}
</a>
</h3>
<p *ngIf="product.description">
Description: {{ product.description }}
</p>
<button (click)="share()">
Share
</button>
<app-product-alerts [product]="product" (notify)="onNotify()">
</app-product-alerts>
</div>
You can convert it to the {N} template by replacing:
-
the
<div>
container with a<StackLayout>
<StackLayout class="list-group-item"> </StackLayout>
-
the
<h3><a>
product name&link with a<Label>
β where product.name is provided with[text]
and navigation path is provided with[nsRouterLink]
:<Label [nsRouterLink]="['/products', productId]" [text]="product.name" textWrap="true" class="title"> </Label>
-
the
p
element again with a<Label>
where we can preserve the*ngIf
directive and add atext
property as follows:
<Label *ngIf="product.description"
text="Description: {{ product.description }}" textWrap="true">
</Label>
-
the
<button>
with a<Button>
β where you use the(tap)
event instead of the(click)
event (as you don't click on touch screens), and the text of the button is provided with[text]
:<Button (tap)="share()" text="Share" class="btn-blue"></Button>
-
the
<app-product-alerts>
component is not mobile ready yet, so just leave it as a comment:<!-- <app-product-alerts [product]="product" (notify)="onNotify()"> </app-product-alerts> -->
The whole template should look like this:
product-list.component.tns.html
<ActionBar title="Products">
</ActionBar>
<StackLayout>
<ListView [items]="products" class="list-group" height="100%">
<ng-template let-product="item" let-productId="index">
<StackLayout class="list-group-item">
<Label [nsRouterLink]="['/products', productId]"
[text]="product.name" textWrap="true" class="title">
</Label>
<Label *ngIf="product.description"
text="Description: {{ product.description }}" textWrap="true">
</Label>
<Button (tap)="share()" text="Share" class="btn-blue"></Button>
<!--
<app-product-alerts [product]="product" (notify)="onNotify()">
</app-product-alerts>
-->
</StackLayout>
</ng-template>
</ListView>
</StackLayout>
After you save, the app should look like this:
Next, you should tackle the ProductAlerts component, which is used as a presentation component in the ProductList template.
Step 1
First, run the migration schematic:
ng g mc --name=product-alerts
Step 2
Next β based on the web template β you need to update the mobile template:
product-alerts.component.html
<p *ngIf="product.price > 700">
<button (click)="notify.emit()">Notify Me</button>
</p>
You should update the mobile template like this:
product-alerts.component.tns.html
<StackLayout *ngIf="product.price > 700">
<Button (tap)="notify.emit()" text="Notify Me" class="btn-green"></Button>
</StackLayout>
Step 3
Open product-list.component.tns.html and remove the comments around <app-product-alerts>
:
<ActionBar title="Products">
</ActionBar>
<StackLayout>
<ListView [items]="products" class="list-group" height="100%">
<ng-template let-product="item" let-productId="index">
<StackLayout class="list-group-item">
<Label [nsRouterLink]="['/products', productId]"
[text]="product.name" textWrap="true" class="title">
</Label>
<Label *ngIf="product.description"
text="Description: {{ product.description }}" textWrap="true">
</Label>
<Button (tap)="share()" text="Share" class="btn-blue"></Button>
<app-product-alerts [product]="product" (notify)="onNotify()">
</app-product-alerts>
</StackLayout>
</ng-template>
</ListView>
</StackLayout>
After you save all the files, your app should look like this:
When you test the Share and Notify Me buttons in a mobile application, you will get a long error, which will complain that window.alert()
doesn't exist on the mobile platforms.
There are a few ways of handling this issue:
The alert()
function is available without window
for both web and mobile, so the easiest solution (the recommended solution for this problem) is to change window.alert()
to alert()
.
share() {
alert('The product has been shared!');
}
onNotify() {
alert('You will be notified when the product goes on sale');
}
Create two files:
utils.ts β to re-export the window definition
export default window;
utils.tns.ts β to provide a definition of window, containing the alert function
export default { alert };
Then in the product-list.component.ts import window
, like this:
import window from '../utils';
This would keep the old implementation of window
for web, and provide the new definition of window
for mobile.
The next component to migrate is the Product Details component. This is the component the app navigates to when you click/tap on a name of a phone.
You should be familiar with the routine:
Step 1
Run the migration schematic:
ng g migrate-component --name=product-details
Step 2
Update the navigation configuration in app-routing.module.tns.ts:
import { ProductDetailsComponent } from '@src/app/product-details/product-details.component';
export const routes: Routes = [
{ path: '', component: ProductListComponent },
{ path: 'products/:productId', component: ProductDetailsComponent },
];
Step 3
Translate HTML to NativeScript template.
product-details.component.html
<h2>Product Details</h2>
<div *ngIf="product">
<h3>{{ product.name }}</h3>
<h4>{{ product.price | currency }}</h4>
<p>{{ product.description }}</p>
<button (click)="addToCart(product)">Buy</button>
</div>
The above translates really nicely, as follows:
-
<h2>
=>ActionBar
<ActionBar title="Product Details"> </ActionBar>
-
<div>
=><FlexboxLayout>
with items displayed in a column, and aligned in the center (see docs for more info):<FlexboxLayout flexDirection="column" alignItems="center" class="m-10"> </FlexboxLayout>
-
<h3>
,<h4>
and<p>
=><Label>
<Label text="{{ product.name }}" class="title"></Label> <Label text="{{ product.price | currency }}" class="h2"></Label> <Label text="{{ product.description }}" textWrap="true" class="h3"></Label>
-
<button>
=><Button>
<Button (tap)="addToCart(product)" text="Buy" class="btn-green"></Button>
Like this:
product-details.component.tns.html
<ActionBar title="Product Details">
</ActionBar>
<FlexboxLayout flexDirection="column" alignItems="center" class="m-10">
<Label text="{{ product.name }}" class="title"></Label>
<Label text="{{ product.price | currency }}" class="h2"></Label>
<Label text="{{ product.description }}" textWrap="true" class="h3"></Label>
<Button (tap)="addToCart(product)" text="Buy" class="btn-green"></Button>
</FlexboxLayout>
Step 4
Fix the call to window.alert
in product-details.component.ts.
addToCart(product) {
alert('Your product has been added to the cart!');
this.cartService.addToCart(product);
}
After you save all the files, and navigate to details, your app should look like this:
Migrating the Cart component, is a little more challenging task, as it uses FormBuilder
, which is not directly supported in NativeScript. This makes for a great example on how to handle web and mobile specific code.
One of the best ways to handle a scenario like is this to extract the constructing of a FormBuilder
to a service. Then have two implementations of the same service, one that uses FormBuilder
, and the other that recreates the same functionality without a FormBuilder
.
Step 1
Create a Checkout Form service:
ng generate service form/checkout-form
Remove providedIn
from the directive definition, as you will provide this service directly in the Cart component.
Using Dependency Injection, request FormBuilder
, then add a method called prepareCheckoutForm()
, which should return a form with name
and address
properties. Like this:
form/checkout-form.service.ts
import { Injectable } from '@angular/core';
import { FormBuilder } from '@angular/forms';
@Injectable()
export class CheckoutFormService {
constructor(private formBuilder: FormBuilder) { }
prepareCheckoutForm() {
return this.formBuilder.group({
name: '',
address: ''
});
}
}
Step 2
Make a copy of checkout-form.service.ts and call it checkout-form.service.tns.ts.
In there you need to add an implementation of CheckoutForm
class with name
and address
properties and the reset()
method.
Then update the prepareCheckoutForm()
in CheckoutFormService
to return a new CheckoutForm
.
Like this:
form/checkout-form.service.tns.ts
import { Injectable } from '@angular/core';
export class CheckoutForm {
public name = '';
public address = '';
public reset() {
this.name = '';
this.address = '';
}
}
@Injectable()
export class CheckoutFormService {
public prepareCheckoutForm() {
return new CheckoutForm();
}
}
Step 3
Finally, you need to update the CartComponent class to use the CheckoutFormService
.
Add CheckoutFormService
to the @Component
=> providers
, like this:
cart/cart.component.ts
import { CheckoutFormService } from '@src/app/form/checkout-form.service';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.css'],
providers: [CheckoutFormService]
})
Update the constructor to use CheckoutFormService
instead of FormBuilder
, like this:
cart/cart.component.ts
constructor(
private cartService: CartService,
private formService: CheckoutFormService
) {
this.items = this.cartService.getItems();
this.checkoutForm = this.formService.prepareCheckoutForm();
}
Also, make sure to remove all references to FormBuilder
.
cart.component.ts should look like this:
import { Component } from '@angular/core';
import { CartService } from '@src/app/cart.service';
import { CheckoutFormService } from '@src/app/form/checkout-form.service';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.css'],
providers: [CheckoutFormService]
})
export class CartComponent {
items;
checkoutForm;
constructor(
private cartService: CartService,
private formService: CheckoutFormService
) {
this.items = this.cartService.getItems();
this.checkoutForm = this.formService.prepareCheckoutForm();
}
onSubmit(customerData) {
console.warn('Your order has been submitted', customerData);
this.items = this.cartService.clearCart();
this.checkoutForm.reset();
}
}
Splitting platform specific functionality into separate files/services, allows you to handle code differences in an elegant fashion, whilst keeping the common functionality shared.
Finally, you can update the Cart component to be code-sharing ready.
Step 1
Run the migration schematic:
ng g migrate-component --name=cart
Step 2
Update the navigation configuration in app-routing.module.tns.ts:
import { CartComponent } from '@src/app/cart/cart.component';
export const routes: Routes = [
{ path: '', component: ProductListComponent },
{ path: 'products/:productId', component: ProductDetailsComponent },
{ path: 'cart', component: CartComponent },
];
Step 3
Update ProductList template, to add a navigation link (ActionItem
) for cart in the ActionBar
.
product-list.component.tns.html
<ActionBar title="Products">
<ActionItem ios.position="right" [nsRouterLink]="['/cart']">
<Label text="Cart π" class="action-bar-item"></Label>
</ActionItem>
</ActionBar>
The Product List should look like this:
Step 4
Translate HTML to NativeScript template.
cart.component.html
<h3>Cart</h3>
<p>
<a routerLink="/shipping">Shipping Prices</a>
</p>
<div class="cart-item" *ngFor="let item of items">
<span>{{ item.name }} </span>
<span>{{ item.price | currency }}</span>
</div>
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit(checkoutForm.value)">
<div>
<label>Name</label>
<input type="text" formControlName="name">
</div>
<div>
<label>Address</label>
<input type="text" formControlName="address">
</div>
<button class="button" type="submit">Purchase</button>
</form>
The above translates really nicely, as follows:
-
<h3>
=>ActionBar
<ActionBar title="Cart"> </ActionBar>
As a container we could use a StackLayout
and position it inside a ScrollView
to provide a scrollable area when the content is larger than its bounds.
<ScrollView>
<StackLayout>
</StackLayout>
</ScrollView>
Then:
<p>
<a routerLink="/shipping">Shipping Prices</a>
</p>
goes to
<Button row="0"
text="Shipping Prices" nsRouterLink="/shipping" class="btn btn-outline">
</Button>
and
<div class="cart-item" *ngFor="let item of items">
<span>{{ item.name }} </span>
<span>{{ item.price | currency }}</span>
</div>
goes to
<Label row="1" *ngIf="!items.length"
text="No Items in the Cart" class="h2 text-center m-10">
</Label>
<StackLayout row="1" class="m-8">
<GridLayout *ngFor="let item of items" columns="* auto" class="list-group cart-item">
<Label col="0" [text]="item.name" class="list-group-item"></Label>
<Label col="1" [text]="item.price | currency" class="list-group-item"></Label>
</GridLayout>
</StackLayout>
The form
could transfer from
<form [formGroup]="checkoutForm" (ngSubmit)="onSubmit(checkoutForm.value)">
<div>
<label>Name</label>
<input type="text" formControlName="name">
</div>
<div>
<label>Address</label>
<input type="text" formControlName="address">
</div>
<button class="button" type="submit">Purchase</button>
</form>
to
<GridLayout row="2" rows="auto auto auto" columns="auto *" class="form">
<Label row="0" col="0" text="Name"></Label>
<TextField row="0" col="1" [(ngModel)]="checkoutForm.name" hint="name..."></TextField>
<Label row="1" col="0" text="Address"></Label>
<TextField row="1" col="1" [(ngModel)]="checkoutForm.address" hint="address..."></TextField>
</GridLayout>
<Button text="Purchase" (tap)="onSubmit(checkoutForm)" class="btn-green"></Button>
Before we can use the ngModel
directive in data binding, we must import the NativeScriptFormsModule
and add it to the Angular module's imports list.
Open app.module.tns.ts where you will find a commented import for NativeScriptFormsModule class (line 10). Uncomment it, and add NativeScriptFormsModule to @NgModule imports, like this:
import { NativeScriptFormsModule } from 'nativescript-angular/forms';
@NgModule({
...
imports: [
NativeScriptModule,
AppRoutingModule,
NativeScriptHttpClientModule,
NativeScriptFormsModule
],
Finally, the cart.component.tns.html should look like this:
<ActionBar title="Cart">
</ActionBar>
<ScrollView>
<StackLayout>
<Button row="0"
text="Shipping Prices" nsRouterLink="/shipping" class="btn btn-outline">
</Button>
<Label row="1" *ngIf="!items.length"
text="No Items in the Cart" class="h2 text-center m-10">
</Label>
<StackLayout row="1" class="m-8">
<GridLayout *ngFor="let item of items" columns="* auto" class="list-group cart-item">
<Label col="0" [text]="item.name" class="list-group-item"></Label>
<Label col="1" [text]="item.price | currency" class="list-group-item"></Label>
</GridLayout>
</StackLayout>
<GridLayout row="2" rows="auto auto auto" columns="auto *" class="form">
<Label row="0" col="0" text="Name"></Label>
<TextField row="0" col="1" [(ngModel)]="checkoutForm.name" hint="name..."></TextField>
<Label row="1" col="0" text="Address"></Label>
<TextField row="1" col="1" [(ngModel)]="checkoutForm.address" hint="address..."></TextField>
</GridLayout>
<Button text="Purchase" (tap)="onSubmit(checkoutForm)" class="btn-green"></Button>
</StackLayout>
</ScrollView>
The Cart page should look like this:
The final component to migrate is the Shipping component. This is the component the app navigates to during cart checkout for reviewing the available shipping options.
You could execute the routine:
Step 1
Run the migration schematic:
ng g migrate-component --name=shipping
Step 2
Update the navigation configuration in app-routing.module.tns.ts:
import { ShippingComponent } from '@src/app/shipping/shipping.component';
export const routes: Routes = [
{ path: '', component: ProductListComponent },
{ path: 'products/:productId', component: ProductDetailsComponent },
{ path: 'cart', component: CartComponent },
{ path: 'shipping', component: ShippingComponent },
];
Step 3
Update the NativeScript template:
shipping.component.tns.html
<ActionBar title="Shipping Prices">
</ActionBar>
<StackLayout class="m-8">
<GridLayout *ngFor="let shipping of shippingCosts | async" rows="auto" columns="120 *"
class="shipping-item list-group">
<Label col="0" [text]="shipping.type" class="list-group-item"></Label>
<Label col="1" [text]="shipping.price | currency" class="list-group-item"></Label>
</GridLayout>
<!-- <ListView [items]="shippingCosts | async" class="list-group" height="100%">
<ng-template let-shipping="item">
<GridLayout rows="auto" columns="120 *" class="shipping-item">
<Label col="0" [text]="shipping.type" class="list-group-item"></Label>
<Label col="1" [text]="shipping.price | currency" class="list-group-item"></Label>
</GridLayout>
</ng-template>
</ListView> -->
</StackLayout>
Don't worry if the app doesn't work yet. Follow the below step before you test.
Step 4
NativeScript build process consists of two steps: bundling and building the native app. By default, some files do not include in the application's bundle such as fonts and images. The webpack bundler is explicitly instructed to copy these types of files to the native application in its configuration file.
Therefore, to make it possible for NativeScript to load .json files from the assets folder, you need to give information Webpack to copy the file into the native application. This can be done with the help of CopyWebpackPlugin
like this:
new CopyWebpackPlugin([
{ from: { glob: "assets/*.json" } },
])
As it is already in use, just open webpack.config.js, find the comment line // Copy assets to out dir. Add your own globs as needed.
and update as follows:
webpack.config.js
// Copy assets to out dir. Add your own globs as needed.
new CopyWebpackPlugin([
{ from: { glob: "assets/*.json" } },
{ from: { glob: "fonts/**" } },
{ from: { glob: "**/*.jpg" } },
{ from: { glob: "**/*.png" } },
], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }),
In order to apply the changes to the webpack.config.js
file, we need to stop the currently running process and start it again to pick up its new configuration. So, go back to your console/terminal and execute again tns preview
.