简体   繁体   中英

How to stay DRY when using both Javascript and ERB templates (Rails)

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!

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, ie:

$('#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.

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.

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();
});

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!

为了能够使用胡子模板在javascript和rails之间共享模板,有smt_rails: https//github.com/railsware/smt_rails (“rails 3的共享胡子模板”)以及Poirot: https:// github .com / olivernn / poirot

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM