This topic has been superseded by Developing Discourse Plugins - Part 1 - Create a basic plugin
EDIT: Updated the tutorial & fixed the plugin code.
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 PostMenu
view which renders the menu below a post using buffered rendering for performance. We’ve already seen the buffer rendering technique in the previous tutorial. It just means that we’re pushing strings into a buffer.
This view has 3 interesting methods:
render: function(buffer) {
var post = this.get('post');
buffer.push("<nav class='post-controls'>");
this.renderReplies(post, buffer);
this.renderButtons(post, buffer);
this.renderAdminPopup(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 (renderReplies()
) if there is any. Then, it will render all the buttons (renderButtons()
) and finally, it will render the admin popup (renderAdminPopup()
).
Let’s dig into the renderButtons
method:
renderButtons: function(post, buffer) {
// ...
var yours = post.get('yours');
Discourse.SiteSettings.post_menu.split("|").forEach(function(i) {
var creator = self["buttonFor" + i.replace(/\+/, '').capitalize()];
if (creator) {
var button = creator.call(self, post);
if (button) {
// ...
visibleButtons.push(button);
}
}
});
// ...
buffer.push('<div class="actions">');
visibleButtons.forEach(function (b) {
b.render(buffer);
});
buffer.push("</div>");
},
I’ve simplified this function a bit for a better comprehension. It basically iterates through the post_menu
site setting and calls the methods named buttonFor<Button>
if they are defined on the view. For example, for the reply
button, it will call the method named buttonForReply
. This method should return a Button
object that is then added to a list of visibleButtons
which are rendered into the view at the end of the method. That Button
class is an helper class used to render buttons.
Finally, the 3rd interesting method:
// Delegate click actions
click: function(e) {
var $target = $(e.target),
action = $target.data('action') || $target.parent().data('action');
if (!action) return;
var handler = this["click" + action.capitalize()];
if (!handler) return;
handler.call(this, this.get('post'));
}
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, this allows us to control both the rendering of our button by defining a buttonForSomething
method and 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.2
# authors: Régis Hanol
Note that there’s nothing to add. We’ll add a .js.es6
which will be automatically loaded by the plugin system.
Let’s create assets/javascripts/initializers/hide-button.js.es6
and add the following code:
import { Button } from "discourse/views/post-menu";
export default {
name: "hide-posts",
initialize: function (container) {
var PostMenuView = container.lookupFactory("view:post-menu");
PostMenuView.reopen({
buttonForHide: function (post, buffer) {
var direction = !!post.getWithDefault("temporarily_hidden", false) ? "down" : "up";
return new Button("hide", direction, "chevron-" + direction);
},
clickHide: function () {
$("#post_" + this.get("post.post_number") + " .cooked").toggle();
this.toggleProperty("post.temporarily_hidden");
}
});
}
};
This file uses the ES6 syntax to import and export modules. The first line imports the Button
class from the post-menu
view so that we can use it in our code. Then, we declare the default module we’re exporting and it’s an simple javascript object.
This object has a name
property and an initialize
method that takes a container
for argument. This is an initializer. It will be called whenever the application boots up.
Inside the initialize
method, we use the container
object to lookup for the post-menu
view and use that reference to reopen
the class. This 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: buttonForHide
and clickHide
.
The buttonForHide
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.
PostMenuView.reopen({
shouldRerenderHideButton: Discourse.View.renderIfChanged("post.temporarily_hidden"),
// rest of the code...
})
And voilà.
Here’s a screenshot with the button on the left (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.