Advanced View Modeling

This section describes some advanced techniques for working with ViewModels.

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—

  1. The article first accesses the intermediate model (CloseUp or Panorama).
  2. 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.

../../../../_images/unwrap-diagram.svg

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

package content.article;

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 /* The Model for CloseUp */
 package content.article;

 public class CloseUp extends ImageOption {

     @Embedded
     private Image image;

     public Image getImage() {
         return image;
     }

     public void setImage(Image image) {
         this.image = image;
     }

     @Override
     public Object unwrap() {
         return getImage();
     }
 }

 /* The Model for Panorama */
 package content.article;

 public class Panorama extends ImageOption {

     private Image image;

     public Image getImage() {
         return image;
     }

     public void setImage(Image image) {
         this.image = image;
     }

     @Override
     public Object unwrap() {
         return getImage();
     }

 }

In the previous snippet, lines 17–20 and 38–41 implement the unwrap method. When accessing CloseUp or Panorama, a parent Model can immediately access the lower-level Image.

Step 3: Implement Abstract Model

package content.article;

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 above snippet is an abstract marker class. Any of its concrete subclasses become available to its enclosing class.

Step 4: Implement Top-Level Model

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 package content.article;

 import com.psddev.cms.db.Content;

 public class Article extends Content implements Page, ImageOption {

     private ImageOption imageOption;

     public ImageOption getImageOption() {
         return imageOption;
     }

 }

In the previous snippet, line 7 declares the imageOption field. Because ImageOption is an abstract class, any concrete subclass becomes an option for Image Option in the content edit form.

../../../../_images/selections-from-abstract-marker-class.png

Step 5: Implement Low-Level Handlebars

This file is saved as Image.hbs.

<div>
    <img src="{{imageSrc}}">
</div>
<div>
    {{caption}}
</div>

Step 6: Implement Low-Level JSON

This file is saved as Image.json.

1
2
3
4
5
 {
     "_template": "Image.hbs",
     "imageSrc": "http://url/to/any/image.jpg",
     "caption": "Static caption"
 }

In the previous snippet, line 2 provides the link between the image’s View and the Handlebars file.

Step 7: Implement Low-Level ViewModel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 package content.article;

 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();
     }

 }

The previous snippet provides the data for the embedded image’s view. Line 10 retrieves the URL, and line 14 retrieves the caption.

Because of the implementation of unwrap, there is no need to create a ViewModel for the intermediate-level CloseUp or Panorama.

Rendering Rich Text with RichTextViewBuilder

By default, Brightspot’s Rich-Text Editors store escaped HTML tags. For example, rich-text editors store <b>bold text</b> as &lt;b&gt;bold text&lt;/b&gt;. 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 package content.article;

 import com.psddev.cms.db.Content;
 import com.psddev.cms.db.ToolUi;


 public class Article extends Content implements Page {

     @ToolUi.RichText
     private String body;

     public String getBody() {
         return body;
     }

     public void setBody(String body) {
         this.body = body;
     }
 }

In the previous snippet, the annotation in line 9 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.

../../../../_images/rte-annotation.png

For additional information about this annotation, see @ToolUi.RichText.

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 ViewModel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 package content.article;

 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));
     }

 }

In the previous snippet—

  • Lines 10–13 implement the getBody method declared in the View interface.
  • The method buildHtml in lines 11–12 takes the escaped HTML from the model’s body and combines it with a standard rich-text view class to produce unescaped HTML.
../../../../_images/rendered-rich-text.png

Conditional Redirect

Because the onCreate event occurs before Brightspot generates any of the View’s components, you can use it to perform conditional redirects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 package content.article;

 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;
         }

     }

 }

In the previous snippet—

  • The ViewResponse parameter passed to onCreate in line 8 contains a variety of fields you can initialize and send back to the client. For details, see View Response.
  • The throw statement in line 11 terminates the onCreate method, so Brightspot does not create the View.