Retool HTML component events: why every handler fires at once
reading · 00%

Retool HTML component events: why every handler fires at once

Why one click triggers every handler on a Retool HTML component, and how to route each event to the right one.

Published
Reading4 min
CategoryRetool
Words891

An HTML component holds two buttons, Cancel and Submit, each with its own Click event handler. Clicking Cancel runs both handlers. The same happens with inputs: a change in either text field fires every Change handler on the component. Strip the markup down to one button, and its Click handler does not fire at all.

The cause is one attribute. Each interactive element needs a data-click-target or data-change-target value, and each handler needs a condition that checks it. This note covers why, the pattern that routes every event to the right handler, and what to check when it still misbehaves.

Why use the HTML component at all

Retool's built-in components cover most screens, and they are the right default: tables, forms, filters, and standard button rows are quicker to build with them and easier to maintain. The HTML component is for layouts that need full control of markup and CSS. A hero section with a full-bleed background image and action buttons laid over it is a typical case.

Our rule of thumb: built-in components when their layout fits, the HTML component when the design needs control over every element. Once buttons and inputs live inside that HTML, they need a way to tell Retool which one was used.

How events reach Retool

The HTML component exposes two event types to the inspector: Click and Change. Every handler of a type listens to the whole component. So with five buttons and five Click handlers, any click reaches all five handlers.

To tell them apart, Retool reads a data attribute on the element that raised the event:

  • data-click-target on buttons and other clickable elements.

  • data-change-target on inputs, selects, and checkboxes.

Retool passes the attribute's value to the handler as target.type. The handler's "Only run when" condition compares it against the value it should respond to. The attribute is also required for a handler to run at all, so a single button without one triggers nothing.

The pattern

Give each element its own target value:

HTML
<div>
  <input
    type="text"
    data-change-target="Change Action 1"
    placeholder="Enter value..."
  />
  <input
    type="text"
    data-change-target="Change Action 2"
    placeholder="Enter value..."
  />
  <button data-click-target="Click Action 1">Cancel</button>
  <button data-click-target="Click Action 2">Submit</button>
</div>

Then, in the inspector, give each handler an "Only run when" condition that matches one value exactly:

JavaScript
// Click handler for Cancel
{{ target.type === 'Click Action 1' }}

// Click handler for Submit
{{ target.type === 'Click Action 2' }}

// Change handler for the first input
{{ target.type === 'Change Action 1' }}

// Change handler for the second input
{{ target.type === 'Change Action 2' }}

Each handler now runs only for its own element. The strings must match exactly, including case and spaces.

What the target object carries

Inside any HTML component event handler, target is in scope with three properties:

  • type, the string set in the data-click-target or data-change-target attribute.

  • value, the current value of an input.

  • checked, the state of a checkbox.

That makes the handler useful beyond routing. A Change handler can write the input's value straight into app state:

HTML
<label>
  <input type="checkbox" data-change-target="Urgent" />
  Mark as urgent
</label>
<input type="text" data-change-target="Note" placeholder="Add a note" />
JavaScript
// Change handler: Set variable isUrgent, only run when
{{ target.type === 'Urgent' }}
// Value
{{ target.checked }}

// Change handler: Set variable note, only run when
{{ target.type === 'Note' }}
// Value
{{ target.value }}

Sharing a handler

Two elements can share a target value on purpose. If a header button and a footer button should both submit, give both data-click-target="Submit". One handler with {{ target.type === 'Submit' }} then serves both, with no duplicated logic.

A layout example

This is the kind of markup that justifies the HTML component: a hero with a background image and two actions over it.

HTML
<section class="hero">
  <h2>Quarter-end review</h2>
  <p>Items waiting for a decision this week.</p>
  <button data-click-target="Open queue">Open the queue</button>
  <button data-click-target="Export">Export as CSV</button>
</section>
CSS
.hero {
  min-height: 240px;
  padding: 32px;
  background: url("https://example.com/cover.jpg") center / cover;
  color: #ffffff;
}

.hero button {
  margin-right: 8px;
}

Two Click handlers, one per target value, run the queue query and the export.

When handlers still misbehave

  • Nothing fires. Check that the element has a data-click-target or data-change-target attribute.

  • Everything fires. Check that each handler has its own "Only run when" condition. One missing condition is enough.

  • The wrong one fires. Compare the strings character by character. A trailing space is enough to break a match.

  • A Change handler sees the wrong value. Read target.value or target.checked inside the handler, rather than a value captured elsewhere.

We found the attribute through a thread on the Retool community forum and confirmed the behavior by testing. The forum is worth searching early when the HTML component behaves unexpectedly.

Our view

We recommend giving every interactive element in an HTML component its own target value from the start, even when there is only one. That keeps each handler tied to one element, because the condition, not the order of handlers, decides what runs. For screens that do not need custom layout, use Retool's built-in components, because they handle events without any of this wiring.

When the layout needs more than HTML and CSS can give, a custom component is the next step. What is a custom Retool component covers when that is worth it, and our open-source components are on GitHub.

Filed underRetool
Worth passing along? Share this note.
LinkedInX
Newsletter

Get the next one in your inbox

We write when something is worth reading. Subscribing means you are happy to hear from us when that happens. Leave any time. Privacy policy.

Ahmed Sadek

Keep readingRetool
Related notes

Still here? Good sign

More notes from real builds, written up for the next person who needs one.

All articles
ContactLet's chat
Let's chat

Tell us what
you're building

Describe the process that is slowing your team down: the inbox, the spreadsheet, the portal nobody owns. We'll come back with a concrete approach, a rough timeline, and, where it helps, a working prototype within weeks.

What happens next
  1. We read every messageAn architect reads what you sent before anyone replies.
  2. You hear back in ≤ 2 daysA short reply with first questions and a suggested next step.
  3. We scope and prototypeA scoping call, then a working surface, not a slide deck.
Where are you in the process?