Model

From Spire Trading Inc.
Revision as of 09:31, 10 August 2023 by Jon (talk | contribs) (Describes model.txt in the context of user interface specifications, its role in defining the behavior of a component, the formal sections and explanations for example excerpts.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

User interfaces are composed of reusable semantic elements called components. Each component is specified by a set of documents: model.txt, preview.xd, components.xd, layout.xd, user_flow.drawio, and use_cases.drawio. This page covers the model.txt document.

model.txt provides the complete description of the data the component operates on and displays, the state of the component, and the signals it produces. Any component with new behavior requires model.txt. Components that simply customize the properties of existing components omit model.txt as there is nothing to describe that is not already defined in components.xd.

Data and Behavior

model.txt is necessary when the component being defined has specific data that it displays or manipulates or introduces novel behavior not found elsewhere in the UI Kit.

Example: AccountListItem

Data:
  identicon: A unique icon identifier for the account.
  id: A unique username for the account.
  name: The full name associated with the account.

The model for AccountListItem is very simple and deals only with data. There is no special behavior associated with AccountListItem: it behaves no differently than any other ListItem that has been defined elsewhere. AccountListItem takes a structured set of data:

  • identicon
  • id
  • name

Using a specific AccountListItem for displaying user accounts in this way keeps components contained and simple.

Example: Button

Data:
  body: The component to display inside of the button.

State:  
  press: Whether the button is currently pressed.

Signals:
  Click: Indicates that the button was clicked.

Button is a more generic component. From body, we see that it can be used to display any component, so there is no specific data associated with it. What distinguishes Button from Component is its behavior:

  • press is a new State that Button can be in.
  • Click is a new Signal that Button can send.

Getting Started

model.txt is written after preview.xd. Clarifying in writing what data the component uses greatly simplifies the making of components.xd, use_cases.drawio and user_flow.drawio.

Most expected behavior is specified in Component, the base component for all UI components. Any property or signal found in Component can be used by any component without repeating them in the model. This makes the model for most components sparse and keeps the focus on their unique data and behavior.

Components are built using composition: complexity is built up from simple elements that manage a small set of responsibilities. These smaller elements are the children and when we consider the model for a component it is helpful to think about what responsibilities are handled by its children rather than being its concern.

Example: DropDownBox

Preview of a drop down box in open and closed states

DropDownBox lets the user select a single value from a list of options and submit that value. It has a child DropDownList: the list of options and whether the list is open or closed can be thought of as properties of DropDownList.

To describe DropDownBox, we need to understand a little more of its behavior:

  • It represents a single value that the user can select.
  • Selecting a value is different from submitting a value. The user can select a value without submitting it.
  • The user can revert a selection to what was last submitted (by pressing the Escape key).
  • Focusing out of the DropDownBox is treated as a submission when the selected value has changed from the last submitted value.
  • It can be set to be read only.

Based on its behavior, we need a few properties:

  • A property to track the selected value
  • A property to track the last submitted value
  • A property to indicate if it is read only

Other components will need to be notified when the user has made a submission so they can respond to changes. We can add one more requirement:

  • A signal to indicate a submission
Data:
  read_only: Whether the value is modifiable by the user.
  current: The item currently represented by the DropDownBox.

State:
  submission: The last value submitted by the user.
  modified: Whether current has been modified since last Submit.

Signals:
  Submit: Indicates the user is submitting current.

Reviewing the model.txt, we see the following:

  • current to track the selected value
  • submission to track the last submitted value
  • read_only to indicate if it is read only
  • modified to track whether current changed from submission (not strictly necessary)
  • Submit, a signal to indicate a submission

We note that read_only and current have been placed in Data, while submission and modified are in State. This means that external components can modify read_only and current, while DropDownBox manages submission and modified itself. Submit is placed in Signals. Properties are named using snake_case while signals use PascalCase.

It may be useful for external components to respond to changes in current as well as submission, such as showing a preview based on the current selection, but a Current signal is not necessary for this. Because properties in Data are read-write for external components, components can subscribe to property changes directly without requiring a signal.

Anatomy

The structure of model.txt is a linear document divided into preset sections. These sections are as follows:

  • Definitions
  • Styles
  • Selectors
  • Pseudo-Elements
  • Data
  • State
  • Signals

Definitions serve as an optional index for terms used throughout the model. Selectors are used in older specs but should be avoided for simplicity. Pseudo-Elements define sub-sections of the component and make them available for styling. The remaining sections Styles, Data, State, and Signals can be conceptualized as relating to the interface as shown:

Diagram of a component with Data, Styles, State and Signals shown as wires between and within the boundary

Data flows through the interface, depending on the component, both external resources and the user may be able to modify Data properties.

Styles can be set by external components to change the appearance of the component. Generally, the user doesn’t directly modify the style of a component, so these can be thought of as one-way.

State is managed internally by the component. It can be influenced by user interactions, by Data, or any combination of properties, but it remains encapsulated.

Signals are messages sent out by the component. They inform other components about changes that are useful to know about resulting from a user action.

Breakdown

Within each section is a list of properties followed by their definitions.

Section Heading:
  property_name: Property description. Optional default.

Property names should balance brevity with self-description. Leaning on familiar and established terminology is preferred over introducing new terms with the same meaning.

Descriptions should be precise, concise, and readable, expressing in plain language the meaning of the property. If a property can only be one of a finite set of values, the possible values are enumerated directly in the definition by nesting them below the property name and definition.

Section Heading:
  property_name: Property description.
    POSSIBLE_VALUE_1: Value description for 1. Default.
    POSSIBLE_VALUE_2: Value description for 2.

Values are distinguished using UPPERCASE.

Properties can consist of sub-properties, such as list or tabular data, or properties consisting of multiple orthogonal components such as the x and y components of a position. Sub-properties are nested beneath properties.

Section Heading:
  property_name: Property description. Optional default.
    sub_property_name: Sub property description. Optional default.

Descriptions can include inline expressions provided they are trivial. With expressions, it is understood that they represent a binding: they update automatically without requiring a sub-flow in user_flow.drawio. When an expression is used, it is placed on its own line below the last sentence for the description. The equality is implied.

Section Heading:
  property_name_1: Property description. Optional default.
  property_name_2: Property description.
                   property_name_1 + 10

Example: Component

Styles:
  visibility: Shows or hides the component.
    VISIBLE: The component is visible, (default).
    INVISIBLE: The component is invisible.
    NONE: The component is both invisible and removed from the layout.

In the above excerpt from model.txt for Component, the visibility property is defined within the Styles section. visibility can be one of three values:

  • VISIBLE
  • INVISIBLE
  • NONE

Each possible value is named and defined directly below the definition for visibility.

Data:
  parent: The parent of this component, for top-level components this is NULL.
  window: The window that this component belongs to.
  children: The list of this component's children.
  width: The width of the component in pixels.
  height: The height of the component in pixels.
  position: The position of the component relative to its parent, if the
            component has no parent, then the position is equal to
            screen_position.
    x - The x-coordinate in pixels.
    y - The y-coordinate in pixels.

Continuing to the Data section, the position property is a composite of two independent values: x and y. These properties can be referenced in other documentation with dot notation as position.x and position.y.

Example: SaleConditionBox

State:
  conditions_list: A list of all conditions.
    code: The identifier for the condition.
    name: The name for the condition.
    terms: The list of words from the name for the condition.
           name.split(' ')

conditions_list is a list of items, where each item has the fields specified. Visually it can be shown as tabular data. Below is what some of this data might look like:

code name terms
@ Regular Settlement Regular, Settlement
C Cash Settlement Cash, Settlement
N Next Day Settlement Next, Day, Settlement
R Seller Settlement Seller, Settlement
F Intermarket Sweep Intermarket, Sweep
O Opening Print Opening, Print
4 Derivative Priced Derivative, Priced
5 Re-Opening Print Re-Opening, Print
6 Closing Print Closing, Print

terms is clearly derived from name, the result of the inline expression. If the expression is non-trivial or not unconditionally true, then it does not belong in model.txt. If you are unsure if an expression improves the clarity of the model, leave it out.

Example: InputBox

Boolean (true or false) properties have implied values.

Data:
  read_only: Whether the InputBox is in read_only mode.
  rejected: Whether the last submission was rejected.

InputBox has two boolean properties: read_only and rejected. Each may be true or false, which is implied from their definitions.

Definitions

This section is optional. It provides definitions for terms used throughout the model to improve clarity.

Example: TimeAndSalesWindow

Definitions:
  TimeAndSale: A trade on a security.
    timestamp: The time of the trade.
    price: The price of the trade.
    size: The number of shares traded.
    condition: A code that indicates the conditions of the trade.
    market_center: The market that facilitated the trade.
    bbo_indicator: An indicator that relates the price to the bbo.
      BBO_UNKNOWN: The bbo is unknown.
      ABOVE_ASK: The price is above the ask.
      AT_ASK: The price is equal to the ask.
      INSIDE: The price is between the bid and the ask.
      AT_BID: The price is equal to the bid.
      BELOW_BID: The price is below the bid.

Definitions is used to define a TimeAndSale. Each TimeAndSale has a set of properties associated with it:

  • timestamp
  • price
  • size
  • condition
  • market_center
  • bbo_indicator

TimeAndSale can be referenced elsewhere in the model.

It is at the designer’s discretion whether to include a Definitions section or to provide the description inline where the term is used. The following heuristics are helpful in determining whether to place a definition in a Definitions section:

  • The term is referenced by multiple properties throughout the model
  • The definition is long or contains multiple levels of nesting which would impede the readability if included directly in the normal flow of the document

When unsure, err on the side of putting the description inline rather than including a Definitions section.

Styles

Styles is for defining any style properties available on the component. Style properties affect the visual appearance of a component but do not directly relate to the data it displays. Style properties can be inputs that are provided externally to the component.

Example: Box

Styles:
  background_color: The background color. #FFFFFF by default.
  border_top_size: The height of the top border.
  border_right_size: The width of the right border.
  border_bottom_size: The height of the bottom border.
  border_left_size: The width of the left border.
  ...

Box defines a number of style properties for customizing its color, border, and padding. External components using Box can make use of any of these properties as needed.

No matter how deep in the component hierarchy a Box is nested as a child, the component can access the properties for that Box and make customizations. This avoids creating redundant properties to enable customization.

Example: TimeAndSale

Styles:
  font: The font used to render the text in the cells. 
        Default 10px Roboto 500.
  bbo_unknown: The bbo is unknown.
    background_color: Default #FFFFFF.
    text_color: Default #000000.
  above_ask: The price is above the ask.
    background_color: Default #D2F6E0.
    text_color: Default #007735.
  at_ask: The price is equal to the ask.
    background_color: Default #D2F6E0.
    text_color: Default #007735.
  inside: The price is between the bid and the ask.
    background_color: Default #FFFFFF.
    text_color: Default #000000.
  at_bid: The price is equal to the bid.
    background_color: Default #FAD8D9.
    text_color: Default #B71C1C.
  below_bid: The price is below the bid.
    background_color: Default #FAD8D9.
    text_color: Default #B71C1C.

TimeAndSale defines a number of style properties specific to its function. font is defined in TextBox and ordinarily does not need to be redefined: here it is done for convenience so that all the children of TimeAndSale are styled with the same font without having to make separate declarations for each child. The remaining properties relate to TimeAndSale: they depend on a comparison between the price of the TimeAndSale to the bid and ask prices.

Note that each comparative style (bbo_unknown, above_ask, etc.) is a composite style of background_color and text_color: coordinating separate styles into a set of presets is a common use case for Styles.

The basic styles are all available in Box and TextBox. Components should only define styles that are sensible to their function. Styles are not restricted to the properties and selectors of CSS or other languages, they are defined within model.txt to suit the specific needs of the component.

Selectors

Selectors may be deprecated. They were used to bundle Styles and State in an opt-in manner. It is recommended to use Styles in combination with State for any properties that can be used to style the component.

Pseudo-Elements

Pseudo-elements are a way to give full styling properties to portions of a component that are not components themselves. They are used infrequently but can be useful for fundamental components that consist of semantic portions that are not components. Consider the following usage in TextBox:

Pseudo-elements:
  placeholder: Styles the placeholder sub-component.

This enables the placeholder to be styled using the same properties as for TextBox, even though it is not itself a component. Text is the most obvious use case for pseudo-elements, as it can be broken up semantically into granular parts. See Pseudo-Elements on Web.dev and Pseudo-Elements on MDN for more detail.

Data

Data is for properties that do not deal directly with styling and are provided externally. Properties in Data are read-write to external components and may be modifiable by the user (such as the value of an input field).

Data properties are not fixed, they can be updated from an external source at any time. It is common to have Data properties that represent a stream, such as quotes from an exchange, which update as soon as they change without requiring a user-initiated update request. Anytime a property in Data is changed, the change is immediately reflected in the component. It is sufficient to declare where the value resides in the component and it will always reflect the latest value.

A default value is often declared. Where the data comes from is not specified (e.g. from a backend or a local file), that should be determined by the type of data and the application architecture.

The following heuristic may be useful in determining whether property values belong in Data or State. It belongs in Data if:

  • The value can be modified by the user through interaction with the component
  • The value is read from an external source requiring real-time updates
  • There are obvious use-cases where external components will need to read or write to the value

Otherwise, put the value in State.

Example: TextBox

Data:
  current: The text currently represented by the TextBox.
  cursor_position: The current cursor position. When 0, the cursor is at the
                   start of the TextBox.
  selection_start: The index position of the character in current at
                   the start of selection.
  selection_end: The index position of the character in current at the
                 end of selection. When selection_end < selection_start,
                 the direction of selection is opposite of reading order.
  read_only: Whether the text is modifiable by the user.
  placeholder: The text to be displayed when current is empty.
  max_length: The maximum number of characters to accept.

TextBox has a number of bi-directional properties: current, cursor_position, selection_start, and selection_end are all typically driven by user actions, but they can also be set by external components or when the component is customized, such as setting an initial current value to display. There are also properties that are not modifiable by the user: read_only, placeholder, and max_length. However, it is possible for external components to modify these, so a component could be made that let the user change these properties.

Standard Property Names

Properties are directly defined in the model.txt and may be anything the designer can define. That said, there are commonly recurring properties that have come to form an implicit convention and it is preferable to use consistent names and descriptions when the intended behavior is the same.

Current

In the TextBox example, the text is named current and not text or value. This is intentional: current is a standard property name.

Example: DecimalBox

In DecimalBox, current refers to a number rather than a text string.

Data:
  current: The current number represented by the DecimalBox.

Example: DateBox

In DateBox, current refers to a date value, not a number or text string.

Data:
  current: The current date represented by the DateBox, in ISO format
           (YYYY-MM-DD).

By convention, current refers to the current value of the component. The type of the value is generally defined by what the component displays.

Example: ScalarFilterPanel

If there is no logical single current value for the component, it should not have current.

Data:
  min: If set defines the minimum value to include.
  default_min: The default min value.
  max: If set defines the maximum value to include.
  default_max: The default max value.

ScalarFilterPanel has two DecimalBox, one aliased as MinValue and another aliased as MaxValue, and has access to MinValue.current and MaxValue.current. It does not have a current as it does not represent a single value.

Example: TableBody

current can be made more specific where appropriate.

Data:
  rows: A list of table_rows each having an equal number of columns, used to
        populate the body.
  row_selection_mode: The selection mode used by rows (NONE by default).
  row_selection: Stores the list of selected rows.
  column_selection_mode: The selection mode used by columns (NONE by default).
  column_selection: Stores the list of selected columns.
  cell_selection: Stores the list of selected cells, indexed by row and column.
  cell_selection_mode: The selection mode used by cells (NONE by default).
  current_cell: The currently selected cell (null by default). The current cell
           can be used to implicitly specify a current row and current column
           if such functionality is desirable, for example the current cell's
           row is implicitly the current row.

TableBody has a current_cell property but no current property. Because TableBody can only have one cell as current, it makes sense that this is a property of TableBody and not of any of its children. However, TableBody does not represent a single cell, so current without added specificity could cause confusion.

Example: ListView

Do not use current for lists of data: current refers to one value that varies in time.

Data:
  current: The view's current value, can be null.
  selected: The value of the selected list item.
  items: A list of items used to populate the list.
    component: The component to display in the list.
    value: A text value associated with an item, used to select an item by
           keyboard.

ListView uses current for the single value that is current, and items contains the list of values that make up the list. While ListView represents a list of values, only a single value can be current.

Body

body represents a slot that fills the entirety of the component. Some components, like Button, are generic enough that they can be used to display anything, and body is the name these components are assigned to.

Example: Box
Data:
  body: The component displayed in the box.

Example: Button
Data:
  body: The component to display inside of the button.

Items

items represents a generic list of values that are managed by a single component.

Example: ListView
Data:
  current: The view's current value, can be null.
  selected: The value of the selected list item.
  items: A list of items used to populate the list.
    component: The component to display in the list.
    value: A text value associated with an item, used to select an item by
           keyboard.

Note that, strictly following the body naming convention, items.component should be named items.body.

State

State is for declaring properties that are internal to the component. These are properties that are useful to have because they simplify descriptions of the component’s behavior and map to the user’s mental model of the component. A common use-case for State is for declaring properties that can be used for styling the component.

Example: Component

State:
  active: Whether the component belongs to the currently active window.
  enabled: The negation of disabled, enabled = !disabled.
  hover: Whether the mouse is located on top of the component.
  focus: Whether the component receives keyboard input.
  focus_in: Whether the component or any of its descendants has focus.
  focus_visible: Whether the box has focus and it is determined that focus
                 should be indicated on the box to assist the user:
                   - the user focused the box via non-pointing device
                   - the box received programmatic focus from a component that 
                     had focus_visible

component declares a number of State properties that are used for styling purposes by many components. It is common for a component to have a certain appearance when it is hover. Because hover is defined in component, it is trivial to style a component like a button based on its hover state. Each component doesn’t need to reinvent or redeclare the logic for what hover means.

Example: SecurityBox

A common use-case for State is for storing data that is intrinsic to the component that the user cannot manipulate.

State:
  security_list: A list of all tradeable securities.
    symbol: The symbol of the security.
    name: The full name of the security.
    country: The ISO country code for the country in which the security's
             exchange is located in.

In SecurityBox, security_list is all tradeable securities. This list is dynamic, since securities are delisted and new securities listed all the time. It could be obtained from a remote resource that is periodically updated. Unlike with Data, it is not automatically assumed that this data is continuously flowing as a stream. Because the security_list is neither a real-time feed or modifiable by the user, State is an appropriate location for it. This also ensures that security_list cannot be tampered with by external components and makes using SecurityBox more ergonomic: SecurityBox always contains the list of all tradeable securities.

Signals

Signals are how components notify other components about changes to their internal state. Any external component can listen for a signal from a specific component and respond accordingly. Use signals whenever there is a change in a component that it would be useful for external components to know about based on real use cases.

Signals are not necessary for notifying external components about simple changes in Data: the properties in Data are already externally accessible: an external component can see if DecimalBox.current changed without needing a signal. However, sometimes a component will need to perform some operations on the data when it changes, and it should send a signal when these operations are completed.

Signals should reflect the behavior of the component they belong to. Components should not pass along signals from their children if it does not make sense to their function.

Components can define signals with the same name as their children. For a component with a child DecimalBox, it can define its own Submit signal which is what is referenced by A.Submit. The DecimalBox is specified with dot notation as A.DecimalBox.Submit. Ideally, only A should be interested in DecimalBox.Submit, while other components should subscribe to A.Submit.

Standard Signal Names

As with properties, there are a few standard signals.

Change

The Change signal may be fired by a component that needs to notify other components of a change to the current of its children. This may be useful when some logic should be performed such as validation before sending out the signal so that components listening for changes do not concern themselves with handling an invalid change. It is similar to the Input Event event in HTML

Submit

Submit indicates that the user has submitted the value. A Submit signal should only be generated in response to an intentional user action, such as a key press. Submit is conventionally signaled on a FocusOut event when current has changed from its previous value: the user’s change in focus is interpreted as an implicit submission. Submit typically follows an Enter key press. It is similar to the Change Event in HTML.

Reject

Reject is fired to indicate that a submitted value was rejected.

Payload

When a signal is fired, it carries with it a payload that components can read. For the Submit signal, its payload consists of submission by default. The payload of any signal can be customized to better suit the use-cases.

Example: TimeFilterPanel

Signals:
  Submit: The user has submitted their time range.
    start: The submitted start time of the range or NULL if offset is set.
    end: The submitted end time of the range or NULL if offset is set.
    offset: The submitted offset to filter by or NULL if start and end are set.

The Submit payload carries three values: start, end, and offset. This reflects TimeFilterPanel which lacks a current or submission but has start, end, and offset values. Within user_flow.drawio, the values used for each payload slot are clarified.

Ordering

Keep the sections ordered as they appear in this overview and repeated below:

  • Definitions
  • Styles
  • Data
  • State
  • Signals

Omit any section that is unused.

Usage

When placing data into components.xd, use the property names exactly as they appear in model.txt. States used by selectors in components.xd should also appear exactly as they appear in model.txt. The property and signal names should also carry over to layout.xd, use_cases.drawio, and user_flow.drawio.

model.txt is the most powerful tool in the designer’s toolkit; taking time upfront to provide good descriptions in the model can save a lot of designer and developer headaches later on.

References