Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using variables as snippet name in render tags #1269

Open
matthewbeta opened this issue Jul 24, 2020 · 14 comments
Open

Using variables as snippet name in render tags #1269

matthewbeta opened this issue Jul 24, 2020 · 14 comments

Comments

@matthewbeta
Copy link

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
Copy link
Contributor

The reasoning was provided in the pull request that introduced the render tag (#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
Copy link

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
Copy link

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

@dylanahsmith
Copy link
Contributor

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
Copy link

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
Copy link

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link

muchisx commented Oct 17, 2022

Any updated on this? The feature would be awesome!

@davidfmiller
Copy link

+1, please.

@jack-fdrv
Copy link

jack-fdrv commented Feb 6, 2024

+ 2 please

@jack-fdrv
Copy link

+1

@darkamenosa
Copy link

darkamenosa commented Sep 6, 2024

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
Copy link

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests