Custom Components in NativeScript

   JavaScript
Custom Components in NativeScript

There aren’t too many things as satisfying as doing something in less than half the lines of code than if we were being sloppy. Case in point, we’ll be exploring how to write custom components in Nativescript to make our code reusable and DRY (as in “Don’t Repeat Yourself”, not the absence of moisture). Let’s dive right in.

Setup

Feel free to clone the repo from github which includes all the examples we’ll go through here. Or, if you are that kind of person, create a new project from scratch with:

tns create mycomponents
cd mycomponents
tns platform add ios
mkdir app/components
tns livesync ios --emulator --watch

All the components we’ll create will reside in app/components.

A simple static component

Sometimes, we find ourselves writing the same UI markup in more than one XML file. Examples of such situations may include sidebar, action bar (title bar), copyright info, etc. In that case, all we really need is to park that repeated XML somewhere and reference it. Here’s a quick example on how to do that:

<!-- app/components/copyright/copyright.xml> -->

<Label text="© 2016 Akash Agrawal"/>

And this is all it takes to create a bare-bones component. Now, we can place this in our xml view.

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:mycomponent="components/copyright"
    navigatingTo="onNavigatingTo">
    <StackLayout id="container">
        <mycomponent:copyright/>
    </StackLayout>
</Page>

There are several important points to note here:

  • We need to remember that whenever we want to include a custom component in our view, we need to create a namespace for it. It’s the second line in the above example.
  • The path we provide in that namespace is relative to app directory and should point to the directory in which our component XML and JS (if we’ve created it) files are located.
  • When actually referencing the component, we follow the syntax of <namespace:component/> where namespace is the name we gave to the path (again, second line above) and component is the filename (without extension) of our component XML. As a tip, there’s no problem naming your namespace to be the same as your component, and then placing it like <copyright:copyright/>. The above nomenclature is just to differentiate what’s what.

Dynamic component without arguments

Another use case is that we want a fully independent component which also has some behavior attached to its XML. An example of this may be a simple clock that we might want to place somewhere in our project. In this case, XML alone will not suffice since we’ll need to update the text with the current time. Fortunately, it’s dead simple to attach behavior with XML. We just have to create a javascript (or typescript) file with the same name as the component XML. Observe:

<!-- app/components/clock/clock.xml -->

<Label loaded="onLoad"/>
/* app/components/clock/clock.js */

exports.onLoad = args => {
    const label = args.object;

    setInterval(() => {
        label.text = new Date().toString();
    }, 1000);
};

And we place it in our page the exact same way we did for our static component. Nativescript automatically searches for a javascript (or typescript) file if it’s present with the same name and in the same location as the XML file and attaches them together.

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:clock="components/clock"
    navigatingTo="onNavigatingTo">
    <StackLayout id="container">
        <clock:clock/>
    </StackLayout>
</Page>

Providing arguments to components

This is where it gets interesting. Most of the good stuff that we’ll likely use the components for involves them taking some arguments from the page. This too, however, is fairly straightforward. Code or it didn’t happen, right?

<!-- app/components/greeter/greeter.xml -->

<StackLayout loaded="onLoad">
    <Label id="fL"/>
    <Label id="nL"/>
</StackLayout>
/* app/components/greeter/greeter.js */

exports.onLoad = args => {
    const container = args.object;

    const frameworkLabel = container.getViewById('fL');
    const nameLabel = container.getViewById('nL');

    frameworkLabel.text = `Hello ${container.framework || 'Nativescript'}`;
    nameLabel.text = `My name is ${container.name.first} ${container.name.last}`;
};

Notice how all the arguments we provided are exposed on the root view of our component. If we have multiple views at the root of our component’s XML, only the last view counts. All the others before it are as good as absent. They will not trigger any load events, won’t appear on page (even if you load static content into them) and don’t receive any arguments. To prevent confusion, we should always have only one view at the root. If we need more than one, group them under a layout (that way, the layout becomes the root).

Here’s how we reference them from our page:

<!-- app/main-page.xml -->

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:greeter="components/greeter"
    navigatingTo="onNavigatingTo">
    <StackLayout id="container">
        <greeter:greeter framework="angular-nativescript" name="{{name}}"/>
    </StackLayout>
</Page>
/* app/main-page.js */

exports.onNavigatingTo = args => {
    const page = args.object;
    page.bindingContext = {
        name: {
            first: 'Akash',
            last: 'Agrawal'
        }
    };
};

We could have set framework inside the binding context too and passed it all as one argument instead of two. The example above demonstrates how to pass simple strings (the framework argument) right from the XML.

Loading custom components from javascript

Up till now, we’ve seen how we can use our components by declaring them under a namespace and referencing them with their filename. What if we want to add the component to our layouts directly from javascript? Use cases for this may include adding a profile image component after the user has loaded. Note that there are ways to do this using the usual XML way and Observables. Still, contrary to what memes will have you believe, there’s no such thing as knowledge overload.

We’ll use the same greeter component from the above example. No changes to it whatsoever are required. The modifications will be where and how we use it.

<!-- app/main-page.xml -->

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    navigatingTo="onNavigatingTo">
    <StackLayout id="container">
    </StackLayout>
</Page>

Notice that we’re no longer using the namespace. That’s because we’ll insert the component purely from javascript. However, we’ve given an id to our layout to make it easy for us to insert the component at the right place. Here’s the required js code:

/* app/main-page.js */

const builder = require('ui/builder');

exports.onNavigatingTo = args => {
    const page = args.object;
    const myName = {
        first: 'Akash',
        last: 'Agrawal'
    };
    const container = page.getViewById('container');

    const greeter = builder.load({
        path: 'components/greeter',
        name: 'greeter'
    });

    container.addChild(greeter);

    greeter.framework = 'angular-nativescript';
    greeter.name = myName;

};

Easy-peasy. We use the load method from ui/builder. The path property points to the name of the directory and the name property specifies the file name (without extensions). Nativescript will take care of loading XML and JS from it. It returns an instance of the View class on which we can set any attributes we might want to pass. We then add it to container to actually place it in UI.

Component in Component

This works exactly how we’d guess it would. The following is a component called greeterTime which is exactly the same as the greeter above, except that it also has the clock component we saw earlier. The only change is in the XML.

<!-- app/components/greeterTime.xml -->

<StackLayout loaded="onLoad"
    xmlns:clock="components/clock">
    <Label id="fL"/>
    <Label id="nL"/>
    <clock:clock/>
</StackLayout>

Of course, we can also insert it from javascript using the same method we saw in the previous section.

Communication with Components using Observable

From the examples above, we’ve already established that we can easily pass whatever we wish to our components. However, there may be situations where we want to keep our data from the parent and component in sync. Or, at the very least, be notified if data changes in any place so we can take some action. We can easily do this using the Observable class. The example below creates a simple two field name form component. The objective is to keep the data in UI of parent and child component in sync with each other.

<!-- app/components/nameForm.xml -->

<StackLayout loaded="onLoad">
    <TextField text="{{ first }}" />
    <TextField text="{{ last }}" />
</StackLayout>
/* app/components/nameForm.js */
exports.onLoad = args => {
    const container = args.object;

    container.bindingContext = container.fullName;
};
<!-- app/main-page.xml -->

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:nameForm="components/nameForm"
    navigatingTo="onNavigatingTo">
    <StackLayout id="container">
        <StackLayout orientation="horizontal">
            <Label text="{{name.first}}" />
            <Label text="{{name.last}}" />
        </StackLayout>
        <nameForm:nameForm fullName="{{name}}"/>
    </StackLayout>
</Page>
/* app/main-page.js */

const Observable = require('data/observable').Observable;

exports.onNavigatingTo = args => {
    const page = args.object;
    const myName = {
        first: 'Akash',
        last: 'Agrawal'
    };

    page.bindingContext = {
        name: new Observable(myName)
    };
};

And that’s it. We’ve got a set of labels in our main page, each pointing to a part of the name. In our custom component, instead of labels, we’ve got two text fields to edit the name. Since we’ve wrapped the name in an Observable, editing any text field in the component will automatically update the label in the main page too. For information on more cool stuff we can do with Observable, including subscribing to change events on properties, Nativescript’s official docs are a good place to start.

Conclusion

I am really grateful to Nathanael Anderson for taking the time to answer my questions about adding components dynamically via javascript. He was extremely helpful and kind in getting this stuff pushed through my skull.

There’s a lot of cool stuff we can do with Nativescript. Hopefully this post will help you understand how to isolate that cool stuff and use it the right way. Use the comments below for any suggestions, corrections or general discussion.


Like What You See?

Got any questions?