DEV Community

Jeremy Woertink
Jeremy Woertink

Posted on • Edited on

Render Component from Action in Lucky

EDIT: Since this writing, Lucky now has a component method (built-in) you can use in your actions. No need to create a custom macro!

For one of my Lucky apps, I'm using Stimulus which is light-weight. I wanted to avoid doing a ton on the client side so that way I can keep all the nice benefits of Crystal and Lucky.

In a recent instance, I had to load some comments on to a page asynchronously. Normally in this case, you might write some javascript to make an api call, then gather all of your records in a giant JSON object. Then you'd probably iterate over the result, and create some sort of component. Maybe in Vue, or in React, and have those render each comment. With Stimulus, you don't have built-int templating. You're left with writing a lot of document.createElement, and element.setAttribute type stuff. Or you just use lots of innerHTML = '' type calls.

What I wanted was to write my markup in Lucky, then make an API call that returns the markup, and I can just shove that in to an element and be done with it.

By default, the actions in Lucky want you to render an HTML Page. But I didn't want to make a blank layout just for these. So here's what I did:

# src/components/comments_list.cr
class CommentsList < BaseComponent
  needs save_comment : SaveComment
  needs comments : CommentQuery

  # Renders the comments form, and each comment
  def render
    # This could also move to it's own component!
    form_for Comments::Create do
      textarea save_comment.body, class: "field"
      submit "Post Comment"
    end

    div class: "comments" do
      @comments.each do |comment|
        mount Comment.new(comment)
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now I just need a handy helper macro in my ApiAction.

# src/actions/api_action.cr
abstract class ApiAction < Lucky::Action
  accepted_formats [:json], default: :json

  macro render_component(component_class, **assigns)
    send_text_response({{ component_class }}.new(
      {% for key, value in assigns %}
        {{ key }}: {{ value }},
      {% end %}
    ).render_to_string, "text/html")
  end
end
Enter fullscreen mode Exit fullscreen mode

Then I can use this new macro in my api actions!

# src/actions/api/comments/index.cr
class Api::Comments::Index < ApiAction
  get "/api/comments" do
    save_comment = SaveComment.new
    comments = CommentQuery.all

    render_component CommentList, save_comment: save_comment, comments: comments
  end
end
Enter fullscreen mode Exit fullscreen mode

Lastly, my javascript is pretty simple now.

import { Controller } from 'stimulus'

export default class extends Controller {

  connect() {
    fetch("/api/comments")
      .then(res => res.text())
      .then(html => element.innerHTML = html)
  }
}
Enter fullscreen mode Exit fullscreen mode

Thoughts

I know I left out several bits like, setting up stimulus, or what the main action / page look like, but all of those should be fairly straight forward. I'll probably do a writeup later on integrating stimulus with Lucky. Hopefully these examples should get you close should you need to use this!

Top comments (0)