View models
In the model-view-view model pattern, the view model receives raw data from the model and transforms that data to produce a view. Brightspot’s view models accomplish this transformation in two steps. In the first step, the view model receives two categories of data:
- Content, such as text, images, and videos available through methods in a received model object.
- Web environment, such as HTTP header fields, query strings, and cookies available through annotations.
The view model implements methods specified in an underlying Java interface such that there is one method for each item appearing in the view. For example, if a view has three elements (headline, body, and date published), the view model includes three methods to return each element individually. If those elements require additional logic, such as instantiating a locale to properly format a date, the view model implements the corresponding methods and constructors accordingly.
In the second step, the view model transforms the extracted data into a form the view is expecting.
In the following example, the view model uses the getHeadline and getBody methods to access those model elements; it also uses the @HttpHeader annotation and getDatepublished method to access and format the publication date in a manner indicated by the client’s locale.
import com.psddev.cms.view.ViewModel;
import com.psddev.cms.view.servlet.HttpHeader;
public class ArticleViewModel extends ViewModel<Article> {
@Override
public String getHeadline() {
return model.getHeadline();
}
@Override
public String getBody() {
return model.getBody();
}
@HttpHeader("Accept-Language")
public String acceptLanguage;
@Override
public String getDatepublished() {
Date datePublished = model.getDatePublished();
String httpLocale = this.acceptLanguage.substring(0,5);
Locale readerLocale = null;
if (httpLocale.equals("en-US")) {
readerLocale = new Locale("en","us");
} else {
readerLocale = new Locale("en","au");
}
DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, readerLocale);
return "Date published: " + df.format(datePublished);
}
}
-
Returns the article’s headline.
-
Returns the article’s body.
-
Extracts the Accept-Language field from the HTTP header. The first five characters of this field provide the locale, such as en-us.
-
Returns a date in a format as expected by the client’s locale prepended with the string Date published: .
-
Retrieves the date published from the model.
-
Extracts the first five characters from the Accept-Language field to get the client’s language and country.
-
Declares a standard Locale variable; set it to en-us or en-au depending on the characters extracted from the Accept-Language field.
-
Instantiates a DateFormat variable that formats the date based on the discovered locale.
-
Returns the formatted date prepended with the string Date published: .
The following image is an example of the previous snippet’s output.
You can modularize your code by implementing the interface PageEntryView—a marker interface you can use to determine the required view model based on the model’s class or other criteria. See the following example.
public class ArticleViewModel extends ViewModel<Article> implements PageEntryView {
if ((model instanceof DailySection) || (model instanceof WeeklySection)) {
return createView(SectionViewModel.class, model);
} else {
return createView(ArticleViewModel.class, model);
}
}
Any view model you create is a subclass of the abstract class ViewModel and optionally an implementation of an interface based on your view.
In the following declaration, ArticleViewModel extends the abstract class ViewModel for an Article.
import com.psddev.cms.view.ViewModel;
public class ArticleViewModel extends ViewModel<Article> { }
ViewModel provides several methods and callbacks you can use to fine-tune a view’s generation. For details, see ViewModel.
View model Lifecycle
The order of events in the view model’s lifecycle is as follows:
-
shouldCreate
-
onCreate
- Extract data from model, optionally using
createView
orcreateViews
.
The following sections describe these methods.
shouldCreate
This method is called prior to onCreate
, and indicates if Brightspot should actually generate the view when a client requests a particular model instance. The inherited value from the abstract class is true, which means Brightspot always generates the view for the requested object. You can introduce logic to test if Brightspot should continue creating the view. For example, if a user is not authorized to view an article, you can prevent Brightspot from generating the view.
protected boolean shouldCreate() {
return user.isAuthorized() ? true : false;
}
onCreate
This method is called after shouldCreate
and before generating any of the view’s components. You can use this method for retrieving or building additional content not included in the received model. For example, when a user requests an article, the model received by the view model includes properties pertaining to the article itself, typically heading, author, and body. To display a list of articles recently viewed by the user, or any other information not related to the article itself, implement additional logic in the onCreate method. The following snippet is an example of finding articles recently viewed by a user at the time of rendering the requested article.
public class ArticleViewModel extends ViewModel<Article> {
private List<Article> histViewedArticles;
@Override
protected void onCreate(ViewResponse response) {
super.onCreate(response);
histViewedArticles = historyItem.findByUser(user,20);
}
protected List<Article> getArticleHistory() {
return histViewedArticles;
}
}
-
Overrides the abstract method onCreate to retrieve the last 20 articles viewed by the current user. For details about the methods available from the ViewResponse parameter, see ViewResponse.
-
Returns the list of recently viewed articles.
createView
This method creates a view using the specified view-model class and model. You can use this method to modularize your code. For example, you can have a single view model associated with all your content types. Regardless of the item a client requests, the single view model is run, and inside that view model you can identify the correct view model for creating the view.
if (model instanceof Section) {
return createView(SectionViewModel.class, model);
} else {
return createView(ArticleViewModel.class, model);
}
createViews
This method creates an Iterable over views using the specified view-model class and model. You can use this method to create a series of related views, such as a series of comments to an article, which you incorporate into a parent view.
public class CommentListViewModel extends ViewModel<CommentList> {
public Iterable<CommentsView> getComments() {
return createViews(CommentsViewModel.class, model.getComments());
}
}
In the previous snippet creates an Iterable of comment views based on data provided by the model CommentList.
createView(CommentListViewModel.class, CommentListViewModel.getComments());
The previous snippet creates a single view comprised of the comments provided by the view model CommentListViewModel.
This section describes some advanced techniques for working with view models.
ModelWrapper
Some models wrap models which themselves wrap models. For example, an article can contain two types of image models, one a closeup and another a panorama. Each of those models wraps an image model that is comprised of the raw image and a caption. For the article to access the image—
- The article first accesses the intermediate model (CloseUp or Panorama).
- From the intermediate model, accesses the image itself.
Brightspot provides a convenience function unwrap that saves the second step. Placing unwrap in the intermediate model allows access to the image directly.
Referring to the previous illustration—
- Article includes an abstract marker class ImageOption. This is a useful technique for easily including additional classes into Article: any concrete subclass of ImageOption automatically appears as an option in Article.
- Each concrete subclass of ImageOption, CloseUp, and Panorama implements unwrap. This method returns the Image wrapped in either model.
The following example describes how to implement unwrap as envisioned in the previous illustration.
Step 1: Implement low-level model
import com.psddev.cms.db.Content;
import com.psddev.dari.util.StorageItem;
public class Image extends Content {
private StorageItem rawImage;
private String caption;
/* Getters and setters */
}
Step 2: Implement intermediate-level models
Model for CloseUp
public class CloseUp extends ImageOption {
@Embedded
private Image image;
public Image getImage() {
return image;
}
@Override
public Object unwrap() {
return getImage();
}
}
-
Implements the unwrap method. When accessing CloseUp, a parent model can immediately access the lower-level Image.
Model for Panorama
public class Panorama extends ImageOption {
private Image image;
public Image getImage() {
return image;
}
@Override
public Object unwrap() {
return getImage();
}
}
-
Implements the unwrap method. When accessing Panorama, a parent model can immediately access the lower-level Image.
Step 3: Implement abstract model
import com.psddev.cms.db.Content;
import com.psddev.cms.view.ModelWrapper;
import com.psddev.dari.db.Recordable;
@Recordable.Embedded
public abstract class ImageOption extends Content implements ModelWrapper, Directory.Item {
}
The previous snippet is an abstract marker class. Any of its concrete subclasses become available to its enclosing class.
Step 4: Implement top-level model
import com.psddev.cms.db.Content;
public class Article extends Content implements Page, ImageOption {
private ImageOption imageOption;
public ImageOption getImageOption() {
return imageOption;
}
}
-
Declares the imageOption field. Because ImageOption is an abstract class, any concrete subclass becomes an option for Image Option in the content edit form.
Step 5: Implement low-level template
This file is saved as Image.hbs.
<div>
<img src="{{imageSrc}}">
</div>
<div>
{{caption}}
</div>
Step 6: Implement low-level data file
This file is saved as Image.json.
{
"_template": "Image.hbs",
"imageSrc": "http://url/to/any/image.jpg",
"caption": "Static caption"
}
-
Provides the link between the image’s view and the template.
Step 7: Implement low-level view model
The following snippet provides the data for the embedded image’s view.
import com.psddev.cms.db.ImageTag;
import com.psddev.cms.view.ViewModel;
import styleguide.content.article.ImageView;
public class ImageViewModel extends ViewModel<Image> implements ImageView {
public String getImageSrc() {
return new ImageTag.Builder(model.getRawImage()).toUrl();
}
public String getCaption() {
return model.getCaption();
}
}
-
Retrieves the URL.
-
Retrieves the caption.
Because of the implementation of unwrap, there is no need to create a view model for the intermediate-level CloseUp or Panorama.
By default, Brightspot’s rich-text editors store escaped HTML tags. For example, rich-text editors store bold text</b> as <b>bold text</b>. To ensure unescaped HTML tags appear in your rendered pages, you need to use the RichTextViewBuilder.buildHtml method. The following example shows how to implement a rich-text editor and render its output.
Step 1: Render string field as rich-text editor
import com.psddev.cms.db.Content;
import com.psddev.cms.db.ToolUi;
public class Article extends Content implements Page {
@ToolUi.RichText
private String body;
}
In the previous snippet, the annotation @ToolUi.RichText renders the body field as a rich-text editor. For detailed information about customizing the appearance and functionality of the rich-text editor, see Rich text.
Step 2: Implement templates
The following file is saved as Article.hbs.
<div>
{{headline}}
{{body}}
</div>
The following file is saved as Article.json.
{
"_template": "Article.hbs",
"headline": "This is a placeholder for the headline",
"body": "This is a placeholder for rich text"
}
Step 3: Implement view model
import com.psddev.cms.rte.RichTextViewBuilder;
import com.psddev.cms.view.ViewModel;
import styleguide.content.article.ArticleView;
public class ArticleViewModel extends ViewModel<Article> implements ArticleView {
@Override
public CharSequence getBody() {
return RichTextViewBuilder.buildHtml(model.getBody(),
rte -> createView(RichTextElementView.class, rte));
}
}
-
Implements the getBody method declared in the view interface.
-
Takes the escaped HTML from the model’s body and combines it with a standard rich-text view class to produce unescaped HTML.
Because the onCreate event occurs before Brightspot generates any of the view’s components, you can use it to perform conditional redirects.
import com.psddev.cms.view.ViewModel;
import com.psddev.cms.view.ViewResponse;
public class ArticleViewModel extends ViewModel<Article> implements ArticleView {
protected void onCreate(ViewResponse response) {
if (somestatement == true) {
response.redirectTemporarily("http://domain/temporary-redirect-page.html");
throw response;
}
}
}
-
The ViewResponse parameter passed to onCreate contains a variety of fields you can initialize and send back to the client. For details, see View Response.
-
Terminates the onCreate method, so Brightspot does not create the view.
A view-model overlay is a mechanism for adding data to a view. Brightspot “overlays” the additional data on top of the views’ data produced by the view models. The data may be related to the underlying model producing the view, or it could be used to provide common data across all views. For example, you can easily add a permalink to all of your project’s pages with a view-model overlay. This feature also saves you the effort of possibly extending existing models or creating a new class hierarchy to add the object to many view models.
You can directly add a specific view model overlay key’s value to every view. The following example describes how to create a view model overlay that inserts a permalink into each view. This example works best if you define a default URL for the site as described in Global-only settings.
Step 1: Create model
You can use any new or existing model with an overlay. No extra coding is required. For an example of a simple model, the snippet "Simple content type with text and image fields."
Step 2: Create view model overlay
Creating a view model overlay
package content.article;
import com.psddev.cms.view.ViewModelOverlay;
import com.psddev.dari.db.State;
import com.psddev.dari.util.CompactMap;
import java.util.Map;
import java.util.function.Supplier;
import com.psddev.cms.db.Directory;
public class PermalinkViewModelOverlay implements ViewModelOverlay {
@Override
public Map<String, Supplier<Object>> create(Object model, String viewTemplate) {
Map<String, Supplier<Object>> overlay = new CompactMap<>();
overlay.put("fullPermalink",() -> State.getInstance(model).as(Directory.ObjectModification.class).getFullPermalink());
return overlay;
}
}
-
Declares a class that implements ViewModelOverlay. Any class that implements this interface injects its key-value pairs into every view.
-
Creates a map of key-value pairs.
-
Retrieves the current page’s permalink, and assigns it to the key fullPermalink. (For this example to work you also need to configure the Default Site URL as explained in Settings for the Global site.) You can add any number of key-value pairs to the overlay.
Step 3: Create data file
You can use any new or existing Styleguide JSON data file. No extra coding is required.
Step 4: Insert overlay key Into template
Referring to the snippet "Creating a view model overlay," you now have access to the key fullPermalink in every view by inserting that key as a placeholder in a Handlebars template. When rendering the view, Brightspot retrieves the value for the placeholder without your implementing a method in any view model.
<html>
<body>
<div class="Article">
<div class="Article-headline">
<h1>{{headline}}</h1>
</div>
<div class="Article-body">
{{body}}
</div>
<div class="Article-body">
Permalink to this item: {{fullPermalink}}
</div>
</div>
</body>
</html>
In the previous snippet, the placeholder {{fullPermalink}} is the key contained in the overlay.
You may need to overlay a view on a given view’s value. To do this, you create a view model overlay that returns an object for a particular key. When the view model overlay adds the object to the view model, it converts the object to a view using the view system to find an appropriate view model. The view model for this particular object must be marked with the ViewModelOverlayValueEntryView interface to indicate to the view system the appropriate view to use for the object in the context of the view model overlay.
The following example describes how to use a view model overlay to add a footer to an existing view for articles. The example assumes you already have a working parent model, view, and view model into which you want to insert the overlay’s view.
Step 1: Create child model
Create a model Footer.java for the footer.
import com.psddev.cms.db.Content;
import com.psddev.dari.util.StorageItem;
public class Footer extends Content {
private StorageItem imageFile;
private String caption;
/* Getters and setters */
}
Step 2: Create child template
Create a template Footer.hbs for the footer. The overlay injects this template as it renders the parent view.
<div class="Footer">
<p class="Footer-p">{{caption}}</p>
<p class="Footer-p"><img src="{{imagefile}}"/></p>
</div>
Step 3: Create child data file
In the same directory as Footer.hbs, create the corresponding data file Footer.json.
{
"_template": "Footer.hbs",
"caption": "{{words(4)}}",
"imagefile": "{{image(100, 100)}}"
}
Step 4: Create view model overlay
Declare a view model overlay class FooterViewModelOverlay, and insert an instance of the footer’s model into the overlay.
import com.psddev.cms.view.ViewModelOverlay;
import com.psddev.dari.util.CompactMap;
import com.psddev.dari.util.UrlStorageItem;
public class FooterViewModelOverlay implements ViewModelOverlay {
@Override
public Map<String, Supplier<Object>> create(Object model, String viewTemplate) {
if (model instanceof Footer) {
return null;
}
Map<String, Supplier<Object>> overlay = new CompactMap<>();
Footer footer = new Footer();
UrlStorageItem file = new UrlStorageItem();
file.setPath("http://cdn.perfectsense.psdweb.psdops.com/6d/5d/efd1da434d6abd8b1b08b8b29c4c/brightspot-logo-183.png");
footer.setImageFile(file);
footer.setCaption("This web site delivered by:");
overlay.put("footer", () -> footer);
return overlay;
}
}
-
Declares a class that implements ViewModelOverlay. Any class that implements this interface makes its overlay map available to calling parent views.
-
Tests if the current model is a Footer. If it is, do not inject the view model into the current view model. (Without this test, Brightspot enters an infinite loop, always inserting a footer view model into another footer view model.)
-
Instantiates an overlay map.
-
Instantiates the child model and set its properties.
-
Adds the model to the overlay map using the key footer.
Step 5: Create child view model
In the same directory as Footer.java, create a view model FooterViewModel.java.
package content.article;
import com.psddev.cms.view.ViewModel;
import com.psddev.cms.view.ViewModelOverlayValueEntryView;
import styleguide.content.article.FooterView;
public class FooterViewModel extends ViewModel<Footer> implements FooterView, ViewModelOverlayValueEntryView {
@Override
public CharSequence getImagefile() {
return model.getImageFile().getPublicUrl();
}
@Override
public CharSequence getCaption() {
return model.getCaption();
}
}
-
Adds the marker interface ViewModelOverlayValueEntryView. Brightspot uses this marker as an indication to insert the rendered view in any parent view that calls the corresponding overlay key you created in Step 4.
Step 6: Add overlay key to parent template
Open the template for which you want to include the overlay, and use the overlay key used in Step 4.
<html>
<body>
<div class="Article">
<div class="Article-headline">
<h1>{{headline}}</h1>
</div>
<div class="Article-body">
{{body}}
</div>
<div>
{{footer}}
</div>
</div>
</body>
</html>
-
At run time, Brightspot replaces the placeholder {{footer}} with the entire template created in Step 2, along with the associated view model logic and styling.