Skip to content

Highlight Active Search

Example

This hyperComponent is a prototype for hypermedia exchange because it illustrates a front-end coupled to backend routes that respond by returning only hypertext.

An active search <input> is utilized which is based on the Active Search example from the htmx documentation. However, this hyperComponent expands on that example by providing live hilighting of matched substrings. In addition, the hilight color can be selected from a dropdown menu displaying the entire Tailwind color pallet. Clicking a color swatch in the pallet changes the highlight color. Another dropdown menu provides options for changing the match criteria.

All functionality is powered by htmx and Hypersript. There are no JavaScript imports or <script> tags anywhere in this component.

A mini-project is included below to illustrate how this hyperComponent works. The project presents a table of famous English language speeches. Clicking any table row loads the full speech transcript and then switches focus to the search <input> where any substring entered by the user is matched against the full transcript using Regular Expressions.

Successful matches are surrounded with <mark> tags styled with the user’s choice of Tailwind bg-color-* utility class. The server then returns the entire transcript as html annotated with <mark> elements. The user’s experience is analagous to using a felt tipped highlight marker on a printed document.

Take a tour of the example project below by clicking on a famous speech. Click the Reset button (or select another speech) to start over.

The subsequent discussions go into great detail about all the source code used to create this hyperComponent.

The full source code for the mini-project can be found here which shows how every component is used and how each backend route is accessed.

Sample Highlight Active Search Control

Famous Speeches in the English Language

Use the Search control below to highlight words from the most famous speeches that shaped our world.

Click to select any Speech from the list

SpeakerTitleDate

Description

Hypermedia exchanges require that the backend return formatted, finished html. Thus the developer must have control over the server’s response. The Astro build tool is perfect for this because it is easy to create custom API endpoints. All that is needed is to place a .js file into the pages folder. In that file, you must export a GET, POST, PATCH or DELETE function and then Astro will build the correct route and respond to those AJAX requests.

The GET and POST requests in this example access data stored on disc (in a JSON file), but does not return the raw data. Instead, the data is used by the server to format finished html fragments which are returned in the response. HTMX attributes are used to generate these requests and also to specify how and where the returned markup is inserted into the DOM.

Read through all the example code below to see how this was accomplished.

Features

This Highlight Active Search hyperComponent includes:

  1. A searchPath prop which accepts a path to a backend search route and is used to configure the hx-post attribute of the <input> element
  2. A target prop which accepts a CSS selector that is used to update the hx-target attribute. The target prop identifies the DOM element which will updated with the returned hypertext.
  3. A swapStrategy prop which is used to update the hx-swap attribute thereby determining how htmx will update the target element.
  4. An hx-include=<input/> attribute which signals htmx to bundle the values stored in all hidden inputs with the request in order to transmit them to the backend along with the request.
  5. Hyperscript code updates the hidden fields with values needed for the backend.
  6. Search requests are initiated by multiple events, including keystrokes, pasting text into the <input>, clearing the field, etc.
  7. A dropdown menu from which the user can select a filter for the matching algorithm (case sensitive, whole word, or others),
  8. Scroll smoothing back and forth between the target and the search input
  9. Search criteria in the options dropdown can be customized.
  10. A Tooltip is displayed on hover over the search-options menu indicating the current selection for search filter.
  11. Stylings of the <input> and dropdown menus are fully customizable using Tailwind utility classe.s
  12. A ‘rightIcon’ slot which displays magnifying glass icon by default but which can accept another dropdown menu or other content to be displayed at the right border of the search container.

Searchbox Component

There is a single main component which provides the GUI for active searching and highlighting.

The code for the Searchbox.astro component is shown below. In order for this component to function as intended backend code the developer must also provide backend coded to process the request and turn html as discussed in the API Routes section below.

Dropdown menus within the Searchbox control are implemented using the ClickDropdownMenu_A component. A search-options dropdown is appears to the left of the <input/> indicated by a down-caret icon. Multiple <li> elements are passed into the default slot of this ClickDropdownMenu_A which provide the options for modifying the RegEx match criteria used by the backend. To tell the backend of the user’s selection, the textContent of the selected <li> is stored in a hidden field.

Another ClickDropdownMenu_A instance is used to present a pallet of Tailwind colors allowing the user to change the color of the highlighted matched substrings. A PalletGrid component is passed to the default slot of this ClickDropdownMenu_A to display the grid of swatches. A Hyperscript is used to store the user’s selection into the pallet’s ‘bgColor’ attribute and also update another hidden <input> so the highlit color can be transported to the backend with each request.

Props

interface Props {
id?:string
placeholder?:string
searchPath:string
target:string
swapStrategy:string,
height?:string
}

id

Each Searchbox component recieves a unique identifier as a prop. This identifier is used by the Hyperscripts to perform certain updates and styling changes.

placeholder

This string is displayed in dimmed font which prompts the user to enter a substring into the <input>.

target

This prop accepts a string value representing the DOM element where the response will be placed. You should pass an htmx-compatible CSS selector here. The actual value you pass will then be referenced by the hx-target attribute. The response html will be swapped into this DOM position according to the next prop which specifies the swap strategy.

swapStrategy

HTMX provides a liberal set of options for how response markup used to update the DOM. In the example project a <div> is used to display the full speech transcript and thus is the natural target for the highlited version of the transcript. The example uses the innerHTML swap strategy.

height

This prop allows you to adjust the height of the control to meet the needs of your layout. Any valid Tailwind height class will work.

There is no width prop. To adjust the width, wrap this hyperComponent in a container and then adjust the container’s width.

Slots

rightIcon

The magnifyingf glass icon to the right of the <input> is actually the default content of the rightIcon slot. Most search controls display a magnifying glass icon as a symbol of the control’s function. In this example, the icon is inert (does not respond to clicks).

You may substitute another icon (or even another Dropdown menu) to replace the default magnifying glass by passing markup into the rightIcon slot. In Astro, the syntax for sending markup into a named slot is <div slot='slotName'><!-- slot content --></div>

Attributes

data-hilight-color

This attribute of the <div id="search-container"> is assigned a default value from a local constant defaultBackgroundColor. The example project uses bg-yellow-200.

The value of this attribute is picked up by the Hyperscript code and used to dynamically change the background color of the entire component when the user picks a swatch from the color pallet dropdown.

In addition, this color is sent to the backend along with the request in order to provide styling for the <mark> tags which bracket the matched substrings in the response html.

In this manner the data-hilight-color is used as a local cache for the Hyperscript.

Styling

For coding convenience all Tailwind classes are extracted into a local tw constant as discussed in the Tailwind Issues page.

Transitions

Smooth scrolling to and from the Search control is provided by Hyperscript. Inspect the script code below to see how this is done.

Code

interface Props {
id?:string // a unique identifier
placeholder?:string //default placeholder text in the <input> field
searchPath:string //api backend route for s
target:string //where the response will be placed
swapStrategy:string, //using this swap strategy
height?:string //optional classes for setting control height
}
const {
id,
placeholder = "Search for...",
searchPath,
target,
swapStrategy,
height = 'h-10'
} = Astro.props

API routes

All routing in Astro is file-based, including backend API routes.

Here is the folder/file structure used for the example mini-project.

  • Directorysrc
    • Directorydata
      • famousSpeeches.json
    • Directorypages
      • Directoryapi
        • Directorysearch
          • [id].js
          • index.js
          • match.js

The Tab panel below is displays the entire source code for each backend route.

famousSpeaches.json

The famousSpeeches.json file contains the raw data for each Speech. There is too much text to show all the data here so just one sample Speech object is included to illustrate the shape of the data. All other Famous Speech entries use the same object structure.

In a real world application, the data will likely be stored in a database. If you use this hyperComponent in your project, you will need to adjust the data source and provide the appropriate async lookup functions in the next three API files.

[id].js

The [id].js route decodes the index of the speech from params.id value of the Context object provided by Astro as an argument to the GET function by Astro. Also the transcript=true field is decoded from the url.searchParams. Using this information, the raw text of an individual speech is pulled from the appropriate array index in the famousSpeeches.json file.

The Response object then returns an html fragment containing the entire speech transcript with <p> tags swapped for line breaks.

index.js

A list of speeches is loaded into a <table> using the index.js route. The raw data is looped using a .map() function returning a single <tr> for each Speech containing the speakers name, title of the speech, and the date.

Notice that a Hyperscript is also added to each <tr> so that a click on the row loads the individual itranscript into a target <div>.

match.js

This route receives search, speech-index, search-criteria, and hilight-color values from the request initiated by the hx-post attribute. Here is the shape of the data received by this route:

request.text()
search=xyz&speech-index=15&search-criteria=Contains&hilight-color=bg-yellow-200

In Astro you get access to the data by calling the .text() function on the request property of the Context object.

This yields a URL encoded string containing & delimited key-value pairs which the code splits apart ‘and stores into local variables.

The search field of the request contains the substring which the user wishes to test against the speech transcript.

The speech-index field contains the array index for the data in famousSpeeches.json

The search-criteria field contains a filter to be applied to the regular expression, such as ‘Whole Word’ or ‘Case Sensitive’, etc. The value of this field is set when the user chooses an option from the dropdown at the left of the search <input>.

The hilight-color is transmitted to the backend from a value cached in the data-hilight-color attribute of the div#search-container and specifies a Tailwind background color used by all the <mark> tags in the final response.

The remaining code in match.js uses a regular expression to match substrings in the speech transcript and then bracket each match with <mark> tags containing the specfied Tailwind bg-color* class.

The resulting html markup is returned to the caller via the Response object where the hx-swap attribute is configured to replace the innerHTML of the speech transcript <div>.

Examine the code in each Tab below to review all the backend functionality used in the mini-example project.

Code

import famousSpeeches from '../../../data/famousSpeeches.json?json'
export const tw = {
p: 'pb-6 text-md lg:text-lg text-gray-800 dark:text-neutral-100 '
}
const speechInfo = (speech) => {
return speech.title + ' by ' + speech.speaker + ' on ' + speech.date
}
export function swapLineEndings(text) {
//add a beginning and ending <p> tag
let markup = `<p class="${tw.p}">` + text + '</p>'
//then replace all \\n with <p> tags with appropriate styling
markup = markup.replace(/\\n/g, `</p><p class="${tw.p}">`);
return markup;
}
//GET
export async function GET({ params, url }) {
const speechID = parseInt(params.id)
const speech = famousSpeeches[speechID]
if (!speech) {
return new Response ('Speech not found')
}
//What is being requested?
const searchParams = url.searchParams;
const transcriptOnly = searchParams.get('transcript') === 'true';
if (transcriptOnly) {
return new Response(swapLineEndings(speech.transcript), {status: 200})
}
else {
return new Response (speechInfo(speech), {status: 200})
}
}

Usage

To visualize how the Searchbox component is used in a real world situation, an mini-project is included in the Examples section above.

There are many use cases for active search and even a highlited active search. In fact, too many use cases to cover here. The bottom line is that most Web sites include a Search control somewhere in their head element identified to the user by a magnifying glass icon.

The capabilities of returning hypertext that is marked up to reveal each matched substring illustrates how hypermedia exchanges work and the advantages of using them. There are many use cases, which you can explore on your own.

For instructional purposes, the source code for implementing the Famous Speeches mini-project is included below in full detail. Notice where and how the Searchbox control integerates with the other DOM elements to create the entire experience.

If you copy this code into your own project, you will have to adjust the import file paths to match your folder structure.

Also notice how explanting and coalescing the Tailwind classes into a local Astro tw constant unclutters the html markup. In this example, the tw constant is an object with nested objects that permit clear and unambigous organization for the styling code which enhances both readability and maintainability.

Mini-Project Code

FamousSpeeches.astro
---
import Searchbox from '/components/searchbox.astro'
import ClickDropdownMenu_A from '/components/ClickDropdownMenu_A.astro'
import PalletGrid from '/components/PalletGrid.astro'
const defaultSpeechTitle = 'Click on the any of the famous orations listed above.'
const defaultSpeechTranscript = 'The full transcript will be displayed here. Use the Search box to dynamnically highlight any part of the transcript'
const tw = {
introduction: {
h3: 'text-center',
h4: 'pt-8 pb-4 text-center',
p: 'text-center font-semibold'
},
table:'mx-auto my-4 max-w-max ',
searchExample:{
container: 'pt-12 flex flex-nowrap justify-between items-center gap-2',
colorPallet: 'colorPallet size-12 rounded-2xl ' +
'bg-sky-700 dark:bg-indigo-600 ',
palletIcon: 'inline-block size-6' ,
resetButton: 'px-4 py-3 cursor-pointer rounded-xl ' +
'text-lg text-zinc-50 dark:text-zinc-300 ' +
'bg-sky-700 dark:bg-indigo-600',
},
speech:{
wrapper:'w-full mx-auto max-h-[50rem] overflow-y-auto rounded-t-lg ' +
'bg-gray-100 dark:bg-gray-950',
title: 'p-4 text-center text-balance text-xl ' +
'text-indigo-900 dark:text-amber-100/80 ' +
'border-b-2 border-gray-200 dark:border-gray-700 ',
transcript: 'p-4'
},
}
const hs = {
resetButton:`
on load or click
put the "${defaultSpeechTitle}" into the #{'speech-title'} then
put the "${defaultSpeechTranscript}" into the #{'speech-transcript'} then
send reset to the <table#all-speeches/>
go to the #{'introduction'} smoothly
end `,
table: `
on reset
remove .${tw.rowHilight} from <tr/>
end `,
colorPallet: `
on click
-- cache the hilight color in a data-* attribute
-- transmit the highlight color to the backend via a hidden <input/>
set originalHilightColor to the @data-hilight-color of the #{'search-container'}
remove .{originalHilightColor} from the #{'search-container'}
set newHilightColor to my @bgColor
add .{newHilightColor} to the #{'search-container'}
set the @data-hilight-color of the #{'search-container'} to the newHilightColor
set the value of the #{'hilight-color'} to the newHilightColor
trigger update on the #{'searchbox'}
end
`
}
---
{/* Introduction */}
<div id="introduction" class="w-10/12 mx-auto">
<h3 class={tw.introduction.h3}>Famous Speeches in the English Language</h3>
<h4 class={tw.introduction.h4}>Use the Search control below to highlight words from the most famous speeches that shaped our world.</h4>
<p class={tw.introduction.p}>Click to select any Speech from the list</p>
</div>
{/* Table of Famous Speeches */}
<table class={tw.table}
id="all-speeches"
hx-trigger="load"
hx-get="/api/speeches/"
hx-target="<tbody/>"
hx-swap="innerHTML"
script={hs.table}>
<thead>
<tr><th>Speaker</th><th>Title</th><th>Date</th></tr>
</thead>
<tbody></tbody>
</table>
{/* Search Box Example*/}
<div class={tw.searchExample.container}>
<Searchbox id="searchbox"
searchPath="/api/speeches/match/"
target="#speech-transcript"
swapStrategy="innerHTML"
height="h-12"
/>
<!-- Color Picker -->
<ClickDropdownMenu_A id='colorpallet'
moveXY='-200,18'
buttonStyles={tw.searchExample.colorPallet}
duration='180ms'>
<img slot="leftIcon" src='/images/tailwind_icon.svg'
alt='tailwind icon' class={tw.searchExample.palletIcon}/>
<PalletGrid orientation='vertical' hs={hs.colorPallet}/>
</ClickDropdownMenu_A>
<!-- Reset Button -->
<button class={tw.searchExample.resetButton}
script={hs.resetButton}>Reset</button>
</div>
{/* Our Speech */}
<div class={tw.speech.wrapper}>
<h4 id="speech-title" class={tw.speech.title}></h4>
<div id="speech-transcript" class={tw.speech.transcript}>
</div>
</div>