Shopify / liquid

Liquid markup language. Safe, customer facing template language for flexible web apps.
https://shopify.github.io/liquid/
MIT License
11.03k stars 1.38k forks source link

Using variables as snippet name in render tags #1269

Open matthewbeta opened 4 years ago

matthewbeta commented 4 years ago

Hi!

It seems pretty unreasonable (as an outsider) that I can't do this:

{% assign my_template = 'my-template' %}
{% render my_template %}

I can't understand why render should care if the string it uses is inline or from a variable, so long as its a string.

This feature would be useful for rendering partials based on things that need to be dynamic (For example rendering a different partial if a user was logged in or out) or when you want to make components more generic.

For example, say I have a bunch of svg icons as .liquid files. I want to render them, wrapped with some markup. Rather than edit each icon with repetitive wrapping HTML, I want to create an adapter component and pass in the icon name so I could write:

{% render 'icon', icon_partial: 'cart', text: 'View cart' %}

And then inside snippets/icon.liquid

<div class="icon">
{% render icon_partial %}
{{ text }}
</div>  

Am I being dense? This feels like a step back from include tags.

Cheers

Matt

dylanahsmith commented 4 years ago

The reasoning was provided in the pull request that introduced the render tag (https://github.com/Shopify/liquid/pull/1122)

Unlike include, render does not permit specifying the target template using a variable, only a string literal. For example, this means that {% render my_dynamic_template %} is invalid syntax. This will make it possible to statically analyze the dependencies

If you are just rendering a different partial based on whether the user is logged in or out, then you could just use conditional control flow to render the appropriate nested partial.

If you want your partial to be more generic so that it isn't coupled to the nested content, then you could capture the rendering of the nested content and pass the rendered content into the generic partial. For example, that would allow you to pass in different attributes to different nested partials, as you have done with text: 'View cart'.

deadlyengineer commented 2 years ago

What we will do if things go like this? I need to show flags according to user input, there are literally hundreds of them. Writing hundreds of if-else statements will be somewhat cumbersome.

Ross-Angus commented 2 years ago

What puzzles me about this answer is that the official documentation seems to contradict it, but I can't get it to work.

dylanahsmith commented 2 years ago

What we will do if things go like this? I need to show flags according to user input, there are literally hundreds of them. Writing hundreds of if-else statements will be somewhat cumbersome.

Do you mean you have literally hundreds of snippets that you might need to dynamically include? If so, that seems cumbersome even with the include tag.

Global objects don't need to be passed down. They are accessible from all files.

That isn't referring to assigns. Look at the global objects in the documentation that it links to. Are any of those not accessible from all files?

aphillips8 commented 2 years ago

I just had this issue when trying to dynamically render a snippet, but instead of Shopify showing a liquid error like it usually does it just broke the whole page with no indication of the error. Can that be resolved please. I don't remember that happening in the past

Error

.

consofas commented 2 years ago

Found this workaround. By appending a string (even empty) it transforms the variable into a string.

{%- render variable_name | append: ".liquid" -%}

Found on this thread. https://community.shopify.com/c/shopify-design/how-can-i-render-a-snippet-this-name-is-defined-by-a-variable/td-p/640212

dylanahsmith commented 2 years ago

Found this workaround. By appending a string (even empty) it transforms the variable into a string.

That is clearly relying on a bug. Intentionally relying on a bug is fragile to changes we make to liquid or Shopify's extensions to liquid, since it puts you code at risk from breaking from a bug fix.

dylanahsmith commented 2 years ago

I just had this issue when trying to dynamically render a snippet, but instead of Shopify showing a liquid error like it usually does it just broke the whole page with no indication of the error.

Looks like that was another bug in Shopify that has already been fixed. It should now render to the error: <!-- Syntax error in tag 'render' - Template name must be a quoted string -->.

muchisx commented 1 year ago

Any updated on this? The feature would be awesome!

davidfmiller commented 1 year ago

+1, please.

jack-fdrv commented 7 months ago

+ 2 please

jack-fdrv commented 1 month ago

+1

darkamenosa commented 1 week ago

I found a solution for this one. Just extend the Render class and create a CustomRender class. The code should be something like this:

  class CustomRender < Liquid::Render
    SYNTAX = /(?:#{QuotedFragment}|#{VariableSegment})(\s+(with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o

    def initialize(tag_name, markup, options)
      super
      # Update the syntax to support variables or string fragments
      match = markup.match(SYNTAX)
      template_name = match[0]

      @variable_name_expr = match[3] ? parse_expression(match[3]) : nil
      @template_name_expr = parse_expression(template_name)
      @alias_name = match[5]
      @attributes = {}

      markup.scan(TagAttributes) do |key, value|
        @attributes[key] = parse_expression(value)
      end
    end

    def render_tag(context, output)
      # Allow variable-based template names
      template_name = context.evaluate(@template_name_expr)

      raise ::ArgumentError unless template_name.is_a?(String)

      # Load the partial (same as the original code)
      partial = PartialCache.load(
        template_name,
        context: context,
        parse_context: parse_context,
      )

      context_variable_name = @alias_name || template_name.split('/').last

      render_partial_func = ->(var, forloop) {
        inner_context               = context.new_isolated_subcontext
        inner_context.template_name = partial.name
        inner_context.partial       = true
        inner_context['forloop']    = forloop if forloop

        @attributes.each do |key, value|
          inner_context[key] = context.evaluate(value)
        end

        inner_context[context_variable_name] = var unless var.nil?
        partial.render_to_output_buffer(inner_context, output)
        forloop&.send(:increment!)
      }

      variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil

      # Support for loops (same as the original code)
      if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
        forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
        variable.each { |var| render_partial_func.call(var, forloop) }
      else
        render_partial_func.call(variable, nil)
      end

      output
    end
  end

Then override the render tag or name it anything you want (In my case, I override my render tag):

Liquid::Template.register_tag('render', CustomRender)
AriBahman commented 2 days ago

You can use this method instead

Snippet

<div>
  {{ content }}
</div>

In a section

<section>
    {% capture my_content %}
      {% render 'icon' %}
    {% endcapture %}

    {% render 'button', content: my_content %}
 </section>

Render

<section>
    <div>
      <svg>
         ...
      </svg>
     </div>
</section>