Skip to content

Reusable Components

manolo edge edited this page Jun 30, 2025 · 7 revisions

It's very easy to create reusable components in Cardboard. They're plain functions, that return a Tag. We capitalize them to keep with common practices in other frameworks.

Counter component

const Counter = () => {
  let count = state(0);

  return button(
    text(`Clicked $count times`, { count })
  ).clicked((_) => count.value++);
};

They can be used as any other tag:

div(Counter());

Todo component

export default function TodoItem(
  content: State<TodoItem>,
  remove: (self: CTag, content: State<TodoItem>) => void
) {
  const isComplete = grab(content, 'complete', false);
  const todoItem = grab(content, 'item', 'Empty TODO');

  return div(
    input()
      .setAttrs({ type: 'checkbox' })
      .on('change', (self, evt) => {
        content.value.complete = self.checked;
      }),
    h4(todoItem)
      .stylesIf(isComplete, { textDecoration: 'line-through' }),
    button('-')
      .clicked((self) => {
        if (remove) remove(self, content);
      }), // self.parent will be div
  );
}

Styling components

To style a component you can either use inline styles with CTag.addStyle and .setStyle, or adding a class to the component and defining a style for it.

But Note that using inline style will duplicate the style for each instance of the component.

// Your component's file
tag('(head)').append(style({
  '.styled_thing': {
    backgroundColor: 'lightblue',
    padding: '10px',
    borderRadius: '5px',
    color: 'darkslategray',
    textAlign: 'center',
    margin: '10px 0',
  }
}));

const StyledThing = () => {
  return div(
    p('This is a styled component'),
  ).addClass('styled_thing');
}


// In main file
init().append(
  StyledThing(),
  StyledThing(),
);

Lifecycle events

In some cases, you might want to do stuff when the element is added to the page, or when it's removed. For that, Cardboard offers the function `withLifecycle(tag, { mounted, unmounted, beforeUnmounted }).

const Clock = () => {
  const seconds = state('00');
  const minutes = state('00');
  const hours = state('00');

  const setTime = () => {
    const date = new Date();
    seconds.value = date.getSeconds();
    minutes.value = date.getMinutes();
    hours.value = date.getHours();
  };

  let interval: any;

  return withLifecycle(
    div(hours, ':', minutes, ':', seconds),
    {
      mounted() {
        // Add the interval when the element is added
        setTime();
        clearInterval(interval);
        interval = setInterval(setTime, 500);
      },
      beforeUnmounted() {
        // You can return a Promise<boolean>, that if false, it will cancel the remove operation
        // Or if it's true or not return anything, the element will be removed
      },
      unmounted() {
        // Clear the interval when the element is removed
        clear interval(interval);
      },
    },
  );
};

It's handy to have this control, as you don't want the interval to keep going if the element is not on the page.

mounted

It fires whenever the element is added to the page. This can be handy when you want to make an action when the element is added to the page. For example, running an animation, Tween, etc...

beforeUnmounted

It fires before the element is removed from the page. If you return a promise that's false, the element will not be removed. If the promise returns true, or you don't return anything, the element will be removed as normal.

This can be very useful when you want to perform an action before the element is removed. For example, if you want to animate an element being removed from the page. You can return a promise that resolves when the animation or Tween ends. This way, the animation ends and then the element is removed.

unmounted

It fires whenever the element is removed from the page. A good place to remove side effects that might stay running even if the element is removed. Like in the example above, where we need to stop the interval when the element is removed. Otherwise, it stays running infinitely.

Request more events

If you miss any event, please let me know by dropping an issue here with your suggestion.


Component extension

Cardboard provides a Component helper to simplify the creation of reusable, stateful UI components. This function allows you to encapsulate state and logic, and returns a tag that updates automatically when its state changes.

Usage example:

import { Component } from '../../dist/ext/components.js';

const Counter = Component(({ state }) => {
  return button(
    text(`Clicked $count times`, { count: state })
  ).clicked(() => state.value++);
});

You can use this component just like any other tag:

div(Counter());

Styling Component-based components

You can style components created with Component in several ways:

Inline styles:

Use .addStyle('property', value) or .setStyle({ ... }) on the returned tag.

CSS classes:

Use .addClass('your-class') and define the class in a stylesheet.

.styled helper:

The Component function provides a .styled property for convenient styling.
Example:

const BlueCounter = Counter.styled({
  color: 'blue',
  fontWeight: 'bold',
  padding: '8px',
});

This returns a new component with the given styles applied to the root tag, the root element will get a class applied to it to identify those styles.

If you want your component to always be styled you can run styled within the component definition:

const Counter = Component(({ state }) => {
  return button(
    text(`Clicked $count times`, { count: state })
  ).clicked(() => state.value++);
}).styled(styles.CounterStyles);

Note: Inline styles are duplicated for each instance, while classes and .styled styles are more efficient for many components.

See more examples in the examples/components folder.
The implementation can be found in src/ext/components.js.

Clone this wiki locally