Templatetag component with blocks

I have a modal which I want to turn into a templatetag component. The template needs to ammodate parameters, blocks, and extra content. An example of the template is below. I have something working for the args, and extract content (which is just injected to a {{ content }} tag. But I’m not sure how to do the blocks. Can anyone help?

template

<div id="{{ id|default:'modal-0' }}"
     data-modal-placement="{{ placement|default:'center-center' }}"
     {% include 'common/components/attrs.html' %}>
  <div class="relative h-full w-full p-4 md:h-auto {{ size|default:'max-w-2xl' }}">
    <div class="relative rounded-lg bg-white shadow">
      {% block modal_header %}
        <div class="flex items-start justify-between rounded-t border-b p-4">
          <h3 class="text-xl font-semibold text-gray-900">{{ title }}</h3>
          {% block modal_close %}
            <button type="button" class="ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900" data-modal-toggle="{{ id|default:'modal-0' }}">
              {% heroicon_outline 'x-mark' stroke_width=2 size=20 %}
              <span class="sr-only">Close modal</span>
            </button>
          {% endblock modal_close %}
        </div>
      {% endblock modal_header %}
      {% block modal_body %}
        <div class="p-6">
          {{ content }}
          {% block modal_content %}
            <p class="response-message text-base leading-relaxed text-gray-500"></p>
          {% endblock modal_content %}
        </div>
      {% endblock modal_body %}
      {% block modal_footer %}
        <div class="flex justify-end space-x-2 rounded-b border-t border-gray-200 p-6">
          {% block modal_buttons %}
          {% endblock modal_buttons %}
        </div>
      {% endblock modal_footer %}
    </div>
  </div>
</div>
class RenderComponentNode(template.Node):

    def __init__(self, template_name, context_args, context_kwargs, nodelist):
        self.template_name = template_name
        self.context_args = context_args or {}
        self.context_kwargs = context_kwargs or {}
        self.nodelist = nodelist

    def render(self, context):
        resolved_context_args = {
            key: (value.resolve(context) if not isinstance(value, str) else value)
            for key, value in self.context_args.items() if value
        }
        resolved_context_kwargs = {
            key: (value.resolve(context) if not isinstance(value, str) else value)
            for key, value in self.context_kwargs.items() if value
        }
        content = self.nodelist.render(context)

        ctx = {'content': content, 'attrs': resolved_context_kwargs}
        ctx.update(resolved_context_args)

        return render_to_string(self.template_name, ctx)


@register.tag
def render_component(parser, token, template_name, context_args):
    try:
        tag_name, *args = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(f"{tag_name} tag requires at least one argument.")

    context_kwargs = {}

    for arg in args:
        try:
            name, value = arg.split('=')
            if name in context_args.keys():
                context_args[name] = parser.compile_filter(value)
            else:
                context_kwargs[name] = parser.compile_filter(value)
        except ValueError:
            context_args[arg] = None

    nodelist = parser.parse(('end_' + tag_name,))
    parser.delete_first_token()

    return RenderComponentNode(template_name, context_args, context_kwargs, nodelist)

any ideas on how to get blocks to pass as context via a tag?

I thought you’d be all over this @KenWhitesell?

Hello there!
Do you know this package django-components already? It’s pretty much what you’re trying to solve here

Since you tagged me, I’ll go ahead and respond.

Honestly? I don’t believe I’m understanding what you’re trying to do here. (It may just be an issue that you’re using terminology in a manner that I’m unfamiliar with.)

My gut reaction to your post was that if you’re trying to do what I think you’re trying to do, I’d be taking a completely different approach. I’d define the modal as a separate html file that would be included in the page’s html. What I think you might be trying to do with these blocks would be better represented as nested includes. I wouldn’t be approaching this as a new or custom tag.

But, you’ve been around here long enough that I would also assume that the straight-forward solution is inadequate for what you’re trying to do - and so I’m kinda stuck with no direct advice to give.

I wasn’t aware of django-components. Does indeed seem to do what I need, and much more. I could try and strip out the block context injection piece. Or just use it direct if that’s too difficult. Thanks for that!

Yeh, that’s right. I could use the template as an includes. That’s how I’m currently using it. But I’ve already converted over custom fields and other simpler components (e.g. buttons without blocks) to templatetags. And wanted to use that system for everything component wise.

Didn’t realize it would be so difficult to extract blocks from provided templatetag context, and then render my template blocks accordingly. As you say, Django does this if I use as an includes anyway.

I’ll look at django-components, and see if I can target exactly what code I need. As I already have a workable solution for how I deal with defined/ undefined parameters and other nested context tags.

Probably a bit late to the game but I think a package called cotton can do some of the lifting for you. With it, you could create a file call ‘modal.cotton.html’ with the following: (I am skipping some of the implementation for brevity)

<!-- Set your non-attribute props here so you can define the defaults and also they will automatically be excluded from the {{ attrs }} variable -->
<c-props id='modal-0' placement='center-center' size='max-w-2xl' />

<div {{ attrs }} id="{{ id|default:'modal-0' }}" 
  data-modal-placement="{{ placement|default:'center-center' }}">
  <div class="relative rounded-lg bg-white shadow">
    <div>
      {{ title }}
    </div>
      
    <!-- slot will always put the content found between the component tags (see below) --> 
    {{ slot }}
    
    <!-- Cotton's ethos is that it replaces all block, extends and includes with a more expressive, familiar, html tag like syntax and hierarchy -->
    {% if modal_buttons %}
      <div class="flex justify-end space-x-2 rounded-b border-t border-gray-200 p-6">
        {{ modal_buttons }}
      </div>
    {% endif %}
  </div>
</div>

As long as this file is saved in templates/cotton/modal.cotton.html this would then be called with:

<c-modal id="modal-1" ...>
  I am modal content

  <!-- in order to pass HTML to the component we use 'named slots' -->
  <c-slot name="modal_buttons">
    <a href="#" @click="close()">Cancel</a>
  </c-slot>
</c-modal>

For more info checkout:

https://github.com/wrabit/django-cotton
https://django-cotton.com/

I should say, disclaimer, I am the creator of cotton, django-components may also tick the boxes!