Tutorial: Creating custom form input experiences in Brightspot
Occasionally, there is a need to provide users with a form input that does not save anything to the database upon submission. Instead, submitting the form should trigger some other action or side effect. Although you could create such a form from scratch, it is often much easier to leverage Brightspot's native form rendering system.
In this tutorial, you will use Brightspot's form rendering capabilities outside of a standard content edit page. As a concrete example, you will build a form that allows users to send an email with a link to edit the content they are viewing.
Despite not saving data to the database, this custom form will still benefit from Brightspot's built-in form rendering, validation, and error handling mechanisms. By following this tutorial, you' will learn how to create a fully functional custom form input experience that provides a seamless and user-friendly interface for your Brightspot application.
The example form you will build will allow users to input the following fields:
- From Email
- To Email(s)
- Subject
- Message Body
Upon submission, the form will send an email with the provided details, including a link to edit the current content object.
Before diving into creating the custom form experience, you need to model the form itself. Since you will be leveraging Brightspot's native form rendering system, you can take advantage of Brightspot's data modeling techniques and features. This gives us access to everything Brightspot's data models and form UI have to offer, from custom validation to dynamic placeholders and notes.
The ShareEditLinkForm
class below defines the form you want users to fill out:
class ShareEditLinkForm extends Record {
@Required
@DynamicPlaceholderMethod("getDefaultFromEmail")
private String fromEmail;
private Set<String> to;
private String subject;
@Note("Use {{editLink}} to include the edit link in the message.")
private String message;
public String getFromEmail() {
return Optional.ofNullable(fromEmail).orElseGet(this::getDefaultFromEmail);
}
public Set<String> getTo() {
if (to == null) {
to = new HashSet<>();
}
return to;
}
//other getters and setters omitted
private String getDefaultFromEmail() {
return WebRequest.getCurrent().as(ToolRequest.class).getCurrentUser().getEmail();
}
}
Note that this class extends Record
even though you will never save it to the database. This is required to allow you to use Brightspot's form rendering capabilities. The class then defines fields for the from email, to email(s), subject, and message body. The fromEmail
field is marked as @Required
and uses the @DynamicPlaceholderMethod
annotation to provide a default value (the current user's email address) if no value is provided.
Next, you need to create the ToolPage that will render the custom form input experience. This is done in the ShareEditLinkToolPage
class:
@WebPath("/share-edit")
public class ShareEditLinkToolPage extends ToolPage {
@WebParameter
private UUID contentId;
@WebParameter
private UUID id;
public void setContentId(UUID contentId) {
this.contentId = contentId;
}
...
}
-
Defines the CMS path where this ToolPage will serve requests from.
-
The
contentId
field allows us to pass in the ID of the content that the content edit link should point to. Using@WebParameter
allows Brightspot to automatically bind the request parameter to the field. It also allows us to create URLs in type-safe manner viaUrlBuilder
which is shown later on in the tutorial. -
The
id
field is used internally in this class to maintain a consistentState#id
for theShareEditLinkForm
object.
The onGet
method is responsible for rendering the initial form:
@Override
protected void onGet() throws Exception {
ShareEditLinkForm form = new ShareEditLinkForm();
if (id != null) {
State.getInstance(form).setId(id);
}
writePageResponse(() -> createForm(form));
}
It creates a new instance of the ShareEditLinkForm
class and sets the ID of the current content object if it's available. It then calls the createForm
method to render the form HTML. Note that the createForm
method is passed as a lambda expression to the helper method writePageResponse
(also shown below), which wraps the main content of the pop-up in some standard HTML and includes error handling.
private Collection<FlowContent> createForm(ShareEditLinkForm form) throws Exception {
return Collections.singleton(FORM
.method(FormMethod.POST)
.action(new UrlBuilder(this.getClass()).build())
.className("standardForm")
.with(
INPUT.typeHidden().name("id").value(form.getId().toString()),
INPUT.typeHidden().name("typeId").value(form.getState().getTypeId().toString()),
capture(page, p -> p.writeFormFields(form)),
DIV.className("actions")
.with(
INPUT.typeSubmit()
.className("button")
.value("Submit"))
)
);
}
private void writePageResponse(Callable<Collection<FlowContent>> getWidgetMainContent) {
response.toBody().write(DIV.className("widget").with(div -> {
div.add(H1.with("Share Edit Link"));
try {
div.addAll(getWidgetMainContent.call());
} catch (Exception e) {
FormRequest formRequest = WebRequest.getCurrent().as(FormRequest.class);
formRequest.getErrors().add(e);
div.add(formRequest.getErrorMessages());
}
}));
}
The createForm
method creates an HTML <form>
element with the appropriate attributes, including hidden inputs for the ID and type ID. It then uses ToolPageContext#writeFormFields
to render the individual form fields based on the ShareEditLinkForm
class, and adds a submit button.
The onPost
method is responsible for processing form submissions:
@Override
protected void onPost() throws Exception {
ShareEditLinkForm form = new ShareEditLinkForm();
if (id != null) {
State.getInstance(form).setId(id);
}
page.updateUsingParameters(form);
if (form.getState().validate()) {
//Send share edit link email
MailProvider mailProvider = MailProvider.Static.getDefault();
for (String to : form.getTo()) {
mailProvider.send(new MailMessage()
.to(to)
.from(form.getFromEmail())
.subject(form.getSubject())
.bodyPlain(form.getMessage().replace("{{editLink}}", editUrl)));
}
writePageResponse(() -> Arrays.asList(
DIV.with(
DIV.className("message message-success").with("Email sent!"),
BR,
DIV.className("actions")
.with(INPUT.typeSubmit()
.className("button")
.attr("onClick", "window.location.reload();")
.value("Done")))));
} else {
writePageResponse(() -> createForm(form));
}
}
-
This ensures the form object has the correct
State#id
based on the request parameters. Without this Line # 7 will not function correctly. -
Populates the form object with form submission data.
-
In this case, the "Done" button refreshes the page. You could choose other methods of closing the pop-up window if desired.
-
The form does not pass validation, so we write the form again which will include the error messages
This method creates a new instance of the ShareEditLinkForm
class and populates it with the request parameters using ToolPageContext#updateUsingParameters
. If the form is valid, it sends an email using the MailProvider
with the data from the form (to email(s), from email, subject, and message body). If the form is invalid, it re-renders the form, which will include any error messages.
Lastly, now that you have the ShareEditLinkForm
and ToolPage
defined, you need to create a way for users to access the custom form input experience. In this case, you are going to leverage the ContentEditAction
API to place a link in the Content Tools dropdown on the Content Edit page. When clicked, this link will open the target URL in a pop-up window in the CMS.
Note that using ContentEditAction
for this step is just one option of many. The link could just as well have been included in a Widget or in a Dynamic Note on a field. How you give users access to your form is based on your unique feature requirements.
public class ShareEditLink implements ContentEditAction {
@Override
public void writeHtml(ToolPageContext page, Object content) throws IOException {
page.write(LI
.with(
A.href(new UrlBuilder(
ShareEditLinkToolPage.class,
p -> p.setContentId(State.getInstance(content).getId())).build())
.target("request")
.with("Share Edit Link")
)
);
}
}
-
Uses Dari HTML and
UrlBuilder
to create an HTML link to theShareEditLinkToolPage
-
The
request
link target tells Brightspot to load the URL in a pop-up window.
Conclusion
With all the steps completed, you now have a fully functional custom form input experience that allows users to share an edit link for the current content object via email. By leveraging Brightspot's native form rendering system, you have been able to create a user-friendly interface with built-in validation and error handling, while still maintaining the flexibility to perform custom actions upon form submission.
Here's the full code for the tutorial:
package brightspot.docs;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import com.psddev.cms.ui.ToolRequest;
import com.psddev.cms.ui.form.DynamicPlaceholderMethod;
import com.psddev.cms.ui.form.Note;
import com.psddev.dari.db.Record;
import com.psddev.dari.web.WebRequest;
class ShareEditLinkForm extends Record {
@Required
@DynamicPlaceholderMethod("getDefaultFromEmail")
private String fromEmail;
private Set<String> to;
private String subject;
@Note("Use {{editLink}} to include the edit link in the message.")
private String message;
public String getFromEmail() {
return Optional.ofNullable(fromEmail).orElseGet(this::getDefaultFromEmail);
}
public Set<String> getTo() {
if (to == null) {
to = new HashSet<>();
}
return to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
private String getDefaultFromEmail() {
return WebRequest.getCurrent().as(ToolRequest.class).getCurrentUser().getEmail();
}
}
package brightspot.core.article;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.Callable;
import com.psddev.cms.ui.ToolPage;
import com.psddev.cms.ui.form.FormRequest;
import com.psddev.dari.db.State;
import com.psddev.dari.html.content.FlowContent;
import com.psddev.dari.html.enumerated.FormMethod;
import com.psddev.dari.util.MailMessage;
import com.psddev.dari.util.MailProvider;
import com.psddev.dari.web.UrlBuilder;
import com.psddev.dari.web.WebRequest;
import com.psddev.dari.web.annotation.WebParameter;
import com.psddev.dari.web.annotation.WebPath;
import static com.psddev.cms.ui.Components.*;
@WebPath("/share-edit")
public class ShareEditLinkToolPage extends ToolPage {
@WebParameter
private UUID contentId;
@WebParameter
private UUID id;
public void setContentId(UUID contentId) {
this.contentId = contentId;
}
@Override
protected void onGet() throws Exception {
ShareEditLinkForm form = new ShareEditLinkForm();
if (id != null) {
State.getInstance(form).setId(id);
}
writePageResponse(() -> createForm(form));
}
@Override
protected void onPost() throws Exception {
ShareEditLinkForm form = new ShareEditLinkForm();
if (id != null) {
State.getInstance(form).setId(id);
}
page.updateUsingParameters(form);
if (form.getState().validate()) {
String editUrl = new UrlBuilder("/content/edit.jsp").setParameter("id", contentId).build();
MailProvider mailProvider = MailProvider.Static.getDefault();
for (String to : form.getTo()) {
mailProvider.send(new MailMessage()
.to(to)
.from(form.getFromEmail())
.subject(form.getSubject())
.bodyPlain(form.getMessage().replace("{{editLink}}", editUrl)));
}
writePageResponse(() -> Arrays.asList(
DIV.with(
DIV.className("message message-success").with("Email sent!"),
BR,
DIV.className("actions")
.with(INPUT.typeSubmit()
.className("button")
.attr("onClick", "window.location.reload();")
.value("Done")))));
} else {
writePageResponse(() -> createForm(form));
}
}
private Collection<FlowContent> createForm(ShareEditLinkForm form) throws Exception {
return Collections.singleton(FORM
.method(FormMethod.POST)
.action(new UrlBuilder(this.getClass()).build())
.className("standardForm")
.with( INPUT.typeHidden().name("id").value(form.getId().toString()),
INPUT.typeHidden().name("typeId").value(form.getState().getTypeId().toString()),
capture(page, p -> p.writeFormFields(form)),
DIV.className("actions")
.with(
INPUT.typeSubmit()
.className("button")
.value("Submit"))
)
);
}
private void writePageResponse(Callable<Collection<FlowContent>> getWidgetMainContent) {
response.toBody().write(DIV.className("widget").with(div -> {
div.add(H1.with("Share Edit Link"));
try {
div.addAll(getWidgetMainContent.call());
} catch (Exception e) {
FormRequest formRequest = WebRequest.getCurrent().as(FormRequest.class);
formRequest.getErrors().add(e);
div.add(formRequest.getErrorMessages());
}
}));
}
}
package brightspot.core.article;
import java.io.IOException;
import com.psddev.cms.tool.ContentEditAction;
import com.psddev.cms.tool.ToolPageContext;
import com.psddev.dari.db.State;
import com.psddev.dari.web.UrlBuilder;
import static com.psddev.dari.html.Nodes.*;
public class ShareEditLink implements ContentEditAction {
@Override
public void writeHtml(ToolPageContext page, Object content) throws IOException {
page.write(LI
.with(
A.href(new UrlBuilder(
ShareEditLinkToolPage.class,
p -> p.setContentId(State.getInstance(content).getId())).build())
.target("request")
.with("Share Edit Link")
)
);
}
}