Data-binding in custom components


#1

I’m trying to understand how to make data-binding work in a custom component. For the sake of illustration, let’s say I want to create a component called TextFieldWithLabel, that can be used in a page as follows:

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:components="components">

    <components:TextFieldWithLabel labelText="User Name" inputText="{{ username }}" />
</Page>

The component is defined in XML as follows (assume the component is to be provided its own binding context with labelText and inputText properties):

<StackLayout>
	<Label text="{{ labelText }}" />
	<TextField text="{{ inputText }}" />
</StackLayout>

What I can’t figure out is what JavaScript I need to write to “connect” the inputText property in the component’s binding context to the username property in the parent page’s binding context, such that when text is entered into the TextField, the username property in the parent binding context is updated. (i.e. kind of a “two-level” data-binding, where typing into the TextField updates the inputText property, which then cascades up to the parent’s username property.)

I have looked at the following pages for guidance

https://moduscreate.com/blog/custom-components-in-nativescript/
https://docs.nativescript.org/ui/basics#custom-components

but none of the examples seem to cover my use case.


#2

@jres Child components inherits parent’s bindingContext. So below one simply works as expected,

View

<Page class="page" loaded="pageLoaded" xmlns="http://schemas.nativescript.org/tns.xsd" xmlns:components="components">
	<components:TextFieldWithLabel />
</Page>

TextFieldWithLabel

<StackLayout>
	<Label text="{{ labelText }}" />
	<TextField text="{{ inputText }}" />
</StackLayout>

Controller

import { EventData } from 'data/observable';
import { StackLayout } from 'ui/layouts/stack-layout';
import { HomeViewModel } from './home-view-model';

export function pageLoaded(args: EventData) {
    let page = <StackLayout>args.object;
    page.bindingContext = new HomeViewModel();
}

View Model

import { Observable } from 'data/observable';

export class HomeViewModel extends Observable {
    constructor() {
        super();
        this.set('labelText', 'Username');
        this.set('inputText', 'NativeScript');
    }
}

#3

@manojdcoder Thank you for your reply, but I don’t think that approach will meet my needs. For example, what happens if I need to use the component multiple times on the same page for different fields, e.g.

<Page xmlns="http://schemas.nativescript.org/tns.xsd"
    xmlns:components="components">

    <components:TextFieldWithLabel labelText="User Name" inputText="{{ username }}" />
    <components:TextFieldWithLabel labelText="Email Address" inputText="{{ emailAddress }}" />
</Page>

There needs to be a way of mapping the component’s inputText property to different properties on the parent’s bindingContext, in order for the component to be truly reusable. Am I missing something?


#4

@jres At least in Angular world we have Input decorators that fulfills this scenario.

With Core NativeScript, you have to handle this case on your own as Builder allows us to set the attributes of parent element only.

I have created a working example for you that should meet your needs but there are still different or even better approaches.


#5

Thank you creating the working example - I appreciate your efforts.

It still seems wrong to me that we have to pass the names of the parent properties

<components:TextFieldWithLabel labelText="User Name" inputText="username" />
<components:TextFieldWithLabel labelText="Email Address" inputText="emailAddress" />

as opposed to using actual binding to the parent properties:

<components:TextFieldWithLabel labelText="User Name" inputText="{{ username }}" />
<components:TextFieldWithLabel labelText="Email Address" inputText="{{ emailAddress }}" />

because this effectively breaks the composability of the system. It means that things have to be done one way for a built-in component such as TextField, and another way for a custom component like TextFieldWithLabel. Surely Nativescript has been designed to support such composability?

Anyhow, thanks again for your help.


#6

Your requirement is very custom and it’s up to you how you want to build that. As I already mentioned, even in Angular world you have just Input decorators using which you can fulfill similar scenarios with decent effort.

Moreover labelText / inputText is not a predefined property so you can’t expect the builder to understand that.


#7

If you place the binding object you want passed to your custom component in the bindingContext attribute you’re all set.

For example:

<!-- app/xml-components/ -->
<StackLayout>
    <Label text="{{ title }}"/>
    <TextField text="{{ value }}"/>
</StackLayout>

<!-- app/main-page.xml -->
<Page loaded="pageLoaded" xmlns:Components="xml-components" xmlns="http://www.nativescript.org/tns.xsd">
    <DockLayout>
        <Component:TextFieldWithLabel bindingContext="{{ personal }}"/>
        <Component:TextFieldWithLabel bindingContext="{{ professional }}"/>
    </DockLayout>
</Page>
/* app/main-page.js */
const { FormViewModel } = require("./main-page-model");

exports.pageLoaded = args => {
  const page = args.object;

  page.bindingContext = new FormViewModel();
}

/* app/main-page-model.js */
const { fromObjectRecursive } = require("data/observable");

function FormViewModel() {
    return new fromObjectRecursive({
        personal: { title: "Name", value: "..." },
        professional: { title: "Role", value: "..." },
    })
}

module.exports.FormViewModel = FormViewModel;

Now when you update the value of your text field the binding property will be updated too.


#8

Mudlabs, I used your example, and it worked … kinda.

Here is a link to the Playground testing I did.

First, I found I didn’t need the “bindingContext” attribute in my XML for the custom component, and it works without it. Is there a benefit to using the “bindingContext” attribute?

Second, I tied the binding item “showItem” to the visibility attribute of my custom component. It works in that, if I set showItem = true, the element shows, and if I set it to false, the element does not show. BUT I can’t figure out how to toggle the visibility from the code-behind.

In my “toggleItem” function, I can see the current value of the showItem variable, and I can change it, but the change isn’t reflected in the UI. I tried reverting it to your “bindingContext” example, but that didn’t change anything.

Any help you (or anyone else) can offer?

Thanks in advance :smiley:


#9

Use same instance of bindingContext


#10

Ohhh … wow. Thanks so much. I need dig into that and figure out why, but that puts me on the right track. Thanks again.