Story representation of Drupal render arrays

Created on 21 March 2024, 8 months ago

Problem/Motivation

We are converting https://www.drupal.org/project/governor β†’ to use Single Directory Components and Storybook.

Here is the problem snippet from the "usa_footer" twig template:

      <div class="mobile-lg:grid-col-8">
        <nav class="usa-footer__nav" aria-label="Footer navigation">
          {{ footer_primary }}
        </nav>
      </div>

footer_primary when coming from Drupal is a render array.

There does not seem to be a means to replicate that structure within the arguments of stories.twig

Note that because of template nesting, our strong preference is to use the following story structure:

    {% include 'governator:usa_footer' %}

instead of using an embed with syntax.

Here are the elements of the component as they stand now, but this seems insecure if the twig file is also used to render Drupal content. I

usa_footer.component.yml

$schema: https://git.drupalcode.org/project/sdc/-/raw/1.x/src/metadata.schema.json
name: USWDS Footer
status: experimental
group: USWDS
props:
  type: object
  properties:
    attributes:
      type: Drupal\Core\Template\Attribute
    org_name:
      type: string
      title: Name of the organization
    phone:
      type: string
      title: Phone number (eg (800) 354-2337)
    email:
      type: string
      title: Organization email address
    logo:
      title: Logo
      type: object
      required:
        - path
        - alt
        - width
        - height
      properties:
        path:
          type: string
          title: Logo path
        alt:
          type: string
          title: Alt text
        width:
          type: number
          title: Width of the image
        height:
          type: number
          title: Height of the image
slots:
  footer_primary:
    title: Footer Primary
  footer_secondary:
    title: Footer Secondary

usa_footer.stories.twig

{# Arguments #}
{% set args_default = {
  org_name: '<Agency Contact Center>',
  phone: '<(800) 555-GOVT>',
  email: '<info@agency.gov>',
  logo: {
    path: 'themes/contrib/governator/dist/uswds/img/logo-img.png',
    alt: 'Agency Logo',
    width: 350,
    height: 150,
  },
  footer_primary:
    '<ul class="grid-row grid-gap">
      <li class="mobile-lg:grid-col-6 desktop:grid-col-auto usa-footer__primary-content">
        <a class="usa-footer__primary-link" href="https://designsystem.digital.gov/components/footer/#slim-footer">Primary link</a>
      </li>
      <li class="mobile-lg:grid-col-6 desktop:grid-col-auto usa-footer__primary-content">
        <a class="usa-footer__primary-link" href="https://designsystem.digital.gov/components/footer/#slim-footer">Primary link</a>
      </li>
      <li class="mobile-lg:grid-col-6 desktop:grid-col-auto usa-footer__primary-content">
        <a class="usa-footer__primary-link" href="https://designsystem.digital.gov/components/footer/#slim-footer">Primary link</a>
      </li>
      <li class="mobile-lg:grid-col-6 desktop:grid-col-auto usa-footer__primary-content">
        <a class="usa-footer__primary-link" href="https://designsystem.digital.gov/components/footer/#slim-footer">Primary link</a>
      </li>
    </ul>',
} %}

{# Stories definition. #}
{% stories usa_footer with {
  title: 'Components/USWDS/USA Footer',
  component: 'USA Footer',
  parameters: {
    componentSubtitle: '',
    docs: {
      description: {
        component: '
A footer serves site visitors who arrive at the bottom of a page without finding what they want.

**References**
* https://designsystem.digital.gov/components/footer/
        ',
      },
    },
  },
  argTypes: {
    org_name: {
      description: 'The organization name.',
      control: 'text',
      table: {
        type: {summary: 'string'},
        defaultValue: {summary: args_default.org_name},
      },
    },
    phone: {
      description: 'The phone number of the organization.',
      control: 'text',
      table: {
        type: {summary: 'string'},
        defaultValue: {summary: args_default.phone},
      },
    },
    email: {
      description: 'The email of the organization.',
      control: 'text',
      table: {
        type: {summary: 'string'},
        defaultValue: {summary: args_default.email},
      },
    },
    footer_primary: {
      description: 'This is a drupal block for footer primary',
      control: 'text',
      table: {
        type: {summary: 'html'},
        defaultValue: {summary: args_default.footer_primary},
      },
    },
  }
} %}

  {# Default story #}
  {% story default with {
    name: '1. Default',
    args: args_default,
  } %}
    {% include 'governator:usa_footer' %}
  {% endstory %}

{% endstories %}

usa_footer.twig

{% set variant = 'slim' %}

{%
  set classes = [
    'usa-footer',
    'usa-footer--' ~ variant,
  ]
%}

{% if variant == 'slim' %}
<footer{{ attributes.addClass(classes) }}>
  <div class="grid-container usa-footer__return-to-top">
    <a href="#">{{ 'Return to top'|t }}</a>
  </div>
  <div class="usa-footer__primary-section">
    <div class="usa-footer__primary-container grid-row">
      <div class="mobile-lg:grid-col-8">
        <nav class="usa-footer__nav" aria-label="Footer navigation">
          {# The `is iterable` test lets us pass HTML from a story when a render array is expected #}
          {{ footer_primary is iterable ? footer_primary : footer_primary|raw }}
        </nav>
      </div>
      {% if phone or email %}
        <div class="mobile-lg:grid-col-4">
          <address class="usa-footer__address">
            <div class="grid-row grid-gap">
              {% if phone %}
                <div class="grid-col-auto mobile-lg:grid-col-12 desktop:grid-col-auto">
                  <div class="usa-footer__contact-info">
                    <a href="tel:{{ phone|trim }}">{{ phone }}</a>
                  </div>
                </div>
              {% endif %}
              {% if email %}
                <div class="grid-col-auto mobile-lg:grid-col-12 desktop:grid-col-auto">
                  <div class="usa-footer__contact-info">
                    <a href="mailto:{{ email }}">{{ email }}</a>
                  </div>
                </div>
              {% endif %}
            </div>
          </address>
        {% endif %}
      </div>
    </div>
  </div>
  <div class="usa-footer__secondary-section">
    <div class="grid-container">
      {# {{ footer_secondary is iterable ? footer_secondary : footer_secondary|raw }} #}
      <div class="usa-footer__logo grid-row grid-gap-2">
        <div class="grid-col-auto">
          <img class="usa-footer__logo-img" src="{{ logo.path }}" alt="{{ logo.alt }}" width="{{ logo.width }}" height="{{ logo.height }}">
        </div>
        <div class="grid-col-auto">
          <p class="usa-footer__logo-heading">{{ org_name }}</p>
        </div>
      </div>
    </div>
  </div>
</footer>
{% endif %}

Proposed resolution

Essentially, I think we need a better way to define slots in the story.

It could be that I am simply missing something in the argument definition.

It could also be that we need to redefine the component to remove the arbitrary "slots" with structured data.

Might we want a twig filter that recognizes if the arguments are coming from Storybook, so that we can safely print markup.

✨ Feature request
Status

Active

Version

1.0

Component

Twig

Created by

πŸ‡ΊπŸ‡ΈUnited States agentrickard Georgia (US)

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Production build 0.71.5 2024