/**
* Validates an email address.
* @param {string} email - The email address to validate.
* @returns {string} A message indicating whether the email address is valid or not.
*/
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email.length === 0) {
return '<div class="alert alert-warning interactive-message">Email is required</div>';
}
if (!emailRegex.test(email)) {
return `<div class="alert alert-warning interactive-message">Invalid email address: ${email}</div>`;
}
return `<div class="alert alert-success interactive-message">The email ${email} is valid</div>`;
}
Adding interactivity
Quarto is a static site generator, but that doesn’t mean we can’t add interactivity to our website using JavaScript.
Include Vanilla JavaScript
We can include JavaScript code in our Quarto document similarly to how we include HTML. We can write the code inside a <script>
tag, and it will execute when the page loads. Let’s test this with a simple example—a button that changes the color of a circle when clicked. We can include code like the following directly in the Quarto document body:
<!-- div to display the circle -->
<div id="circle" style="width: 100px; height: 100px; background-color: red; border-radius: 50%;"></div>
<!-- button to trigger the change of the color -->
<button onclick="changeCircleColor()">Change Circle Color</button>
<!-- function to get the circle element, choose a random element from the colors array, and change the background color of the circle html element -->
<script>function changeCircleColor() {
const circle = document.getElementById("circle");
const colors = ["red", "blue", "green", "yellow", "purple"];
const randomIndex = Math.floor(Math.random() * colors.length);
.style.backgroundColor = colors[randomIndex];
circle
} </script>
The code above works as follows:
One downside of this approach is that the code is not visible in the document—it’s embedded directly in the Quarto document. However, this is still a good approach for adding interactivity when we don’t need external libraries or want to display the code.
Another approach is to use Observable JavaScript, which allows us to use JavaScript in a more structured way while still displaying the code in the document.
Observable JavaScript
Quarto supports using JavaScript with Observable JS. This approach relies almost entirely on vanilla JavaScript while allowing us to display the code in the document. Additionally, Observable JS lets you use well-known libraries like D3.js or NPM packages served from the jsDelivr CDN.
A useful feature we can create with OJS is a simple form to validate user input. For example, let’s create a function to validate email addresses.
We can call the validateEmail
function from another cell and create an input form for users to enter an email address:
= Inputs.text({
viewof emailInput label: "Enter your email",
placeholder: "name@example.com",
value: "",
attributes: {
class: "form-control mb-3"
} })
To validate the email from the emailInput
view, we can call our validateEmail
function:
= '<div class="alert alert-info interactive-message">Validation message will be displayed here</div>';
placeholder
= Inputs.button(html`<i class="bi bi-chevron-double-right"></i>`,
viewof validationButton
{label: "Validate Email",
value: placeholder,
reduce: () => validateEmail(emailInput)
;
})
= html`${validationButton}`; validationButtonDisplay
Taking Advantage of Observable JS’s Reactive Nature
In the previous example, we used a button click to trigger the validation. However, one of the key features of Observable JS is its reactivity—it automatically updates when dependencies change. For example, we can create a form that retrieves information from a public API by changing the domain, path, and parameters.
= Inputs.select(["GET"], {
viewof methodDetails label: "HTTP Method",
attributes: {
class: "form-select mb-3"
}
})
= Inputs.text({
viewof domainDetails label: "Domain",
placeholder: "collectionapi.metmuseum.org",
value: "collectionapi.metmuseum.org",
attributes: {
class: "form-control mb-3"
}
})
= Inputs.text({
viewof pathDetails label: "Path",
placeholder: "/public/collection/v1/objects/",
value: "/public/collection/v1/objects/",
attributes: {
class: "form-control mb-3"
}
})
= Inputs.text({
viewof parameterDetails label: "Parameter",
placeholder: "Write the object id here",
value: "570744",
attributes: {
class: "form-control mb-3"
}
})
async function fetchFromApiDetails(method, domain, path, parameter) {
try {
const baseUrl = `https://${domain}`;
const url = `${baseUrl}${path}${parameter}`;
const response = await fetch(url);
const status = {
code: response.status,
ok: response.ok,
text: response.statusText
;
}if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}const data = await response.json();
return { data, status };
catch (error) {
} return { error: error.message };
}
}
= {
responseDetails const result = await fetchFromApiDetails(methodDetails, domainDetails, pathDetails, parameterDetails);
return result;
}
= {
viewof prettyResponseDetailsContainer let content;
if (responseDetails.data.Message) {
= html`<div class="alert alert-warning m-0">${responseDetails.data.Message}</div>`;
content else {
} = html`<pre class="card-body m-0" style="background-color: #f8f9fa; max-height: 400px; overflow-y: auto;">${JSON.stringify(responseDetails.data, null, 2)}</pre>`;
content
}
const badgeClass = responseDetails.status.ok ? "bg-success" : "bg-danger";
const container = html`<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Response</span>
<span class="badge ${badgeClass}">${responseDetails.status.code} ${responseDetails.status.text}</span>
</div>
${content}
</div>`;
return container;
}
And that allows us to retrieve, for instance, the image of the object, by changing the key of the image in the response.
= Inputs.text({
viewof imagekey label: "Image Key",
placeholder: "primaryImageSmall",
value: "primaryImageSmall",
attributes: {
class: "form-control mb-3"
}
})
= {
viewof primaryImage const primaryImage = responseDetails.status.ok ? responseDetails.data[imagekey] : "https://placehold.co/600x400";
if (primaryImage) {
const img = html`<img src="${primaryImage}" alt="Primary Image">`;
return img;
else {
} return html`<img src="https://placehold.co/600x400" alt="Placeholder">`;
}
}
In Conclusion
Adding interactivity to a Quarto document opens new possibilities for creating engaging, dynamic content. While Quarto is primarily a static site generator, we can enhance user experience by incorporating custom JavaScript or leveraging Observable JS for reactive components. Whether you’re building simple input forms or integrating data from external APIs, these techniques allow you to create rich, interactive experiences while maintaining transparency by showing the underlying code.
Experiment with these features and take full advantage of Quarto’s flexibility to bring your documents to life!