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.
Step 1: Defining the Form
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:
1class ShareEditLinkForm extends Record {23@Required4@DynamicPlaceholderMethod("getDefaultFromEmail")5private String fromEmail;67private Set<String> to;89private String subject;1011@Note("Use {{editLink}} to include the edit link in the message.")12private String message;1314public String getFromEmail() {15return Optional.ofNullable(fromEmail).orElseGet(this::getDefaultFromEmail);16}1718public Set<String> getTo() {19if (to == null) {20to = new HashSet<>();21}22return to;23}2425//other getters and setters omitted2627private String getDefaultFromEmail() {28return WebRequest.getCurrent().as(ToolRequest.class).getCurrentUser().getEmail();29}30}
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.
Step 2: Create the ToolPage
Next, you need to create the ToolPage that will render the custom form input experience. This is done in the ShareEditLinkToolPage class:
1@WebPath("/share-edit")2public class ShareEditLinkToolPage extends ToolPage {3@WebParameter4private UUID contentId;56@WebParameter7private UUID id;89public void setContentId(UUID contentId) {10this.contentId = contentId;11}1213...14}
- 1. Defines the CMS path where this ToolPage will serve requests from.
- 4. The
contentIdfield allows us to pass in the ID of the content that the content edit link should point to. Using@WebParameterallows Brightspot to automatically bind the request parameter to the field. It also allows us to create URLs in type-safe manner viaUrlBuilderwhich is shown later on in the tutorial. - 7. The
idfield is used internally in this class to maintain a consistentState#idfor theShareEditLinkFormobject.
Step 3: Rendering the Form
The onGet method is responsible for rendering the initial form:
1@Override2protected void onGet() throws Exception {3ShareEditLinkForm form = new ShareEditLinkForm();4if (id != null) {5State.getInstance(form).setId(id);6}7writePageResponse(() -> createForm(form));8}
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.
1private Collection<FlowContent> createForm(ShareEditLinkForm form) throws Exception {2return Collections.singleton(FORM3.method(FormMethod.POST)4.action(new UrlBuilder(this.getClass()).build())5.className("standardForm")6.with(7INPUT.typeHidden().name("id").value(form.getId().toString()),8INPUT.typeHidden().name("typeId").value(form.getState().getTypeId().toString()),9capture(page, p -> p.writeFormFields(form)),10DIV.className("actions")11.with(12INPUT.typeSubmit()13.className("button")14.value("Submit"))15)16);17}1819private void writePageResponse(Callable<Collection<FlowContent>> getWidgetMainContent) {20response.toBody().write(DIV.className("widget").with(div -> {21div.add(H1.with("Share Edit Link"));22try {23div.addAll(getWidgetMainContent.call());24} catch (Exception e) {25FormRequest formRequest = WebRequest.getCurrent().as(FormRequest.class);26formRequest.getErrors().add(e);27div.add(formRequest.getErrorMessages());28}29}));30}
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.
Step 4: Processing Form Submissions
The onPost method is responsible for processing form submissions:
1@Override2protected void onPost() throws Exception {3ShareEditLinkForm form = new ShareEditLinkForm();4if (id != null) {5State.getInstance(form).setId(id);6}7page.updateUsingParameters(form);89if (form.getState().validate()) {10//Send share edit link email11MailProvider mailProvider = MailProvider.Static.getDefault();12for (String to : form.getTo()) {13mailProvider.send(new MailMessage()14.to(to)15.from(form.getFromEmail())16.subject(form.getSubject())17.bodyPlain(form.getMessage().replace("{{editLink}}", editUrl)));18}1920writePageResponse(() -> Arrays.asList(21DIV.with(22DIV.className("message message-success").with("Email sent!"),23BR,24DIV.className("actions")25.with(INPUT.typeSubmit()26.className("button")27.attr("onClick", "window.location.reload();")28.value("Done")))));29} else {30writePageResponse(() -> createForm(form));31}32}
- 5. This ensures the form object has the correct
State#idbased on the request parameters. Without this Line # 7 will not function correctly. - 7. Populates the form object with form submission data.
- 27. In this case, the "Done" button refreshes the page. You could choose other methods of closing the pop-up window if desired.
- 30. 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.
Step 5: Create an access link to your Custom Form
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.
1public class ShareEditLink implements ContentEditAction {23@Override4public void writeHtml(ToolPageContext page, Object content) throws IOException {5page.write(LI6.with(7A.href(new UrlBuilder(8ShareEditLinkToolPage.class,9p -> p.setContentId(State.getInstance(content).getId())).build())10.target("request")11.with("Share Edit Link")12)13);14}15}
- 5. Uses Dari HTML and
UrlBuilderto create an HTML link to theShareEditLinkToolPage - 10. The
requestlink 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.
Full Code
Here's the full code for the tutorial:
1package brightspot.docs;23import java.util.HashSet;4import java.util.Optional;5import java.util.Set;67import com.psddev.cms.ui.ToolRequest;8import com.psddev.cms.ui.form.DynamicPlaceholderMethod;9import com.psddev.cms.ui.form.Note;10import com.psddev.dari.db.Record;11import com.psddev.dari.web.WebRequest;1213class ShareEditLinkForm extends Record {1415@Required16@DynamicPlaceholderMethod("getDefaultFromEmail")17private String fromEmail;1819private Set<String> to;2021private String subject;2223@Note("Use {{editLink}} to include the edit link in the message.")24private String message;2526public String getFromEmail() {27return Optional.ofNullable(fromEmail).orElseGet(this::getDefaultFromEmail);28}2930public Set<String> getTo() {31if (to == null) {32to = new HashSet<>();33}34return to;35}3637public String getSubject() {38return subject;39}4041public void setSubject(String subject) {42this.subject = subject;43}4445public String getMessage() {46return message;47}4849public void setMessage(String message) {50this.message = message;51}5253private String getDefaultFromEmail() {54return WebRequest.getCurrent().as(ToolRequest.class).getCurrentUser().getEmail();55}56}
1package brightspot.core.article;23import java.util.Arrays;4import java.util.Collection;5import java.util.Collections;6import java.util.UUID;7import java.util.concurrent.Callable;89import com.psddev.cms.ui.ToolPage;10import com.psddev.cms.ui.form.FormRequest;11import com.psddev.dari.db.State;12import com.psddev.dari.html.content.FlowContent;13import com.psddev.dari.html.enumerated.FormMethod;14import com.psddev.dari.util.MailMessage;15import com.psddev.dari.util.MailProvider;16import com.psddev.dari.web.UrlBuilder;17import com.psddev.dari.web.WebRequest;18import com.psddev.dari.web.annotation.WebParameter;19import com.psddev.dari.web.annotation.WebPath;2021import static com.psddev.cms.ui.Components.*;2223@WebPath("/share-edit")24public class ShareEditLinkToolPage extends ToolPage {2526@WebParameter27private UUID contentId;2829@WebParameter30private UUID id;3132public void setContentId(UUID contentId) {33this.contentId = contentId;34}3536@Override37protected void onGet() throws Exception {38ShareEditLinkForm form = new ShareEditLinkForm();39if (id != null) {40State.getInstance(form).setId(id);41}42writePageResponse(() -> createForm(form));43}4445@Override46protected void onPost() throws Exception {47ShareEditLinkForm form = new ShareEditLinkForm();48if (id != null) {49State.getInstance(form).setId(id);50}51page.updateUsingParameters(form);5253if (form.getState().validate()) {54String editUrl = new UrlBuilder("/content/edit.jsp").setParameter("id", contentId).build();55MailProvider mailProvider = MailProvider.Static.getDefault();56for (String to : form.getTo()) {57mailProvider.send(new MailMessage()58.to(to)59.from(form.getFromEmail())60.subject(form.getSubject())61.bodyPlain(form.getMessage().replace("{{editLink}}", editUrl)));62}6364writePageResponse(() -> Arrays.asList(65DIV.with(66DIV.className("message message-success").with("Email sent!"),67BR,68DIV.className("actions")69.with(INPUT.typeSubmit()70.className("button")71.attr("onClick", "window.location.reload();")72.value("Done")))));73} else {74writePageResponse(() -> createForm(form));75}76}7778private Collection<FlowContent> createForm(ShareEditLinkForm form) throws Exception {79return Collections.singleton(FORM80.method(FormMethod.POST)81.action(new UrlBuilder(this.getClass()).build())82.className("standardForm")83.with( INPUT.typeHidden().name("id").value(form.getId().toString()),84INPUT.typeHidden().name("typeId").value(form.getState().getTypeId().toString()),85capture(page, p -> p.writeFormFields(form)),86DIV.className("actions")87.with(88INPUT.typeSubmit()89.className("button")90.value("Submit"))91)92);93}9495private void writePageResponse(Callable<Collection<FlowContent>> getWidgetMainContent) {96response.toBody().write(DIV.className("widget").with(div -> {97div.add(H1.with("Share Edit Link"));98try {99div.addAll(getWidgetMainContent.call());100} catch (Exception e) {101FormRequest formRequest = WebRequest.getCurrent().as(FormRequest.class);102formRequest.getErrors().add(e);103div.add(formRequest.getErrorMessages());104}105}));106}107108}
1package brightspot.core.article;23import java.io.IOException;45import com.psddev.cms.tool.ContentEditAction;6import com.psddev.cms.tool.ToolPageContext;7import com.psddev.dari.db.State;8import com.psddev.dari.web.UrlBuilder;910import static com.psddev.dari.html.Nodes.*;1112public class ShareEditLink implements ContentEditAction {1314@Override15public void writeHtml(ToolPageContext page, Object content) throws IOException {16page.write(LI17.with(18A.href(new UrlBuilder(19ShareEditLinkToolPage.class,20p -> p.setContentId(State.getInstance(content).getId())).build())21.target("request")22.with("Share Edit Link")23)24);25}26}