Views

Views represent the user interface of a Typescene application.

Creating views

Views are renderable components that represent (parts of) the user interface of an application.

Typescene views are platform independent: the code that’s responsible for drawing elements on the screen, or inserting them into a document such as a Web page, is separate from the view component classes themselves.

As such, views don’t necessarily correspond 1-to-1 with the generated output. Creating views for a Typescene application is certainly not the same as writing HTML code — even if the output gets inserted into the DOM (document object model) of a Web page.

Preset constructors

View rendering is initiated by the application’s Activities. Typically, each activity encapsulates exactly one view component, which is created and destroyed as the activity transitions between active and inactive states.

Even though views are created as Component classes, you don’t need to instantiate any of them yourself. Each activity takes care of instantiating its own view — as long as the activity itself is preset with a view constructor.

For more details on how components are ‘preset’ to create new component constructors, refer to the following page.

See also: Concepts > Components

// UIButton is a view class in itself:
const simplestView = UIButton;

// Preset constructors provide property values,
// bindings, and event handlers:
const boundView = UIButton.with({
  label: bind("buttonLabel"),
  icon: "warning",
  onClick: "sayHi()"
});

// Nested views can also be preset
// (result is still a class, not an object) --
const cellView = UICell.with(
  UICenterRow.with(
    { height: 48 },
    UILabel.withText("Hello, world!"),
    UIButton.with({
      label: bind("buttonLabel"),
      onClick: "sayHi()"
    })
  )
);

// Activities are also preset, and then extended:
class MyActivity extends ViewActivity.with(cellView) {
  path = "/";
  buttonLabel = "Say hi";
  sayHi() { alert("Hello, world!") }
}

In the above example, when an instance of MyActivity is activated, it immediately creates an instance of cellView, and assigns it to the view property of the activity object.

View objects

You can interact with the instantiated view objects after they are created, for example from an event handler.

class MyActivity extends ViewActivity.with(cellView) {
  // ...
  sayHi(e: UIComponentEvent<UIButton>) {
    let button = e.source;  // <= button instance
    console.log(button.label);  // => "Say hi"
  }
}

You can even create view components on the fly, and add them to containers, although this is largely unnecessary. Also keep in mind that bindings do not work if the view was never preset on its bound parent component.

class MyActivity extends ViewActivity.with(cellView) {
  // ...
  sayHi(e: UIComponentEvent) {
    let row = e.source.getParentComponent(UIRow)!;
    row.content.add(new UILabel("More hello"));
  }
}

JSX syntax

In addition to the regular JavaScript/TypeScript syntax that includes method calls to the static .with() method (as illustrated above), Typescene supports ‘JSX’ syntax. This syntax was originally created for the React framework, but is now officially supported by non-React tools — importantly, TypeScript and Babel transpilers.

Conversion from JSX syntax to plain JavaScript/TypeScript is done at build time, by inserting calls to a single function at runtime (exported by the Typescene library as JSX). This function simply calls the static with method on a component class, making the following code snippets equivalent:

// JSX syntax:
const view = (
  <cell onBeforeRender="doSomething()">
    <centerrow height={40}>
      <label>Hello, {bind("name")}</label>
    </centerrow>
  </cell>
);
// JavaScript syntax without JSX:
const view = UICell.with(
  { onBeforeRender: "doSomething()" },
  UICenterRow.with(
    { height: 40 },
    UILabel.withText(bindf("Hello, ${name}"))
  )
);

Note: Unlike with other frameworks (including React itself), Typescene JSX tags do not result in an object. The ‘value’ of a JSX tag such as <label>...</label> is still a constructor. You cannot refer to variable values within JSX blocks, since preset values would never change afterwards. However, JSX views may include bindings — just wrap the call to bind in a pair of curly braces as in the example above.

Certain components, such as labels and buttons, can be declared with text within their opening and closing brackets. If there are any bindings within this text (such as in the above example), the surrounding text gets wrapped in a call to bindf.

bindf()
Create a new binding using a format string

UI components

Typescene defines a number of ‘primitive’ components — those that can be rendered using as native controls. A basic user interface can be created using these components alone, and more complex elements can be creates as custom views (see below).

Where several variants of the same type of component are available, a base component class is extended with additional styles that change how a UI element looks, but not its behavior.

Labels

Label components display plain text or HTML-formatted text, either in a single line, with hard line breaks, or wrapped automatically based on the width of the component.

UILabel
Represents a UI component that contains a piece of text
UICloseLabel
A label with reduced vertical spacing
UIExpandedLabel
A label that occupies as much space as possible
UIHeading1
A heading label, level 1
UIHeading2
A heading label, level 2
UIHeading3
A heading label, level 3
UIParagraph
An expanded label with text that is wrapped across multiple lines when needed

Buttons

Button components are styled to appear interactive, in a number of different ways. The predefined styles used by each of these variants are customizable using styles or themes.

UIButton
Represents a button that can be clicked/tapped to perform an action
UIBorderlessButton
A button without a border or space around its label text
UIIconButton
A button that contains a single icon
UILargeButton
A prominent button that is larger in size
UILinkButton
A button that is displayed as a hyperlink
UIOutlineButton
A button that has a colored outline
UIPrimaryButton
A prominent button that represents the primary action
UISmallButton
A button that is smaller in size

Input elements

These components interact with a bound form context component (a UIFormContext instance) to get and set input values dynamically. The initial value is taken either from a preset property or from the form context. Events are emitted when the user changes the current value, and changes are updated back to the form context instance.

UITextField
Represents a text input field or multiline text area
UIBorderlessTextField
A text field without a border or space around its input text
UIToggle
Represents a check box or toggle switch

Visual elements

UIImage
Represents an image element
UISeparator
Represents a horizontal or vertical line separator
UISpacer
A blank element, occupying as much space as possible, or a space of specific dimensions

Layout and control

UI elements such as buttons and labels can be laid out in two different ways:

  • Positioned at a specific position determined by at least two coordinates (similar to CSS absolute positioning), or
  • Positioned together with other components along the primary axis of a container, e.g. in a row or a column.

You can nest different types of containers to build more complex layouts. In addition, you can use styles (see below) to specify dimensions and other properties of a container.

Cells

Cells are containers that allow for decoration of the space they occupy, using e.g. background colors, borders, and drop shadows.

By default, cells occupy as much space as possible (except for UIFlowCell), with content arranged top-to-bottom.

UICell
Cell base class
UICoverCell
Cell that is positioned to cover the exact dimensions of its parent container
UIFlowCell
Cell that does not expand within its parent container; allows content to ‘flow’ vertically
UIScrollContainer
Container that occupies a scrolling area, and emits scroll-related events

Rows and columns

Rows and columns are containers that arrange their content along a horizontal and vertical axis, respectively.

Content may be separated using fixed-width/height spacers (an 8-pixel gap is used by default, which is customizable as part of the theme), or separated using vertical/horizontal lines.

By default, rows and columns occupy as little space in the cross-axis direction as possible (i.e. row height, and column width), although a specific height or width can be set.

UIRow
Represents a horizontal line of UI elements
UICenterRow
A row that positions components in the center, horizontally
UICloseRow
A row without spacing between components
UIOppositeRow
A row that positions components at the end (the right hand side, for left-to-right languages)

UIColumn
Represents a vertical line of UI elements
UICloseColumn
A column without spacing between components

Controller views

These elements don’t output their own elements on screen directly, but contain other elements that are controlled by these components in different ways.

UIConditional
Controls the display of a child component depending on the state of a boolean property
UIForm
Encapsulates a UICell with child components, bound to a specific UIFormContext instance
UIFormContextController
Controls child components, bound to a specific UIFormContext instance
UIListCellAdapter
Encapsulates a UICell with child components, bound to a specific list item
UIListController
Controls a list of components, generated dynamically based on elements in another list
UIMenu
Generates a dropdown menu based on a list of options
UISelectionController
Controls selection state among child components
UIStyleController
Applies a style to its child component depending on the state/value of a property
UIViewRenderer
Renders a referenced view component that is not a direct child component

Styles and themes

View components have a number of properties that can be used to change the appearance of the rendered output element; these can be either preset (using with) or changed dynamically on an existing component instance.

  1. style property — an instance of UIStyle
    • When preset, the styles from a given UIStyle object are mixed into the current style of the preset component.
    • When preset as a string, styles from a named style in the current theme (see below) are mixed in.
    • When set on an existing view component, the current UIStyle instance is replaced entirely, removing its existing styles.
  2. Style overrides — objects with individual properties that override the styles defined in the current UIStyle instance.
    • When preset or set on an existing view component, the given properties are applied on top of the styles from the current UIStyle instance.

Note that the style property always refers to a UIStyle instance, whereas style overrides are (read-only) plain objects that expose individual settings — whether taken from the current UIStyle instance, or overridden by assigning a new object to these properties.

Updating any of the individual properties of a style override object on an existing view component has no immediate effect, since only the override properties themselves are observed by the component renderer.

UIStyle
Encapsulates a set of style objects with properties that alter the appearance of a UI element

// define a custom style (name is optional)
const myStyle = UIStyle.create("MyStyle", {
  textStyle: { bold: true },
});
const view = (
  <row>
    <button
      style={myStyle}
      decoration={{ background: "@yellow" }}
      onBeforeRender="setButtonStyle()"
    >
      Click me
    </button>
  </row>
);

class MyActivity extends ViewActivity.with(view) {
  // ...
  
  setButtonStyle(e: UIComponentEvent<UIButton>) {
    let btn = e.source;  // <= the button
    console.log(btn.textStyle.bold); // => true
    btn.dimensions = { minWidth: 200 };
  }
}

In this example:

  1. The myStyle variable is set to a UIStyle instance, which only contains the ‘bold’ text style flag.
  2. This style is preset on the UIButton constructor (i.e. the <button> JSX tag) — which means the bold flag is mixed in with the regular styles for a button.
  3. The decoration property is also preset, which means that these settings will be applied on top of the UIStyle settings from style by the button renderer.
  4. An event handler is added, which gets called right before the button is rendered. The event handler method of the activity is able to find a reference to the UIButton instance (not constructor) and manipulate its properties.
  5. The value of btn.textStyle.bold is true, because the textStyle object is directly taken from the button’s combined styles (after mixing in myStyle).
  6. When the dimensions property is set to a new object, its style settings also get applied on top of the UIStyle settings from style by the button renderer.
  7. As a result, the button becomes yellow, 200px wide, with bold text.

Style settings

Refer to the following interface definitions for a list of settings that are available on both UIStyle instances and overrides.

UIStyle.Dimensions
Options for the dimensions of a view component
UIStyle.Position
Options for component positioning within parent component(s)
UIStyle.TextStyle
Options for text styles
UIStyle.Decoration
Options for the appearance of UI elements and cell containers
UIStyle.ContainerLayout
Options for layout of UI elements within a container

Measurements

All measurements may either be specified using ‘CSS units’ (i.e. strings that conform with one of the formats allowed by the CSS specification) or using numbers that designate a length in ‘device-dependent pixels’ (dp). These are equivalent to physical pixels on a screen when not zoomed in or out, and may be scaled automatically on high-density displays.

const view = UICell.with(
  { dimensions: { maxHeight: "50%" } },  // CSS units
  UIRow.with(
    { height: 48 },  // value in dp
    UIButton.with({
      label: "Click me",
      dimensions: { minWidth: 200 }  // value in dp
    })
  )
)

Note: In Web applications, 1dp is defined as the equivalent of 1/16th of a rem unit. This value can be changed at runtime using the BrowserApplication.setGlobalDpSize() method, which scales all measurements up or down according to a given factor (defaults to 1), or by redefining the font-size CSS property of the html element.

Colors

Color values may be specified in one of the following formats:

  • A CSS value, e.g. "blue", "#1122AA", or "rgba(0,0,0,0.5)".
  • The name of a color that is defined in the current theme (see below), preceded with an @ sign — e.g. "@blue", "@yellow", or "@slate"; or one of the symbolic colors such as "@primary" "@background", or "@text".
  • A theme color followed by an optional caret sign and a percentage, to darken or brighten the theme color by the given percentage — e.g. "@green-20%" (darken), "@green+20%" (lighten), "@green^-20%" (darken light colors, lighten dark colors), "@green^+20%" (lighten dark colors, darken light colors).
  • A theme color followed by a slash and a percentage, to set the opacity of the resulting color — e.g. "@green/80%" for a green color with a transparency level of 20% (i.e. 80% opaque).
  • A theme color followed by .text, which is replaced with a suitable text color (nearly black or nearly white) depending on the brightness of the theme color — e.g. "@primary.text", or "@green-10%.text".
const greenBgStyle = UIStyle.create({
  decoration: {
    background: "@green",
    textColor: "@green.text"
    // ^ same as textStyle.color
  }
});
const view = UICell.with(
  { background: "@background" },
  UIRow.with(
    UIButton.with({
      label: "Click me",
      style: greenBgStyle
    })
  )
)

Note: All default colors for UI elements are defined using combinations of "@background" and "@text" (which is itself defined as "@background.text"). This makes it possible to implement ‘dark mode’ by simply changing the background color value to a dark color, and a primary color that is relatively bright. Afterwards, call app.renderContext.emitRenderChange() to re-render all visible view components using the new color values.

Conditional styles (state)

You can include style settings that change the appearance of a UI element based on its current state without having to add event handlers. Use the addState method on a UIStyle` object to add conditional styles.

UIStyle.addState()
Add conditional styles to a UIStyle instance

const myStyle = UIStyle.create({
  textStyle: { bold: true },
}).addState("hover", {
  decoration: { background: "@primary" },
  textStyle: { italic: true, color: "@primary.text" }
});

The following states are available:

  • pressed — used for buttons that are in ‘pressed’ state (while held down)
  • hover — used for UI elements and cells that are current under the user’s mouse cursor
  • focused — used for input elements and buttons (or any other focusable element) that currently has input focus
  • disabled – used for input elements and buttons for which input has been disabled
  • selected — used for UI elements and cells that have emitted a Select event, and have not yet emitted a Deselect event (often used together with the UISelectionController class, which emits Deselect events on the currently selected component as soon as another element emits a Select event).

Style groups

You can avoid creating separate variables for each UIStyle instance by creating style groups. These are sets of related styles, that are defined using a call to UIStyle.group.

This method accepts an object with style names as properties, and UIStyle instances or plain objects as values. Additionally, existing groups can be extended by including them as an argument as well.

UIStyle.group()
Create a style group with a given set of styles and/or other groups

// create a style group
const bannerStyles = UIStyle.group({
  banner: {
    decoration: { background: "@primary" }
  },
  bannerButton: UIStyle.create({
    decoration: { background: "@primary^-20%" }
  }).addState("hover", {
    decoration: { background: "@primary^-30%" }
  })
});

// extend this style group
const fancyBannerStyles = UIStyle.group(
  bannerStyles,
  {
    bannerButton: {
      textStyle: { fontFamily: "serif" }
    }
  }
);

// use styles from this group
const view = UIRow.with(
  { style: fancyBannerStyles.banner },
  UIButton.with({
    label: "Foo",
    style: fancyBannerStyles.bannerButton
  })
)

Themes

A theme is an instance of the UITheme class. The current theme is referenced by the static UITheme.current property.

To change the default appearance of all components, colors, icons, and other style defaults, set the current property to a new UITheme instance. You can copy the current theme using the clone() method.

UITheme
Represents a set of default styles and component presets

const defaultTheme = UITheme.current.clone();
const darkTheme = UITheme.current.clone();
darkTheme.colors["background"] = "#222";
darkTheme.colors["primary"] = "@green";

let dark = false;
function switchTheme() {
  if (dark) UITheme.current = defaultTheme;
  else UITheme.current = darkTheme;
  dark = !dark;
  
  // trigger re-rendering of all components:
  Application.active.first()?.renderContext?.emitRenderChange();
}

Custom views

There are a few different approaches for creating views that can be reused throughout an application.

Using a preset constructor

The simplest approach to creating reusable views is to define a preset constructor, and use it as-is.

// create a special button constructor that can be reused
const MyButton = UIButton.with({
  style: UIStyle.create("FancyButton", {
    textStyle: { fontFamily: "serif" }
  })
});

// ...elsewhere:
const view = UIRow.with(
  MyButton.withLabel("Click me")
); 

You can also extend the preset constructor into a class, if needed.

class MyButton extends UIButton.with({ style: myStyle }) {
  label = "Click to show the time";
  updateLabel() { this.label = String(new Date()) }
}
MyButton.addObserver(class {
  constructor (public button: MyButton) { }
  onClick() { this.button.updateLabel() }
});

// ...elsewhere:
const view = UIRow.with(
  MyButton
);

Using a view activity

Sometimes it may be appropriate to define a new activity class to encapsulate a reusable view. For example, a modal dialog can be displayed independently of other views — in this case, the DialogViewActivity class provides all of the required functionality.

const dialogView = (
  <flowcell
    background="@background"
    textColor="@text"
    padding={16}
    position={{ gravity: "center" }}
  >
    <centerrow>
      <label>Hello, {bind("name")}!</label>
    </centerrow>
  </flowcell>
);

class MyDialogActivity extends DialogViewActivity.with(dialogView) {
  public name?: string;
}

let dialog = new MyDialogActivity();
dialog.name = "world";
setTimeout(() => {
  Application.active.first()?.showViewActivityAsync(dialog);
}, 100);

Using a ViewComponent class

In most other cases, whenever you need custom binding properties but not an entire activity, reusable views can be implemented as a preset ViewComponent constructor.

This has the added advantage of enabling code completion for preset properties, and allowing for nested content — for code in both JSX and native JavaScript syntax.

ViewComponent
Represents a custom view component

class MyView extends ViewComponent.with({
  view: (
    <flowcell padding={16} onClick="cellClicked()">
      <centerrow>
        <label>Hello, {bind("name")}!</label>
      </centerrow>
      <separator />
      <viewcontent />
    </flowcell>
  ),
  defaults: {
    name: ""  // this becomes a property
  }
}) {
  cellClicked() {
    this.name = "Clicked";
  }
}

// ...elsewhere:
const view = (
  <cell>
    <MyView name="World">
      <row>
        <label>Content goes here</label>
      </row>
    </MyView>
  </cell>
);

Next steps

Learn more about the other parts of a Typescene application:

  • Activities encapsulate views and display them when activated.
  • Services encapsulate global state (data), made available to all other components.