Understanding NativeScript binding patterns

NativeScript has one terrific, yet sometimes misunderstood, feature -- data bindings. In this post I would like to lay it out so that it's easy to grasp and start using.

From Model to View

Let's take this simple XML template consisting of just one field.

<!-- demo.xml -->  
<Page loaded="loaded">  
  <Label text="{{ name }}" />
</Page>  

It has a page with the loaded event handler bound to the co-named method of the controller and a single label with the text bound to the property of the page context named name.

// demo.js

var model = { name: "NativeScript };

exports.loaded = function(e) {  
  var page = e.object;
  page.bindingContext = model;
};

This is a really simple controller. The loaded method is called when the XML page is loaded. The single argument is an event object. The object property points to the source of event -- this page. The most important part here is the assignment of the model to page.bindingContext. This is how you pass the context to the XML view. This is how you tell what the name is in your template. Easy.

From View to Model

Now that we know how to display data from controller in the view, let's pass the data back.

<!-- demo2.xml -->  
<Page loaded="loaded">  
  <StackLayout orientation="vertical">
    <TextField text="{{ firstName }}" />
    <Button text="Save" tap="save" />
  </StackLayout>
</Page>  

Slightly more complex template with two components arranged vertically. The text field linked to the firstName context (model) property and the button that calls exported save method on tap.

// demo2.js
var model = { firstName: "Mark" };

exports.loaded = function(e) {  
  var page = e.object;
  page.bindingContext = model;
};

exports.save = function(e) {  
  alert(model.firstName);
};

Here the save handler is of interest. We don't look up the view and don't query its text property. Instead, we pick the current value from the model. NativeScript keeps the property in the model in sync with the view. When you change it in code, the view updates. When the user types something new in the view, the model in memory changes (and property change events are fired).

From Controller to Controller (forward)

Now it's turn to understand how do you pass data from one controller to the next. Let's assume that we have two pages -- an index list of items and the details view.

<!-- index.xml -->  
<Page loaded="loaded">  
  <ListView items="{{ items }}" itemTap="onItemSelected">
    ...
  </ListView>
</Page>  

We'll use a list view to display items. I don't go into the details of the view implementation. All you need to know is that the list view calls itemTap handler every time you tap on an item in the list.

// index.js

exports.loaded = function(e) {  
  var page = e.object;

  var items = [
    { id: 1, name: "Apples" },
    { id: 2, name: "Grapes" }
  ];

  page.bindingContext = {
    items: items
  };
};

exports.onItemSelected = function(e) {  
  var itemView = e.object;

  // Item is going to be one of the items from the list
  // that we bound to the list view:
  // { id: 1, name: "Apples" }, or
  // { id: 2, name: "Grapes" }
  var item = itemView.bindingContext;

  Frame.topmost().navigate({
    moduleName: './details',
    context: item
  });
};

We setup items list as an array of items with some data. When the list view is rendered, every item view in the list is assigned its own bindingContext (just like the one we assign to the page in the loaded handler.) It's done automatically by the list view component.

When an item is tapped, ListView calls a method bound to itemTap property and passes the item view in the object property of the event. This view has the item it renders in the binding context. In our case it is one of two hashes (with names "Apples" or "Grapes").

We take the item from the bindingContext and pass it on to the details view as that view context. Look how elegantly have we passed the tapped item to the details controller. You an pass any data to controllers this way.

// details.js

exports.loaded = function(e) {  
  var page = e.object;

  // Get the context passed to this controller by the previous one:
  // In our case: { id: x, name: y }
  var item = page.navigationContext;

  page.bindingContext = item;
};

The details controller is very simple. It takes the context that we sent to it (tapped item) and assigns it to the binding context of the page, so that the view could access the id and name properties. You aren't limited to this, of course. For example, you could use item.id to load more data.

Sending data back from Controller to the parent Controller

Sometimes we need to return some results from the controller. For example, when you select an avatar from a gallery, or a country from a list in a separate controller and then bring the selection back to the previous screen. How do we pass the results back?

The answer lies in the context again.

// first.js

var model = {};

exports.navigatingTo = function(e) {  
  if (e.isBackNavigation) {
    // returning from the second.js
    alert(model.result);
  } else {
    // entering this controller initially
  }
};

export.onGotoSecond = function() {  
  Frame.topmost().navigate({
    moduleName: './second',
    context: model
  });
};

Instead of the loaded handler we use navigatingTo handler that is called earlier, before the page is loaded, and contains the isBackNavigation flag that is set to TRUE when we are returning from the later controller back to this one.

It's not important when the onGotoSecond is called. It can be a button touch or some other event. When we navigate to the second screen we pass our model as the context and expect to see the result in it. This trick works since in JavaScript data is passed by references, not values. For us it means that whatever changes the second controller makes to the model we pass to it, the first controller "sees."

// second.js

var model = null;

exports.loaded = function(e) {  
  var page = e.object;
  model = page.navigationContext;
};

exports.onSubmit = function() {  
  model.result = "the result of this screen";
  Frame.topmost().goBack();
};

Here in the second controller, we save navigation context into own model variable, and upon the onSubmit event, set the result property of it to something useful. When going back, we get into the e.isBackNavigation === true branch of the first controller's navigatingTo handler and can work on the results.

Hope this intro to practical data binding patterns was helpful.

comments powered by Disqus