9

I'm building a Rails app that uses Pusher to use web sockets to push updates to directly to the client. In javascript:

channel.bind('tweet-create', function(tweet){ //when a tweet is created, execute the following code:
  $('#timeline').append("<div class='tweet'><div class='tweeter'>"+tweet.username+"</div>"+tweet.status+"</div>");
});

This is nasty mixing of code and presentation. So the natural solution would be to use a javascript template. Perhaps eco or mustache:

//store this somewhere convenient, perhaps in the view folder:
tweet_view = "<div class='tweet'><div class='tweeter'>{{tweet.username}}</div>{{tweet.status}}</div>"

channel.bind('tweet-create', function(tweet){ //when a tweet is created, execute the following code:
    $('#timeline').append(Mustache.to_html(tweet_view, tweet)); //much cleaner
});

This is good and all, except, I'm repeating myself. The mustache template is 99% identical to the ERB templates I already have written to render HTML from the server. The intended output/purpose of the mustache and ERB templates are 100% the same: to turn a tweet object into tweet html.

What is the best way to eliminate this repetition?

UPDATE: Even though I answered my own question, I really want to see other ideas/solutions from other people--hence the bounty!

user94154
  • 16,176
  • 20
  • 77
  • 116
  • 1
    for future reference: Mustache (that you already mentioned) also allows for server-side rendering (See: http://mustache.github.com/ for implementations). The flow now becomes: 1 inital server-side rendering using a mustache-template + N consecutive client-side renderings using the very same mustache-template – Geert-Jan Jul 26 '11 at 09:31
  • fyi: I've asked a similar question just now (looking for alternatives to Mustache bc. of limiations) although the flow would stay the same. Perhaps that helps as well. see: http://stackoverflow.com/questions/6831718/client-side-templating-language-with-java-compiler-as-well-dry-templating – Geert-Jan Jul 26 '11 at 18:29

5 Answers5

5

imo the easiest way to do this would involve using AJAX to update the page when a new tweet is created. This would require creating two files, the first being a standard html.erb file and the second being a js.erb file. The html.erb will be the standard form which can iterate through and display all the tweets after they are pulled from the database. The js.erb file will be your simple javascript to append a new tweet upon creation, i.e.:

$('#timeline').append("<div class='tweet'><div class='tweeter'><%= tweet.username %></div><%= tweet.status %></div>")

In your form for the new tweet you would need to add:

:remote => true

which will enable AJAX. Then in the create action you need to add code like this:

def create
...Processing logic...
  respond_to do |format|
    format.html { redirect_to tweets_path }
    format.js
  end
end

In this instance, if you post a tweet with an AJAX enabled form, it would respond to the call by running whatever code is in create.js.erb (which would be the $('#timeline').append code from above). Otherwise it will redirect to wherever you want to send it (in this case 'Index' for tweets). This is imo the DRYest and clearest way to accomplish what you are trying to do.

Will Ayd
  • 6,767
  • 2
  • 36
  • 39
  • Thanks for the response, but how does this solve the problem of repetition? The divs in the js.erb file would be written the exact same way in an html.erb partial. – user94154 Mar 01 '11 at 18:48
  • 2
    well what you could do is have a third shared partial between the two (shared/_tweet_item.html.erb). so for instance in your html file you could just do a render '<%= render :partial => 'shared/tweet_item', :collection => @tweets_array %>' which will iterate over the partial with all the tweets in your array. similarly in your create.js.erb file you could do something like '$("timeline").append("<%= escape_javascript(render('shared/tweet_item', @tweet)) %>")' which will append a new partial given that you pass it a @tweet instance variable – Will Ayd Mar 01 '11 at 19:11
  • didnt mention it but i am implying here that all you have to do is define the div's and format and everything one time in that shared partial. then its just a matter of iterating over the collection in your html.erb file and rendering the partial for each item in the collection, or passing an instance variable to the partial and simply have that appended all via an AJAX call. no duplication and good separation of content. – Will Ayd Mar 01 '11 at 19:14
3

Thus far, the best solution I found was Isotope.

It lets you write templates using Javascript which can be rendered by both the client and server.

user94154
  • 16,176
  • 20
  • 77
  • 116
3

I would render all tweets with Javascript. Instead of rendering the HTML on the server, set the initial data up as JS in the head of your page. When the page loads, render the Tweets with JS.

In your head:

%head
  :javascript
    window.existingTweets = [{'status' : 'my tweet', 'username' : 'glasner'}];

In a JS file:

$.fn.timeline = function() {
  this.extend({
    template: "<div class='tweet'><div class='tweeter'>{{tweet.username}}</div>{{tweet.status}}</div>",
    push: function(hash){
      // have to refer to timeline with global variable
      var tweet = Mustache.to_html(timeline.template, hash)     
      timeline.append(tweet);
    }
  });  

  window.timeline = this;

  channel.bind('tweet-create', this.push);  

  // I use Underscore, but you can loop through however you want
  _.each(existingTweets,function(hash) {
    timeline.push(hash);
  });

  return this
};  


$(document).ready(function() {
  $('#timeline').timeline();
});
Jordan
  • 1,230
  • 8
  • 8
2

I haven't tried this, but this just occurred to me as a possible solution:

In your view create a hidden div which contains an example template (I'm using HAML here for brevity):

#tweet-prototype{:style => "display:none"}
    = render :partial => Tweet.prototype

Your tweet partial can render a tweet as you do now.

.tweet
    .tweeter
        = tweet.username
    .status
        = tweet.status

When creating a tweet prototype you set the fields you want to the js-template replacement syntax, you could definitely dry this up, but I'm including it here in full for example purposes.

# tweet.rb
def self.prototype
    Tweet.new{:username => "${tweet.username}", :status => "${tweet.status}"}
end

On the client you'd do something like:

var template = new Template($('#tweet-prototype').html());
template.evaluate(.. your tweet json..);

The last part will be dependent on how you're doing your templating, but it'd be something like that.

As previously stated, I haven't tried this technique, and it's not going to let you do stuff like loops or conditional formatting directly in the template, but you can get around that with some creativity I'm sure.

This isn't that far off what you're looking to do using Isotope, and in a lot of ways is inferior, but it's definitely a simpler solution. Personally I like haml, and try to write as much of my mark up in that as possible, so this would be a better solution for me personally.

I hope this helps!

jonnii
  • 28,019
  • 8
  • 80
  • 108
1

To be able to share the template between the javascript and rails with a mustache template there is smt_rails: https://github.com/railsware/smt_rails ("Shared mustache templates for rails 3") and also Poirot: https://github.com/olivernn/poirot.

bolmaster2
  • 9,376
  • 4
  • 20
  • 16