Brightspot CMS Developer Guide

Tutorial: Creating custom form input experiences in Brightspot

Creating Custom Form Input Experiences in Brightspot Screenshot

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 via UrlBuilder which is shown later on in the tutorial.
  • The id field is used internally in this class to maintain a consistent State#id for the ShareEditLinkForm 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 the ShareEditLinkToolPage
  • 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")
            )
        );
    }
}

Previous Topic
Building a custom secret service
Next Topic
Rich text
Was this topic helpful?
Thanks for your feedback.
Our robust, flexible Design System provides hundreds of pre-built components you can use to build the presentation layer of your dreams.

Asset types
Module types
Page types
Brightspot is packaged with content types that get you up and running in a matter of days, including assets, modules and landing pages.

Content types
Modules
Landing pages
Everything you need to know when creating, managing, and administering content within Brightspot CMS.

Dashboards
Publishing
Workflows
Admin configurations
A guide for installing, supporting, extending, modifying and administering code on the Brightspot platform.

Field types
Content modeling
Rich-text elements
Images
A guide to configuring Brightspot's library of integrations, including pre-built options and developer-configured extensions.

Google Analytics
Shopify
Apple News