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.
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.
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.
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`
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.