{"id":37745,"date":"2014-12-02T05:55:42","date_gmt":"2014-12-02T13:55:42","guid":{"rendered":"http:\/\/localhost\/exoblog\/?p=8001"},"modified":"2023-06-05T16:49:50","modified_gmt":"2023-06-05T14:49:50","slug":"developing-juzu-portlets-step-4-adding-likes-and-comments-for-secret","status":"publish","type":"post","link":"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-4-adding-likes-and-comments-for-secret\/","title":{"rendered":"Developing Juzu portlets \u2013 Step 4: adding Likes and Comments for Secret"},"content":{"rendered":"<p><a href=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series.jpg\"><img decoding=\"async\" loading=\"lazy\" src=\"\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series.jpg\" alt=\"04-Juzu-tutorial-series\" width=\"650\" height=\"220\" class=\"aligncenter size-full wp-image-8013\" srcset=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series.jpg 650w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series-300x102.jpg 300w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series-500x169.jpg 500w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series-360x122.jpg 360w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series-200x68.jpg 200w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series-100x34.jpg 100w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-Juzu-tutorial-series-70x24.jpg 70w\" sizes=\"(max-width: 650px) 100vw, 650px\" \/><\/a><\/p>\n<p><em>Previous steps:<br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/learn-how-to-develop-great-juzu-portlets-for-exo-platform\/\">Learn how to develop great Juzu portlets for eXo Platform!<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-2-viewing-and-posting-secrets\/\">Step 2: viewing and posting secrets<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-3-building-a-sexy-secret-wall\/\">Step 3: Building a sexy secret wall<\/a><br \/>\n<\/em><\/p>\n<p>In the <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-3-building-a-sexy-secret-wall\/\">previous blog post<\/a>, we implemented the UI of our JuZcret application. We are now able to look at Secret on the secret wall and share a new secret via the \u201cadd secret\u201d form. But what would a <strong>social feature<\/strong> be without any user interaction? Nothing\u2026<\/p>\n<p>That\u2019s why, during this step, we\u2019ll add two very original social features to our <strong>JuZcret<\/strong> application: the \u201ccomment\u201d feature and the &#8220;like&#8221; feature. Crazy! \ud83d\ude09<\/p>\n<p>By implementing these new features we will learn, among other things, how to use <strong>Ajax<\/strong> with Juzu, <strong>interact with Portlet Container<\/strong>, and add an <strong>EDIT mode<\/strong> to our Portlet.<\/p>\n<p><em>We are assuming that you have already implemented step 3. If not, you can still <a href=\"https:\/\/github.com\/juzu\/portlet-tutorial\/tree\/step-3\/tutorial-juzcret\" target=\"_blank\" rel=\"noopener\">download the source code resulting from step 3 on Github<\/a> in order to start this step.<\/em><br \/>\n<!--more--><br \/>\nLet\u2019s start by improving the application models a little bit.<\/p>\n<h2>Models<\/h2>\n<p>Here we don\u2019t need to use Juzu, we will just <strong>improve our current model<\/strong>. Let\u2019s add the top of the tree, <strong>Model<\/strong>. This class contains common attributes for <strong>Secret<\/strong> and <strong>Comment<\/strong>:<\/p>\n<p><a href=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2014\/12\/01-Model.png\" alt=\"01-Model\" width=\"650\" class=\"aligncenter size-full wp-image-8006\" srcset=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model.png 726w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-300x162.png 300w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-720x389.png 720w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-500x270.png 500w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-360x194.png 360w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-200x108.png 200w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-100x54.png 100w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/01-Model-56x30.png 56w\" sizes=\"(max-width: 726px) 100vw, 726px\" \/><\/a><\/p>\n<p>In the <span class\"nacode\"=\"\">org.juzu.tutorial.models<\/span> package, create a Model class as shown below:<\/p>\n<pre class=\"lang:default decode:true \">package org.juzu.tutorial.models;\n\nimport java.io.Serializable;\nimport java.util.Date;\n\npublic class Model implements Serializable {\n    private String id;\n    private Date createdDate;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public Date getCreatedDate() {\n        return createdDate;\n    }\n\n    public void setCreatedDate(Date createdDate) {\n        this.createdDate = createdDate;\n    }\n}<\/pre>\n<p>Then add a <strong>Comment<\/strong> class:<\/p>\n<pre class=\"lang:default decode:true \">package org.juzu.tutorial.models;\n\npublic class Comment extends Model {\n    private String userId;\n    private String content;\n\n    public String getUserId() {\n        return userId;\n    }\n\n    public void setUserId(String userId) {\n        this.userId = userId;\n    }\n\n    public String getContent() {\n        return content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n}<\/pre>\n<p>We also need to improve Secret:<\/p>\n<ul>\n<li>Make Secret <strong>extend the Model class<\/strong> to inherit the parent attributes.<\/li>\n<li>A Secret object will contain <strong>several Comment relationships<\/strong>, and <strong>several Like relationships<\/strong> (consisting of a simple list of users):<\/li>\n<\/ul>\n<pre class=\"lang:default decode:true \">package org.juzu.tutorial.models;\n\nimport java.util.HashSet;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Set;\n\npublic class Secret extends Model {\n\n    private String message;\n\n    private String imageURL;\n\n    private Set&lt;String&gt; likes;\n\n    private List&lt;Comment&gt; comments;\n\n    public Secret() {\n        likes = new HashSet&lt;String&gt;();\n        comments = new LinkedList&lt;Comment&gt;();\n    }\n\n    public Set&lt;String&gt; getLikes() {\n        Set&lt;String&gt; lks = new HashSet&lt;String&gt;(likes);\n        return lks;\n    }\n\n    public void setLikes(Set&lt;String&gt; likes) {\n        this.likes = likes;\n    }\n\n    public List&lt;Comment&gt; getComments() {\n        List&lt;Comment&gt; cms = new LinkedList&lt;Comment&gt;(comments);\n        return cms;\n    }\n\n    public void setComments(List&lt;Comment&gt; comments) {\n        this.comments = comments;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    public String getImageURL() {\n        return imageURL;\n    }\n\n    public void setImageURL(String imageURL) {\n        this.imageURL = imageURL;\n    }\n}<\/pre>\n<p>That\u2019s all we have to do! Our model is <strong>ready for the comment and like features<\/strong>. Now we need to improve the Secret service by providing an API to make it possible to add comments and like secrets.<\/p>\n<h2>Improve Secret Service<\/h2>\n<p>Our Secret service needs <strong>two more methods<\/strong> to manage the new functionalities: one to <strong>add a comment to Secret <\/strong>(the add Comment method) and another to <strong>like a secret<\/strong> (the add Like method).<\/p>\n<p><em>Note: As in step 2, our data is still saved in memory for now.<\/em><\/p>\n<p>First, declare these two methods in the Secret service interface:<\/p>\n<pre class=\"lang:default decode:true \">import org.juzu.tutorial.models.Comment;\n...\nimport java.util.Set;\n\n...\n\npublic Comment addComment(String secretId, Comment comment);\n\npublic Set&lt;String&gt; addLike(String secretId, String userId);\n<\/pre>\n<p>Then implement these 2 methods in the <span class=\"navCode\">SecretServiceMemImpl<\/span> and update the <span class=\"navCode\">addSecret<\/span> function:<\/p>\n<pre class=\"lang:default decode:true \">import org.juzu.tutorial.models.Comment;\n[...]\nimport java.util.*;\n[...]\n\npublic void addSecret(String message, String imageUrl) {\n    Secret secret = new Secret();\n    secret.setId(UUID.randomUUID().toString());\n    secret.setMessage(message);\n    secret.setImageURL(imageUrl);\n    secret.setCreatedDate(new Date());\n    secretsList.add(secret);\n  }\n\npublic Comment addComment(String secretId, Comment comment) {\n        Secret secret = getSecret(secretId);\n        if (secret != null) {\n            comment.setId(UUID.randomUUID().toString());\n            comment.setCreatedDate(new Date());\n\n            List&lt;Comment&gt; comments = secret.getComments();\n            comments.add(comment);\n            secret.setComments(comments);\n        }\n        return comment;\n    }\n\n    public Set&lt;String&gt; addLike(String secretId, String userId) {\n        Secret secret = getSecret(secretId);\n        if (secret != null) {\n            Set&lt;String&gt; likes = secret.getLikes();\n            \/\/ You can like only one time\n            if (!likes.contains(userId)) {\n                likes.add(userId);\n            }\n            secret.setLikes(likes);\n            return likes;\n        }\n        return null;\n    }\n\n    private Secret getSecret(String secretId) {\n        Secret secret = null;\n        for (Secret s : getSecrets()) {\n            if (s.getId().equals(secretId)) {\n                secret = s;\n            }\n        }\n        return secret;\n    }\n\n    ...<\/pre>\n<p>Now we are done with the <strong>service layer<\/strong>!<\/p>\n<p>This two methods are pretty simple and self-explanatory, so we don\u2019t have to spend much time on them.<\/p>\n<p><em>Note: In case you are in some doubt, I remind you that each step is available on a dedicated branch. To <a href=\"https:\/\/github.com\/juzu\/portlet-tutorial\/tree\/step-4\/tutorial-juzcret\" target=\"_blank\" rel=\"noopener\">download the result of step 4 on Github, click<\/a> here.<\/em><\/p>\n<p>Now it\u2019s time to go back to Juzu and improve the <strong>presentation layer<\/strong>.<\/p>\n<h2>Present like and comment<\/h2>\n<p>The comment and like actions will be managed using Ajax via the <strong>@Ajax<\/strong> Juzu annotation from the Juzu Ajax plugin. The Ajax plugin, like the Binding plugin, is already included in Juzu\u2019s core, so there is no need to add new dependencies in our pom.<\/p>\n<p>It\u2019s important to know that the <strong>Juzu-Ajax plugin<\/strong> depends on <strong>jQuery<\/strong>. So it\u2019s mandatory to declare jQuery, and we must do so in the previous step if we want to use this plugin.<\/p>\n<p>Then you are ready to <strong>use @Ajax in our controller<\/strong>. So let\u2019s add two new controller methods in JuZcretApplication.java:<\/p>\n<pre class=\"lang:default decode:true \">import juzu.*;\nimport juzu.plugin.ajax.Ajax;\nimport juzu.request.SecurityContext;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.juzu.tutorial.models.Comment;\nimport org.juzu.tutorial.services.SecretService;\n\nimport javax.inject.Inject;\nimport java.security.Principal;\nimport java.util.Set;\n\n...\n\n  private static final String ANONYMOUS = \"Anonymous\";\n\n  @Ajax\n    @Resource\n    public Response addComment(String secretId, @Mapped Comment comment, SecurityContext context) {\n      comment.setUserId(getCurrentUser(context));\n      Comment result = secretService.addComment(secretId, comment);\n      if (result != null) {\n        return Response.ok(new JSONObject(result).toString()).withMimeType(\"text\/json\");\n      } else {\n        return Response.status(503);\n      }\n    }\n\n    @Ajax\n    @Resource\n    public Response addLike(String secretId, SecurityContext context) {\n      Set&lt;String&gt; likes = secretService.addLike(secretId, getCurrentUser(context));\n      if (likes != null) {\n        return Response.ok(new JSONArray(likes).toString()).withMimeType(\"text\/json\");\n      } else {\n        return Response.status(503);\n      }\n    }\n\n    private String getCurrentUser(SecurityContext context) {\n        Principal user = context.getUserPrincipal();\n        if (user == null) {\n          return ANONYMOUS;\n        } else {\n          return user.getName();\n        }\n      }<\/pre>\n<p>The<strong> @Ajax annotation<\/strong> comes from the <strong>Juzu-Ajax plugin<\/strong>; it provides us with a convenient <strong>Ajax calling method: jzLoad, jzAjax<\/strong>. We\u2019ll use this later in secret.js.<\/p>\n<p><strong>@Resource<\/strong> is a new type of Controller. Resource controllers operate pretty much the way a view controller does, except that they must produce the entire response sent to the client. That is a perfect method for implementing an Ajax request.<\/p>\n<p><strong>@Mapped<\/strong> allows us to map the request parameters to Bean types. Juzu automatically does the conversion between the primary types and the request parameters, but for a Bean, we need to declare the parameters with @Mapped. Consequently, the parameters of the \u201cadd secret\u201d form will be automatically mapped to the attributes of the @Mapped Bean.<\/p>\n<p>Juzu also <strong>automatically<\/strong> <strong>injects <\/strong>some <strong>contextual objects<\/strong> that you can use:<\/p>\n<ul>\n<li>SecurityContext (provides security info, such as the current logged in user)<\/li>\n<li>HttpContext<\/li>\n<li>RequestContext<\/li>\n<li>ApplicationContext<\/li>\n<li>UserContext<\/li>\n<li>ClientContext<\/li>\n<\/ul>\n<p>You just need to declare the contextual objects in the <strong>method sign,<\/strong> as we do above for <strong>SecurityContext,<\/strong> and Juzu will <strong>inject them automatically<\/strong> at runtime.<\/p>\n<p>You may have noticed that we sent a Json data response to our client by declaring the <strong>MimeType<\/strong> as text\/json. Now we need to handle this response on the client side.<\/p>\n<h2>Template<\/h2>\n<p>We need to add two new buttons to <strong>like a secret<\/strong> and <strong>comment a secret<\/strong> in the secretWall.gtmpl. Then, for the <strong>Comment feature,<\/strong> we also need to display a popover to show the list of current comments and add a new comment.<\/p>\n<p>Replace the current content of &lt;ul class=&#8221;secret-wall-list clearfix&#8221;&gt; as follows:<\/p>\n<pre class=\"lang:default decode:true \">[...]\n&lt;ul class=\"secret-wall-list clearfix\"&gt;\n\t&lt;% secretsList.each { secret -&gt; %&gt;\n\t&lt;li class=\"secret\" data-secretId=\"${secret.id}\"&gt;\n\t\t&lt;div class=\"secret-image\" style=\"background-image: url('${secret.imageURL}')\"&gt;\n\n\t\t\t&lt;div class=\"secret-mesage\"&gt;${secret.message}&lt;\/div&gt;\n\n\t\t\t&lt;div class=\"secret-action\"&gt;\n\t\t\t\t&lt;a class=\"btn-like secr-toggle-link toggle-like-comment\" href=\"#\"&gt;\n\t\t\t\t\t&lt;i class=\"uiIconThumbUp uiIconWhite\"&gt;&lt;\/i&gt;&lt;span class=\"numb\"&gt;&lt;\/span&gt;\n\t\t\t\t&lt;\/a&gt;\n\t\t\t\t&lt;a class=\"btn-popup-comment secr-toggle-link toggle-write-comment\" href=\"#\"&gt;\n\t\t\t\t\t&lt;i class=\"uiIconComment uiIconWhite\"&gt;&lt;\/i&gt;&lt;span class=\"numb\"&gt;&lt;\/span&gt;\n\t\t\t\t&lt;\/a&gt;\n\t\t\t&lt;\/div&gt;\n\n\t\t\t&lt;div class=\"popover popover-secret fade top\"&gt;\n\t\t\t\t&lt;button class=\"closePopover close\" type=\"button\"&gt;&amp;times;&lt;\/button&gt;\n\t\t\t\t&lt;div class=\"arrow\"&gt;&lt;\/div&gt;\n\n\t\t\t\t&lt;div class=\"popover-content\"&gt;\n\t\t\t\t\t&lt;div class=\"secr-comments-box\"&gt;\n\t\t\t\t\t\t&lt;ul class=\"secr-comments-list\"&gt;\n\t\t\t\t\t\t\t&lt;% secret.getComments().each { comment -&gt; %&gt;\n\t\t\t\t\t\t\t&lt;li&gt;&lt;!--Add class .open-popover to display popover --&gt;\n\t\t\t\t\t\t\t\t&lt;div class=\"media\"&gt;\n\t\t\t\t\t\t\t\t\t&lt;a class=\"pull-left\" href=\"https:\/\/localhost:8080\/portal\/intranet\/profile\/${comment.userId}\"&gt;\n\t\t\t\t\t\t\t\t\t\t&lt;img src=\"https:\/\/localhost:8080\/social-resources\/skin\/images\/ShareImages\/UserAvtDefault.png\" alt=\"avatar\"&gt;\n\t\t\t\t\t\t\t\t\t&lt;\/a&gt;\n\n\t\t\t\t\t\t\t\t\t&lt;div class=\"media-body\"&gt;\n\t\t\t\t\t\t\t\t\t\t&lt;div&gt;\n\t\t\t\t\t\t\t\t\t\t\t&lt;a class=\"cm-user-name\" href=\"https:\/\/localhost:8080\/portal\/intranet\/profile\/${comment.userId}\"&gt;${comment.userId}&lt;\/a&gt;\n\t\t\t\t\t\t\t\t\t\t\t&lt;span class=\"cm-time\"&gt;${comment.createdDate}&lt;\/span&gt;\n                                        &lt;\/div&gt;\n\n\t\t\t\t\t\t\t\t\t\t&lt;div class=\"cm-content\"&gt;${comment.content}&lt;\/div&gt;\n\t\t\t\t\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t\t\t&lt;\/li&gt;\n\t\t\t\t\t\t\t&lt;% } %&gt;\n\t\t\t\t\t\t&lt;\/ul&gt;\n\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t&lt;div class=\"secr-create-comment clearfix\"&gt;\n\t\t\t\t\t\t&lt;button class=\"btn-comment btn btn-primary pull-right\"&gt;Comment&lt;\/button&gt;\n\n\t\t\t\t\t\t&lt;div class=\"secr-write-comment \"&gt;\n\t\t\t\t\t\t\t&lt;div class=\"inner\"&gt;\n\t\t\t\t\t\t\t\t&lt;div class=\"media\"&gt;\n\t\t\t\t\t\t\t\t\t&lt;a href=\"#\" class=\"pull-left\"&gt;\n\t\t\t\t\t\t\t\t\t\t&lt;img src=\"https:\/\/localhost:8080\/social-resources\/skin\/images\/ShareImages\/UserAvtDefault.png\" alt=\"avatar\"&gt;\n\t\t\t\t\t\t\t\t\t&lt;\/a&gt;\n\n\t\t\t\t\t\t\t\t\t&lt;div class=\"media-body\"&gt;\n\t\t\t\t\t\t\t\t\t\t&lt;textarea name=\"comment\" class=\"secret-add-comment\" placeholder=\"Add your comment\"&gt;&lt;\/textarea&gt;\n\t\t\t\t\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t\t&lt;\/div&gt;\n\t\t\t\t&lt;\/div&gt;\n\t\t\t&lt;\/div&gt;\n\t\t&lt;\/div&gt;\n\t&lt;\/li&gt;\n\t&lt;% } %&gt;\n&lt;\/ul&gt;<\/pre>\n<p>After that we need to improve our juzcret.less file to manage the new added class. Update the existing less file with these:<\/p>\n<pre class=\"lang:default decode:true \">\/\/Variables\n\/\/====================\n\n[...]\n\n@secretActionHeight: 43px;\n\n\/\/Mixins\n\/\/====================\n\n[...]\n\n\/\/Border Radius CSS3\n.border-radius(@border-radius) {\n  -webkit-border-radius: @border-radius;\n  -moz-border-radius: @border-radius;\n  -ms-border-radius: @border-radius; \/\/IE9 only\n  border-radius: @border-radius;\n}\n\/\/Transform CSS3\n.transform(@transform) {\n  -webkit-transform: @transform;\n  -moz-transform: @transform;\n  -ms-transform: @transform; \/\/IE9 only\n  transform: @transform;\n}\n\/\/Transitions CSS3\n.transition(@transition) {\n  -webkit-transition: @transition;\n  -o-transition: @transition;\n  transition: @transition;\n}\n\/\/Translate CSS\n.translate(@x; @y) {\n  -webkit-transform: translate(@x, @y);\n  -ms-transform: translate(@x, @y); \/\/IE9 only\n  -o-transform: translate(@x, @y);\n  transform: translate(@x, @y);\n}\n\n\/\/Common Style\n\/\/====================\n\n[...]\n\n\/\/After secret-wall-heading, remove the secret-wall-list section and replace by:\n\/\/After secret-wall-heading, remove the secret-wall-list section and replace by:\n.secret-wall-list {\n  margin: 0 -@secretItemGutter;\n  &gt; li {\n    float: left;\n    padding: @secretItemGutter;\n    width: 100% \/ 3;\n    .secret-image {\n      background-repeat: no-repeat;\n      background-size: cover;\n      background-color: #000;\n      position: relative;\n      height: @heightSecretItem;\n      width: 100%;\n      display: block;\n      &amp;:before {\n        background: none repeat scroll 0 0 rgba(0, 0, 0, 0.5);\n        content: \"\";\n        display: block;\n        height: 100%;\n        position: absolute;\n        width: 100%;\n      }\n    }\n    .secret-mesage {\n      bottom: 65px;\n      color: #fff;\n      font-size: 20px;\n      font-weight: normal;\n      left: 25px;\n      line-height: 24px;\n      position: absolute;\n      right: 25px;\n      text-align: center;\n      top: 25px;\n    }\n    .secret-action {\n      border-top: 1px solid rgba(255, 255, 255, 0.5);\n      bottom: 0;\n      height: 0;\n      left: 0;\n      line-height: @secretActionHeight;\n      padding: 0 25px;\n      position: absolute;\n      right: 0;\n      text-align: right;\n      overflow: hidden;\n      .transition(all 200ms ease-out 0s);\n\n      .secr-toggle-link {\n        + .secr-toggle-link {\n          margin-left: 15px;\n        }\n        &gt; i {\n          margin-right: 5px;\n        }\n        .numb {\n          color: #fff;\n          font-size: 13px;\n        }\n        .uiIconComment {\n          margin-top: 2px;\n        }\n      }\n    }\n    .popover {\n      max-width: 500px;\n      top: auto;\n      bottom: 46px;\n      left: auto;\n      right: -205px;\n      width: 500px;\n      margin: 0px;\n    }\n    .close {\n      line-height: 16px;\n      padding: 1px 5px;\n      position: absolute;\n      right: 0;\n      top: 0;\n    }\n    .media {\n      &gt; .pull-left {\n        &gt; img {\n          width: 36px;\n          height: 36px;\n          .border-radius(2px);\n        }\n      }\n    }\n    &amp;:hover, &amp;.open-popover {\n      .secret-action {\n        height: @secretActionHeight;\n      }\n    }\n    &amp;.open-popover {\n      .popover-secret {\n        .opacity(1);\n        display: block;\n      }\n    }\n    &amp;:nth-child(3n+3) {\n      .popover{\n        right: -1px;\n        .arrow {\n          left: auto;\n          right: 34px;\n        }\n      }\n    }\n  }\n}\n.secret-popup {\n  width: 500;\n  height: 280px;\n  background: #fff;\n  border: 1px solid rgba(0, 0, 0, 0.5);\n  display: none;\n  &amp;.in {\n    display: block;\n  }\n}\n.popover-secret {\n  .popover-content {\n    padding: 15px;\n  }\n}\n.secr-comments-box {\n  .secr-viewall {\n    font-size: 13px;\n    margin-bottom: 15px;\n  }\n}\n.secr-comments-list {\n  margin-bottom: 20px;\n  max-height: 150px;\n  overflow: auto;\n  &gt; li {\n    line-height: 18px;\n    + li {\n      margin-top: 20px;\n    }\n    .media {\n      &gt; .pull-left {\n        display: block;\n      }\n    }\n    .cm-user-name {\n      font-weight: bold;\n    }\n    .cm-time {\n      color: #999999;\n      font-size: 12px;\n      margin-left: 5px;\n    }\n  }\n}\n.secr-create-comment {\n  .btn-primary {\n    float: right;\n    margin-left: 10px;\n    margin-top: 3px;\n  }\n  .secr-write-comment {\n    .fluid-colum {\n      float: left;\n      width: 100%;\n      &gt; .inner {\n        margin-left: 46px;\n      }\n    }\n    .media {\n      &gt; .media-body {\n        margin-left: 46px;\n        padding-top: 3px;\n      }\n    }\n    textarea {\n      height: 29px;\n      resize: none;\n      width: 100%;\n      &amp;:focus {\n        box-shadow:none;\n      }\n    }\n  }\n}\n\n[...]<\/pre>\n<p><em>Note: [\u2026] means sections already added in step 3. If you feel a little bit lost, look for the <a href=\"https:\/\/github.com\/juzu\/portlet-tutorial\/blob\/step-4\/tutorial-juzcret\/src\/main\/java\/org\/juzu\/tutorial\/assets\/styles\/juzcret.less\" target=\"_blank\" rel=\"noopener\">juzcret.less file<\/a> directly on the github project.<\/em><\/p>\n<p>Now we have two buttons for the <strong>comment and like features<\/strong> and a popover to display the list of comments.<\/p>\n<p>Now that we have finished the UI part, we need to add some <strong>js handlers<\/strong> to manage these two features using Ajax.<\/p>\n<h3>Javascript Handler<\/h3>\n<p>Update the secret.js file by adding the <strong>snippet<\/strong> in charge of the like feature:<\/p>\n<pre class=\"lang:default decode:true \">(function ($) {\n\n    $(document).ready(function () {\n\n        [...]\n\n    });\n\n    \/\/Ajax for managing like function\n    $(document).on('click.juzu.secret.addLike', '.btn-like', function () {\n        var jLike = $(this);\n        var jSecret = jLike.closest('.secret');\n        var secretId = jSecret.attr('data-secretId');\n\n        jLike.jzAjax('JuZcretApplication.addLike()', {\n            data: {'secretId': secretId},\n            success: function (data) {\n                var jLikeIcon = jSecret.find('.btn-like');\n                jLikeIcon.find('.numb').text($(data).size());\n            }\n        });\n        return false;\n    });\n\n})($);<\/pre>\n<p>This snippet registers an event on our <strong>Like<\/strong> button. The interesting line to notice here is<\/p>\n<pre class=\"lang:default decode:true \">jLike.jzAjax('JuZcretApplication.addLike()', [...]);<\/pre>\n<p>The<strong> jzAjax and jzLoad<\/strong> functions are <strong>jQuery plugins<\/strong> provided by the Juzu Ajax plugin. They replace the standard Ajax and Load jQuery functions. They accept the <strong>same arguments<\/strong> but the <strong>URL is replaced by the controller method<\/strong>.<\/p>\n<p>All we need to do is provide the controller method, like <span class=\"navCode\">JuZcretApplication.addLike()<\/span>, and Juzu takes care to find the expected URL, and performs the <strong>Ajax request<\/strong> (using jQuery).<\/p>\n<p>Similarly, we also have another three JS listeners for the <strong>comment feature<\/strong>. Add them just after the snippet which we just added above:<\/p>\n<pre class=\"lang:default decode:true \">   \/\/Open the popover for displaying and adding comments\n    $(document).on('click.juzu.secret.openPopover', '.btn-popup-comment', function () {\n        var jComment = $(this);\n        var jSecret = jComment.closest('.secret');\n        jSecret.addClass('open-popover');\n    });\n\n    \/\/Close the popover for displaying and adding comments\n    $(document).on('click.juzu.secret.closePopover', '.closePopover', function () {\n        var jComment = $(this);\n        var jSecret = jComment.closest('.secret');\n        jSecret.removeClass('open-popover');\n    });\n\n    \/\/Ajax for managing comment function\n    $(document).on('click.juzu.secret.addComment', '.btn-comment', function () {\n        var jComment = $(this);\n        var jSecret = jComment.closest('.secret');\n        var secretId = jSecret.attr('data-secretId');\n\n        jComment.jzAjax('JuZcretApplication.addComment()', {\n            data: {'secretId': secretId, 'content': jSecret.find('.secret-add-comment').val()},\n            success: function (data) {\n                if (typeof(data) == 'string') {\n                    \/\/error response\n                    alert(data);\n                } else {\n                    \/\/update html\n                    var cList = \"\";\n                    var cCounter = 0;\n                    $(data).each(function (idx, elem) {\n                        if (elem.content) {\n                            cList +=\n                                \"&lt;div class='media'&gt;\" +\n                                \"&lt;a class='pull-left' href='https:\/\/localhost:8080\/portal\/intranet\/profile\/\" + elem.userId + \"'&gt;\" +\n                                \"&lt;img src='https:\/\/localhost:8080\/social-resources\/skin\/images\/ShareImages\/UserAvtDefault.png' alt='avatar'&gt;\" +\n                                \"&lt;\/a&gt;\" +\n                                \"&lt;div class='media-body'&gt;\" +\n                                \"&lt;div&gt;\" +\n                                \"&lt;a class='cm-user-name' href='https:\/\/localhost:8080\/portal\/intranet\/profile\/\" + elem.userId + \"'&gt;\" + elem.userId + \"&lt;\/a&gt; \" +\n                                \"&lt;span class='cm-time'&gt;\" + elem.createdDate + \"&lt;\/span&gt;\" +\n                                \"&lt;\/div&gt;\" +\n                                \"&lt;div class='cm-content'&gt;\" + elem.content + \"&lt;\/div&gt;\" +\n                                \"&lt;\/div&gt;\" +\n                                \"&lt;\/div&gt;\";\n                            cCounter++;\n                        }\n                    });\n                    var html = jSecret.find('.secr-comments-list').html();\n                    jSecret.find('.secr-comments-list').html(html + cList);\n                    var jCommentIcon = jSecret.find('.btn-popup-comment');\n                    var jCommentNumb = jCommentIcon.find('.numb').text();\n                    jCommentIcon.find('.numb').text(jCommentNumb+cCounter);\n                }\n            }\n        });\n        return false;\n    });<\/pre>\n<p>One major difference in this second handler is that we <strong>handle error response<\/strong>. Why? To prevent a user from submitting an empty comment.<\/p>\n<p>This is why our Juzu controller should be aware of invalid data that a user submitted. Let\u2019s move to the <strong>data validation<\/strong> and <strong>error handling<\/strong> provided by Juzu.<\/p>\n<h3>Adding validation<\/h3>\n<p>Juzu provides controller handler <strong>parameter validation<\/strong> via the <strong>Bean Validation framework<\/strong>. To use it, we need to add the juzu-validation plugin in our <span class=\"navCode\">pom.xml<\/span>:<\/p>\n<pre class=\"lang:default decode:true \">&lt;dependency&gt;\n  &lt;groupId&gt;org.juzu&lt;\/groupId&gt;\n  &lt;artifactId&gt;juzu-plugins-validation&lt;\/artifactId&gt;\n  &lt;version&gt;1.0.0-cr1&lt;\/version&gt;\n&lt;\/dependency&gt;<\/pre>\n<p>And now, all we need to do is add an annotation to the model attributes. Update the Comment class as shown below:<\/p>\n<pre class=\"lang:default decode:true \">package org.juzu.tutorial.models;\n\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Pattern;\n\npublic class Comment extends Model {\n\n  private String userId;\n  @Pattern(regexp = \"^.+$\", message = \"Comment content must not be empty\")\n  @NotNull(message = \"Comment content is required\")\n  private String            content;\n\n  [...]\n}<\/pre>\n<p>Thanks to the <strong>@Pattern and @NotNull annotations<\/strong>, the validation framework will validate the parameter and <strong>throw a validation error<\/strong> if needed.<\/p>\n<p>We need to also declare our intention to <strong>perform this validation<\/strong> in the controller. In our case, we want to validate any new comment coming from a user. This process is managed by the addComment Resource in JuZcretApplication, where we need to add the <strong>@Valid annotation<\/strong> to the Comment parameter:<\/p>\n<pre class=\"lang:default decode:true \">import javax.validation.Valid;\n\n[...]\n\n  @Ajax\n  @Resource\n  public Response addComment(String secretId, @Mapped @Valid Comment comment, SecurityContext context) {\n  [...]\n  }<\/pre>\n<p>Now if a user tries to enter an invalid comment, the validation framework will throw an error. Our job is not totally finished\u2026We also need to properly cache this error.<\/p>\n<p>Juzu provides <a href=\"https:\/\/juzuweb.org\/reference\/index.html#_handling_validation_errors\" target=\"_blank\" rel=\"noopener\" class=\"broken_link\">two solutions for handling<\/a> errors:<\/p>\n<ul>\n<li>Use the request lifecycle.<\/li>\n<li>Use the error handler.<\/li>\n<\/ul>\n<p>In our case, we will use the request lifecycle that allows us to handle the error in the controller.<\/p>\n<p>What we need is to <strong>analyze the Response<\/strong> and check to see if the type is a Validation Error. If it is, we simply get the error message and update the response to send it properly to the client.<\/p>\n<p>To do this we need our controller, JuZcretApplication, to implement the interface RequestLifeCycle and override the endRequest method:<\/p>\n<pre class=\"lang:default decode:true \">import juzu.plugin.validation.ValidationError;\nimport juzu.request.RequestContext;\nimport juzu.request.RequestLifeCycle;\n[...]\nimport javax.validation.ConstraintViolation;\n\npublic class JuZcretApplication implements RequestLifeCycle {\n\n      @Override\n      public void endRequest(RequestContext context) {\n          Response response = context.getResponse();\n          if (response instanceof ValidationError) {\n              ValidationError error = (ValidationError)response;\n              Set&lt;ConstraintViolation&lt;Object&gt;&gt; violations = error.getViolations();\n\n              String msg = violations.iterator().next().getMessage();\n              response = Response.ok(msg).withMimeType(\"text\/html\");\n              context.setResponse(response);\n          }\n      }\n\n      @Override\n      public void beginRequest(RequestContext context) {\n      }\n}<\/pre>\n<p>On the client side, we have already implemented our JS handler to display the error message:<\/p>\n<pre class=\"lang:default decode:true \">     ...\n\n      success: function(data) {\n        if (typeof(data) == 'string') {\n          \/\/error response\n          alert(data);\n        } else {\n             ...\n        }<\/pre>\n<p>Our JuZcret app now provides a pretty good feature for the end user:<\/p>\n<p><a href=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2014\/12\/02-comment.png\" alt=\"02-comment\" width=\"650\" class=\"aligncenter size-medium wp-image-8007\" srcset=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment.png 999w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-300x95.png 300w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-768x244.png 768w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-720x228.png 720w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-500x159.png 500w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-360x114.png 360w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-200x63.png 200w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-100x32.png 100w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/02-comment-70x22.png 70w\" sizes=\"(max-width: 999px) 100vw, 999px\" \/><\/a><\/p>\n<p><em>Note: If you implement this step just after following the previous steps, you just have to compile your project, paste the new created war in eXo Platform, and start the server. Then access your JuZcret page and take a look at the result. If the result is not correct, <a href=\"https:\/\/community.exoplatform.com\/portal\/g\/:spaces:juzu\/juzu\/wiki\/Develop_Juzu_Portlet_with_JRebel\" target=\"_blank\" rel=\"noopener\">configure your project to use JRebel<\/a>, compile the code, and deploy it in eXo Platform as explained in step 1.<\/em><\/p>\n<p>What is missing, so far, is an <strong>administrator<\/strong> to manage our application. An administrator must have the availability to configure the portlet. For instance, he or she may want to disable the comment feature.<\/p>\n<p>To do this, what could be better than adding a <strong>portlet edit mode<\/strong>?<\/p>\n<h3>Portlet Edit Mode<\/h3>\n<p>Juzu portlet is <strong>JSR286 compliant portlet<\/strong>. To provide an edit mode, we need to tell the portlet container that our portlet support should show an <strong>edit mode<\/strong>. That\u2019s why we need to modify our <span class\"navcode\"=\"\">portlet.xml<\/span> as shown below:<\/p>\n<pre class=\"lang:default decode:true \">&lt;portlet&gt;\n  &lt;portlet-name&gt;JuzcretApplication&lt;\/portlet-name&gt;\n   ...\n   &lt;supports&gt;\n       &lt;mime-type&gt;text\/html&lt;\/mime-type&gt;\n       &lt;portlet-mode&gt;edit&lt;\/portlet-mode&gt;\n     &lt;\/supports&gt;\n...\n&lt;\/portlet&gt;<\/pre>\n<p>Now the JuZcret portlet has two modes: <strong>edit and view mode<\/strong>. We need to create a new template for the edit mode. In the templates package, add a new file, <span class\"navcode\"=\"\">editMode.gtmpl<\/span>, to display a checkbox to enable the user to comment on secrets, or not:<\/p>\n<pre class=\"lang:default decode:true \">#{param name=enableComment\/}\n&lt;form action=\"@{JuZcretApplication.enableComment()}\" method=\"POST\" role=\"form\"&gt;\n    &lt;h5&gt;Configuration&lt;\/h5&gt;\n    &lt;input type=\"checkbox\" name=\"enableComment\" &lt;%=enableComment ? \"checked\" : \"\" %&gt;\/&gt;Enable Comment\n    &lt;button type=\"submit\"&gt;Save&lt;\/button&gt;\n&lt;\/form&gt;<\/pre>\n<p>Our JuZcret application configuration will rely on the <strong>portlet preference mechanism<\/strong>.<\/p>\n<p>Juzu\u2019s framework provide a <strong>juzu-portlet plugin<\/strong> which helps to bind the portlet preference to our IOC container and allows the user to inject and use <strong>PortletPreferences<\/strong> in our controller to store the configuration data of our portlet.<\/p>\n<p>To use this plugin, we need to add <strong>juzu-plugins-portlet<\/strong> and <strong>portlet-api<\/strong> dependency in the pom.xml:<\/p>\n<pre class=\"lang:default decode:true \">&lt;dependency&gt;\n\t&lt;groupId&gt;javax.portlet&lt;\/groupId&gt;\n\t&lt;artifactId&gt;portlet-api&lt;\/artifactId&gt;\n\t&lt;version&gt;2.0&lt;\/version&gt;\n\t&lt;scope&gt;provided&lt;\/scope&gt;\n&lt;\/dependency&gt;\n&lt;dependency&gt;\n\t&lt;groupId&gt;org.juzu&lt;\/groupId&gt;\n\t&lt;artifactId&gt;juzu-plugins-portlet&lt;\/artifactId&gt;\n\t&lt;version&gt;1.0.0-cr1&lt;\/version&gt;\n&lt;\/dependency&gt;<\/pre>\n<p>Now we can inject in our JuZcretApplication controller PortletPreferences using the @Inject annotation. We use it in a new <strong>action controller<\/strong> method, named enableComment, which manages the submission of the edit form:<\/p>\n<pre class=\"lang:default decode:true \">[...]\nimport juzu.bridge.portlet.JuzuPortlet;\n[...]\nimport javax.portlet.PortletMode;\nimport javax.portlet.PortletPreferences;\nimport javax.portlet.ReadOnlyException;\nimport javax.portlet.ValidatorException;\nimport java.io.IOException;\n[...]\n\npublic class JuZcretApplication  implements RequestLifeCycle {\n\n    @Inject\n    PortletPreferences prefs;\n\n    public static final String ENABLE_COMMENT = \"enableComment\";\n\n    @Action\n    public Response.View enableComment(String enableComment) throws ReadOnlyException, ValidatorException, IOException {\n        if (\"on\".equals(enableComment)) {\n            enableComment = \"true\";\n        }\n        prefs.setValue(ENABLE_COMMENT, enableComment);\n        prefs.store();\n        return JuZcretApplication_.index().with(JuzuPortlet.PORTLET_MODE, PortletMode.VIEW);\n    }<\/pre>\n<p>After saving the portlet preference, notice that we <strong>redirect<\/strong> the portlet to <strong>View mode<\/strong> by responding with a <strong>Juzu property<\/strong>, the <span class\"navcode\"=\"\">JuzuPortlet.PORTLET_MODE<\/span> property type with the value <span class\"navcode\"=\"\">PortletMode.VIEW<\/span>.<\/p>\n<p>Now JuZcret can be configured to <strong>disable the comment feature<\/strong>. This means that we have to adapt our <span class\"navcode\"=\"\">secretWall.gtmpl<\/span> template to display or not display the form for submitting comment by using the enableComment parameter:<\/p>\n<pre class=\"lang:default decode:true \">#{param name=enableComment\/}\n\n  [\u2026]\n\n  &lt;% if (enableComment) { %&gt;\n      &lt;div class=\"secret-action\"&gt;\n      [\u2026]\n      &lt;\/div&gt;\n  &lt;% } %&gt;\n\n  [\u2026]<\/pre>\n<p>From a security perspective, hiding the social toolbar on the bottom is not enough to prevent a user from commenting, but for the sake of simplicity, we have decided that this solution is acceptable for this tutorial. So when you have disabled comments, it is no longer possible to like or comment on a secret. Social features are deactivated.<\/p>\n<p>The last step is to inject the new <span class\"navcode\"=\"\">editMode.gtmpl<\/span> template to the controller and modify the index View controller to <strong>adapt the display<\/strong> in accordance with the current <strong>Portlet mode<\/strong>:<\/p>\n<pre class=\"lang:default decode:true \"> @Inject\n  @Path(\"editMode.gtmpl\")\n  org.juzu.tutorial.templates.editMode editMode;\n\n  @View\n  public Response.Content index(RequestContext context) {\n    boolean enableComment = Boolean.parseBoolean(prefs.getValue(ENABLE_COMMENT, \"false\"));\n\n    if (PortletMode.EDIT.equals(context.getProperty(JuzuPortlet.PORTLET_MODE))) {\n      return editMode.with().enableComment(enableComment).ok();\n    } else {\n      return secretWall.with().enableComment(enableComment)\n.secretsList(secretService.getSecrets()).ok();\n    }\n   }<\/pre>\n<p>To find out the current Portlet mode, we use the RequestContext object, automatically injected by Juzu, that allows us to check the property <span class\"navcode\"=\"\">JuzuPortlet.PORTLET_MODE<\/span>.<\/p>\n<p>Recompile your application with<\/p>\n<pre class=\"lang:default decode:true \">$ mvn clean install<\/pre>\n<p>Then stop the eXo Platform, copy\/paste the new created war in the webapp folder, and restart the eXo Platform.<\/p>\n<p>Go to the JuZcret page and click on Edit \u2192 Page \u2192 Edit Layout. Then mouse over the &#8220;Juzu Secret Application&#8221; and click on the &#8220;Edit Portlet&#8221; icon.<\/p>\n<p>Here you can disable comments:<\/p>\n<p><a href=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments.png\" alt=\"03-disable-comments\" width=\"650\" class=\"aligncenter size-medium wp-image-8008\" srcset=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments.png 1270w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-300x160.png 300w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-1024x545.png 1024w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-768x409.png 768w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-1250x665.png 1250w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-720x383.png 720w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-500x266.png 500w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-360x192.png 360w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-200x106.png 200w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-100x53.png 100w, https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/03-disable-comments-56x30.png 56w\" sizes=\"(max-width: 1270px) 100vw, 1270px\" \/><\/a><\/p>\n<p>After unchecking &#8220;enable comment,&#8221; save and close the edit mode. You <strong>cannot add a new comment<\/strong> via the secret wall:<\/p>\n<p><a href=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/12\/04-disabled-comments.png\" class=\"broken_link\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2014\/12\/04-disabled-comments.png\" alt=\"04-disabled-comments\" width=\"650\" class=\"aligncenter size-medium wp-image-8009\"><\/a><\/p>\n<p>We are at the end of step 4 with a nice social application, but our JuZcret application is still missing something important before it will be ready for production: data is not persistent yet. To fix this, come back to this blog next week to read step 5.<\/p>\n<p><em>The final source of step 4 is available for <a href=\"https:\/\/github.com\/juzu\/portlet-tutorial\/tree\/step-4\/tutorial-juzcret\" target=\"_blank\" rel=\"noopener\">downloading on Github<\/a>.<\/em><\/p>\n<p><em>Previous and next steps:<br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/learn-how-to-develop-great-juzu-portlets-for-exo-platform\">Learn how to develop great Juzu portlets for eXo Platform!<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-2-viewing-and-posting-secrets\/\">Step 2: viewing and posting secrets<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-3-building-a-sexy-secret-wall\/\">Step 3: Building a sexy secret wall<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-4-adding-likes-and-comments-for-secret\/\">Step 4: adding Likes and Comments for Secret<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-5-saving-secrets-in-the-jcr\/\">Step 5: Saving Secrets in the JCR<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-6-let-the-whole-world-create-new-secrets\/\">Step 6: Let the whole world create new secrets<\/a><br \/>\n&#8211; <a href=\"https:\/\/www.exoplatform.com\/blog\/developing-juzu-portlets-step-7-finishing-the-job-properly-with-unit-test\/\">Step 7: Finishing the job properly with unit test<\/a><\/em><\/p>\n<p><b><a href=\"https:\/\/community.exoplatform.com\/portal\/dw\/\" target=\"_blank\" rel=\"noopener\">Join the eXo tribe<\/a> by registering for the community and get updates, tutorials, support, and access to the Platform and add-on downloads!<\/b><\/p>\n<p><!--begin adv-events--><\/p>\n<div class=\"adv-events\" style=\"background: #476fad;padding: 30px 20px;color: white\">\n<div class=\"media\">\n<div class=\"pull-right\"><a href=\"#\"><br \/>\n<img decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-6587 alignright\" src=\"https:\/\/www.exoplatform.com\/blog\/wp-content\/uploads\/2014\/02\/how-to-make-the-most-of-eXo-platform41.png\" alt=\"make-the-most-out-of-eXo-platform4\" width=\"161\" height=\"85\"><br \/>\n<\/a><\/div>\n<div class=\"media-body\">\n<h4 class=\"media-heading\">Make the most out of eXo Platform 4<\/h4>\n<p>Register to the next webinar and get a complete overview of what you can do with eXo Platform 4. <strong><a href=\"https:\/\/www.exoplatform.com\/contact-us\/\">Reserve your seat now!<\/a><\/strong><\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!--end adv-events--><\/p>\n","protected":false},"excerpt":{"rendered":"Previous steps: &#8211; Learn how to develop great Juzu portlets for eXo Platform! &#8211; Step 2: viewing and posting secrets &#8211; Step 3: Building a sexy secret wall In the previous blog post, we implemented the UI of our JuZcret application. We are now able to look at Secret on the secret wall and share [&hellip;]","protected":false},"author":7,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[699],"tags":[],"lang":"en","translations":{"en":37745},"pll_sync_post":[],"_links":{"self":[{"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/posts\/37745"}],"collection":[{"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/comments?post=37745"}],"version-history":[{"count":0,"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/posts\/37745\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/media?parent=37745"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/categories?post=37745"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.exoplatform.com\/blog\/wp-json\/wp\/v2\/tags?post=37745"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}