{distill} Contact Form
Purpose
Can we use use RStudio Connect to build an internal contact form on a {distill} website?
{distill} is a great R library for building static websites and blogs but has the same limitations of other static websites, namely no server-side programming. This means that implementing a contact form requires using a third party service. This can, however, be accomplished when hosting via RStudio Connect using a plumber API.
Attempt 1
This first attempt works but since we are using a POST request a response is always returned. This means that the webpage is updated with either a null or whatever has been returned by the API function. The output consists of three files with a {distill} R website:
- index.Rmd - a markdown file to hold the contact form
- contact_form.html - an html contact form that can be inserted into a markdown page
- style.css - some css styling (pulled from w3schools)
and a plumber API:
- plumber.R - a plumber API
Both the {distill} static site and plumber API are published to the same RStudio Connect instance.
plumber.R
The plumber API takes parameters from a contact form and constructs a linux mail command line, sending a message to a mailbox. The API contains a single POST request to /email.
1library(plumber)
2
3#* @apiTitle Email
4
5#* Send out an email message
6#* @param email return email address
7#* @param name sender name
8#* @param subject email subject
9#* @param message email message
10#* @param to recipient email address
11#* @post /email
12function(email = NULL, name = NULL, subject = NULL, message = NULL, to = NULL) {
13
14 ## need a recipient to send an email
15 if (!is.null(to)) {
16
17 if (is.null(message)) {
18 message <- ""
19 } else {
20 message <- gsub("\\r", "", message)
21 }
22
23 ## add sender's name
24 if (!is.null(name)) {
25 message <- paste0(message, "\n\nFrom: ", name)
26 }
27
28 ## build up email string
29 email_string <- "echo -e "
30 email_string <- paste0(email_string, "\"", message, "\" | mail ")
31
32 if (!is.null(subject)) {
33 email_string <- paste0(email_string, "-s \"", subject, "\" ")
34 } else {
35 email_string <- paste0(email_string, "-s \"no subject\" ")
36 }
37
38 if (!is.null(email)) {
39 email_string <- paste0(email_string, "-S from=", email, " ")
40 }
41
42 email_string <- paste0(email_string, to)
43
44 system(email_string)
45 return(email_string)
46 }
47}
index.Rmd
1---
2title: "Test Contact 1"
3---
4
5```{r, echo=FALSE}
6htmltools::includeCSS("style.css")
7
8htmltools::includeHTML("contact_form.html")
9```
contact_form.html
1<div class="form-container">
2 <form action="*url pointing to API*" id="my-form" method="POST">
3
4 <label for="name">Name</label>
5 <input type="text" id="name" name="name" placeholder="Your name..">
6
7 <label for="email">Email</label>
8 <input type="text" id="email" name="email" placeholder="Your email address..">
9
10 <label for="subject">Subject</label>
11 <input type="text" id="subject" name="subject" placeholder="Subject">
12
13 <label for="message">Message</label>
14 <textarea id="message" name="message" placeholder="Your message.." style="height:200px"></textarea>
15
16 <input type="hidden" name="to" value="*mailbox*" />
17
18 <input type="submit" value="Submit">
19
20 </form>
21</div>
In the contact_form.html file, url pointing to API (line 2) refers to the url of the plumber API, hosted on the same RStudio Connect server as the distill site. mailbox (line 16) refers to the receiving mailbox. It is included as a hidden element in the form so that it may be passed to the API.
style.css
1input[type=text], select, textarea {
2 width: 100%; /* Full width */
3 padding: 12px; /* Some padding */
4 border: 1px solid #ccc; /* Gray border */
5 border-radius: 4px; /* Rounded borders */
6 box-sizing: border-box; /* Make sure that padding and width stays in place */
7 margin-top: 6px; /* Add a top margin */
8 margin-bottom: 16px; /* Bottom margin */
9 resize: vertical /* Allow the user to vertically resize the textarea (not horizontally) */
10}
11
12/* Style the submit button with a specific background color etc */
13input[type=submit] {
14 background-color: #04AA6D;
15 color: white;
16 padding: 12px 20px;
17 border: none;
18 border-radius: 4px;
19 cursor: pointer;
20}
21
22/* When moving the mouse over the submit button, add a darker green color */
23input[type=submit]:hover {
24 background-color: #45a049;
25}
26
27/* Add a background color and some padding around the form */
28.form-container {
29 border-radius: 5px;
30 background-color: #f2f2f2;
31 padding: 20px;
32}
Attempt 2
The second attempt builds on the first. Here the submit button is intercepted by a little javascript function which executes the POST request and captures the output. Running this way means that the webpage does not update once the request is run. In addition, we can trigger notification that the form was sent (in this case a simple alert).
Here we have four files with a {distill} R website:
- index.Rmd - a markdown file to hold the contact form
- contact_form.html - an html contact form that can be inserted into a markdown page
- style.css - some css styling (pulled from w3schools)
- script.js - javascript function to intercept the submit button press
and a plumber API:
- plumber.R - a plumber API
Both the {distill} static site and plumber API are published to the same RStudio Connect instance.
plumber.R
The plumber API differs from the one in Attempt 1 by reading a json encoded version of the body. The API contains a single POST request to /email.
1library(plumber)
2library(jsonlite)
3
4#* @apiTitle Email
5
6#* Send out an email message
7#* @param req request body
8#* @post /email
9function(req) {
10
11 ## get the message body
12 body <- jsonlite::fromJSON(req$postBody)
13
14 email <- body$email
15 name <- body$name
16 subject <- body$subject
17 to <- body$to
18 message <- body$message
19
20 ## need a recipient to send an email
21 if (!is.null(to)) {
22
23 if (is.null(message)) {
24 message <- ""
25 } else {
26 message <- gsub("\\r", "", message)
27 }
28
29 ## add sender's name
30 if (!is.null(name)) {
31 message <- paste0(message, "\n\nFrom: ", name)
32 }
33
34 ## build up email string
35 email_string <- "echo -e "
36 email_string <- paste0(email_string, "\"", message, "\" | mail ")
37
38 if (!is.null(subject)) {
39 email_string <- paste0(email_string, "-s \"", subject, "\" ")
40 } else {
41 email_string <- paste0(email_string, "-s \"no subject\" ")
42 }
43
44 if (!is.null(email)) {
45 email_string <- paste0(email_string, "-S from=", email, " ")
46 }
47
48 email_string <- paste0(email_string, to)
49
50 system(email_string)
51 return(email_string)
52 }
53}
index.Rmd
The markdown file is very similar to the original, with an additional line to include the javascript file.
1---
2title: "Test Contact 1"
3---
4
5```{r, echo=FALSE}
6htmltools::includeCSS("style.css")
7htmltools::includeScript("script.js")
8
9htmltools::includeHTML("contact_form.html")
10```
contact_form.html
The contact form does not change significantly from the original, the only difference being the removal of method="POST" in the form element.
1<div class="form-container">
2 <form action="*url pointing to API*" id="my-form">
3
4 <label for="name">Name</label>
5 <input type="text" id="name" name="name" placeholder="Your name..">
6
7 <label for="email">Email</label>
8 <input type="text" id="email" name="email" placeholder="Your email address..">
9
10 <label for="subject">Subject</label>
11 <input type="text" id="subject" name="subject" placeholder="Subject">
12
13 <label for="message">Message</label>
14 <textarea id="message" name="message" placeholder="Your message.." style="height:200px"></textarea>
15
16 <input type="hidden" name="to" value="*mailbox*" />
17
18 <input type="submit" value="Submit">
19
20 </form>
21</div>
In the contact_form.html file, url pointing to API (line 2) refers to the url of the plumber API, hosted on the same RStudio Connect server as the distill site. mailbox (line 16) refers to the receiving mailbox. It is included as a hidden element in the form so that it may be passed to the API.
style.css
No change to the style.css file.
script.js
1window.addEventListener("load", function() {
2
3 document.getElementById("my-form-2").addEventListener("submit", formsubmit);
4
5 async function formsubmit(e) {
6
7 e.preventDefault();
8
9 // get event-handler element
10 const form = e.currentTarget;
11
12 // get form url
13 const url = form.action;
14
15 // get form data as json string
16 formdata = new FormData(form);
17 const plainFormData = Object.fromEntries(formdata.entries());
18 const jsonFormData = JSON.stringify(plainFormData);
19
20 // send request and capture output
21 out = await fetch(form.action, {
22 method: 'POST',
23 body: jsonFormData,
24 headers: {
25 "Content-Type": "application/json",
26 "Accept": "application/json"
27 }
28 });
29
30 // notification of sent message
31 alert("message sent to " + plainFormData.to);
32
33 }
34
35});
in this case we have a contact form which sends a message to an email inbox. A simple alert
confirms that the form has been intercepted and an email sent. The contact form looks as follows: