Welcome to the NativeScript Workshop!
To get started you'll need the following pre-requisites:
NativeScript is a framework for building native iOS and Android apps using JavaScript and CSS. NativeScript renders UIs with the native platform’s rendering engine, no WebViews, resulting in native-like performance and UX.
NativeScript provides a best-of-both-worlds development experience. Our cross-platform JavaScript modules give you the convenience of writing iOS and Android apps from a single JavaScript code base, while our runtimes give you the power of accessing native APIs, SDKs, and frameworks when you need them—all without needing to open Xcode or Android Studio. NativeScript was created and is supported by Telerik.
To learn more about how NativeScript works, check out this quick 3-minute video.
The NativeScript Playground is a browser-based environment for developing simple NativeScript apps. It’s a great place to get started learning NativeScript, as you can develop apps without needing to install the various SDKs and tools needed for native iOS and Android development. All you need is your browser, and an Android or iOS mobile device connected to the internet to get started.
In this workshop, we'll be using the NativeScript Playground to quickly prototype an app. It's a powerful tool, and we believe you'll like this approach because you don't need to install anything on your computer to get started.
In this workshop you'll learn how to build iOS and Android mobile apps using NativeScript. Throughout the workshop, you'll be building a SpaceX Launch Information app that uses the SpaceX launch API. The app will list past and future SpaceX launches, and allow you to learn more about each launch.
Key concepts and take-aways:
You can find additional workshop materials and presentation content at the locations below:
The first place you can go to learn more about NativeScript is by reading our book, The NativeScript Book, available FREE from Telerik.
There is also a huge community around NativeScript, we've compiled a brief list of additional resources below.
A portion of this workshop was inspired by a fun weekend project Mike built with his son. We're really into building robots right now, so we built a cardboard robot with his tablet as the head. I ended up writing a quick NativeScript soundboard app so the robot could talk back to him.
We promise - this is the last plug. We wrote a book about NativeScript. And you can download it. For free. Yes, free. Free, like beer.
In this chapter, you'll learn how to use the NativeScript Playground to get started quickly, build your first app, and run the app on your phone.
The NativeScript Playground is a browser-based environment for developing simple NativeScript apps. It’s a great place to get started learning NativeScript, as you can develop apps without needing to install the various SDKs and tools needed for native iOS and Android development. All you need is your browser, and an Android or iOS mobile device connected to the internet to get started.
Now that you know about the NativeScript Playground, let's get started.
In this exercise, you'll explore the NativeScript Playground and learn how to run an app built in the Playground on your mobile device.
Before we get started, you'll need to install 2 apps on your mobile device:
In your respective App Store (Google Play or Apple App Store), locate both apps and install them on your mobile device. If you're having trouble finding the app, use the links above.
Next, navigate to the NativeScript Playground in your browser.
Click "Play now" to begin.
After clicking "Play now" a QR code will appear, and you'll be notified that your Playground is ready:
Open the NativeScript Playground app on your mobile device, and select the "Scan QR code" action.
Using the QR code scanner in the NativeScript Playground app, scan the QR code displayed on the NativeScript Playground website.
NOTE
Do not scan the QR code on this page. Scan the QR code in the Playground.
After scanning the QR code, the NativeScript Playground app will open the NativeScript Preview app and dynamically load the code from the Playground Website and launch a native mobile app on your mobile device!
When all of this happens, you should see this on your mobile device:
...and you should also see the following on the Playground website:
Congratulations! You just used the Playground to write your first mobile app with Angular. Then, you ran the app on your phone. That was fast - and you didn't need to install anything on your computer!
OK, we know you didn't have to do much of anything to write this app, but we promise you'll learn more and build something more advanced during the workshop.
Before we move on, let's take a minute to understand what happened and explore the NativeScript Playground UI.
The NativeScript Playground is a web-based IDE. You write your Angular code in the browser, then save it. When you scan the QR code, the NativeScript Playground app coordinates with the NativeScript Preview app and reaches out to the Playground website. The code is pulled down, loaded into the NativeScript runtime in the Preview app, and renders native iOS/Android UI on your mobile device.
Now that you've learned how the Playground works, let's explore the UI.
As you'll notice, the Playground UI is similar to a basic IDE, like Visual Studio Code, Visual Studio, Eclipse, or Web Storm.
On the left is a file explorer, where you can add or update files and folders to your mobile app:
The large central area is a file editor with Intellisense:
The bottom central area shows you which mobile devices are connected to the Playground and device/error logs for debugging purposes.
Connected Devices
Hold on...the Playground shows you connected devices. Yes, that's right. When your mobile device connects to the Playground, it registers itself with the website. You can see a variety of information about a connected device.
Pretty cool, right? Well, there's something even cooler about the Playground: LiveSync.
LiveSync
LiveSync is a technology that syncs changes made in the Playground down to your mobile device in real time. This means that you can quickly make changes to your app in the Playground and the changes will be instantly pushed to connected devices. If you've ever done mobile development before, you'll understand how useful this is - getting feedback from your code running on real mobile devices usually takes time, but with LiveSync, it's instantaneous.
Now that you've learned about LiveSync, let's see it in action.
In this exercise, you'll learn how to use LiveSync by interacting wth the Playground website and seeing the app on your mobile device update automatically.
Before we get started with this exercise, be sure to have your Playground app loaded in the NativeScript Preview app.
If your app isn't running, press the "QR code" button in the Playground website, and re-scan the code on your mobile device:
Now that your app is running on your mobile device, let's make a change.
Start by finding the home.component.html file on the left, then open it for editing by clicking on it:
You'll notice some markup in the home.component.html file. It's not important for you to understand the markup right now, but you should know that any markup you place in this file will affect the native UI generated on the home page of your app.
Even though you may not understand all of the markup, you can probably guess at the meaning on most tags. NativeScript UI markup is can be easy to understand, because it's similar to HTML.
For example, look at the markup that displays a label on the screen that contains the text "Play with NativeScript!":
<Label
textWrap="true"
text="Play with NativeScript!"
class="h2 description-label">
</Label>
Let's change the text in the home.component.html file.
Change the text that reads "Play with NativeScript!" to somethings else. Feel free to use the code snippet below, or your own text.
<Label
textWrap="true"
text="Using LiveSync is easy and convenient!"
class="h2 description-label">
</Label>
After making this change, press the "Save" button:
Then, look back at your mobile device, and watch the UI of the app change:
Easy and cool!
We'll continue to rely on LiveSync throughout this workshop to get instant feedback in our apps.
Let's continue on and get our Playground app started.
In the final portion of chapter 1, you'll be cleaning up the default Angular template in the Playground to get it ready for the SpaceX app. Let's jump in!
In this exercise, you'll reorganize files and folders in the Playground website to get ready for the SpaceX app.
You may be wondering why we're cleaning up the default Playground template. The default Playground template works well, but doesn't structure app pages into a "views" folder (which is a preference of ours).
Let's get started by removing the home folder. Find it in the Explorer, select it, click the "...", and select the "Delete" option:
Confirm the delete:
Add a folder named "views" to the "app" folder:
Now, create a "home" folder inside of the "views" folder:
Then add the "home" component by selecting the "..." next to the "home" folder and select "add component". Name the component "home":
By using the "add component" option, the Playground will scaffold a CSS, TypeScript, and HTML file for you automatically:
Next, let's add the home component to the app module. Open the app.module.ts file, and add a reference to the home component.
You can add the home component manually or copy/paste the code below.
import { NgModule, NgModuleFactoryLoader, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { HomeComponent } from "./views/home/home.component";
@NgModule({
bootstrap: [
AppComponent
],
imports: [
NativeScriptModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
})
export class AppModule { }
Lastly, update the app-routing.module.ts file to reflect the changes in the home component:
import { NgModule } from "@angular/core";
import { Routes } from "@angular/router";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { HomeComponent } from "./views/home/home.component";
const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "home", component: HomeComponent }
];
@NgModule({
imports: [NativeScriptRouterModule.forRoot(routes)],
exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }
You're finished. You replaced the default home component with a new home component located in the views folder.
Save your Playground project online and let LiveSync do it's work. If your app got disconnected from LiveSync, just re-scan your QR code to load it up.
When you launch your app, you should see a blank page with Home at the top:
That concludes chapter 1. In the next chapter, you'll pick up from here, build out the home page, and learn about the various NativeScript UI components.
Now that you understand the basics of NativeScript and how the Playground you're ready to start creating your first (or maybe not your first) NativeScript app. Over the course of this chapter and the next few chapters you will be creating an app the reads and displays SpaceX flight data.
In this chapter will focus on designing the home page and learn to use the following NativeScript elements:
<Button>
<StackLayout>
<ScrollView>
Before we go too much further, I need to introduce you to something called a layout. Layouts are UI elements (similar to the <Label />
element) that allow you to organize the UI of each page.
If you look at the home-component
that you scaffolded in the previous chapter, you will see there is already code added to the home.component.html
for you.
<ActionBar title="Home" class="action-bar">
</ActionBar>
<ScrollView class="page">
<StackLayout class="home-panel">
<!--Add your page content here-->
</StackLayout>
</ScrollView>
The ScrollView
and StackLayout
elements are two basic layout elements (we'll talk about the ActionBar
in a later chapter). To display multiple elements on a page, you need to use a layout like the ScrollView
or StackLayout
and in most cases you will want to use both. The ScrollView
functions exactly as you would expect, it makes the content inside of it scrollable. The StackLayout
element organizes child elements vertically or horizontally.
Stack Layout
The
StackLayout
organizes child elements stacked on top of each other vertically by default.
Now that we've talked about basic layouts, let's start designing the home view.
First thing is first, go ahead and remove the ActionBar
element from home.component.html
because you will not need this on the home view. After deleting the ActionBar
your app should have a blank home view inside the NativeScript preview app.
Next, let's create a class to style the home view called base
to create a nice background image for our view. Add the base
class to the home.component.css
file (you can remove the existing home-panel
class because you will not be using that).
.base {
background-image: url("https://raw.githubusercontent.com/mikebranstein/nativescript-spacex-workshop/master/app/views/home/spacex_home.png");
background-repeat: no-repeat;
background-size: contain;
background-color: black;
background-position: center;
}
Delete the contents of the home.component.html
file and update it to use your new base
class.
<ScrollView class="base">
<StackLayout>
</StackLayout>
</ScrollView>
Your app should now have a SpaceX background image in the preview app.
The last step in creating your home view is to add a button. Add a <Button>
element inside of the <StackLayout>
in the home.component.html
file.
<ScrollView class="base">
<StackLayout>
<Button text="View Launches" class="btn btn-primary"></Button>
</StackLayout>
</ScrollView>
What are those CSS class names?
You may have noticed that you added the
btn
andbtn-primary
classes to the<Button>
that you created and that the button styled blue. Thebtn
andbtn-primary
classes are part of the NativeScript Themes that NativeScript provides to you out of the box to help style your app.
You're almost done with the home view. To make the home view look more complete we should move the button we added to the bottom of view so that it looks more polished. To complete the view, update the home.component.html
to set the vertical alignment property of the <StackLayout>
.
<ScrollView class="base">
<StackLayout verticalAlignment="bottom">
<Button text="View Launches" class="btn btn-primary"></Button>
</StackLayout>
</ScrollView>
Next add a margin to the <Button>
element by updating the home.component.css
file.
button {
margin-bottom: 100;
}
You should now be able to view the completed home view in the NativeScript preview app. In the next chapter you will work on implementing an event handler when the view launches button is tapped.
NOTE
If you're running on an Android device, you may have a top status bar that is still showing. Don't worry, we'll address that issue in the next chapter.
This concludes the exercise.
In this chapter, you'll be creating a page to view recent and upcoming SpaceX launches, then you'll learn how to navigate to and from it. You'll also learn about these NativeScript elements:
*ActionBar
In the last chapter, you refined the Home page of your app, and finished by adding a button labeled View Launches. In this section, you'll put that button to use.
But, before we can start using it, we'll need to create a page to navigate to. Let's start by adding the List page.
In this exercise, you'll be adding the List page.
In a previous chapter you learned how to add a page (or component) via the Playground. You'll recall it's straight-forward and can be done by clicking the 3 dots next to a folder and selecting the "Add component" option.
If you're comfortable doing this on your own, feel free to proceed by creating a folder underneath views named "list", then adding the List component.
After adding the List component folder and files, you need to register it in the
app.module.ts
file.
If you can get this far on your own, skip to the end of this exercise. If not, it's ok. Let's review how to add a component.
The first step to creating a new page (or component) is to create a folder for the page. As you've learned earlier, Angular components should be placed inside a folder.
Start by adding a folder named "list" to the "views" folder. Click the 3 dots next to the "views" folder:
Select "Add folder" and name the folder "list":
Then, create the "list" component by clicking the 3 dots next to the "list" folder, selecting "Add component":
After adding the list component, you should see:
Finally, open the app.module.ts
file and add the List component to the app module.
import { NgModule, NgModuleFactoryLoader, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { HomeComponent } from "./views/home/home.component";
import { ListComponent } from "./views/list/list.component";
@NgModule({
bootstrap: [
AppComponent
],
imports: [
NativeScriptModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
ListComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
})
export class AppModule { }
This concludes the exercise.
Now that you've created the List page, let's go back the button on the Home page and add navigation.
In this exercise, you'll be modifying the Home component's home.component.html
and home.component.ts
files to add navigation to the List page.
Let's start by opening the Home component's markup file (home.component.html).
In the last chapter, you added a button labeled "View Launches":
<Button
text="View Launches"
class="btn btn-primary">
</Button>
Add a tap event handler attribute to the button's markup. Don't worry if you don't understand what's going on right now, we'll explain it in a moment:
<Button
text="View Launches"
class="btn btn-primary"
(tap)="onViewLaunchesTap()">
</Button>
What did I just do?
First, you need to know that every UI element in NativeScript has a variety of events. We don't have enough time to detail each event, and many of the events are intuitive. For example, buttons have "tap" events. In Angular, you can bind (or attach) a function to an event. By adding
(tap)="onViewLaunchesTap()"
to the button element, you told your app to run theonViewLaunchesTap()
function when the button is tapped.
Angular: Where to learn more
Ok, we'll admit it - if you've used Angular before, you'll know there's a lot more to the binding syntax we just covered. Unfortunately, in a short workshop, we don't have time to cover it in greater detail. If you'd like to learn more, check out the Angular documentation.
NativeScript: Where to learn more
If you'd like to learn more about the events available to various UI elements, check out the NativeScript documentation.
Let's get back to navigation...
Now that you've added the markup to bind the "View Launches" button to the onViewLaunchesTap()
, let's add that function.
Open the home.component.ts
file and replace the Home component code with the code below.
Note
We know there's a lot of new code here, and we'll walk you through it in just a moment.
import { Component, OnInit } from "@angular/core";
import { RouterExtensions } from "nativescript-angular/router";
@Component({
selector: "home",
moduleId: module.id,
templateUrl: "./home.component.html",
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
constructor(
private routerExtensions: RouterExtensions) {
}
ngOnInit(): void {
}
onViewLaunchesTap(): void {
this.routerExtensions.navigate(["list"]);
}
}
In this updated code, you'll notice 3 major changes:
import { RouterExtensions } from "nativescript-angular/router";
private routerExtensions: RouterExtensions
onViewLaunchesTap()
functionLet's walk through each of these one by one.
At the top of the Home component code, you imported an additional class named RouterExtensions from nativescript-angular/router
. This class is a NativeScript-specific class for interacting with Angular's routing/navigation system. There's not much more to learn about it, so just remember that you need to import it on every page that you'll need to use navigation.
And here's an extra secret...we've found that you'll need this in about every component you create.
The second addition to the Home component is passing an instance of the RouterExtensions class into the constructor:
constructor(
private routerExtensions: RouterExtensions) {
}
By adding this to the Home component's constructor, you have access to an instance of the RouterExtensions class from within the Home component class.
But, how does this work?
You may be interested into how the Home component actually gets an instance of the RouterExtensions class. Angular's dependency injection does this heavy lifting. If you're not familiar with dependency injection, we recommend you read up on Angular's site.
The final change to the Home component is the addition of the onViewLaunchesTap()
function:
onViewLaunchesTap(): void {
this.routerExtensions.navigate(["list"]);
}
As you'll recall, we imported the RouterExtensions class at the top of the file and injected an instance of RouterExtensions into the Home component. This allows us to access it via this.routerExtensions
.
The RouterExtensions class has a function named navigate()
, which takes an array of strings as a parameter. The first value in that array is the name of a registered route in our application.
Registered routes?
A registered route is a key-value pair, where the key is a string (like "list") and the value is a component (like the List component we just created). You may have noticed we haven't "registered" the "list" route yet, but we'll do that next.
Now that you know how the navigate()
function works, you can see that when the "View Launches" button is tapped, the onViewLaunchesTap()
function navigates to the registered route named "list".
To close out our journey to navigate between pages / components, we need to register the "list" route and point it to our List component.
Open the app-routing.module.ts
file, import the List component, and add an entry to the Routes array:
import { NgModule } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { Routes } from "@angular/router";
import { HomeComponent } from "./views/home/home.component";
import { ListComponent } from "./views/list/list.component";
const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "home", component: HomeComponent },
{ path: "list", component: ListComponent },
];
@NgModule({
imports: [NativeScriptRouterModule.forRoot(routes)],
exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }
Now that we've finished adding navigation, reload the app on your mobile device. When you tap the "View Launches" button, you should navigate to the List page/component:
This concludes the exercise.
Nice work! You've just learned how to add navigation to your app. As a quick reminder, there are a few steps to take:
app.module.ts
file)(tap)="onTapFunction()"
RouterExtensions
into the originating component's class codeRouterExtensions
instance into the originating component's constructorapp-routing.module.ts
file NOTE
Yeah, we know, it's a lot of code just to navigate, but that's the Angular way ;-)
In the final part of this chapter, you'll learn how to use NativeScript's Action Bar.
Action Bar
The Action Bar is a UI element that controls the upper area of the screen, allowing you to create (or customize) a "header" of the page.
Here's the area of the page we're going to modify:
Action Bars are a versatile UI element in NativeScript, and you can use them to customize your app in various ways, including: navigation to the previous page, header icons, and page title/headings. We're not going to dive into the intimate details of Action Bars, so if you're interested in learning more, check out the official documentation.
Now that you know a little bit about the Action Bar,let's dive in and customize it.
In this exercise, you'll be modifying the List component's Action Bar (ever so slightly) to change the header text of the List page.
Earlier in this chapter, you created the List component, and left the UI markup alone. Open the list.component.html
file again and review it's contents. It should look similar to:
<ActionBar title="List" class="action-bar">
</ActionBar>
<ScrollView class="page">
<StackLayout class="home-panel">
<!--Add your page content here-->
</StackLayout>
</ScrollView>
As shown in the markup above, an Action Bar can be customized by adding a title attribute. The title attribute is straight-forward and sets the title of the page. The action-bar
class name helps to style it to match your overall app theme's and applies default styles using the NativeScript Theme plugin.
Theme Details
For more information about styling the Action Bar with the Theme plugin, check out the official documentation.
As for the markup following the Action Bar, ignore it. You'll be replacing it later.
Let's make a simple modification to the Action Bar and update the title.
Change the title to "SpaceX Launches" or something equally appropriate:
<ActionBar title="SpaceX Launches" class="action-bar">
</ActionBar>
Refresh the app on your mobile device and you should see something similar on the List page:
NOTE
This was a simple example on using an Action Bar. If you're in for a challenge, try adding an icon to the Action Bar, customizing the background color, or changing the back button icon. There are a ton of online resources for customizing the Action Bar in NativeScript. Let me Google that for you.
This concludes the exercise.
In the last chapter, we said you'd be updating the Home page action bar to fix an Android issue.
Cross-platform Differences
In the cross-platform work, rendering native UI components (like NativeScript does) is a tough problem because of subtleties in the underlying mobile platforms. As a mobile developer, you'll run into these from time to time. NativeScript does well at abstracting these differences from you, but dealing with Angular's status bar / action bar is one of those times you need to do a little more legwork. Let's get started and fix the problem.
Let's start by investigating the problem on Android. In the last chapter, we removed the Action Bar element from the UI. When you did this, it removed it from iOS devices, but it did not remove it from Android devices.
The appearance of the status bar / action bar at top makes the app less appealing. So let's remove it.
Start by opening the home.component.ts
file.
Import the Page
class at top:
import { Page } from "ui/page";
Next, update the constructor
and ngOnInit()
function:
constructor(
private routerExtensions: RouterExtensions,
private page: Page) {
}
ngOnInit(): void {
this.page.actionBarHidden = true;
}
We're not going to dive deep on what you just did right now, but we promise to cover it later. For now, just know that when your page loads, it will set the action bar to hidden using this code: this.page.actionBarHidden = true;
.
And let's see how it looks on Android:
Beautiful.
This concludes the exercise.
This concludes the chapter.
Now that you know how to navigate between different views, you can work on making the list component more complex by displaying data. In this chapter you will learn about two new NativeScript elements:
ListView
TabView
As you already know, the NativeScript playground provides numerous components you can drag and drop into your project. Go ahead and find the ListView
component in the list of components and drag it inside the stack layout.
And you're done! you created a list view! Just kidding, but the playground does make it easy to get started learning NativeScript components. Go ahead and save your project and take a look at your app in preview.
You may have noticed that there is data showing up. That's because when you drag a component in the playground it scaffolds out a lot of code for you. Take a look at list.component.ts
and you will see the code that is generating the data in the ListView
that you drag-and-dropped.
Before we replace this code, let's jump back over to the view code and update it a bit. Replace the current ScrollView
and StackLayout
with a single GridLayout
containing a Label
and ListView
.
<GridLayout rows="auto,*" columns="*" class="page">
<Label class="h2 text-center" text="Upcoming Launches"></Label>
<ListView row="1" [items]="launches" class="list-group">
<ng-template let-item="item">
<GridLayout rows="*, *, *" columns="auto, *" class="list-group-item">
<Image row="0" col="0" rowSpan="3" [src]="item.links.mission_patch" class="img-circle"></Image>
<Label row="0" col="1" [text]="item.rocket.rocket_name"></Label>
<Label row="1" col="1" [text]="item.launch_date_utc"></Label>
<Label row="2" col="1" textWrap="true" [text]="item.launch_site.site_name_long"></Label>
</GridLayout>
</ng-template>
</ListView>
</GridLayout>
Let's talk about the ListView
in more depth. We're using the Angular syntax to bind the items in the ListView
to a collection of elements called launches
:
<ListView [items]="launches" class="list-group">
When using the ListView
component we typically define a template.
<ng-template let-item="item">
<GridLayout rows="*, *, *" columns="auto, *" class="list-group-item">
<Image row="0" col="0" rowSpan="3" [src]="item.links.mission_patch" class="img-circle"></Image>
<Label row="0" col="1" [text]="item.rocket.rocket_name"></Label>
<Label row="1" col="1" [text]="item.launch_date_utc"></Label>
<Label row="2" col="1" textWrap="true" [text]="item.launch_site.site_name_long"></Label>
</GridLayout>
</ng-template>
Let's take a moment to review the ListView
template that we just created. You added a new layout type called GridLayout
. The GridLayout
allows you to place each nested element into a row or column. Rows and columns are defined in a comma separated list the the rows
and columns
properties. Rows and columns can be sized in the following three ways:
In our grid example we use *
for the rows to make each row the same size. Then our first column is auto-sized based on the image in with the second column taking up the remaining space.
The rest of the element in our GridLayout
(the Image
and Label
), should be familiar to you because we used them earlier. However, we're using them differently: they're being bound to properties of item
. The let-item
syntax of Angular defines the item
variable that is used to bind to and render each template for the list of launches
that we are binding to (similar to how a for loop works).
This concludes the exercise.
Now that you have the view updated we need to produce the list of launches that our UI is binding to. In order to bind data to our UI we will first need to create some models and return them from a service. Let's get started.
The first order of business is to generate some models so you can bind your data to strongly typed objects. First create a new folder underneath the app
folder called models
.
Next, create 4 TypeScript interfaces inside of the models
folder.
Specific data on the rocket that was launched.
export interface Rocket {
rocket_id: string;
rocket_name: string;
}
Metadata about the rocket launch.
export interface Links {
mission_patch: string;
article_link: string;
video_link: string;
}
Location data about where the rocket was launched from.
export interface LaunchSite {
site_id: string;
site_name: string;
site_name_long: string;
}
Full launch data.
import { Rocket } from "./rocket";
import { LaunchSite } from "./launchSite";
import { Links } from "./links";
export interface Launch {
flight_number: number;
launch_year: string;
launch_date_utc: string;
launch_success: boolean
details: string;
rocket: Rocket;
launch_site: LaunchSite;
links: Links;
}
Now that you have created the models to hold the SpaceX launch data, you need to create a service to return it. In this chapter we will be creating a service that returns some hard-coded data. In the next chapter we will pull the data from the SpaceX public api. Start by adding another folder to help keep your code organized called services
underneath the app
folder.
Next, add a launchService.ts
file to the folder that you just created.
import { Injectable } from "@angular/core";
import { Launch } from '../models/launch'
@Injectable()
export class LaunchService {
private launches: Launch[];
constructor() {
this.launches = [
{
"flight_number": 42,
"launch_year": "2017",
"launch_date_utc": "2017-06-23T19:10:00Z",
"rocket": {
"rocket_id": "falcon9",
"rocket_name": "Falcon 9",
},
"launch_site": {
"site_id": "ksc_lc_39a",
"site_name": "KSC LC 39A",
"site_name_long": "Kennedy Space Center Historic Launch Complex 39A"
},
"launch_success": true,
"links": {
"mission_patch": "https://i.imgur.com/VAvulaO.png",
"article_link": "https://en.wikipedia.org/wiki/BulgariaSat-1",
"video_link": "https://www.youtube.com/watch?v=Y8mLi-rRTh8"
},
"details": "Second time a booster will be reused: Second flight of B1029 after the Iridium mission of January 2017. The satellite will be the first commercial Bulgarian-owned communications satellite and it will provide television broadcasts and other communications services over southeast Europe."
},
{
"flight_number": 56,
"launch_year": "2018",
"launch_date_utc": "2018-02-22T14:17:00Z",
"rocket": {
"rocket_id": "falcon9",
"rocket_name": "Falcon 9",
},
"launch_site": {
"site_id": "vafb_slc_4e",
"site_name": "VAFB SLC 4E",
"site_name_long": "Vandenberg Air Force Base Space Launch Complex 4E"
},
"launch_success": true,
"links": {
"mission_patch": "https://i.imgur.com/6iUJpn4.png",
"article_link": "https://spaceflightnow.com/2018/02/22/recycled-spacex-rocket-boosts-paz-radar-satellite-first-starlink-testbeds-into-orbit/",
"video_link": "https://www.youtube.com/watch?v=-p-PToD2URA"
},
"details": "First flight with fairing 2.0. Will also carry two SpaceX test satellites for the upcoming Starlink constellation."
}
];
}
public getUpcoming(): Launch[] {
return this.launches;
}
public getPast(): Launch[] {
return this.launches;
}
}
For now, we will create some dummy data and return it from our service so we can quickly test our UI code. Our service exposes 2 methods - getUpcoming()
and getPast()
. Both return the same dummy data (for now) You'll implement the actual API calls in the next chapter.
Next, call the service from the when it loads. To do this, update the list.component.ts
file.
import { Component, OnInit } from "@angular/core";
import { Launch } from "../../models/launch";
import { LaunchService } from "../../services/launchService";
@Component({
selector: "list",
moduleId: module.id,
templateUrl: "./list.component.html",
styleUrls: ['./list.component.css'],
providers: [LaunchService]
})
export class ListComponent implements OnInit {
launches: Launch[];
constructor(private launchService: LaunchService) { }
ngOnInit(): void {
this.launches = this.launchService.getPast();
}
}
NOTE
In the updated ListComponent class, there is a new line defining
providers: [LaunchService]
. Providers are part of Angular dependency injection. In a nutshell, when Angular creates a component, it automatically creates instances of the the items in the providers array (the dependencies); the lifecycle of these dependencies is tied to the component that they are providing. For additional information you can read more about Angular Dependency Injection.Classes must be marked
@Injectable()
for Angular to be able to recognize them as providers. Notice that we you marked theLaunchService
as injectable earlier in this chapter.
Go ahead and take a look at your app in the NativeScript preview app.
The images aren't showing correctly! This is because you haven't given our images a proper size so you just need to update the list.component.css
file.
image {
width: 40;
margin-right: 16;
}
After saving everything looks great!
This concludes the exercise.
Right now you have only added the upcoming launches to the list view, well, it's time to learn about the TabView
component and have previous launches and upcoming launches on the same ListComponent
. Let's get started!
Update the list.component.html
to contain 2 lists, one for upcoming launches and one for previous launches.
<ActionBar title="SpaceX Launches" class="action-bar">
</ActionBar>
<TabView class="tab-view" selectedIndex="0">
<GridLayout rows="auto,*" columns="*" class="page" *tabItem="{title: 'Upcoming Launches'}">
<Label class="h2 text-center" text="Upcoming Launches"></Label>
<ListView row="1" [items]="upcoming" class="list-group">
<ng-template let-item="item">
<GridLayout rows="*, *, *" columns="auto, *" class="list-group-item">
<Image row="0" col="0" rowSpan="3" [src]="item.links.mission_patch" class="img-circle"></Image>
<Label row="0" col="1" [text]="item.rocket.rocket_name"></Label>
<Label row="1" col="1" [text]="item.launch_date_utc"></Label>
<Label row="2" col="1" textWrap="true" [text]="item.launch_site.site_name_long"></Label>
</GridLayout>
</ng-template>
</ListView>
</GridLayout>
<GridLayout rows="auto,*" columns="*" class="page" *tabItem="{title: 'Previous Launches'}">
<Label class="h2 text-center" text="Previous Launches"></Label>
<ListView row="1" [items]="past" class="list-group">
<ng-template let-item="item">
<GridLayout rows="*, *, *" columns="auto, *" class="list-group-item">
<Image row="0" col="0" rowSpan="3" [src]="item.links.mission_patch" class="img-circle"></Image>
<Label row="0" col="1" [text]="item.rocket.rocket_name"></Label>
<Label row="1" col="1" [text]="item.launch_date_utc"></Label>
<Label row="2" col="1" textWrap="true" [text]="item.launch_site.site_name_long"></Label>
</GridLayout>
</ng-template>
</ListView>
</GridLayout>
</TabView>
Tabs inside of the TabView
component are denoted using the *tabItem=
syntax. You have created 2 ListView
components in 2 tabs. The list views should look like the ones that you just created in the last exercise. Notice that the you are binding the separate ListViews
to different arrays named upcoming
and past
but the rest of the syntax is the same.
NOTE
The
TabView
component sets a default selected tab using theselectedIndex
property:<TabView class="tab-view" selectedIndex="0">
.
Update the list.component.ts
to add the upcoming
and past
arrays.
import { Component, OnInit } from "@angular/core";
import { Launch } from "../../models/launch";
import { LaunchService } from "../../services/launchService";
@Component({
selector: "list",
moduleId: module.id,
templateUrl: "./list.component.html",
styleUrls: ['./list.component.css'],
providers: [LaunchService]
})
export class ListComponent implements OnInit {
upcoming: Launch[];
past: Launch[];
constructor(private launchService: LaunchService) { }
ngOnInit(): void {
this.past = this.launchService.getPast();
this.upcoming = this.launchService.getUpcoming();
}
}
Save your project and test out your tabs!
This concludes the exercise.
Nice work, and congrats on getting through a lot of code in this chapter! In th next chapter, you'll continue to update the app by obtaining live launch data from an API.
The SpaceX app is starting to look great but you still have some work to do. Static data isn't much fun so let's work on getting data from the SpaceX API.
NOTE
This chapter isn't going to cover anything specific to NativeScript. Instead you'll learn how to integrate a restful API into Angular. Even though this isn't NativeScript-specific, it's still important. Almost every mobile app you'll write will integrate with an API.
Let's jump in!
Before you call the SpaceX API we need to first discuss RxJS. You may have heard of it before - if so feel free to skip ahead. RxJS is the ReactiveX library for JavaScript. (ok but what does that mean?)
RxJS is a library for using Observables. We aren't going to discuss observables in detail, but, you do need to know that observables are objects that allow you to subscribe events. For example, if a value changes in an observable, subscribers will be notified of the change. Luckily for us NativeScript and Angular already know how to use observables so we can bind observable objects straight to our UI when we receive them back from our API calls.
To use the SpaceX API you need to update the LaunchService
and add the following import statements.
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/map';
import { Observable } from "rxjs/Observable";
Next add methods to make the API calls.
public getUpcomingFromAPI(): Observable<Launch[]> {
return this.http.get<Launch[]>('https://api.spacexdata.com/v2/launches/upcoming'); // upcoming launches
}
public getPastFromAPI(): Observable<Launch[]> {
return this.http.get<Launch[]>('https://api.spacexdata.com/v2/launches?start=1-1-2017&final=12-31-2020&order=desc'); // past launches
}
Where does
this.http
come from?This (no pun intended) is an instance of the Angular
HttpClient
which needs to be injected via the constructor. Go ahead and update the constructor of theLaunchService
to inject theHttpClient
:constructor(private http: HttpClient) {
.
The full code listing is below.
import { Injectable } from "@angular/core";
import { Launch } from '../models/launch';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/map';
import { Observable } from "rxjs/Observable";
@Injectable()
export class LaunchService {
private launches: Launch[];
constructor(private http: HttpClient) {
this.launches = [
{
"flight_number": 42,
"launch_year": "2017",
"launch_date_utc": "2017-06-23T19:10:00Z",
"rocket": {
"rocket_id": "falcon9",
"rocket_name": "Falcon 9",
},
"launch_site": {
"site_id": "ksc_lc_39a",
"site_name": "KSC LC 39A",
"site_name_long": "Kennedy Space Center Historic Launch Complex 39A"
},
"launch_success": true,
"links": {
"mission_patch": "https://i.imgur.com/VAvulaO.png",
"article_link": "https://en.wikipedia.org/wiki/BulgariaSat-1",
"video_link": "https://www.youtube.com/watch?v=Y8mLi-rRTh8"
},
"details": "Second time a booster will be reused: Second flight of B1029 after the Iridium mission of January 2017. The satellite will be the first commercial Bulgarian-owned communications satellite and it will provide television broadcasts and other communications services over southeast Europe."
},
{
"flight_number": 56,
"launch_year": "2018",
"launch_date_utc": "2018-02-22T14:17:00Z",
"rocket": {
"rocket_id": "falcon9",
"rocket_name": "Falcon 9",
},
"launch_site": {
"site_id": "vafb_slc_4e",
"site_name": "VAFB SLC 4E",
"site_name_long": "Vandenberg Air Force Base Space Launch Complex 4E"
},
"launch_success": true,
"links": {
"mission_patch": "https://i.imgur.com/6iUJpn4.png",
"article_link": "https://spaceflightnow.com/2018/02/22/recycled-spacex-rocket-boosts-paz-radar-satellite-first-starlink-testbeds-into-orbit/",
"video_link": "https://www.youtube.com/watch?v=-p-PToD2URA"
},
"details": "First flight with fairing 2.0. Will also carry two SpaceX test satellites for the upcoming Starlink constellation."
}
];
}
public getUpcoming(): Launch[] {
return this.launches;
}
public getPast(): Launch[] {
return this.launches;
}
public getUpcomingFromAPI(): Observable<Launch[]> {
return this.http.get<Launch[]>('https://api.spacexdata.com/v2/launches/upcoming'); // upcoming launches
}
public getPastFromAPI(): Observable<Launch[]> {
return this.http.get<Launch[]>('https://api.spacexdata.com/v2/launches?start=1-1-2017&final=12-31-2020&order=desc'); // past launches
}
}
NOTE
Notice that
Observables
are returning from the SpaceX API. Go ahead and wire up theListComponent
to call from the API now by updating thelist-component.ts
file to use the new methods you made.
import { Component, OnInit } from "@angular/core";
import { Launch } from "../../models/launch";
import { LaunchService } from "../../services/launchService";
@Component({
selector: "list",
moduleId: module.id,
templateUrl: "./list.component.html",
styleUrls: ['./list.component.css'],
providers: [LaunchService]
})
export class ListComponent implements OnInit {
upcoming: Launch[] = [];
past: Launch[] = [];
constructor(private launchService: LaunchService) { }
ngOnInit(): void {
this.launchService.getUpcomingFromAPI().subscribe(data => {
data.forEach((model) => {
this.upcoming.push(model);
});
});
this.launchService.getPastFromAPI().subscribe(data => {
data.forEach((model) => {
if (!model.links.mission_patch.includes('https'))
model.links.mission_patch = model.links.mission_patch.replace('http', 'https'); // quick fix
this.past.push(model);
});
});
}
}
The subscribe
method is part of RxJX that we discussed earlier. When the call the SpaceX API completes, the subscribe method automatically gets called. Inside the subscribe method you are able to translate the data coming back into an array of launches which the UI already knows how to use and bind to properly.
NOTE
The if statement inside
getPastFromAPI()
may look a bit strange to you (because it is). This if statement exists because by default iOS blocks non-https calls; therefore, when attempting to load the mission patch images on iOS it will just fail. This limitation of iOS can be turned off in NativeScript, but in the Playground you can't change this setting. Keep in mind that the Playground is gets you running fast, and is not meant for production code. In a production app, we'd fix this issue by ensuring all links were HTTPS in our API (that's not a bad idea), or by disabling the iOS-specific setting.
Production Code
In case you were wondering, moving from the Playground to production code is easy. Use the download button to down any code you've already written, ensure NativeScript is installed on your machine, then follow the online documentation for getting ready for deployment. Alternatively, check out the chapters in our book, which are dedicated to getting your apps into the stores.
The last step to making everything work is to import the HttpModule
into the app by updating the app.module.ts
.
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { HttpClientModule } from "@angular/common/http";
import { HomeComponent } from "./views/home/home.component";
import { ListComponent } from "./views/list/list.component";
import { LaunchService } from "./services/launchService";
// Uncomment and add to NgModule imports if you need to use two-way binding
// import { NativeScriptFormsModule } from "nativescript-angular/forms";
// Uncomment and add to NgModule imports if you need to use the HTTP wrapper
// import { NativeScriptHttpModule } from "nativescript-angular/http";
@NgModule({
bootstrap: [
AppComponent
],
imports: [
NativeScriptModule,
AppRoutingModule,
HttpClientModule
],
declarations: [
AppComponent,
HomeComponent,
ListComponent
],
providers: [
],
schemas: [
NO_ERRORS_SCHEMA
]
})
/*
Pass your application module to the bootstrapModule function located in main.ts to start your app
*/
export class AppModule { }
Save your project and take a look!
This concludes the exercise.
Nice work. Adding remote APIs to your mobile apps isn't too difficult. Normally, we wouldn't go through the trouble of adding live data to a prototype app, but we just couldn't resist.
In the next chapter, you'll finish the SpaceX app by adding a launch details page.
In the previous chapter, you learned how to integrate an API into your app.
In this chapter, you'll be wrapping up the SpaceX app by creating a launch detail page. You'll create the Detail page, navigate from the List page, and learn how to pass context (or data) to the Detail page during navigation. Finally, you'll learn how to use various Theme classes to align UI elements.
Now that you've created a page showing the list of launches, let's build a detail page that shows the detail of a launch.
In chapter 3 you added the list page to your app by creating a new component. Adding the detail page is the same process. In this exercise, you'll be creating the Detail page.
We're not going to walk you through this process step by step, so try it out for yourself. If you're new to Angular, that's ok. Don't feel like you need to have mastered what it takes to create a new page without help. We'll give you a few pointers: first, review what you did in chapter 3; second, use this checklist to make sure you didn't forget any steps.
IMPORTANT
Before you get started, we'll be navigating to the detail page in a different way than we did previously. So, you won't need to create a tap event handler in the UI markup or Detail component class. You will still need to register the detail route, so don't forget to create that.
If you need an assist when creating the Detail component, use this checklist:
app.module.ts
file).app-routing.module.ts
file. When you're finished creating the Detail component, you should have:
detail.component.ts
, detail.component.html
, and detail.component.css
file in the detail folder (see contents of these folders below).app.module.ts
file that registers the Detail component (see updated file below).app-routing.module.ts
file (see updated file below).import { Component, OnInit } from "@angular/core";
@Component({
selector: "detail",
moduleId: module.id,
templateUrl: "./detail.component.html",
styleUrls: ['./detail.component.css']
})
export class DetailComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
<ActionBar title="Detail" class="action-bar">
</ActionBar>
<ScrollView class="page">
<StackLayout class="home-panel">
<!--Add your page content here-->
</StackLayout>
</ScrollView>
.home-panel{
vertical-align: center;
font-size: 20;
margin: 15;
}
import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { HttpClientModule } from "@angular/common/http";
import { HomeComponent } from "./views/home/home.component";
import { ListComponent } from "./views/list/list.component";
import { DetailComponent } from "./views/detail/detail.component";
import { LaunchService } from "./services/launchService";
// Uncomment and add to NgModule imports if you need to use two-way binding
// import { NativeScriptFormsModule } from "nativescript-angular/forms";
// Uncomment and add to NgModule imports if you need to use the HTTP wrapper
// import { NativeScriptHttpModule } from "nativescript-angular/http";
@NgModule({
bootstrap: [
AppComponent
],
imports: [
NativeScriptModule,
AppRoutingModule,
HttpClientModule
],
declarations: [
AppComponent,
HomeComponent,
ListComponent,
DetailComponent
],
providers: [
],
schemas: [
NO_ERRORS_SCHEMA
]
})
/*
Pass your application module to the bootstrapModule function located in main.ts to start your app
*/
export class AppModule { }
import { NgModule } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { Routes } from "@angular/router";
import { HomeComponent } from "./views/home/home.component";
import { ListComponent } from "./views/list/list.component";
import { DetailComponent } from "./views/detail/detail.component";
const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "home", component: HomeComponent },
{ path: "list", component: ListComponent },
{ path: "detail", component: DetailComponent },
];
@NgModule({
imports: [NativeScriptRouterModule.forRoot(routes)],
exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }
This concludes the exercise.
That's it! Now that you have the Detail component created, we'll be learning how to navigate and pass data to it.
In chapter 3, you learned how to navigate between components by:
/list
route to the navigate()
function of the RoutingExtensions
class.This is a great way to navigate between pages/components, but there's also a different way. In this section, you'll learn how to use an Angular/NativeScript attribute directive to navigate (and pass data) to the Detail component.
What's an attribute directive?
In Angular, an attribute directive is a piece of code added to the UI markup that modifies the appearance (or behavior) of the UI element. It sounds really fancy (like most Angular terms do), but it's really just like HTML attributes that Angular knows how to decipher. If you're interested in learning more about Angular attribute directives, check out their documentation.
Now that you know a little about Angular attribute directives, let's use one to navigate between the list and detail components!
In this exercise, you'll learn about the nsRouterLink
attribute directive, and see how it can make navigating between components easy.
nsRouterLink Attribute Directive
The
nsRouterLink
attribute directive is an attribute directive provided by NativeScript. It works like the Angular routerLink attribute directive, and allows you to add navigation capabilities to UI elements without needing to write code to bind to the tap event handler.
How do I choose?
You may be thinking that navigation in Angular apps is confusing, because we've introduced 2 ways of navigating between pages/components. When should you write code and bind to the tap event handler, and when should you use
nsRouterLink
? We recommend you usensRouterLink
for navigation, by default. This will navigate you to components the Angular way. If you need to perform other actions before you navigate (like change UI components, submit data to a web API, or some other business logic), use a tap event handler.
NOTE
We are only applying these changes to the Previous Launches list view. Watch out!
Start by opening the list.component.html
file, and locate the ng-template
you added to the Previous Launches ListView
. Your code should look like:
<ng-template let-item="item">
<GridLayout rows="*, *, *" columns="auto, *" class="list-group-item">
<Image row="0" col="0" rowSpan="3" [src]="item.links.mission_patch" class="img-circle"></Image>
<Label row="0" col="1" [text]="item.rocket.rocket_name"></Label>
<Label row="1" col="1" [text]="item.launch_date_utc"></Label>
<Label row="2" col="1" textWrap="true" [text]="item.launch_site.site_name_long"></Label>
</GridLayout>
</ng-template>
Next, add the nsRouterLink
attribute to the GridLayout
:
<ng-template let-item="item">
<GridLayout rows="*, *, *" columns="auto, *" [nsRouterLink]="['/detail', item.flight_number]" class="list-group-item">
<Image row="0" col="0" rowSpan="3" [src]="item.links.mission_patch" class="img-circle"></Image>
<Label row="0" col="1" [text]="item.rocket.rocket_name"></Label>
<Label row="1" col="1" [text]="item.launch_date_utc"></Label>
<Label row="2" col="1" textWrap="true" [text]="item.launch_site.site_name_long"></Label>
</GridLayout>
</ng-template>
That's it! But, let's decode what's happening.
By adding [nsRouterLink]="['/detail', item.flight_number]"
to the GridLayout
element, you tell Angular to navigate to the /detail
route, passing in item.flight_number
to the component linked to that route.
Ok, we may have been a bit misleading by saying "that's it" above. There is one more step to making the nsRouterLink
attribute directive navigate (and pass data) to the Detail component correctly.
As you'll recall, you passed item.flight_number
to the Detail component above. But, the route doesn't know anything about the flight number being passed in, so we need to "educate it" ;-).
Open the app-routing.module.ts
file, and locate the /detail
route you created earlier in the chapter. You should see:
{ path: "detail", component: DetailComponent }
Update the detail route so it expects data will be supplied to the route when it is navigated to:
{ path: "detail/:id", component: DetailComponent }
You're finished! By adding /:id
to the /detail
route, you instruct Angular to expect (and parse) the data passed into it as a variable named id
.
Update the app on your mobile device and check that you can navigate between the List and Detail pages by tapping on a launch.
WARNING
You just added the
nsRouterLink
attribute directive to the Previous Launches template, but didn't add it to the upcoming launches. This was on purpose. In the spirit of time, we won't be showing upcoming launch details because they change often and would require additional coding. So, we're keeping it simple and are only showing previous launch details.
This concludes the exercise. In the next exercise, you'll learn how to access the id
variable of the /detail
route.
In the last section, you learned how to use the nsRouterLink
attribute directive to navigate (and pass data) to the Detail component. Now, let's learn how to access the flight number that you passed into the id
router variable.
In this exercise, you'll learn how to access navigate route data in the Detail component.
Let's start by opening the detail.component.ts
file. We haven't changed the file since we created it, so you should have:
import { Component, OnInit } from "@angular/core";
@Component({
selector: "detail",
moduleId: module.id,
templateUrl: "./detail.component.html",
styleUrls: ['./detail.component.css']
})
export class DetailComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
Update the Detail component with some new code. It's ok if you don't understand it right now, we'll explain.
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
@Component({
selector: "detail",
moduleId: module.id,
templateUrl: "./detail.component.html",
styleUrls: ['./detail.component.css']
})
export class DetailComponent implements OnInit {
constructor(
private route: ActivatedRoute) { }
ngOnInit(): void {
const flightNumber = +this.route.snapshot.params["id"];
}
}
You just added 3 things to the Detail component:
ActivatedRoute
class, which is an Angular class used to access route information (like the id
variable you passed in).ActivatedRoute
class via the constructor (you'll recall you did that previously in the workshop).id
variable and stored it in the flightNumber
variable.Most of the code you added should be familiar (the structure, but perhaps not the actual classes). We're not going to dive deep into the ActivatedRoute
class, but we think it's a good opportunity to explain const flightNumber = +this.route.snapshot.params["id"];
.
Let's start with this.route.snapshot.params["id"]
- this retrieves the value passed into the id
route variable. As you'll recall, this is the flight number we passed in via the nsRouterLink
attribute directive.
Next, what's with that +
in front of this.route.snapshot.params["id"]
? You may not have seen this before, and it's something we often forget. The +
is a JavaScript feature that converts the value of this.route.snapshot.params["id"]
to a number. It's needed because data passed via Angular routes are strings.
OnInit vs Component Constructor
You may be wondering why you didn't retrieve the route variable in the constructor. This is an Angular style guideline. Per Angular guidelines, component constructors should only be used to initialize class references. No processing, business logic, or any action that could take time should go into the constructor. Instead, Angular recommends placing these types of activities (like route parameter retrieval) be done in the
OnInit()
function. There are other detail about whyOnInit()
should be used, but we're not going to cover them. For more details, see a good write-up by Todd Motto.
This concludes the exercise. In the next exercise, you'll reuse the Launch service previously created to retrieve the launch detail.
Cool. You just passed data between pages/components and retrieved the data. Let's keep going.
In this exercise, you'll retrieve launch detail from the launch service.
Let's start by adding a function to the Launch service to retrieve data about a specific launch.
Open the launchService.ts
file.
Add a function named getLaunch()
to the LaunchService
class:
public getLaunch(flight_number: number): Observable<Launch> {
return this.getPastFromAPI()
.map(launches => launches.find(launch => launch.flight_number == flight_number));
}
We're not going to explain the details of this function, but it retrieves a specific launch by searching the the previous launches for a specific launch number. How convenient ;-).
Now, head back to the Detail component, and inject the Launch service:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { LaunchService } from "../../services/launchService";
@Component({
selector: "detail",
moduleId: module.id,
templateUrl: "./detail.component.html",
styleUrls: ['./detail.component.css'],
providers: [LaunchService]
})
export class DetailComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private launchService: LaunchService) { }
ngOnInit(): void {
const flightNumber = +this.route.snapshot.params["id"];
}
}
As you can see, you added a few things:
LaunchService
class.LaunchService
class as a provider.LaunchService
class by adding it to the constructor.You've done this before, so it should look familiar.
Next, update the Detail component to get a launch from the Launch service:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Launch } from "../../models/launch";
import { LaunchService } from "../../services/launchService";
import { Observable } from "rxjs/Observable";
@Component({
selector: "detail",
moduleId: module.id,
templateUrl: "./detail.component.html",
styleUrls: ['./detail.component.css'],
providers: [LaunchService]
})
export class DetailComponent implements OnInit {
launch: Launch;
constructor(
private route: ActivatedRoute,
private launchService: LaunchService) { }
ngOnInit(): void {
const flightNumber = +this.route.snapshot.params["id"];
this.launchService.getLaunch(flightNumber)
.subscribe(launch => {
this.launch = launch;
if (!this.launch.links.mission_patch.includes('https'))
this.launch.links.mission_patch = this.launch.links.mission_patch.replace('http', 'https');
});
}
}
You'll notice that you added a public launch
variable to the Detail component, then populated it in the OnInit()
function by calling the getLaunch()
you created previously.
NOTE
There is a little bit of RxJS mixed in near
getLaunch()
. We're not going to explain it in detail, so just know that we're subscribing to thegetLaunch()
function, and when it returns the launch detail, we're setting it to the publiclaunch
property.Oh, we're also applying the same HTTPS fix to this function so we can download images on iOS.
That's all!
This concludes the exercise. In the next exercise, you'll update the UI markup to display the launch detail.
In this final exercise, you'll finish the SpaceX app by adding UI markup to the Detail page.
In this exercise, you'll update the UI of the Detail page to display launch detail.
In previous chapters, you learned about the ActionBar
, ScrollView
, and StackLayout
elements.
ActionBar
is used to set the page title.ScrollView
allows the page to scroll if content extends past the original view.StackLayout
allows you to stack UI elements above/below in a vertical manner.Update the detail.component.html
file.
<ActionBar title="Launch Detail" icon="" class="action-bar">
</ActionBar>
<ScrollView class="page">
<StackLayout>
</StackLayout>
</ScrollView>
What's the page class name?
That's another use of the NativeScript Theme plugin. The
page
class is a convenient class name that makes your app pages look uniform with very little effort. To learn more, check out the documentation.
With the foundation set, let's start adding various UI components.
Add an image inside the stack layout. You've added images throughout the workshop, and this image is like the images you added to the List component. Note the src
attribute is data-bound to the links.mission_patch
property of the launch
variable.
<Image [src]="launch.links.mission_patch"></Image>
How can the page bind to the launch variable?
You might be wondering why the UI page can bind to the
launch
variable. If you look back earlier in this chapter, you'll recall that we added a public variable namedlaunch
to the DetailComponent class. Public variables can be data-bound.
Next, update the detail.component.css
file to restrict images to 50% width.
image {
width: 50%;
}
Add a flight number by binding to the flight_number
property.
<Label
text="{{ 'Flight Number: ' + launch.flight_number }}"
class="body m-l-20 m-t-20"
textWrap="true">
</Label>
You'll notice that we used an advanced form of Angular data-binding that allows you to mix text and data-bound variables. The double-mustache syntax treats it's internal text as a data-binding expression. Using data-binding expressions like this comes in handy, making it easier to seamlessly blend text and variables.
What do the CSS class names do?
The
body
,m-l-20
, andm-t-20
are more CSS class names from the NativeScript Theme plugin. Thebody
class name styles the label text as long-form paragraphs. You can learn more about text formatting in the text formatting documentation.The other class names also come from the themes plugin, and are short-hand for alignment and spacing. Generally,
m
stands for CSS margin,l
andt
stand for left and top, and the number value stand for 20 space units. So, you can read it as margin-left-20 and margin-top-20. You'll continue to use theme class names like this throughout the page. If you'd like to learn more, check out the padding and margin documentation.
Add three more launch data points the stack layout below the launch number.
<Label
text="{{ 'Date: ' + launch.launch_date_utc }}"
class="body m-l-20 m-t-20"
textWrap="true">
</Label>
<Label
[text]="launch.launch_site.site_name_long"
class="body m-l-20 m-r-20 m-t-20"
textWrap="true">
</Label>
<Label
[text]="launch.details"
class="body m-l-20 m-r-20 m-t-20"
textWrap="true">
</Label>
Each launch has an official article describing the launch. It's nice to show the URL, but on a mobile device, that's not enough. You really need to hyperlink the URL to open in a browser. You'll be adding the URL to the page as a label, and then using a tap event handler to open the URL in a browser.
Start by adding the launch article label to the stack layout.
<Label
text="{{ 'Article: ' + launch.links.article_link }}"
class="body m-l-20 m-r-20 m-t-20"
textWrap="true"
(tap)="onLinkTap(launch.links.article_link)">
</Label>
Next, implement the onLinkTap()
function in detail.component.ts
.
onLinkTap(link: string): void {
// to do: open the link in a browser
}
Now that you have the onLinkTap()
stub added, turn your attention to the implementation of the function.
You're welcome to write custom code for Android or iOS that opens a URL in the appropriate browser, but NativeScript has a collection of utility functions that provide various cross-platform functionalities (like opening a URL). They're done the heavy lifting for you, so let's take advantage of it!
Import the utility classes into detail.component.ts
by adding this import statement.
import * as utils from "utils/utils";
Then, use the utils
class to call the openUrl()
function. Your completed onLinkTap()
function should look like this.
onLinkTap(link: string): void {
utils.openUrl(link);
}
Now, when you tap the launch article URL, a browser will open, loading the URL.
The last UI element to add is the launch video. After all, what's a SpaceX app without a video of the launch (and my personal favorite is the Falcon Heavy - check it out - definitely).
Add another label to the stack layout.
<Label
text="{{ 'Video: ' + launch.links.video_link }}"
class="body m-l-20 m-r-20 m-t-20"
textWrap="true"
(tap)="onLinkTap(launch.links.video_link)">
</Label>
You'll notice you were able to reuse the onLinkTap()
function with this link. Pretty convenient.
This concludes the exercise.
You've worked through a lot of code in this workshop, but we think there's one small change that will make the app look even better: a dark theme!
Themes
We've talked about the NativeScript theme plugin throughout the workshop, and we'd like to see it in action.
NativeScript come with a variety of theme color schemes:
@import '~nativescript-theme-core/css/aqua.css';
@import '~nativescript-theme-core/css/blue.css';
@import '~nativescript-theme-core/css/brown.css';
@import '~nativescript-theme-core/css/forest.css';
@import '~nativescript-theme-core/css/grey.css';
@import '~nativescript-theme-core/css/lemon.css';
@import '~nativescript-theme-core/css/lime.css';
@import '~nativescript-theme-core/css/orange.css';
@import '~nativescript-theme-core/css/purple.css';
@import '~nativescript-theme-core/css/ruby.css';
@import '~nativescript-theme-core/css/sky.css';
This image shows the color schemes.
We think the dark color scheme looks better.
Open the app.css
file and change the theme color scheme to dark:
@import 'nativescript-theme-core/css/core.dark.css';
When you refresh the app on your mobile device, you should see the dark color scheme on the list and details pages.
This concludes the exercise.
Well done. You've finished the workshop, and have a sweet SpaceX mobile app to show your friends and family.
This concludes the workshop.


This plugin allows wrapping and unwrapping text in backticks (`
) via
intention actions, while also supporting multiple carets.
The idea was born because of the inaccessibility of the backtick symbol in
Windows using the Italian keyboard configuration. That means one has to re-map the
keyboard or has to copy-paste the symbol each time.
>
Unwrap/Remove (for plain text and many languages)
Read additional details here at GitHub.


The Service Suggestion Engine (SSE) provides three types of service suggestion.
In order to use the SSE, at least one tool needs to be on the current Workflow Canvas. Tools in the workflow are added to various lists by the user depending on the type of suggestion chosen and the roles these tools should take. The sections below provide information about the three different types of suggestion and their corresponding lists.
When a "Forward Suggestion" is chosen, the SSE will suggest a list of available tools, ranked according to how well they match some desired functionality and their compatibility with the outputs of tools chosen from the Workflow Canvas. The user chooses these Tools to Feed From by selecting them from the corresponding drop-down menu that appears in the "Setup & Run Query" panel to the right of the Workflow Canvas. In the image to the right, the empty box represents the Tools to feed From and the question mark (?) box represents tools that the user would like suggested.
When a "Backward Suggestion" is chosen, the SSE will suggest a list of available tools, ranked according to how well they match some desired functionality and their compatibility with the inputs of tools chosen from the Workflow Canvas. The user chooses these Tools to Feed Into by selecting them from the corresponding drop-down menu that appears in the "Setup & Run Query" panel to the right of the Workflow Canvas. In the image to the right, the empty box represents the Tools to feed Into and the question mark (?) box represents tools that the user would like suggested.
When a "Bidirectional Suggestion" is chosen, the SSE will suggest a list of available tools, ranked according to how well they match some desired functionality and their compatibility with the inputs and outputs of certain tools chosen from the Workflow Canvas. The user chooses Tools to Feed From and Tools to Feed Into by selecting them from the corresponding drop-down menus that appears in the "Setup & Run Query" panel to the right of the Workflow Canvas. In the image to the right, the left box represents the Tools to feed From, the right box represents the Tools to feed Into, and the question mark (?) box represents tools that the user would like suggested.


Originally written by Adam Chase.
Transcribed by Joshua Hodge.
[Scene: Joey's new apartment. Everyone but Chandler is there. Joey has
decorated the place with tons of tacky stuff.]
JOEY: Huh? So whaddya think? Casa de Joey. Huh? I decorated it myself.
ROSS: Get out.
ALL: No.
MONICA: [looking at some kind of glass sculpture thing] Wow Joey, this is, uhh...
JOEY: Art.
MONICA: Art it is.
ROSS: [looking at a glass table with a panther shaped base] Look, check this out. Is it a
coffee table, is it a panther? There's no need to decide.
RACHEL: [holding a pillow made out of 4 inch red fur] Hey, nice pillow. So now tell me, is
this genuine Muppet skin?
PHOEBE: [looking at a water sculpture that looks like a window with rain running down it]
Hey, excellent, excellent water-table thing.
JOEY: Thanks, yeah. I love this but ya know what, it makes me wanna pee.
PHOEBE: Yeah, well me too, yeah. I think that's the challenge.
JOEY: Hey, how come, uhh, Chandler didn't come?
ROSS: Well uh, it's cause he had a thing with, wi-, with the thing.
JOEY: Right, I go-, I got it.
PHOEBE: So why don't ya show us the rest of your casa?
JOEY: Yeah. Uh, oh, OH, the best part, c'mon. [leads them to the bathroom, gestures
towards toilet, everyone stares, uncomprehending] Heh?
RACHEL: Hey, nice toilet.
JOEY: No no no, behind it.
ROSS: Wha-, you have a phone in here?
JOEY: That's right, I have a phone in here.
MONICA: Joey, promise me something.
JOEY: Yeah.

OPENING TITLES
[Scene: Central Perk. Monica, Chandler, and Ross are seated. Rachel is walking over
with coffee and a piece of pie.]
[Someone bumps into Rachel and she drops the pie in a guy's hood that's seated at the
table. She improvises by using the plate as a saucer for the coffee.]
RACHEL: OK, here we go. Honey, I'm sorry, they were all out of apple pie, someone just got
the last piece.
 You are not gonna believe this. I have
just been discovered.
CHANDLER: Now wait a minute, I claimed you in the name of France four years ago.
PHOEBE: Anyway, OK, now promise you won't like, freak out and say how great this is until
I'm done, OK.
ALL: OK.
PHOEBE: OK. I just met this producer of this like, teeny record company, who said that I
have a very fresh, offbeat sound and she wants to do a demo of Smelly Cat.
ALL: [congradulating her and celebrating]
PHOEBE: I told you not to do that yet. And, she wants to do a video.
ALL: [celebrating more]
PHOEBE: I'm not done yet, OK. God. OK, if that goes well, they may even want to make an
album.
[everyone is quiet, unsure if she's done or not]
PHOEBE: I'm done now.
ALL: [celebrating]
[the guy with the pie in his hood get up to leave]
RACHEL: Oh God. Ross, OK, if you care about me at all, you will get the pie out of the
man's hood.
ROSS: Get the what?
RACHEL: Pie in the hood, pie in the hood. Go.
[Ross goes over behind the guy and grabs the pie out of his hood as he leaves]
GUY: What're you doing?
ROSS: I'm sorry, my pie was, was in your hood. Now I just have to get the coffee out of
that guy's pants and I'll be back in the hospital by 7. [swats at an imaginary insect by
his head, guy leaves promptly]
[Scene: Monica and Rachel's apartment. Monica is outside the bathroom yelling at Ross
who's in the bathroom.]

ROSS: Calm down, I'm blow drying.
[Rachel enters with laundry and starts folding]
MONICA: Blow drying what, you have no hair.
RACHEL: What's goin' on?
MONICA: Your boyfriend has been in there for over an hour. I can't believe it, it's like
I'm living with him again. He's here when I go to sleep, he's here when I wake up, he's
here when I want to use the shower, ughh. It's like I'm sixteen all over again .
RACHEL: Well, you're not sixteen, you're both adults now.

RACHEL: Or ya know, he's rubber and you're glue.
ROSS: [comes out] All yours.
MONICA: I hope you cleaned your hair out of the drain.
ROSS: [in a childish voice] I hope you cleaned your hair out of the drain.

MONICA: Cut it out.
ROSS: [childish voice] Mi-mi-mii.
[Monica goes in the bathroom]
RACHEL: [sarcastically] I've never wanted you more.
[Scene: Chandler's apartment. Chandler is sitting on the bar wearing huge dog-slippers]
CHANDLER: So, whaddya say boys, should I call him? [squeezes the ear of one of the
slippers and it barks] Well, ya know what they say. Ask your slippers a question... you're
going crazy.
[Joey's apartment, phone rings]
JOEY: Hello.
CHANDLER: Hey.
JOEY: Hey!
CHANDLER: Listen, I'm, I'm sorry I didn't make it over there today.
JOEY: Oh, that's OK.
CHANDLER: Yeah well, I hear the place looks great.
JOEY: Ahh, forget about it, I'm havin' a ball. How's the apartment doin'
CHANDLER: Oh hey, it's, it's terriffic. I mean it's a regular space... fest.
JOEY: Oh, well great.
CHANDLER: Yeah I just... wanted to call and say hey.
JOEY: Well OK then. [oven timer goes off behind Chandler] Was that the oven timer?
CHANDLER: That's right my friend. It's time for...
BOTH: Baywatch!! [both turn on TV's]
JOEY: Oh, can you believe they gave Stephanie skin cancer?
CHANDLER: I still can't believe they promoted her to lieutenant.
JOEY: Naa, you're just sayin' that 'cause you're in love with Yasmine Blepe.
CHANDLER: Well, how could anyone not be in love with Yasmine Blepe?
JOEY: Hey, hey, they're runnin'
CHANDLER: See, this is the brilliance of the show. I say always keep them running. All the
time, running. Run. Run Yasmine, run like the wind.
[Scene: Central Perk. Joey is sitting between Monica and Phoebe.]
MONICA: But I thought you wanted to live by yourself.
JOEY: I did. I thought it'd be great. I figured I'd have like, time alone with my thoughts
but, ya know, it turns out I don't have as many thoughts as you'd think.
PHOEBE: Joey, why don't you talk to Chandler about moving back?
JOEY: You really think he'd take me?
MONICA: I promise you, he would definitely want you back.
[Scene: Chandler's apartment. Chandler is sitting between Rachel and Ross.]
ROSS: I'm telling you, there's no way he's moving back.
CHANDLER: But we had one of the greatest talks we ever had last night. I mean it was, it
was like when we first started living together.
ROSS: Look, I know you don't want to hear this right now but, we've seen him in his new
place, alright. And he's happy, he's, he's decorated.
RACHEL: Look, Chandler, he has moved on, OK, you have to too.
CHANDLER: But...
ROSS: No.
[Scene: Recording studio. Phoebe is getting ready to record Smelly Cat.]
PRODUCER: OK Phoebe, you ready to try one?
PHOEBE: OK. [singing] Smelly cat, smell-ly cat, what are they feeding you?  I mean like, who was that?
PRODUCER:They're your backup singers... beind you.
PHOEBE: OH!! Oh I thought they were just watching me. You know, like at, like at an
aquarium, ya know.
PRODUCER: Alrighty. From the top.
PHOEBE: OK. [singing] Smelly cat, smell-ly cat, what are they feeding you? Smelly cat
[back up singers - smelly, smelly, smelly, really bad smelly cat, it's not your fault]
OK, sorry. I'm just, I'm just not getting that everyone um, gets how smelly this cat
acually is. I just think that maybe if we could talk about this, 'cause I need to feel
that you really care about the cat.
PRODUCER: Honey, uh we, we can talk about this. It's just that it's costing about a
hundred dollars a minute to be in here.
PHOEBE: Oh OK. So, um, the cat stinks but you love it, let's go.
[Scene: Monica and Chandler's apartment. Ross is on the phone.]
ROSS: No, there is no way he was a velociraptor. No Tony, look at the cranial ridge, OK.
If Dino was a velociraptor, he would have eaten the Flintstones. Yeah, yeah. [Monica comes
out of her room] Oh, were you takin' a nap?
MONICA: I was.
 That's the other line. [gets the other line]
Hello.  OK thanks. [hangs up the other
line] Call Joanna. [back on with Tony] Hi.
MONICA: Did she leave a number?
ROSS: Did you see me write one down?
MONICA: I don't have her number, butt-munch.

MONICA: I'm not a baby, you're the baby.

ROSS: Wait hold on Tony, hold on. [answers second line] Hello. Hi, yeah no, she's right
here.  That's uh, that's my
sister's boyfriend.
MONICA: Give me that.
ROSS: OK.
MONICA: Hi sweetie, look before I forget, did I leave my diaphram at your place? Hi mom.
[she starts throwing oranges at Ross who's looking pleased with himself]
[Scene: Chandler's apartment. Chandler is sitting on the bar, bouncing a ball against
the door. Joey walks in right as he throws the ball and catches it.]
JOEY: Hey.
CHANDLER: So uhh, how's the palace?
JOEY: You know it's funny you should mention that 'cause I was thinkin'... what's with the
boxes?
CHANDLER: Oh, uhh, actually I uh, have some news.
EDDIE: Hey Chan, is that Joey guy gonna come by and pick up his moose hat or should I just
toss it out?
CHANDLER: Well, uh, why don't you ask him yourself. Joey, this is my new roommate Eddie.
EDDIE: Nice to meet ya.
JOEY: Likewise. Uh, I'll take that. [grabs moose hat] It's what I came for. So, this is
new. Where'd you two meet?
EDDIE: At the uh, supermarket, in the uh, ethnic food section. I helped him pick out a
chorizo.
JOEY: Wow.

JOEY: Oh, now it's a spare room?
CHANDLER: Well yeah, in that it's not being used and I... have it to spare.
JOEY: Well I uh, got what I came for. [puts on moose hat] I'll uh, I'll see you guys.
CHANDLER: Hey Jo. When'd you start usin' mousse in your hair?
EDDIE: [annoying laugh] Is this guy great or what?
JOEY: Yeah, yeah he is. [leaves]
[Scene: Monica and Rachel's apartment. Monica, Rachel, and Ross are there.]
MONICA: I can't believe he has a new roommate. Who is this guy?
ROSS: Uh, Eddie something. He just met him.
RACHEL: It'll never last, he's just a rebound roommate.
[Pheobe enters]
PHOEBE: Hey.
ALL: Hey.
PHOEBE: Oh, check it out, oh check it out. It's Smelly Cat the video.
ALL: [cheer]
PHOEBE: Now OK, I haven't seen it yet so, if you don't like it, well, so what, none of you
ever made a video. [puts the tape in] OK.
[The video is a very dramatic episode with an obviously dubbed voice for Phoebe. Everyone
watches in disbeliefe]
PHOEBE: Oh my God.
ROSS: I know.
PHOEBE: I sound amazing. I, I, I've never heard myself sing before.  Oh, this is so cool, now I can hear what you hear.
RACHEL: Pretty uhm, different huh?
PHOEBE: Oh, I am sorry but I am incredibly talented.
[Scene: Chandler and Eddie's apartment. Joey stops by. Chandler is reading the paper
and Eddie is fixing eggs.]
EDDIE: Hi Joey, what's goin' on man?
JOEY: Eddie.
CHANDLER: Morning.
JOEY: Morning. I just uh, came by to pick up my mail. [looks for the mail on the table by
the door, it's not there] Where's the mail?
CHANDLER: Oh it's uh, over there on the table.
JOEY: You don't keep it over here on this table any more?
CHANDLER: No, Eddie likes to keep it over there.
EDDIE: Alright, here you go my friend. Eggs a-la Eddie, huh?
CHANDLER: Oh, ooh.
JOEY: Huh.
CHANDLER: What?
JOEY: No I just uh, thought you liked your eggs with the bread with the hole in the
middle, a-la me.
CHANDLER: Well I do, but uh, Eddie makes them this way and, well they're pretty darn good.
EDDIE: Well you guys, I'm outta here. See ya pals.
CHANDLER: See ya. [Eddie leaves]
JOEY: So how you two gettin' along?
CHANDLER: Oh, I couldn't be happier.
JOEY: Great, well, I'm happy for ya. [picks up the orange juice carton and it's empty]
Alright that's it. He just comes in here, Mr. Jonny Neweggs, with his, his, his movin' the
mail and his, his 'see ya pals'. And now there's no juice. There's no juice f or the
people who need the juice and want the juice. I need the juice.
CHANDLER: There's another carton right over there.
JOEY: Hey, this isn't about juice anymore, alright man.
CHANDLER: Alright, so what's it about?
JOEY: Eggs. Who's eggs do you like better, his or mine, huh?
CHANDLER: Well I like both eggs equally.
JOEY: Oh come on. Nobody likes two different kinds of eggs equally. You like one better
than the other and I wanna know which.
CHANDLER: Well what's the difference? Your eggs aren't here anymore, are they? You took
your eggs and you left. You really expect me to never find new eggs?
[Scene: Monica and Rachel's apartment. Ross and Monica are fighting over the remote.]
MONICA: I wanna watch Entertainment Tonight.
ROSS: Tough noogies, we're watching Predators of the Serengetti.
RACHEL: Would you guys stop.
MONICA: It's my TV.
ROSS: Wha-, oh, quit it.
MONICA: Bite me.
RACHEL: Oh my God.
ROSS: Well, Monica keeps changin' the channel.
MONICA: Oh that's great, why don't you tell mommy on me.
RACHEL: Now I'm mommy in this little play?  So I'm gonna go and take a nice long hot
bubble bath because you kids are driving me crazy. [goes in the bathroom]
MONICA: OK, what're we gonna do about this?
ROSS: Well, I guess we could tape Entertainment Tonight.
MONICA: Not that, this, US.
ROSS: Ow, ow, OK. Alright, alright, Mon, Mon, you've gone ultrasonic again, alright.
MONICA: I just can't stand you being here all the time.
ROSS: Why, why, why can't you stand me being here? I don't, I, we're just, ya know, we're
just havin' fun.
MONICA: Fun? Fun, you think this is fun?
ROSS: Yeah, c'mon I mean I though, you know, I thought we're just foolin' around. Like
when, uh, when we were kids.
MONICA: Ross, I hated you when we were kids.

MONICA: Yes. I hated you. I mean I, I, loved you in a 'you're my brother so I have to'
kind of way, but basically, yeah, I hated your guts.
ROSS: Why did you hate me?
MONICA: Because, you were mean to me and you, you teased me and you always, always got
your way.
ROSS: And that wasn't fun for you?
MONICA: Duh-huh!
ROSS: I can't believe you hated me.
MONICA: Now I love you. And not just 'cause I have to.
ROSS: Really?
MONICA: Yeah.
ROSS: I can do that.

ROSS: So you wanna watch uh, Entertainment Tonight?
MONICA: Yeah, thanks. You know what?
ROSS: What?
MONICA: If you really want to watch that Serengetti thing, you can.
ROSS: Ohh... OK. [changes the channel]
[Scene: Central Perk. Ross, Rachel, and Monica are hanging out.]
[Phoebe enters]
PHOEBE: Hey.
ALL: Hey.
PHOEBE: Listen. You are not going to believe this but, that is not me singing on the
video.
ALL: No.
PHOEBE: Yes.
RACHEL: Well, how did you find out?
PHOEBE: Well, OK, the record company sent over this piece of paper for me to sign, saying
that it's OK for someone else to sing for me. That was my first clue.
MONICA: So what're you gonna do?
PHOEBE: Well, I can't work with people who would do this.
MONICA: Sure.
PHOEBE: I mean this poor woman.
ROSS: What woman?
PHOEBE: The voice woman. Ya know, I mean, she has a great voice but she doesn't have a
video.
RACHEL: OK, Phoebs. But what about you?
PHOEBE: Well I have a video, you have to pay attention. No this, this voice woman, she's
so talented but, according to the producer people, they said she doesn't have like the
right look or something, ya know.   Oh,
oh that song has so many levels.
[Scene: Chandler and Eddie's apartment.]

EDDIE: No thanks man, I'm not uh, I'm not really into sports.
CHANDLER: [stares in disbeliefe] Yeah o-, OK, alright. [oven timer goes off] Doesn't
matter, time for Baywatch.
EDDIE: Y-, y-, you like that show?
CHANDLER: You don't like that show?
EDDIE: Wha-, n-, no. I mean it's just a bunch of pretty people runnin' around on the
beach, ya know.
CHANDLER: Well that's the brilliance of it. The pretty people... and the running.
EDDIE: I tell ya, I-, I'm gonna go read in my room for a little while.
CHANDLER: Oh o-, OK man.
[Joey's place. He's watching Baywatch, lauging. He goes to say something to Chandler in
the other chair but no one's there. He goes to call Chandler but decides not to.]
[Chandler's. He's playing foosball by himself.]
[Joey's. Playing ping pong by himself.]
[All by myself is playing.  We see Joey through a rainy window. The camera zooms out to show it's
just his tabletop water sculpture.]
CLOSING CREDITS
[Scene: Central Perk. Phoebe is performing Smelly Cat.]
PHOEBE: [singing] Smelly cat, smell-ly cat, what are they feeding you? Everybody.
ALL: Smelly cat, smell-ly cat, it's not your fault.
PHOEBE: Monica.
MONICA: [sings] They won't take you to the vet.
PHOEBE: Chandler.
CHANDLER: [reluctantly sings] You're obviously not their favorite pet.
MONICA: Joey.
JOEY: [sings] It may not be a bed of roses.
PHOEBE: Rachel.
RACHEL: [sings] And you're no friend to those with noses.
PHOEBE: Uh, Ross, those are the only lines we have, sorry. OK, you guys, once more.
END


The domains of variables are internally represented compactly as FD set terms. The details of this representation are subject to change and should not be relied on. Therefore, a number of operations on FD sets are provided, as such terms play an important role in the interface. The following operations are the primitive ones:
is_fdset(
+Set)
empty_fdset(
?Set)
fdset_parts(
?Set,
?Min,
?Max,
?Rest)
inf
and sup
, denoting
minus and plus infinity, respectively. Either Set or all the
other arguments must be ground.
The following operations can all be defined in terms of the primitive ones, but in most cases, a more efficient implementation is used:
empty_interval(
+Min,
+Max)
fdset_interval(
?Set,
?Min,
?Max)
fdset_singleton(
?Set,
?Elt)
fdset_min(
+Set,
-Min)
fdset_max(
+Set,
-Min)
fdset_size(
+Set,
-Size)
sup
if Set is infinite.
This operation is linear in the number of intervals of Set.
list_to_fdset(
+List,
-Set)
fdset_to_list(
+Set,
-List)
range_to_fdset(
+Range,
-Set)
fdset_to_range(
+Set,
-Range)
fdset_add_element(
+Set1,
+Elt -Set2)
fdset_del_element(
+Set1,
+Elt,
-Set2)
fdset_disjoint(
+Set1,
+Set2)
fdset_intersect(
+Set1,
+Set2)
fdset_intersection(
+Set1,
+Set2,
-Intersection)
fdset_intersection(
+Sets,
-Intersection)
fdset_member(
?Elt,
+Set)
fdset_eq(
+Set1,
+Set2)
fdset_subset(
+Set1,
+Set2)
fdset_subtract(
+Set1,
+Set2,
-Difference)
fdset_union(
+Set1,
+Set2,
-Union)
fdset_union(
+Sets,
-Union)
fdset_complement(
+Set,
-Complement)
inf..sup
.


LEAGUE: {{club.league}}
MANAGER: {{club.manager}}
MANAGER PHONE: {{club.managerPhone}}
MANAGER EMAIL: {{club.managerEmail}}
HOME KIT COLOR: {{club.teamHomeKit}}
AWAY KIT COLOR: {{club.teamAwayKit}}


Pacman 2.0
<#if RequestParameters['error']??>
An error occurred during the login process. Try again.
#if>
Your NT ID
Password


[ aws ]
Using the Amazon Cognito Your User Pools API, you can create a user pool to manage directories and users. You can authenticate a user to obtain tokens related to user identity and access policies.
This API reference provides information about user pools in Amazon Cognito Your User Pools.
For more information, see the Amazon Cognito Documentation.


{{compCtrl.singleCompany.address1}}
{{compCtrl.singleCompany.city}}
{{compCtrl.singleCompany.zip}}
{{compCtrl.singleCompany.phone}}
{{compCtrl.singleCompany.descrLong}}


Preparation time
less than 30 mins
Cooking time
30 mins to 1 hour
Dietary
This modern version of the retro classic is cooked in the oven instead of steamed.
Preheat the oven to 200C/400F/Gas 6.
Mix the sifted flour, suet, and salt together in a large bowl. Add enough water to make a soft dough, but not sticky.
Knead lightly on a lightly floured board for a few minutes before rolling out to a 1cm/½in thickness and a rough 20cm/8in square shape.

Roll up loosely, pinching the ends together as you go to stop the jam escaping. Place the roll on a large lightly buttered piece of silicon baking parchment, sealed edge down.
Join the ends of the paper and make a pleated join along the top to allow for the pudding to expand, then twist the ends like a sweet wrapper.
Put the pudding into a large loaf tin or a brownie tin. Fill a roasting tin with boiling water and place on the base of the oven. Then put your roly poly on the top rack for 35-40 minutes.
Serve a single thick slice, with homemade custard.


Files Uploaded
The following is a list of uploaded documents and their list of relevant tags.
Upload your own!
|
|
|
{{this.value.attributes.title}} Uploaded by {{this.value.attributes.uploader}}
|
All done?   Logout. |


The contents of this document are a work in progress
NOTE: This document and the CDC itself is currently a work in progress.
The Component Descriptor Creator (or CDC for short) is a tool that will create the components.xml file from your Java code. It uses JavaDoc tags to gather the information it needs to create the component descriptor.
There is a tag for marking components, requirements and configuration. Each of the tags can have parameters as documented below.
| Tag | Required | Description |
| role | y | The role of the component. |
| role-hint | n | The role-hint of the component. |
| version | n | The component version. |
| lifecycle-handler | n | The lifecycle handler of the component. TODO: link to the different lifecycle handlers |
| instantiation-strategy | n | The instantiation strategy of the component. TODO: link to the different instantiation strategies |
package org.foo; /** * @plexus.component * role="com.foo.ActionManager" * lifecycle-handler="plexus-configurable" */ public class DefaultActionManager extends AbstractLogEnabled implements ActionManager, Serviceable {
This will be generated in component.xml:
<component> <role>org.foo.ActionManager</role> <implementation>org.foo.DefaultActionManager</implementation> <lifecycle-handler>plexus-configurable</lifecycle-handler> </component>
These tags are used on fields to state requirements. TODO: it should be possible to override the role for a normal field.
| Tag | Required | Description |
| role | y/n | The role of the component. Only required if the field is a List or Map |
| role-hint | n | The role-hint of the component. |
/** * @plexus.requirement * role-hint="foo" */ private ActionManager actionManager;
This will be generated in component.xml:
<component> <role>org.foo.MyComponent</role> <implementation>org.foo.MyComponentImplementation</implementation> <requirements> <requirement> <role>org.foo.ActionManager</role> <role-hint>foo</role-hint> <field-name>actionManager</field-name> </requirement> </requirements> </component>
Note that the default value tag won't be required in the future when these tags will be used to generate a separate configuration declaration section.
| Tag | Required | Description |
| default-value | y | Sets the default value for the configuration field |
/** * @plexus.configuration * default-value="Trygve" */ private String name;
This will be generated in component.xml:
<component> <role>org.foo.MyComponent</role> <implementation>org.foo.MyComponentImplementation</implementation> <configuration> <name>Trygve</name> </configuration> </component>


crossrefapi
crossrefapi OPTIONS types|works DOI
crossrefapi can retrieve “types” and “works” from the CrossRef API. Is also has the ability to compare the current “works” document with a JSON document retrieved previously. The program uses the CrossRef REST API. It follows the etiquette suggested at
http://api.crossref.org/swagger-ui/index.html
Return the types of objects in CrossRef (e.g. journal articles)
crossrefapi<EMAIL_ADDRESS>types
Return the works for the doi “10.1037/0003-066x.59.1.29”
crossrefapi<EMAIL_ADDRESS>\
works "10.1037/0003-066x.59.1.29"
Compare a previously retrieved “works.json” with the current version.
crossrefapi<EMAIL_ADDRESS>-diff works.json \
works "10.1037/0003-066x.59.1.29"


Communication over the TCP protocol (reliable streaming). More...
Modules | |
| Low level TCP Connections | |
| Low level functions to access TCP. | |
Data Structures | |
| struct | tcp_server_config_t |
| TCP Server configuration options. More... | |
Macros | |
| #define | tcp_server_config() |
| Default TCP server configuration. | |
Typedefs | |
| typedef void() | nodec_tcp_connection_fun_t(int id, nodec_bstream_t *client, lh_value arg) |
| The server callback when listening on a TCP connection. More... | |
Functions | |
| nodec_bstream_t * | async_tcp_connect_at (const struct sockaddr *addr, const char *host) |
| Establish a TCP connection. More... | |
| nodec_bstream_t * | async_tcp_connect_at_host (const char *host, const char *service) |
| Establish a TCP connection. More... | |
| nodec_bstream_t * | async_tcp_connect (const char *host) |
| Establish a TCP connection. More... | |
| void | async_tcp_server_at (const struct sockaddr *addr, tcp_server_config_t *config, nodec_tcp_connection_fun_t *servefun, lh_actionfun *on_exn, lh_value arg) |
| Create a TCP server. More... | |
Communication over the TCP protocol (reliable streaming).
| struct tcp_server_config_t |
TCP Server configuration options.
| typedef void() nodec_tcp_connection_fun_t(int id, nodec_bstream_t *client, lh_value arg) |
The server callback when listening on a TCP connection.
| id | The identity of the current asynchronous strand. |
| client | The connecting client data stream. |
| arg | The lh_value passed from async_tcp_server_at(). |
| nodec_bstream_t* async_tcp_connect | ( | const char * | host | ) |
Establish a TCP connection.
| host | Host address as a url, like "http://www.bing.com" . |
| nodec_bstream_t* async_tcp_connect_at | ( | const struct sockaddr * | addr, |
| const char * | host | ||
| ) |
Establish a TCP connection.
| addr | Connection address. |
| host | Host name, only used for error messages and can be NULL . |
| nodec_bstream_t* async_tcp_connect_at_host | ( | const char * | host, |
| const char * | service | ||
| ) |
Establish a TCP connection.
| host | Host address. |
| service | Can be a port ("8080" ) or service ("https" ). Uses "http" when NULL . |
| void async_tcp_server_at | ( | const struct sockaddr * | addr, |
| tcp_server_config_t * | config, | ||
| nodec_tcp_connection_fun_t * | servefun, | ||
| lh_actionfun * | on_exn, | ||
| lh_value | arg | ||
| ) |
Create a TCP server.
| addr | The socket address to serve. |
| config | The TCP server configuration, can be NULL in which case tcp_server_config() is used. |
| servefun | The callback called when a client connects. |
| on_exn | Optional function that is called when an exception happens in servefun . |
| arg | Optional argument to pass on to servefun , can be lh_value_null . |