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.
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
| Speaker | Title | Date |
|---|
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:
- A
searchPathprop which accepts a path to a backend search route and is used to configure thehx-postattribute of the<input>element - A
targetprop which accepts a CSS selector that is used to update thehx-targetattribute. Thetargetprop identifies the DOM element which will updated with the returned hypertext. - A
swapStrategyprop which is used to update thehx-swapattribute thereby determining how htmx will update the target element. - 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. - Hyperscript code updates the hidden fields with values needed for the backend.
- Search requests are initiated by multiple events, including keystrokes, pasting text into the
<input>, clearing the field, etc. - A dropdown menu from which the user can select a filter for the matching algorithm (case sensitive, whole word, or others),
- Scroll smoothing back and forth between the target and the search input
- Search criteria in the options dropdown can be customized.
- A Tooltip is displayed on hover over the search-options menu indicating the current selection for search filter.
- Stylings of the
<input>and dropdown menus are fully customizable using Tailwind utility classe.s - 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 <div id='search-container' class={tw.container} data-hilight-color=`${defaultBackgroundColors}`> {/* Search Options menu */} <ClickDropdownMenuA id="search-options-menu" buttonStyles={tw.optionsMenu} moveXY="-12,10" > {/* Down Caret icon */} <svg slot="rightIcon" class={tw.icon} fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/> </svg> {/* Search Criteria options */} <li class={tw.menuItem} script={hs.menuItem}>Contains</li> <li class={tw.menuItem} script={hs.menuItem}>Whole Word</li> <li class={tw.menuItem} script={hs.menuItem}>Case Sensitive</li> <li class={tw.menuItem} script={hs.menuItem}>Case Insensitive</li> <li class={tw.menuItem} script={hs.menuItem}>Begins With</li> <li class={tw.menuItem} script={hs.menuItem}>Ends With</li> </ClickDropdownMenuA> {/* Hidden inputs ... values are transmitted to backend via hx-include Do Not Reorder These <input/> b/c the backend is referencing the values from an element array by index */} <input type="hidden" id="speech-index" name="speech-index" value=""/> <input type="hidden" id="search-options" name="search-options" value="Contains"/> <input type="hidden" id="hilight-color" name="hilight-color" value=`${defaultBackgroundColors}`/> {/* Search String Input field */} <input id=`${id}` name="search" type="search" class={tw.input} placeholder={placeholder} hx-post=`${searchPath}` hx-include="<input/>" hx-trigger="input changed delay:500ms, search, update" hx-target=`${target}` hx-swap=`${swapStrategy}` script={hs.searchInput} > </input> {/* Magnify Icon */} <slot name="rightIcon" > <svg fill="currentColor" class={tw.rightIcon} version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 490.4 490.4" xml:space="preserve"> <g id="SVGRepo_bgCarrier" stroke-width="0"> </g> <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"> </g> <g id="SVGRepo_iconCarrier"> <g> <path d="M484.1,454.796l-110.5-110.6c29.8-36.3,47.6-82.8,47.6-133.4c0-116.3-94.3-210.6-210.6-210.6S0,94.496,0,210.796 s94.3,210.6,210.6,210.6c50.8,0,97.4-18,133.8-48l110.5,110.5c12.9,11.8,25,4.2,29.2,0C492.5,475.596,492.5,463.096,484.1,454.796z M41.1,210.796c0-93.6,75.9-169.5,169.5-169.5s169.6,75.9,169.6,169.5s-75.9,169.5-169.5,169.5S41.1,304.396,41.1,210.796z"> </path> </g> </g> </svg> </slot></div>const hs = { menuItem: ` on click send closeDropdown to the #{'search-options-menu'} then settle then set the @title of the #{'search-options-menu'} to my textContent set the value of the #{'search-options'} to my textContent trigger update on the <[name='search']/> end `, searchInput: ` on reset set my value to '' set the value of the #{'search-options'} to 'Contains' set the @title of the #{'search-options-menu'} to 'Contains' set the value of the #{'speech-index'} to '' call me.focus() end`}const tw = {container: `flex-grow flex flex-nowrap items-center overflow-hidden justify-around rounded-2xl ` + defaultBackgroundColors,input: `${height} px-4 w-full font-inherit focus:outline-none ` + 'font-bold text-lg ' + 'text-gray-900 dark:text-zinc-200 ' + 'bg-slate-200 dark:bg-slate-800 '+ 'placeholder-blue-800/30 dark:placeholder-cyan-600/50',icon:'inline-block size-6',rightIcon:'w-12 h-12 px-2 ' + defaultTextColors,optionsMenu: 'p-0 mx-2 mt-1 cursor-pointer focus:ring-0 ' + 'font-extrabold ' + defaultTextColors + ' ' + 'bg-transparent hover:bg-transparent ' + 'hover:text-slate-950 dark:hover:text-yellow-600',menuItem: 'px-4 hover:text-purple-600 hover:dark:text-sky-800 ' +'hover:bg-gray-200 hover:dark:bg-stone-300 ',}---import ClickDropdownMenuA from '../ClickDropdownMenu_A.astro'
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}
{/* Common default styling for several elements */}export const defaultTextColors = 'text-slate-600'export const defaultBackgroundColors = 'bg-yellow-200'
const { id, placeholder = "Search for...", searchPath, target, swapStrategy, height = 'h-10'} = Astro.props
const tw = { container: `flex-grow flex flex-nowrap items-center overflow-hidden justify-around rounded-2xl ` + defaultBackgroundColors, input: `${height} px-4 w-full font-inherit focus:outline-none ` + 'font-bold text-lg ' + 'text-gray-900 dark:text-zinc-200 ' + 'bg-slate-200 dark:bg-slate-800 '+ 'placeholder-blue-800/30 dark:placeholder-cyan-600/50', icon:'inline-block size-6', rightIcon:'w-12 h-12 px-2 ' + defaultTextColors, optionsMenu: 'p-0 mx-2 mt-1 cursor-pointer focus:ring-0 ' + 'font-extrabold ' + defaultTextColors + " " + 'bg-transparent hover:bg-transparent ' + 'hover:text-slate-950 dark:hover:text-yellow-600', menuItem: 'px-4 hover:text-purple-600 hover:dark:text-sky-800 ' + 'hover:bg-gray-200 hover:dark:bg-stone-300 ',}
const hs = { menuItem: ` on click send closeDropdown to the #{'search-options-menu'} then settle then set the @title of the #{'search-options-menu'} to my textContent set the value of the #{'search-options'} to my textContent trigger update on the <[name='search']/> end `, searchInput: ` on reset set my value to '' set the value of the #{'search-options'} to 'Contains' set the @title of the #{'search-options-menu'} to 'Contains' set the value of the #{'speech-index'} to '' call me.focus() end`}
---
<div id='search-container' class={tw.container} data-hilight-color=`${defaultBackgroundColors}`> {/* Search Options menu */} <ClickDropdownMenuA id="search-options-menu" buttonStyles={tw.optionsMenu} moveXY="-12,10" > {/* Down Caret icon */} <svg slot="rightIcon" class={tw.icon} fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/> </svg> {/* Search Criteria options */} <li class={tw.menuItem} script={hs.menuItem}>Contains</li> <li class={tw.menuItem} script={hs.menuItem}>Whole Word</li> <li class={tw.menuItem} script={hs.menuItem}>Case Sensitive</li> <li class={tw.menuItem} script={hs.menuItem}>Case Insensitive</li> <li class={tw.menuItem} script={hs.menuItem}>Begins With</li> <li class={tw.menuItem} script={hs.menuItem}>Ends With</li> </ClickDropdownMenuA> {/* Hidden inputs ... values are transmitted to backend via hx-include Do Not Reorder These <input/> b/c the backend is referencing the values from an element array by index>*/} <input type="hidden" id="speech-index" name="speech-index" value=""/> <input type="hidden" id="search-options" name="search-options" value="Contains"/> <input type="hidden" id="hilight-color" name="hilight-color" value=`${defaultBackgroundColors}`/> {/* Search String Input field */} <input id=`${id}` name="search" type="search" class={tw.input} placeholder={placeholder} hx-post=`${searchPath}` hx-include="<input/>" hx-trigger="input changed delay:500ms, search, update" hx-target=`${target}` hx-swap=`${swapStrategy}` script={hs.searchInput} > </input> {/* Magnify Icon */} <slot name="rightIcon" > <svg fill="currentColor" class={tw.rightIcon} version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 490.4 490.4" xml:space="preserve"> <g id="SVGRepo_bgCarrier" stroke-width="0"> </g> <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"> </g> <g id="SVGRepo_iconCarrier"> <g> <path d="M484.1,454.796l-110.5-110.6c29.8-36.3,47.6-82.8,47.6-133.4c0-116.3-94.3-210.6-210.6-210.6S0,94.496,0,210.796 s94.3,210.6,210.6,210.6c50.8,0,97.4-18,133.8-48l110.5,110.5c12.9,11.8,25,4.2,29.2,0C492.5,475.596,492.5,463.096,484.1,454.796z M41.1,210.796c0-93.6,75.9-169.5,169.5-169.5s169.6,75.9,169.6,169.5s-75.9,169.5-169.5,169.5S41.1,304.396,41.1,210.796z"> </path> </g> </g> </svg> </slot></div>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:
search=xyz&speech-index=15&search-criteria=Contains&hilight-color=bg-yellow-200In 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;}
//GETexport 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}) }}import { twMerge } from 'tailwind-merge';import famousSpeeches from '../../../data/famousSpeeches.json?json'
export const tw = {tr: 'cursor-pointer font-semibold ' + 'even:hover:bg-amber-300/50 odd:hover:bg-amber-400/30 ' + 'even:hover:text-blue-800 odd:hover:text-blue-800 ' + 'even:hover:dark:bg-indigo-950/60 odd:hover:dark:bg-indigo-950 ' + 'even:hover:dark:text-amber-400 odd:hover:dark:text-amber-400',rowHilight: 'text-orange-500'}
const getSpeechData = () => {
const markup = famousSpeeches.map((speech)=>{ return ` <tr index="${speech.id}" class="${tw.tr}" script=" on click fetch /api/speeches/${speech.id} as text then put it into the #{'speech-title'} fetch /api/speeches/${speech.id}?transcript=true as html then put it into the #{'speech-transcript'} set the value of the #{'speech-index'} to the ${speech.id} remove .${tw.rowHilight} from <tr/> add .${tw.rowHilight} to me go to the #{'searchbox'} smoothly then settle then call #{'searchbox'}.focus() end " > <td>${speech.speaker}</td> <td>${speech.title}</td> <td>${speech.date}</td></tr>`})
return markup.join('\n')
}
export const GET = async ({params, request}) => {return new Response (getSpeechData(), {status: 200})
}import famousSpeeches from '../../../data/famousSpeeches.json?json'import {swapLineEndings, tw as baseStyles} from './[id].js'import {defaultBackgroundColors} from '../../../components/search/searchbox.astro'
let hilightColor = defaultBackgroundColors
const tw = { ...baseStyles,}
function markupSpeech(speech, searchString, searchCriteria) { if (searchString === '') { return swapLineEndings(speech.transcript) }
let regex = '\\b' + searchString + '\\w*\\b' let modifiers = 'g'
switch (searchCriteria) { case 'case-sensitive' : modifiers = 'g' break case 'case-insensitive' : modifiers = 'gi' break case 'whole-word' : regex = '\\b' + searchString + '\\b' break case 'begins-with' : regex = '\\b' + searchString + '\\w*\\b' break case 'ends-with' : regex = '\\b\\w*' + searchString + '\\b' break case 'contains': regex = searchString break }
/* Use regex to find matches which are then wrapped with <mark> tags including a Tailwind class for the hilight color */ const searchRegex = new RegExp(regex, modifiers) let markup = speech.transcript markup = markup.replace(searchRegex, `<mark class="${hilightColor}">$&</mark>`); markup = swapLineEndings(markup) return markup}
export const POST = async ({request}) => { // Extract data from the request.text() //data = data search=xyz&speech-index=15&search-criteria=Contains&hilight-color=bg-yellow-200 const data = await request.text() // console.log(`data`, data) const searchString = decodeURIComponent(data.split('&')[0].split('=')[1]) const speechId = parseInt(data.split('&')[1].split('=')[1]) const searchCriteria = decodeURIComponent(data.split('&')[2].split('=')[1]) .toLowerCase() .replace(' ', '-') hilightColor = data.split('&')[3].split('=')[1] // console.log(`searchString`, searchString) // console.log(`searchCriteria`, searchCriteria) // console.log(`hilightColor`, hilightColor)
//now get the transcript const speech = famousSpeeches[speechId] if (!speech) { return new Response ('Speech not found') }
return new Response(markupSpeech(speech, searchString, searchCriteria), {status: 200});}[ ...{ "id": 14, "speaker": "Lou Gehrig", "title": "Luckiest Man", "date": "July 4, 1939", "transcript":"Fans, for the past two weeks you have been reading about a bad break I got.\\nYet today I consider myself the luckiest man on the face of the earth.\\nI have been in ballparks for seventeen years and have never received anything but kindness and encouragement from you fans. Look at these grand men. Which of you wouldn’t consider it the highlight of his career just to associate with them for even one day?\\nSure I’m lucky.\\nWho wouldn’t consider it an honor to have known Jacob Ruppert? Also, the builder of baseball’s greatest empire, Ed Barrow? To have spent six years with that wonderful little fellow, Miller Huggins? Then to have spent the next nine years with that outstanding leader, that smart student of psychology, the best manager in baseball today, Joe McCarthy?\\nSure I’m lucky.\\nWhen the New York Giants, a team you would give your right arm to beat, and vice versa, sends you a gift - that’s something. When everybody down to the groundskeepers and those boys in white coats remember you with trophies -- that’s something.\\nWhen you have a wonderful mother-in-law who takes sides with you in squabbles with her own daughter -- that’s something.\\nWhen you have a father and a mother who work all their lives so you can have an education and build your body -- it’s a blessing.\\nWhen you have a wife who has been a tower of strength and shown more courage than you dreamed existed -- that’s the finest I know.\\nSo, I close in saying that I might have been given a bad break, but I've got an awful lot to live for." },]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
---
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>