stimulus-render icon indicating copy to clipboard operation
stimulus-render copied to clipboard

Server-pre-rendered templates

Open marcoroth opened this issue 2 years ago • 0 comments

Instead of using JSX in the render() function it might be quite common that you would want to pre-render your templates on the server side too.

This idea could be implemented by outputting the template as a <template> element into the controller as a child and then you just need to tell your controller that you want to render your controller using the pre-rendered template.

Maybe the template element doesn't even need to be a child of the controller because it should anyway not differ between controllers. Which would mean, that you could even define them once globally and all controller instances could access them.

The tricky part might be, that you need to bind some values and/or function to the template context for it for work properly.

But the idea is, that it also follows the concept of Stimulus Actions, Targets, Values, Classes and so on.

So it could be implemented by introducing a [data-template] attribute. If you want to provide a template for the whole controller, the one which is going to be used by the render() function, you would add the [data-template="identifier"] attribute to your template.

If you want to provide a template for a target that's going to be rendered by the controller you would do set the attribute to: [data-template="identifier.targetName"].

Controller template

<!-- app/views/some_view.html.erb -->

<div data-controller="counter">
  <template data-template="counter">
    <div id="counter">
      <button data-action="click->counter#increment">
        Count: ${this.counterValue}
      </button>
    </div>
  </template>
</div>

<!-- or -->

<div data-controller="counter">
  <template data-template="counter">
    <%= render MyTemplateComponent.new(some: "argument") %>
  </template>
</div>
// app/javascript/controllers/counter_controller.js

import { Controller } from '@hotwired/stimulus'
import { useRender } from 'stimulus-render'

export default class extends Controller {
  static values = { counter: 1 }

  connect () {
    useRender(this)
  }

  increment () {
    this.counterValue += 1
  }

  // maybe this is what it should default to if it's not overwritten in this controller class
  render () {
    return (
      this.evaluateTemplate(this.renderTemplate)
    )
  }
}

Target template

<!-- app/views/some_view.html.erb -->

<div data-controller="list">
  <template data-template="list.listTarget">
    <span>
      ${processMarkdown(target.dataset.value)}
    </span>
  </template>

  <ul>
    <li data-list-target="item" data-value="# Title 1"></li>
    <li data-list-target="item" data-value="**Two Bold**"></li>
    <li data-list-target="item" data-value="[Three Link](https://github.com/marcoroth/stimulus-render)"></li>
  </ul>
</div>
// app/javascript/controllers/list_controller.js

import { Controller } from '@hotwired/stimulus'
import { useRender } from 'stimulus-render'
import { processMarkdown } from 'some-markdown-rendering-package'

export default class extends Controller {
  static targets = ['item']

  connect () {
    useRender(this)
  }

  // maybe this is what it should default to if it's not overwritten in this controller class
  renderListTarget(target) {
    return (
      this.evaluateTempalte(this.listTargetTemplate)
    )
  }
}

Template value interpolation

In the examples above I just put the JS functions into the templates themselves. This is probably not feasible if you want to properly build this out. So it might be better to have a proper interpolation syntax.

Idea 1: Using attributes (like data-value)

This might be good for values that you store in the Stimulus Values, but not for regular JavaScript interpolation.

  <template data-template="counter">
    <div id="counter">
      <button data-action="click->counter#increment">
        Count: <div data-value="list.counterValue"></div>
        <!-- or an actual custom element -->
        Count: <value key="list.counterValue" />
      </button>
    </div>
  </template>

Idea 2: handlebars-like

Using something like: https://github.com/github/template-parts

  <template data-template="list.listTarget">
    <span>
      ${listItem}
    </span>
  </template> 

Which might need require that you pass something into it in the controller, something like:

  render () {
    return (
      this.evaluateTemplate(this.renderTemplate, {
        listItem: processMarkdown(target.dataset.value)
      })
    )
  }

But maybe it's possible that it just can just access the controller instance and it's functions and execute any arbitraty JavaScript you specify within the braces, how handlebars actually works.

marcoroth avatar Jul 20 '22 05:07 marcoroth