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