Crinja/ECR and Template Fragments/Partials (htmx, web dev)

Hello!

is there any support for rendering fragments/partials in the Crystal lang world?
Like this: </> htmx ~ Template Fragments

I’d be interested in using it together with HTMX, where the problem is that you don’t want to have a thousand little templates for each (htmx/ajax) small update fragment (form, in-place edit…), but it would be handy to write a single template (that renders whole web page on the first render) and then just pull out (render) individual named fragments from the main template on HTMX requests.

ECR isn’t capable of that, but what about Crinja - I thought of doing it somehow through my ad-hoc artificial layout and template blocks, but I would have to have it in a file - I haven’t figured out how to render just one fragment (crinja block) from the crinja template.

Is there any other templating shards?

Thanks.

I’d be very interested in it in ECR too…

Considering that ECR files are just compiled to Crystal, it shouldn’t be impossible to extend it to only render a subset of a file. Might be possible to implement it in a shard.

I tried my hand at making something akin to this idea. Kilt is a crystal template engine that works with a bunch of other templating engines, and so I built a shard called kilt-components with the idea of, well, building smaller templates as components and injecting them into each other. It worked by pairing a crystal class with a template, and the template can contain references to other components (through function calls) that it renders, which themselves are crystal classes. Each crystal class component has a render method that converts that instance into its string representation, such as an html page, a sub part, etc.

Not sure if it’s what you’re looking for, but I’d be happy if it just inspired some new ideas :)

1 Like

I decided to try this because I thought it might not be super difficult. Turns out I wasn’t wrong, at least not with my armature shard.

Armature details

Armature started out as just a web routing framework like Roda, but then later grew the need for HTML components. It uses Armature::Template which is really just ECR (quite literally copy/pasted) but adds HTML sanitization by default. You basically use them like this:

struct MyComponent
  def initialize(@arg1 : String, arg2 : Int32, etc : Time?)
  end

  # Template is `views/components/my_component.ecr`
  def_to_s "components/my_component"
end
<%== MyComponent.new "arg1", arg2, etc %>

I posted an example repo here, with the following notable pieces:

Check out the live demo and notice that the only thing that comes back from the server in the network tab in your browser’s dev tools is the button. The rest of the template is skipped. That’s also why the table has a “last rendered” column, so you can see it isn’t updating the whole row, but instead just the button.

1 Like

Yes, I like the final performance of the app based on the compiled ECR, one fast binary and bye… - but for the development cycle, theming and layouting the web app, when the app grows, the recompilation cycle becomes unbearably annoying.

I find it more convenient to use crinja and templates in files next to the application, with kemal-watcher every change of the web template during the development is immediately reflected in the browser and it’s a completely different experience (flow). Crinja can also cache parsed templates in memory, so it’s still decently fast in production.

There is also a third option, throw away the templating and render pages by code, like in Lucky framework:

then you immediately start thinking more in components, data flows correctly and clearly everywhere - instead of huge templates - and it all starts to make sense. Unfortunately, there are those annoying recompilations in development again.

Tough…

4th option (?): leverage an embedded language like Lua for the templating/logic. We’ve tossed around the idea as an option in Lucky, but haven’t put together a POC yet of anything.

Oh, very interesting, as I’m currently working around Kemals limitations (well, I have more advanced routing needs in my app). I’ve been pondering finding some routing lib rather than go down the framework route.

Ah, but as far as I can tell, you still pass all the data to the template?

Well, I find myself spending most of my time on CSS in that phase, which isn’t impacted by compilation time.

Well, these things go in circles. PHP started as a templating language, when the code got more complicated, the logical first step was using other PHP files as templates for your app PHP… And now we have templating (Twig) implemented in PHP. It depends on whether you value using the same language for app and templating logic, or want the separation of logic. For projects where your templating follows the code tightly, I do like the idea of compiling the templating so you can skip the extra processing in production.

RoutingHandler - Athena could be a good option for you then. Is pretty robust and quite lightweight as it’s literally just routing on top of HTTP::Server.

Oh, looks good too, I’ll have to read up on (maybe even try out) both and see what reads best.

1 Like

Sounds good! Route - Athena has the docs for what each route can match on. E.g. hostname, runtime expressions, path requirements, etc. Let me know if you have any questions.

1 Like

When use with htmx, the data probably only a partial, very small.

I’m not sure what you mean by “to the template”. Rendering in Armature streams directly to the socket rather than rendering to a string in memory and then passing that string to the client. The only buffering is via IO::Buffered on the socket. There’s no post-processing going on.

All of the data is passed to Filter.write, but that’s a no-op for anything outside the named fragment.

What I meant is, if you have an ECR file that renders a full HTML documents, and uses, say the variables header, footer and ‘my_element’, the latter being a fragment, don’t you still have to supply something for header and footer in order to not have the compiler complain about missing variables in the unused part of the .ecr file?

Yep. I can’t imagine there’s a way around that. The way it’s implemented for Armature components, the context for the template is the component instance, so the variables (or methods, in the case of getters) are always in scope. If you want to do something more narrowly scoped, you extract a new component object.

I was pondering something like ECR.partial which would find the file, extract the partial to a temp file and run ecr/process on that. Throw in partial variants for def_to_s and render for good measure.

Ooh, now that’s an interesting approach. :thinking:

Quick(-ish) proof of concept: GitHub - xendk/pecr.cr

If someone could explain why the spec fails with the unhelpful

Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'embed_partial'

Code in spec/pecr_spec.cr:16:5

 16 | ECR.embed_partial "spec/simple.ecr", "first", io
      ^
Called macro defined in src/pecr.cr:11:3

 11 | macro embed_partial(filename, partial, io_name)

Which expanded to:

 > 1 | {{ run("./process_partial", "spec/simple.ecr", "first", "io") }}
          ^--
Error: error executing macro 'run': 

When the src/test.cr file works, that would be nice…

(don’t mind the naive implementation, it’s just to get the ball rolling)

1 Like

The run macro struggles when the file is local to the shard. I ran into the same thing with the implementation I was trying out yesterday. The path to the file passed to run has the same semantics that require does (so it’s looking for spec/process_partial.cr). I have the same problem when developing Armature. I have no idea how to run specs that render templates.

It looks like the embed_partial is escaping the run macro itself, which I’m assuming drops it as is in the spec instead, and then re-evaluates from that relative location (I think). Is there a reason you need to \{{ instead of passing the macro arguments straight in? This is what the kilt-components shard does

(On phone, so I might be missing something obvious)

1 Like

No idea, I just pilfered that from ECR#embed… But indeed, removing the extra escaping makes the test work, hopefully without undesired side effects. I wonder why ECR does it?

I guess that the escaping basically moves the context from the pecr.cr file to the calling file. It works for ECR because it’s locating the file using CRYSTAL_PATH.

1 Like