Reusable Components

Reusable Components

💡

XLMUI has been designed with reusable components in mind. Besides the components out-of-the box, you can use the markup to create and utilize your reusable components within your app.

This article will teach you the gritty-nitty details of component creation and customization.

Defining Reusable Components

💡

Use the <Component> tag to declare a reusable component in the markup. This tag has a mandatory attribute, name, a unique identifier to the component.

components/LabeledValue.xmlui
<Component name="LabeledValue">
  <HStack>
    <Text>Label:</Text>
    <Text>Value</Text>
  </HStack>
</Component>

You can immediately use the new component type, LabeledValue, putting it into your app using the <LabeledValue> tag:

Main.xmlui
<App>
  <LabeledValue />
  -- Separator text --
  <LabeledValue />
</App>

The XMLUI engine displays all LabeledValue component instances:

💡

The component's name must start with an uppercase letter followed by letters, digits, the underscore (_), or the dollar sign ($) character. Components must be placed into separate files in the components folder within the app's root folder. Use the same name for the component as its filename so that the browser can fetch the component.

The component must have content, at least a single nested tag to define the component's visual representation, and, optionally, it may have variables and methods assigned to the component.

Using Properties

💡

Component definitions can refer to properties passed to component instances.

Though reusable components with a static appearance may be helpful, the real power comes when you can define component properties to influence the appearance and behavior of a particular reusable component.

With a few additions, you can allow LabeledValue to receive a label and a value specification:

<Component name="LabeledValue">
  <HStack>
    <Text>{$props.label}:</Text>
    <Text>{$props.value}</Text>
  </HStack>
</Component>

The $props context value defines the properties passed to the custom component. You can use the . operator to access a particular property. So, $props.label and $props.value mark the label and value properties, respectively. As you use these names in binding expressions, the engine will display their run time value.

Use the same markup to pass property values to reusable components as you do for built-in components:

<App>
  <LabeledValue label="Firstname" value="Cameron" />
  -- Separator text --
  <LabeledValue label="Lastname" value="Smith" />
</App>
📔

XMLUI does not require defining component properties in advance. You just use your property through the $props identifier; the engine will immediately understand and render it.

Sometimes, it is helpful to have default values for properties. XMLUI makes it simple using the ?? operator, as you can see in the following component definition:

<Component name="LabeledValue">
  <HStack>
    <Text>{$props.label ?? '[label]'}:</Text>
    <Text>{$props.value ?? '[none]'}</Text>
  </HStack>
</Component>

Using Events

💡

Similarly to properties, you can harness a reusable component with custom events.

Create a new reusable component, <IncButton>, which increments its value for every click. This component can notify its environment about increments by firing an event. This event receives the current counter as an event parameter:

<Component name="IncButton">
  <Button
    label="Click to increment: {count}" var.count="{0}"
    onClick="count++; emitEvent('incremented', count)" />
</Component>

The emitEvent function emits the "incremented" event attached to the reusable component's instance and triggers the particular event handler. The first argument of emitEvent is the event name, and the subsequent ones are the arguments of the specific event.

The following app uses the new event to append the number of clicks to a text. The handler of the incremented event (onIncremented) declares an arrow function where v represents the event value, namely, the count of clicks:

<App var.text="">
  <IncButton onIncremented="v => text += ' ' + v" />
  <Text value="{text}" />
</App>

Try using <IncButton> with the incremented event:

Exposing Component Methods

💡

In addition to properties and events, you can expose custom methods from a particular component.

You can invoke these methods in other components to execute an operation or query some information in the exposing component.

The following code snippet shows a modified <IncButton> component that exposes a method, setValue, to set the button's counter value from outside the component:

<Component
  name="IncButton"
  var.count="{0}"
  method.setValue="v => count = v">
  <Button label="Click to increment: {count}" onClick="count++" />
</Component>
📔

Variables defined within a reusable component are invisible from outside. However, with methods, you can expose them.

The updated component stores the counter value in a variable belonging to the entire component (and not enclosed within the <Button>). This line declares the setValue method with an arrow function with the parameter receiving the new value (v).

The following app adds a second button to set the current value of <IncButton> to 123 on a click. Here, we provide an id for <IncButton> to refer to it from the second button:

<App>
  <HStack>
    <IncButton id="myIncButton" />
    <Button
      label="Set count to 123!"
      onClick="myIncButton.setValue(123)" />
  </HStack>
</App>

The click event handler (onClick) of the second button uses the myIncButton.setValue() expression to invoke the setValue method associated with the myIncButton instance.

Try using this simple app:

<Slot>

💡

You can use the Slot placeholder within a reusable component's definition to mark the location when the reusable component's children should be injected.

Here, Slot is nested into VStack to mark the location to inject MyStack children:

<Component name="MyStack">
  <H2>This is my special Stack</H2>
  <VStack backgroundColor="cyan">
    <Slot/>
  </VStack>
</Component>

This sample injects children into MyStack:

<App>
  <MyStack>
  These are the children injected into the
  <H3>MyStack</H3>
  component's Slot placeholder
  </MyStack>
</App>

MyStack puts the children into the location designated by Slot:

Default Slot Content

💡

The Slot may display default content when the reusable component instance does not define children.

In this example, Slot declares a default view:

<Component name="MyGreeting">
  <Slot>
    <H2>Hi There!</H2>
  </Slot>
</Component>

The following sample displays two MyGreeting instances. The first does not define children (content for Slot); the second does:

<App>
  <MyGreeting />
  <MyGreeting>
    <H1>Howdy!</H1>
  </MyGreeting>
</App>

The engine renders the first MyGreeting instance with the default content, the second with the content defined in the instance:

Slot Context values

💡

The Slot in a reusable component may define arbitrary properties it passes as context values to the children of the particular reusable component instance.

The following component, RandomNumberDisplay, generates a random number between its minValue and maxValue properties and delegates displaying that number to its consumer.

<Component name="RandomNumberDisplay">
  <variable 
    name="randomNumber" 
    value="{Math.floor(Math.random() * ($props.maxValue - $props.minValue)) + $props.minValue}" />
  <Slot number="{randomNumber}"/>
</Component>

Here, the app uses RandomNumberDisplay to generate a number between 10 and 19, which it displays in a shaded box with 4 times zoom. The app receives the random number passed to the number property within the component declaration of RandomNumberDisplay in the $number context value.

<App>
  <RandomNumberDisplay minValue="{10}" maxValue="{20}">
    <CHStack 
      width="200px" 
      height="100px" backgroundColor="cyan">
      <Text zoom="4">[{$number}]</Text>
    </CHStack>
  </RandomNumberDisplay>
</App>

Named Slots

A reusable component may define named Slot elements (each having a unique name property). The reusable component instance can provide separate content for these named slots. Slot names should end with the Template suffix; otherwise, the rendering engine raises an error.

In the following example, FancyCard has a named slot (titleTemplate) with default slot content and a title context value, which wraps the title in several asterisks:

<Component name="FancyCard">
  <VStack border="2px solid orangered" padding="$padding-loose">
    <variable name="fancyTitle" value="*** {$props.title} ***" />
    <Slot name="titleTemplate" title="{fancyTitle}">
      <H1>{fancyTitle}</H1>
    </Slot>
    <VStack backgroundColor="blanchedalmond" padding="$padding-loose">
      <Slot />
    </VStack>
  </VStack>
</Component>

The app uses two instances of FancyCard. The first does not have a definition for the titleTemplate; the engine uses the default content declared in FancyCard. The second instance has template content that renders the title in a shaded box. This template content uses the asterisk-wrapped title via the $title context value.

<App>
  <FancyCard title="Demo Card #1">
    This is my fancy card content #1
  </FancyCard>
  <FancyCard title="Demo Card #2">
    <property name="titleTemplate">
      <VStack padding="$padding-tight" backgroundColor="lightblue">
        <H1>{$title}</H1>
      </VStack>
    </property>
    This is my fancy card content #2
  </FancyCard>
</App>

Reusable Components in Layout Containers

When you use reusable components with layout containers wrapping them, displaying a particular markup is not always obvious. The reusable components may nest other layout containers. As you do not see these nested containers from the markup directly, sometimes you may not understand immediately a particular component arrangement.

In this section, you will learn a few details about how reusable components are displayed within layout containers.

Let's create a reusable component, MyBoxes, with this markup:

<Component name="MyBoxes">
  <Stack width="100px" height="36px" backgroundColor="purple" />
  <Stack width="50px" height="36px" backgroundColor="orange" />
</Component>

MyBoxes displays two boxes with different sizes and background color settings. Its declaration only tells the orange box to follow the purple box.

Reusable Components in a Stack

💡

When you nest a reusable component into a stack, the engine ignores all layout-related properties decorating the reusable component instance.

Nest the MyBoxes instances into an HStack:

<App>
  <HStack>
    <MyBoxes width="50%" />
    <MyBoxes />
  </HStack>
</App>

The engine renders this markup as if your declaration was this (it ignores the width="50% of the first MyBoxes) instance:

<App>
  <HStack>
    <Stack width="100px" height="36px" backgroundColor="purple" />
    <Stack width="50px" height="36px" backgroundColor="orange" />
    <Stack width="100px" height="36px" backgroundColor="purple" />
    <Stack width="50px" height="36px" backgroundColor="orange" />
  </HStack> 
</App>

Thus, the markup with MyBoxes results in this output:

Because stacks use a non-zero gap by default, you see this gap between the children of MyBoxes.

When you wrap MyBoxes into a VStack:

<App>
  <VStack>
    <MyBoxes width="50%" />
    <MyBoxes />
  </VStack>
</App>

The engine renders the markup as if it were this one:

<App>
  <VStack>
    <Stack width="100px" height="36px" backgroundColor="purple" />
    <Stack width="50px" height="36px" backgroundColor="orange" />
    <Stack width="100px" height="36px" backgroundColor="purple" />
    <Stack width="50px" height="36px" backgroundColor="orange" />
  </VStack>
</App>

Thus, the markup with MyBoxes in a wrapping VStack renders this:

Reusable Components in a FlowLayout

💡

FlowLayout wraps each direct child in an internal container and removes the child's width settings. This internal container takes the width from the reusable component definition.

Assume you use MyBoxes with a FlowLayout, like in this example:

<App>
  <FlowLayout>
    <MyBoxes width="50%" />
    <MyBoxes />
  </FlowLayout>
</App>

Because of the internal wrapping, the engine renders a markup like this:

<App>
  <FlowLayout>
    <FlowLayoutItem width="50%">
      <Stack height="36px" backgroundColor="purple" />
      <Stack height="36px" backgroundColor="orange" />
    </FlowLayoutItem>
    <FlowLayoutItem>
      <Stack height="36px" backgroundColor="purple" />
      <Stack height="36px" backgroundColor="orange" />
    </FlowLayoutItem>
  </FlowLayout>
</App>
📔

Note that the original Stack widths within MyBoxes are removed, and the 50% width of the first MyBoxes is transposed into the first virtual FlowLayoutItem.

📔

The purple and orange boxes are adjacent because the virtual FlowLayoutItem container does not use gaps. XMLUI does not provide a FlowLayoutItem component; we use this name here just for explanation.

Using Layout Containers Explicitly

You can explicitly wrap the children of a reusable component into a layout container to mark your intention regarding their arrangement. For example, if you want the purple and orange boxes in a horizontal layout (within the component), you can explicitly declare that intention:

<Component name="MyBoxes">
  <HStack gap="0" border="2px dotted green" >
    <Stack width="100px" height="36px" backgroundColor="purple" />
    <Stack width="50px" height="36px" backgroundColor="orange" />
  </HStack>
</Component>
📔

This definition adds a dotted green border to the component to display the UI patch it fills for demonstration purposes.

Wrap two MyBoxes instances into an HStack:

<App>
  <FlowLayout>
    <MyBoxes width="50%" />
    <MyBoxes />
  </FlowLayout>
</App>

The output differs from the one where you did not have an explicit layout container within MyBoxes. There is no gap between the purple and orange boxes as the HStack sets the gap explicitly to zero.

📔

Remember, an item's width in a horizontal stack (unless explicitly set) accommodates the content's width, which is 100 + 50 = 150 pixels here. The dotted green background signs the boundary of the HStack arranging the purple and orange boxes. Do not forget that stacks ignore the layout properties, including the width set on a reusable component.

You get this output when you wrap the two MyBoxes instances into a VStack:

📔

Remember, an item's width in a vertical stack (unless explicitly set) is the entire width (100%) within the stack's parent. As the dotted green background signs, the HStack is as wide as its parent. Within this stack, the boxes still keep their explicit widths. Do not forget that VStack ignores the layout properties, including the width set on a reusable component.

The display with a FlowLayout:

📔

Remember, an item's width in a FlowLayout (unless explicitly set) is the entire width (100%) within the parent.

Using Explicit Component Width

When you use a layout container within a reusable component, you can assign an explicit width to that container, like in this example:

<Component name="MyBoxes">
  <HStack gap="0" border="2px dotted green" width="180px" >
    <Stack width="100px" height="36px" backgroundColor="purple" />
    <Stack width="50px" height="36px" backgroundColor="orange" />
  </HStack>
</Component>

The purple and orange boxes are in a horizontal stack set to 180 pixels wide. This definition sets a dotted green border around the horizontal stack for demonstration purposes.

Place two MyBoxes instances in an HStack:

<App>
  <HStack>
    <MyBoxes width="50%" />
    <MyBoxes />
  </HStack>
</App>

The engine renders this output:

Here, the dotted green background signs the boundary of the HStack, which arranges the purple and orange boxes. The width of this HStack is 180 pixels, just as specified.

Now, place MyBoxes into a VStack:

<App>
  <VStack>
    <MyBoxes width="50%" />
    <MyBoxes />
  </VStack>
</App>

You can see that each MyBoxes instance is rendered exactly as previously. Nonetheless, they are in a separate row, following a VStack rendering logic:

Try using MyBoxes in a FlowLayout:

<App>
  <FlowLayout>
    <MyBoxes width="50%" />
    <MyBoxes />
  </FlowLayout>
</App>

As you already learned, FlowLayout removes its direct children's explicit widths and uses the width assigned to the reusable component. So, even if you use a 180-pixel wide HStack within MyBoxes, FlowLayout ignores that width. Nonetheless, the two stacks within HStack are non-direct children, so their width is kept.