Skip to main content

Displaying and Managing Data in Components

Formatting data for render

Any component extending DisplayField keeps track of a "current value". This value is stored internally in the component, but is not manually accessible. Values can be edited in two ways - the takeValue method of DisplayField, or via data binding. Because fields are initialized without an existing value, null is always a valid type to pass into takeValue.

Whenever the field receives a new value, it must render it for output to the screen. There is no default rendering behaviour, and therefore, a formatter must be specified in the options object of a DisplayField<T> (see DisplayFieldOptions):

const FieldA = new DisplayField<T>(this, {
formatter: {
// This is automatically picked if the value is `null` - it is optional, and defaults to an empty string.
nullValueString: 'NULL',

// This function is called when the value is **not** `null` - it is not optional, and can return either
// a string or an FmcRenderTemplate. This function takes `T`, because `nullValueString` handles
// the `null` case.
format(value: T): FmcFormatterOutput {
return value.someOperation();
},
},
})

const FieldB = new DisplayField<T>(this, {
formatter: (value: T | null) => 'HELLO', // alternatively, a function taking `T | null` can be specified instead of an object
})

const FieldC = new DisplayField<T>(this, {
formatter: RawFormatter, // a very basic formatter which calls `toString()` is available
// for `string | number` types
})

Accepting input data

Any component extending EditableField can output a value. The type of this value is by default the same as the input type, but this can be changed by specifying two type parameters.

This relies on another interface, Validator<T> (where T is the component's output type), with a single method, parse(input: string): T | null. This parsing method will convert any user input into the value specified by type T, or null if the input is invalid. User input can be provided in two ways - the takeTextInput method of TextInputField, or via a scratchpad.

These components expect this interface to be implemented on the same object that is passed as the formatter - this also means they cannot accept a single function instead of the formatter object.

tip

It is good practice to create avionics-specific formatters implementing FmcFormatter<T> to share formatters between fields that have similar data input/output formats. Likewise, you can also implement common parsers that implement Validator<T>.

Data Binding

One of the major features of components is their ability to automatically render data they are bound to. This is done using the generic subscribable facilities found in the SDK - about which more can be read in the Subscriptions section.

info

All data binding methods available on components exported by the SDK automatically manage the lifetime of the subscriptions they create. This is done using the page-bound subscription lifecycle management feature.

caution

However, if you pass in a Subscribable that is itself a Subscription (such as a MappedSubject), it will not be bound to the page's lifecycle automatically. This is because it would be undesirable, in most cases, to pause a MappedSubject that may very well come from a data provider located outside the page class.

Therefore, it is important to take this into account to avoid potential memory leaks or unnecessary computation.

One-way binding

This ability is introduced by the DisplayField class's bind() method. Any Subcribable passed into it will be subscribed to in order to update the value the field is displaying.

Whenever said value changes, the formatter that is configured on the field is called to generate a new string to render.

const data = Subject.create(0);

const FieldA = new DisplayField(this, {
formatter: (value) => value.toFixed(1),
}).bind(data);

data.set(1); // Field outputs `1.0`
data.set(2); // Field outputs `2.0`
data.set(3.4); // Field outputs `3.4`

Two-way binding

If a MutableSubscribable is passed to the bind() method of an EditableField, the output of that component is automatically piped into it. Note that this only works if the output type of a component is the same as its input type.

const data = Subject.create(0);

const FieldA = new TextInputField(this, {
formatter: {
format: (value) => value.toFixed(1),

parse(input: string): number {
const int = parseInt(input);

if (!Number.isFinite(int)) {
return null;
}

return int;
},
},
}).bind(data);

// *user types 1 into the field* -> `data` becomes 1, field displays `1.0`
// *user types 2 into the field* -> `data` becomes 2, field displays `2.0`
tip

This behavior can be undesirable in certain cases - if there is a special process involved in modifying the data, add an onModified callback, and return true. This will prevent the default behavior from running.

Read more about the different interaction callbacks in Interaction with Components.