2
\$\begingroup\$

I have a website where people can write articles and publish/unpublish them. This is done via single button on the page:

<a id="publish-button" href="#" class="btn btn-default <%= 'btn-success' if @article.published? %>">
    <span class="glyphicon glyphicon-globe"></span>
</a>

As you can see, if an article is already published I'm adding additional btn-success class.

Now, this is the JavaScript code that handles Ajax clicks on that button:

var $editor = $('#editor'); // Article container, it contains update action url as data-url
var $publishButton = $('#publish-button');
var togglePublished = function () {
  var url = $editor.data('url');

  var postData = {
    article: {
      stage: $publishButton.hasClass('btn-success') ? 'unpublished' : 'published'
    },

    _method: 'PUT'
  };

  $.ajax({
    url: url,
    data: postData,
    method: 'POST',
    success: function (article) {
      $publishButton['published' == article.stage ? 'addClass' : 'removeClass']('btn-success');
    }
  });
};
$publishButton.click(function (e) {
  e.preventDefault();
  togglePublished();
});

My controller action just returns a JSON encoded article in this case:

def update
  if @article.update(article_params)
    render json: @article
  else
    render json: { errors: @article.errors.full_messages }, status: :unprocessable_entity
  end
end

There are few things I see wrong with this:

  1. My post data depends on class, which can be changed and changing it would require me to change it on two places: in template and in JS.
  2. If I add data-published attribute to the button I would still have to add/remove class after Ajax has finished resulting in duplicated view logic (in HTML file and JS file).

I can see two ways to avoid this and be DRY:

Add conditional class only in JavaScript and run check upon page load

This would be fairly easy to do but would result in a brief delay because conditional class would be added in JavaScript which runs after all HTML has already loaded.

Extract button to partial view and return it from controller in Ajax

This seems like a more solid approach but since my update action is used in other places too it has to return article as JSON. That's why I would have to create another controller action:

def toggle_published
  @article.toggle_published # Something like this
  render partial: '_publish_button'
end

This reduces "restfulness" of my controller and it seems kind of silly to extract only one HTML element into a partial view.

Is there any better way to do this? It seems like a minor and insignificant code duplication but it's not an isolated example and as website complexity grows these things can multiply a lot and cause a lot of headaches.

\$\endgroup\$
1
\$\begingroup\$

You could theoretically overload the show action to only return the partial if request.xhr?. So if it's an ajax request, only render that button partial. But that's inelegant.

Alternatively, you can simply keep the default show action, send the entire page back, and just pluck out the button element (similar to how Turbolinks work). It'll keep the controller RESTful and keep the update-magic in the JS only. The trade-off is that you'll be rendering stuff that you won't actually use for anything. However, you can avoid some of it by disabling layout rendering for XHR requests (which is somewhat less kludgy than just rendering a partial; at least you're still rendering the show template).

However, having the publish button's state just defined by its color doesn't sounds like a very clear UI to me. Does a (presumably) green button mean "not published; clicking will publish" or "already published; clicking will unpublish"? I'd much prefer the button actually having a verb on it like "Publish" or "Unpublish" (or whatever you'd call it), so it's clear what the button will do.

Regardless, using the btn-sucess class gives you the desired look, but it's semantically meaningless. The class name doesn't really describe the current state or the action the button will take.

Picking the "re-render the show template"-option above will give you a chance to re-render the entire post with a published class. Then you can have a CSS rule like:

#post.published #publish-button {
  // set color etc.
}

Not as simple as using the generic btn-success class, but to me it makes more sense to track the state for the post as a whole, rather than a single button.

I imagine something like this for the view

<article id="post_123" data-url="..." class="<%= 'published' if @article.published? %>">
  <div id="editor">...</div>
  <a id="publish-button" href="#" class="...">
    ...
  </a>
</article>

That'll mean that the element with the update URL is the same element that should be replaced by whatever is being sent back from the ajax-request. The JS only really needs to know that rule: Update the element with the data-url attribute. It doesn't need to know the specifics of re-styling individual elements.

Even if you don't go that route, toggling a published class in both view and JS is at least more readable/straight-forward that toggling a semantically unrelated class like btn-success.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.