This time we're going to add a button after every post that will allow users to hide/show the post.
But first, we need to understand how these buttons are generated.
They are defined by the post_menu
site setting:
This setting determine which items will appear on the post menu and their order:
This list is used by the PostMenuView
which renders the menu below a post using buffered rendering for performance. We've already seen the buffer rendering in the previous tutorial. It just means that we're pushing strings into a buffer.
This view has 2 interesting methods:
render: function(buffer) {
var post = this.get('post');
buffer.push("<nav class='post-controls'>");
this.renderReplies(post, buffer);
var self = this;
Discourse.get('postButtons').toArray().reverse().forEach(function(button) {
var renderer = "render" + button;
if(self[renderer]) self[renderer](post, buffer);
});
buffer.push("</nav>");
},
This render
method is called by EmberJs whenever it needs to render the view. First, it will render the replies button (call to this.renderReplies
) if there is any. Then, it will iterate through Discourse.get('postButtons')
which is a basic wrapper around the post_menu
site setting and will call the methods named render<Button>
if it is defined on the view. For example, for the reply
button, it will call the method named renderReply
.
click: function(e) {
var $target = $(e.target);
var action = $target.data('action') || $target.parent().data('action');
if (!action) return;
var handler = this["click" + action.capitalize()];
if (!handler) return;
handler.call(this);
},
This click
method is called by EmberJs whenever the user clicks on the view (that is, on any button). That method retrieve the name of the action
to call by looking at the data-action
attribute of the targeted element or its parent's. If an action is found, it will look for a method name click<Action>
in the view and call it if it is defined in the the view. For example, if the action is reply
, it will call the clickReply
method.
So, these 2 methods allow us to control both the rendering of our button by defining a renderSomething
method and control the behavior whenever the user clicks on the button by defining a clickSomething
method.
Let's call our button hide and add it to the post_menu
site setting:
We're now all set to start writing our plugin.
Let's start with our usual suspect, plugin.rb
, our entry point:
# name: hide post
# about: add a button at the end of every post allowing users to hide the post
# version: 0.1
# authors: Régis Hanol
register_asset "javascripts/hide_post.js"
This only register a single asset. Let's dig into this javascript file:
Discourse.PostMenuView.reopen({
renderHide: function(post, buffer) {
var direction = !!post.getWithDefault("temporarily_hidden", false) ? "down" : "up";
buffer.push('<button title="' + direction + '" data-action="hide">');
buffer.push('<i class="fa fa-chevron-' + direction + '"></i>');
buffer.push('</button>');
},
clickHide: function() {
$("#post_" + this.get("post.post_number") + " .cooked").toggle();
this.toggleProperty("post.temporarily_hidden");
}
});
The first line (Discourse.PostMenuView.reopen
) is the Ember way of reopening a class from another file. This allows us to add instance methods and properties that are shared across all instances of that class. Which is exactly what we want here. So, as we've seen earlier, we need to define 2 methods: renderHide
and clickHide
.
The renderHide
method is used to render the hide button which is composed of a chevron icon. It looks for the temporarily_hidden
property on post to determine the direction of the chevron.
The clickHide
method is toggling the visibility of the current post via jQuery and the temporarily_hidden
value.
If you try to run the plugin like so, you will notice that it works but the chevron is not changing direction. It's because it's not being rerendered whenever the value of post.temporarily_hidden
is changed. To allow for that, we add a new observer using Discourse.View.renderIfChanged
which will be observing that property and will internally call rerender()
on the view itself.
Discourse.PostMenuView.reopen({
shouldRerenderHideButton: Discourse.View.renderIfChanged("post.temporarily_hidden"),
// rest of the code...
})
And voilà.
Here's a screenshot with the button on the right (the chevron)
When you click on it, it hides the post and the chevron changes direction
Hope you enjoyed it
As usual, sources are available on GitHub.