Skip to content

Add n:tagname macro for changing html tag name #248

@daun

Description

@daun

I'd love to see an official n:tagname macro that allows changing the html tag name of the current element.

This is useful in two scenarios:

  • Semantical & accessible applications that require differing heading levels (h1, h2, h3) throughout the app
  • Markup requirements where just removing an element via n:tag-if is too much and you'd rather turn that a into a span depending on some variable.

If you agree this is a useful feature, see the code below for a working implementation.

Example

{* title.latte *}

{default $heading = null}

<h2 n:tagname="$heading">
    {$page->title}
</h2>

This will create an h2:

{include 'title.latte'}

This will create an h3:

{include 'title.latte', heading => h3}

Prior art

Vue components have this behavior; the macro there is called is which is nice and concise:

<!-- Render a <table> tag -->
<component is="table"></component>

While the is attr is well-known and unambigious, I feel that tagName would be more obvious for quick scanning of the source code.

Implementation

I have implemented this behavior for a project before. See below. Might serve as inspiration or starting point.

    /**
     * n:tagname
     */
    public function macroTagname(MacroNode $node, PhpWriter $writer)
    {
        if (!$node->prefix || $node->prefix !== MacroNode::PREFIX_NONE) {
            throw new CompileException("Unknown {$node->getNotation()}, use n:{$node->name} attribute.");
        }
        if (!$node->args) {
            throw new CompileException("The n:{$node->name} value cannot be empty.");
        }
    }

    /**
     * n:tagname
     *
     */
    public function macroTagnameEnd(MacroNode $node, PhpWriter $writer)
    {
        $exception = CompileException::class;

        $node->openingCode = '<?php
            $__tagMacro = "' . $node->name . '";
            $__tagNameOld = "' . $node->htmlNode->name . '";
            $__tagNameNew = ' . $writer->formatArgs() . ';

            // Enforce valid tag name if defined
            if ($__tagNameNew !== null) {
                if (!is_string($__tagNameNew)) {
                    throw new ' . $exception . '("The tag name passed to n:{$__tagMacro} must be a string.");
                }
                $__tagNameNew = trim($__tagNameNew);
                if (!$__tagNameNew) {
                    throw new ' . $exception . '("The tag name passed to n:{$__tagMacro} must not be empty.");
                }
                try {
                    new \DOMElement($__tagNameNew);
                } catch (\DOMException $e) {
                    throw new ' . $exception . '("Invalid tag name supplied to n:{$__tagMacro}: {$__tagNameNew}.");
                }
            }

            ob_start(function () {});
        ?>';

        $node->closingCode = '<?php
            $__tagContent = ob_get_clean();
            if ($__tagNameNew && $__tagNameOld !== $__tagNameNew) {
                // Replace starting tag
                $__tagContent = preg_replace(
                    "/^(\s*)<" . $__tagNameOld . "\b/",
                    "$1<" . $__tagNameNew,
                    $__tagContent
                );
                // Replace end tag
                $__tagContent = preg_replace(
                    "/<\/" . $__tagNameOld . ">(\s*)$/",
                    "</" . $__tagNameNew . ">$1",
                    $__tagContent
                );
            }
            echo $__tagContent;
        ?>';
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions