ECR <%= %> tags w/ blocks

Is there a technical reason why ECR <%= %> tags don’t work with blocks? For example:

<%= render_thing "foo" do %>
  <p>content to render for the thing</p>
<% end %>

I end up having to do something like this:

<% render_thing "foo", to: response do %>
  <p>content to render for the thing</p>
<% end %>

I remember Rails used to have this exact same constraint on blocks in its earlier days (though it may have been for a different reason) and I was wondering if it could be solved for ECR, as well.

What does render_thing do?

In this particular case it’s an HTML tag.

<%= turbo_frame “stuff” do %>
  <p>...</p>
<% end %>

<turbo-frame id=“stuff”>
  <p>...</p>
<turbo-frame>

On its own, it seems trivial enough that you might think “why would you even need a helper for that?”, but this is going into a shard and it would be nice to have auto-generated API docs for it. There are also others that would be irritating to write by hand that can be done with <%= %>. For example:

<%= turbo_stream_from "stuff" %>

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-id="base64encodedstreamname--hmacsignature"></turbo-cable-stream-source>

And I would prefer to avoid having to use <% %> for some things and <%= %> for others within a single API.

Additionally, for anything that uses <% %>, I have to pass in the IO to render it to (and it must be the same render target as the block will render to), so it becomes this:

<% render_turbo_frame "stuff", to: response %>
  <p>...</p>
<% end %>

Most of the time you’ll be rendering to the HTTP::Server::Response, but if I need to reuse this template to render to a string because I’m going to send it to a WebSocket, I can’t use ECR.render as the entry point to the render even though rendering to a string is the point of ECR.render. I have to use ECR.embed directly, piped to a String::Builder because a method invoked inside that template needs to render to that buffer. And whatever my template calls that buffer, the string builder instance needs to have the same name:

message = String.build { |response| ECR.embed "views/stuff.ecr", response }

Turbo::StreamsChannel.broadcast_replace_to "stuff", message

Seems silly to call this response because it’s just a string buffer, but it’s what the template calls it since it was written with the idea of being served in an HTTP response body.

If we could use <%= %>, internal details like that wouldn’t constrain the top-level call site for the render. It would all render to the same buffer with no effort on the part of the person using it.

Oh, I meant, how is that method implemented in Ruby? That turbo thing. What’s the source code? I’m having a hard time thinking how to trqnslate that ECR to Crystal

To clarify a bit more… this is how ECR works.

ECR is a macro that reads the ecr files and converts it into Crystal code.

For example this ecr file:

hello world

it just this crystal code:

io << "hello world"

where the name io is chosen by the macro (it’s actually __io__)

If you have this:

hello <%= world %>

that’s converted to:

io << "hello " << world

If you have this:

hello
<% foo %>
world

it’s converted to:

io << "hello\n"
foo
io << "world"

And if you have this:

<% 3.times do %>
  hello
<% end %>

it’s converted into this:

3.times do
  io << "hello\n"
end

So my question is… if you have this:

<%= something do %>
  hello world
<% end %>

what Crystal code it translates too?

I can’t understand because I don’t know what Rails does in this case.

I’m guessing something calls the block and expects a string as its value? Then we could translate it to this code:

io << something {
  String.build do |io|
    io << "hello world"
  end
}

And something needs to be defined like this:

def something
  "<tag>#{yield}</tag>"
end

However, that approach is building some intermediate strings. So maybe in Crystal the contract could be: an io is passed as the first argument to the method (in addition to whatever arguments are passed to the method), and it’s the method’s responsibility to write to the io. The method must also yield io. Then this:

<%= something do %>
  hello world
<% end %>

is translated into this:

something(io) do |io|
  io << "hello world"
end

and something is defined like this:

def something(io)
  io << "<tag>"
  yield io # here it's fine to yield the same io
  io << "</tag>"
end

You could also have something like this:

<%= upcase do %>
  hello world
<% end %>

where upcase needs to be defined like this:

def upcase(io)
  String.build do |sub_io|
    yield sub_io
  end.downcase(io)
end

Here a separate io was yielded because we need an intermediate string so we can later downcase it (though we could have a streaming IO that downcases on the fly, if you really wanted that).

My problem with all of this is that it’s not very intuitive… as I said, I have no idea how it works in Ruby, what happens with the block given to that method and what it translates too. But this is something that we could definitely do, we just need to design it and implement it.

1 Like

The Ruby source code isn’t that helpful, tbh. It seems to rely on the idea that Rails templates support blocks because it just passes the block to the tag helper. When I was writing the Crystal version of it, I was mainly looking for how to make a Crystal app output what it outputs and work backwards from there.

It looks like Erubi (the default rendering engine for Rails) doesn’t support capturing block output by default. But it does support it via a slightly different syntax with its CaptureEndEngine:

require “erubi”
require “erubi/capture_end”

puts Erubi::CaptureEndEngine.new(<<-ERB).src
<%|= foo do |i| %>
<p>hi</p>
<%| end %>
ERB
# buf = ::String.new;begin; (__erubi_stack ||= []) << _buf; _buf = ::String.new; __erubi_stack.last << (( foo do |i|  _buf << '
# '.freeze; _buf << '<p>hi</p>
# '.freeze; end )).to_s; ensure; _buf = __erubi_stack.pop; end; _buf << '
# '.freeze;
# _buf.to_s

I don’t know why it uses a different syntax, probably to avoid having to parse the Ruby code inside of it to determine if there’s a block. I’m also not sure how Rails papers over the difference between them, because Rails ERB templates don’t require the <%|= %> syntax.

Yeah, I think this is the tradeoff. If you’re returning a string to render into the target IO, you’d have to build up that string, however large it ends up being. It would result in more memory consumption (and additional CPU time spent in allocating/freeing those intermediate strings), but rendering to a string is easier for a lot of folks to understand.

In Ruby, though, it’s not considered such bad form to generate a bunch of intermediate strings. Likely due to the fact that allocations in the view template aren’t what typically slows down a Ruby web app. :slightly_smiling_face:

A little digging reveals that that’s what Amber is doing, too.

This might work really well, and if it does it sounds like a great reason to go back to the convention of the overload of a method that takes an IO (instead of returning a value) always having the IO as the first argument.

I think doing it with String.build is simpler to understand, even though it consumes a bit more memory. As you mention, that’s generally not the bottleneck.

If I have time this week I’ll try to implement it.

1 Like

Thinking about this a bit more, I don’t know how to know whether <%= ... %> starts a block or not. Of course for a human it’s very simple to see: it ends with “do” or “{”, but if you have <%= {1, 2, 3} %> it also ends with { but it’s not a block. That means to implement this we have to parse the contents of <%= ... %> to determine this, whereas before we didn’t care at all about this: it was just code that we pasted into the generated code.

I don’t know why it uses a different syntax, probably to avoid having to parse the Ruby code inside of it to determine if there’s a block.

Now I understand this :slight_smile:

Yea, also if it ends with do |x| it’s also harder to know. There’s no heuristic we can use, we must use a crystal parser that supports broken expressions (because foo do isn’t valid crystal code).

So I think we can go with <%|= foo do |i| %> like in erubi. It’s probably clearer to the reader what’s going on, I’m not sure.

In any case, I won’t do any of this right now, it’s just too complex for my limited time. Sorry!

1 Like

hehe No worries! I was mainly wondering if ECR not supporting it was an explicit decision. It sounds more like it just wasn’t clear how to accomplish it or whether it was even a good idea while aligning with Crystal’s convention of writing to IO objects instead of returning strings everywhere.

FWIW, that convention is amazing — the fact that building a string is nearly always just a matter of writing to an IO wrapped in String.build still blows my mind sometimes.

Calling helper methods with blocks in templates just seems like a weird edge case regarding ergonomics for that. The name of the target IO may never be exposed in application code (for example, if ECR.embed is encapsulated in library code or even because you’re calling ECR.render) but the template seems to need to hard-code its name for these methods to render to. It’s a wonky set of constraints that I don’t believe has shown up anywhere for me other than this one very specific case. :laughing:

In my own apps, I tend to render more-or-less directly to the HTTP::Server::Response so I guess I’m mainly trying to anticipate what others may run into with this library when they’re using a framework like Lucky or Amber.