Custom Components in Nativescript


These past couple weeks I've been digging into the NativeScript mobile framework, and am just super impressed by the flexibility baked into it. That flexibility makes it crazy simple for you to create custom, and potentially reusable, UI components.


Getting Started

While this post won't go deep into getting started, we'll hit the high points. If you want a deeper walkthrough, NativeScript has an excellent Getting Started tutorial.

Install NativeScript
npm install -g nativescript

Navigate to a directory
cd some/path/to/projects/folder/

  • Don't create the child directory (i.e. myAwesomeApp/) yet - NativeScript will do this when it scaffolds our project

Initialize a NativeScript project
In version 1.6 the CLI was updated so that you can initialize projects based off of templates. For this post, we'll use tns-template-hello-world-ts. This is a simple 'hello world' type app that uses TypeScript.
*I found that it was easier to read through the NativeScript source code once I started using TypeScript
tns create demoApp --template tns-template-hello-world-ts

Navigate to demoApp directory
cd demoApp

Add Platform
tns platform add android
You can also add iOS if you want, but for this post we'll just deal with Android.


Creating the component

Creating a basic custom component is simple, so we'll start there and then add some stuff to it as we go. If you look in your current directory, you'll see an app/ directory. We want to add our application's code, including our custom component, in there. So let's add these directories and files:

  • app/widgets/app-header/ -> new directories to contain our custom component
  • app/widgets/app-header/header.xml -> XML file containing the UI of the widget

Next, let's update the XML file to have some sort of UI components we'd want to reuse:

<GridLayout rows="auto" columns="*,auto" paddingTop="10">
    <Label verticalAlignment="center" marginLeft="5"
        id="lblHeadingText" row="0" col="0" text="My Awesome App" />
    <Image src="res://icon" row="0" col="1" stretch ="none"
        marginRight="5" />
</GridLayout>

We're essentially after a label that gives the heading text on the left, and an image on the right. This post is about working with custom components, so I won't spend much time on the xml attributes, but here are some high points:

  • GridLayout lays things out in, well, a grid (surprise!). Cool aspect of this layout is that you can set sizes of your rows or columns (if you've ever worked with XAML, this will look kind of familiar). Since we have 1 row with 2 columns, we told it to auto-size the first row, set the first column to * and the second column to auto-size. This means the first column will take up all available space left after the second column gets just what it needs.
  • Attributes such as paddingTop, verticalAlignment, and marginLeft are style attributes. NativeScript also supports a subset of CSS that can be used to style your elements - I'm just doing inline for the sake of this post.

Using the component

When you had NativeScript scaffold out the app, it generated the "main" view files for you. You should see the file app/main-page.xml - open it up, and let's pull in our custom component!

We do this by doing these 2 simple things:

  • Declaring the namespace that our component resides in by using the path that holds our custom component's files: xmlns:appHeader="widgets/app-header"
  • Using the component in the view by referencing the namespace and the filename: <appHeader:header />

NativeScript uses the value given after the namespace to know which file(s) to retrieve. If our file was named appHeader.xml instead, then we'd use it like this: <appHeader:appHeader />

<!-- app/main-page.xml -->
<Page xmlns="http://schemas.nativescript.org/tns.xsd"

    xmlns:appHeader="widgets/app-header"

    loaded="pageLoaded">

    <StackLayout>
        <!-- use the header widget that lives inside widgets/app-header -->
        <appHeader:header />

        <Label text="Tap the button" class="title"/>
        <Button text="TAP" tap="{{ tapAction }}" />
        <Label text="{{ message }}" class="message" textWrap="true"/>
    </StackLayout>
</Page>

Now, if we run the app (tns emulate android, optionally specify an image to use via --avd someImageYouHave) we should see that header component right at the top! But, that isn't much fun... so let's add some functionality to our component!


Enhancing the component

Having some sort of 'reusable' header that has hard-coded header text is pretty useless, and not exactly reusable. Let's update our component so that you can use a headerText attribute in the XML to pass in whatever you want the header to be! This will require us to have a 'code behind' for our UI element, so let's create this file: app/widgets/app-header/header.ts and populate it with an event our <GridLayout> can call once it's loaded:

import view = require("ui/core/view");
import label = require("ui/label");

//not exporting this, just creating the class so that TypeScript can understand 
//  that our custom component has a 'headingText' property
class HeadingView extends view.View {
    headingText: string;
}

// this will get called when our <GridLayout> is loaded
export function onLoad(args: any) {
    //args.object is a reference to the view, and every View instance has a "loaded" event
    let thisView: HeadingView = <HeadingView>args.object;

    //only change the label's text if we were given some custom headingText
    if (thisView.headingText) {
        let label = <label.Label>thisView.getViewById('lblHeadingText');
        label.text = thisView.headingText;
    }
}

Now, we just need to wire it up to call the onLoad event in the app/widgets/app-header/header.xml file. Simply add the loaded="onLoad" attribute:

<GridLayout loaded="onLoad">

* omitted all other existing attributes for brevity - do not delete anything from this file, only add the new attribute.

Finally, we simply need to pass a headingText value in our app/main-page.xml file:

<appHeader:header headingText="Main View" />

Now, run the livesync command tns livesync android --emulator --watch and see your custom header using the text you passed into it!


Going Further

This is certainly just the start of what kind of awesomez you can make with custom components! For instance, NativeScript provides Observables where you could have components that are data-bound instead of just passing in a static string. Hopefully this is enough to whet your appetite and get you started!

-Bradley