nativescript-component
by binarynate | v1.2.0
A module for easily creating reusable NativeScript components without Angular.
npm i --save nativescript-component

nativescript-component

A simple way to create reusable NativeScript components without Angular.

Benefits

  • Simple API for defining a component and binding properties to it.
  • Multiple instances of a single component can be used in a page.
  • Each component instance is automatically given its own separate state.
  • Automatically binds XML attributes to the component's binding context.
  • Parent components can safely pass dependencies to their children, because outer components are initialized before nested ones.
  • Automatically binds context properties passed to the component view navigate() and showModal().
  • A component instance is automatically disposed upon its view's unloaded event by default.
  • A component can instead be defined as a singleton so that a single instance is kept throughout the application's lifetime.

Installation

npm install nativescript-component --save

Examples

Example 1: sample-groceries app

This example takes the canonical sample-groceries app from NativeScript's getting started guide and updates it to use ES 6 and nativescript-component for a component-based design. For reference, you can compare it to the original version.

Example 2: Nested components

In this example, we'll create a parent component named details-page which uses multiple instances of another component named editable-text to allow a user data record to viewed and edited.

Directory structure

app
|
|-- components
|
|-- details-page
| |
| |-- details-page.xml
| |-- details-page.js
|
|-- editable-text
|
|-- editable-text.xml
|-- editable-text.js

Styles are omitted from this example for simplicity, but it's good practice to group the component's styles in its directory (e.g. details-page/details-page.css and editable-text/editable-text.css). Check out the Nativescript LESS and SASS precompiler plugins for clean styling.

details-page.xml

The details-page component's template consists of an ActionBar with a single control: a button that for toggling the UI from "view" mode to "edit" mode and vice versa; and a GridLayout listing our fields: first name and last name.

<Page navigatingTo="onNavigatingTo" xmlns:e="components/editable-text">

<Page.actionBar>
<ActionBar title="User Details">
<ActionItem text="Edit" ios.position="right" tap="edit" visibility="{{ controls.edit, controls.edit ? 'collapsed' : 'visible' }}"/>
<ActionItem text="Save" ios.position="right" tap="save" visibility="{{ controls.edit, controls.edit ? 'visible' : 'collapsed' }}"/>
</ActionBar>
</Page.actionBar>

<StackLayout>
<GridLayout columns="*,*" rows="auto,auto">
<Label text="First Name" col="0" row="0"/>
<e:editable-text class="name" record="{{ user }}" fieldName="firstName" controls="{{ controls }}" col="1" row="0"/>

<Label text="Last Name" col="0" row="1"/>
<e:editable-text class="name" record="{{ user }}" fieldName="lastName" controls="{{ controls }}" col="1" row="1"/>
</GridLayout>
</StackLayout>
</Page>
Things of note:
  • The navigatingTo="onNavigatingTo" attribute hooks up the component's built-in onNavigatingTo() hook, which instantiates the component when the view loads.

  • The xmlns:e="components/editable-text" attribute defines a namespace "e" for our component so that it can be referenced in the XML as <e:editable-text/>.

  • In the attribute visibility="{{ controls.edit, controls.edit ? 'collapsed' : 'visible' }}", controls.edit is passed as the first argument to {{ }} in order to instruct NativeScript that the nested edit property is the source that should be observed for the expression following it, rather than the controls object in which it's contained. This requirement is documented in NativeScript's Data Binding documentation.

details-page.js

For this example, let's assume that another page navigates to our details-page component by invoking navigate() like so:

frames.topmost().navigate({
moduleName: 'components/details-page/details-page',
context: {
user: new Observable({ firstName: 'Brendan', lastName: 'Eich' })
}
});

The details-page component's JavaScript file would then look like so:

import { Observable } from 'data/observable';
import Component from 'nativescript-component';

class DetailsPage extends Component {

/**
* Place initialization code in `init`, which is automatically called
* after the parent is initialized and before child components are initialized.
*
* @override
*/

init() {
this.set('controls', new Observable({ edit: false }));
}

/**
* Switches the UI from view mode to edit mode.
*/

edit() {
/** @todo: Check if `this.set('controls.edit', true)` correctly sets the nested proeprty and, if not, implement support for that. */
let options = this.get('controls');
options.set('edit', true);
}

/**
* Switches the UI from edit mode to view mode.
*/

save() {
let options = this.get('controls');
options.set('edit', false);
}
}

DetailsPage.export(exports);
Things of note:
  • The built-in init hook is automatically called after the component's parent (if any) has been initialized. Override this hook to perform any setup using the built-in methods and properties. this.get() and this.set() are used to get and set properties on the component's binding context. Properties set this way can be displayed in the component's template.

  • Parameters passed to navigate() are automatically bound to the component's binding context, which means the user parameter in our example is accessible in JavaScript via this.get('user') and available in the XML template as {{ user }}.

  • export() is used to export the class in the format that the NativeScript runtime expects.

editable-text.xml

The editable-text component can be switched from "view" mode to "edit" mode and vice versa by its parent component (details-page), so its template has a read-only <Label/> that is shown in "view" mode and an editable <TextField/> that is shown in "edit" mode.

<StackLayout loaded="onLoaded">
<Label id="label" visibility="{{ controls.edit, controls.edit ? 'collapsed' : 'visible' }}"/>
<TextField id="input" visibility="{{ controls.edit, controls.edit ? 'visible' : 'collapsed' }}"/>
</StackLayout>

editable-text.js

The editable-text component accepts three parameters from its parent component:

  • record - A data record object that contains a property that we want to view and edit
  • fieldName - The name of the property in the record object we want to view and edit
  • controls - An object with properties that allow the parent component to dynamically control the child. In this example, we just use a single controls.edit property to indicate whether the text field is in edit mode.
import Component from 'nativescript-component';

class EditableText extends Component {

/**
* @override
*/

init() {
// Set up the two-way binding for the data record's specified property.
// This must be done in JavaScript, because NativeScript's XML binding expressions don't currently support dynamic
// property names.
let label = this.view.getViewById('label'),
input = this.view.getViewById('input'),
record = this.get('record'),
fieldName = this.get('fieldName');

label.bind({
sourceProperty: fieldName,
targetProperty: 'text',
twoWay: true
}, record);

input.bind({
sourceProperty: fieldName,
targetProperty: 'text',
twoWay: true
}, record);
}
}

EditableText.export(exports);
Things of note:
  • Parameters passed as XML attributes are automatically set on the component's binding context and are accessible using this.get().

  • The component's view is accessible as this.view.

  • Normally it's not necessary to set up bindings manually as shown here in init, but it's needed in this case in order to allow the fieldName property to dynamically specify the name of the property we're interested in.

For more information, check out the API docs.

Caveats

Embedding a component inside a ListView.itemTemplate or Repeater.itemTemplate

  • nativescript-component serves this scenario well, because it allows each list item to have its own separate state.
  • However, as noted in the sample-groceries app, you must be aware that in this scenario, the list item is automatically set as the component's binding context and, most importantly, is immutable.
  • This means that within your list item component, you can't use this.set('foo', foo) to set a new property on its binding context in order to display the property in the template; you must instead set that property before the object is passed to in an array to the ListView items attribute.
  • You can still use this.foo = foo to set instance properties on the component, but they won't be available to the component's template.

Contributing

Find an issue or have an idea for a feature? Feel free to submit a PR or open an issue.

Building

This module is implemented in ES 6 and transpiled to ES 5 for export. To build the source:

npm run build

There's also a git pre-commit hook that automatically builds upon commit, since the dist directory is committed.

Linting

npm run lint