Add links setting type

Created on 28 February 2023, over 1 year ago
Updated 15 December 2023, 11 months ago

Problem/Motivation

Today, themers don't know what to do with menus and breadcrumb structures.

Obviously, it is not a slot, because it is not possible to put any renderable in it : are too much processes in the templates which expect a strict data structure.

But there is no settings type able to host this data, so themers are using fields anyway, and that's bad because the display can break if we put a renderable (scalar, markup, renderable object, render array, or list of renderable) into this field.

Example: Bootstrap 5

https://git.drupalcode.org/project/ui_suite_bootstrap/-/blob/5.0.x/templ...

  fields:
    items:
      type: "render"
      label: "Menu items"
      description: "Full-height and lightweight navigation (including support for dropdowns)."
      preview:
        - title: "Home"
          url: "https://example.com"
        - title: "Library"
          url: "https://example.com"
          below:
            - title: "Sub 1"
              url: "https://example.com"
            - title: "Sub 2"
              url: "https://example.com"
        - title: "Data"
          url: "route:<nolink>"

https://git.drupalcode.org/project/ui_suite_bootstrap/-/tree/5.0.x/templ...

  fields:
    items:
      type: "render"
      label: "Items"
      preview:
        - text: "Home"
          url: "#"
        - text: "Library"
          url: "#"
        - text: "Data"

Example: Material 2

https://git.drupalcode.org/project/ui_suite_material/-/tree/2.0.x/templa...

  fields:
    items:
      type: render
      label: Menu items
      description: A renderable list component.
      preview:
        - type: pattern
          id: list
          fields:
            items:
              - title: Home
                url: "#"
                attributes: {}
              - title: Library
                url: "#"
                below:
                  - title: Sub 1
                    url: "#"
                  - title: Sub 2
                    url: "#"
              - title: Data
                url: "#"

https://git.drupalcode.org/project/ui_suite_material/-/tree/2.0.x/templa...

  fields:
    items:
      type: render
      label: Items
      preview:
        - text: Home
          url: "#"
        - text: Library
          url: "#"
        - text: Data

Proposed resolution

Create a new plugin: Drupal\ui_patterns_settings\Plugin\UIPatterns\SettingType\MenuSettingType which can host a menu-like structure common between menu.

Configuration form:

  • menu (select), pick one menu or the breadcrumb
  • level (quantity), Initial visibility level
  • depth (quantity): Number of levels to display. This maximum number includes the initial level and the final display is dependant of the pattern template.

The plugin is taking the menu tree, do some parameters processes with the level & the depth, and send the data structure to the pattern.

No more menu--main.html.twig, menu--footer.html.twig... presenter templates!!! :)

So, we are here considering the breadcrumb as a flat menu, because the dat structure are very similar, in order to allow using a menu pattern as breadcrumb and the other way around.

It will be also the opportunity of preprocessing the URL objects from the plugin, so the themer will be able to avoid doing it in preprocess hooks. Example: https://git.drupalcode.org/project/ui_suite_bootstrap/-/blob/5.0.x/src/H...

This feature may replace the planned module.

Remaining tasks

Propose a MR.

Do we implement the "The menu is only visible if the menu link for the current page is at this level or below it. " rule from SystemMenuBlock ?

User interface changes

On Layout Builder:

With ui_patterns_pattern_block :

API changes

Using this new plugin is optional, so it will have no impact on themes and site building not using it.

If a theme want to adopt it, they will need to:

  • replace menu items and breadcrumb fields to settings, which will have no impact on the template structure, so it will not break anything
  • update the structure of the breadcrumb pattern, to make it similar to the menu structure, so do the data conversion from the breadcrumb.html.twig presenter template
Feature request
Status

Fixed

Version

2.0

Component

Code

Created by

🇫🇷France pdureau Paris

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

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • Issue created by @pdureau
  • 🇫🇷France pdureau Paris

    First commit: https://git.drupalcode.org/issue/ui_patterns_settings-3345071/-/commit/3...

    Already working well. Tested with:

    • ui_pattern_settings 2.x
    • ui_suite_bootstrap 4.0.x
    • layout_builder 10.0.x
    • ui_patterns_pattern_block (with a change in info.yml)

    Next steps:

    • Inetgration with breadcrumb
    • Do we need $this->handleInput([$form_id], $def, $form_type);
    • Why the pattern preview doesn't work anymore?
  • 🇫🇷France pdureau Paris

    Is it possible to also manage pager (pagination) components with this plugin?

    Like menu and breadcrumb, pagers have a strict structure of links and labels, so they belongs to props but are still managed as slots.

    Sub-sub elements: items.first, items.previous, items.next, items.last, and each item inside items.pages contain the following elements:

    • href: URL with appropriate query parameters for the item.
    • attributes: A keyed list of HTML attributes for the item.
    • text: The visible text used for the item link, such as "‹ Previous" or "Next ›".

    Source: https://api.drupal.org/api/drupal/core!modules!system!templates!pager.ht...
    Other: https://api.drupal.org/api/drupal/core!modules!views!templates!views-min...

    Example: https://git.drupalcode.org/project/ui_suite_bootstrap/-/blob/5.0.x/templ...

    Only one field with a 2 level structure:

      fields:
        items:
          type: "array"
          label: "Pagination items."
          preview:
            first:
              href: "#"
              text: "« First"
            previous:
              href: "#"
              text: "‹ Previous"
            next:
              href: "#"
              text: "Next ›"
            last:
              href: "#"
              text: "Last »"
            pages:
              1:
                href: "#"
                text: "1"
              2:
                href: "#"
                text: "2"
              3:
                href: "#"
                text: "3"

    Example: https://git.drupalcode.org/project/ui_suite_material/-/blob/2.0.x/templa...

    One field for each first level, a flat structure inside each:

      fields:
        first:
          type: array
          label: First
          description: "Item for the first page; not present on the first page of results."
          preview: null
        previous:
          type: array
          label: Previous
          description: "Item for the previous page; not present on the first page of results."
          preview: null
        next:
          type: array
          label: Next
          description: "Item for the next page; not present on the last page of results."
          preview:
            href: "#"
            text: Page 5
        last:
          type: array
          label: Last
          description: "Item for the last page; not present on the last page of results."
          preview:
            href: "#"
            text: Page 10
    

    Example: https://git.drupalcode.org/project/ui_suite_dsfr/-/blob/1.0.x/templates/...
    Same as Bootstrap

      fields:
        items:
          type: array
          label: Items
          description: "Pagination items.
          The list is keyed by the following elements:
            - first: Item for the first page.
            - previous: Item for the previous page.
            - next: Item for the next page.
            - last: Item for the last page.
            - pages: List of pages, keyed by page number."
          preview:
            pages:
              1:
                href: '#page1'
              2:
                href: '#page2'
              3:
                href: '#page3'
              4:
                href: '#page4'
              5:
                href: '#page5'
            first:
              href: '#first'
            last:
              href: '#last'
            next:
              href: '#next'
            previous:
              href: '#prev'
    
  • 🇫🇷France Grimreaper France 🇫🇷

    Hi,

    About pager, if the structure is ok, why not. But I am surprised of that.

    Also will the plugin will not be too complicated and would it not be better to split into 3 plugins (also to have different settings options)?
    - menus
    - depth of items shown
    - starting level
    - breadcrumb
    - add home link?
    - display current page as title?
    - pager
    - number of items displayed?

    Because also technically when loading the links it is 3 different services:
    - menu
    - breadcrumb builder
    - pager manager

  • 🇫🇷France pdureau Paris

    I would prefer to try without splitting the plugin first. I know it will be complicated, but it seems better to keep complexity here, in a stable generic codebase, and offer to the templates builder a single data structure.

    This data structure will be a list of link items, optionally multi-levels. In a perfect world, they make their templates with this single structure, without overthinking, and the site builder decide which components they use for menus, breadcrumbs and pagers.

  • Status changed to Needs work over 1 year ago
  • 🇫🇷France Grimreaper France 🇫🇷

    Hi,

    Thanks both for the work done here, code and review.

    I have added my comments.

    pager part not implemented yet or did I miss something?

    I hope to be able to help @pdureau with the implementation if needed (not only for pager, I mean).

  • Issue was unassigned.
  • Status changed to Needs review over 1 year ago
  • 🇫🇷France pdureau Paris

    @christian

  • 🇫🇷France pdureau Paris

    "menu" setting type is renamed "links" because it targets also breadcrumbs and pagers

  • Status changed to Needs work about 1 year ago
  • 🇫🇷France pdureau Paris

    Some news about the current work

    Pierre's proposal

    MR: https://git.drupalcode.org/project/ui_patterns_settings/-/merge_requests...

    Naming

    The setting type is called "menu" but we want "links"

    Data structure

    We followed the Drupal menu structure https://api.drupal.org/api/drupal/core!modules!system!templates!menu.htm...

    With addition of link_attributes property (extracted from Url::getOptions()):

    attributes: HTML attributes for the menu item.
    below: The menu item child items.
    title: The menu link title.
    link_attributes: HTML attributes for the link
    url: The menu link URL as string
    localized_options: Menu link localized options.
    is_expanded: TRUE if the link has visible children within the current menu tree.
    is_collapsed: TRUE if the link has children within the current menu tree that are not currently visible.
    in_active_trail: TRUE if the link is in the active trail.

    Same data structure for menus, links (for example language switcher), breadcrumb & pagers (system and views)
    The plugin is doing the conversion. We took menu as base because it is the more capable.

    Challenge

    Cache management was tricky but we did it successfully with: PatternSettingBase::alterElement()

    Twig filter

    The controversial part. We introduced |convertToMenu() (TODO: rename to |normalizeLinks()) filter to do the conversion from presenter templates.

    IMHO, it is OK to have such a filter because never used in components templates

    Christian's rewrite

    Branch: https://git.drupalcode.org/project/ui_patterns_settings/-/tree/ISSUE-334...

    Naming

    The setting type is called "link_lists", it is better than "menu" but IMHO "links" is even better.

    New plugin type: SettingDataProvider

    https://git.drupalcode.org/project/ui_patterns_settings/-/tree/ISSUE-334...

    breadcrumb and menu are implemented with this plugin type, so the SettingType plugin is lighter

    That's great because it is kind of similar to the target architecture of Ui Patterns 2 (source plugins more powerful and compatible with props) .

    Issues

    Where is the code related to pagers and links ? Maybe we need to add them as SettingDataProvider and add an optional context mechanism in this plugin type (like UI Patterns Source plugins)

    The Twig filter is missing. I know it is not nice but we need to find a way to accomodate presenter template. Maybe we can introduce a public static method in the setting type plugin, which will be easy to use in preprocess hooks inside each theme.

  • Assigned to pdureau
  • @pdureau opened merge request.
  • Status changed to Needs review about 1 year ago
  • 🇫🇷France pdureau Paris

    A new MR based on Christian's rewrite: https://git.drupalcode.org/project/ui_patterns_settings/-/merge_requests/16

    The Twig filter is not here anymore, so front developers will need to do preprocess in order to convert the links structure.

    Examples:

    use Drupal\ui_patterns_settings\Plugin\UiPatterns\SettingType\LinksSettingType;
    
    function ui_suite_bootstrap_preprocess_breadcrumb(array &$variables): void {
      $variables["breadcrumb"] = LinksSettingType::normalize($variables["breadcrumb"]);
    }
    
    function ui_suite_bootstrap_preprocess_pager(array &$variables): void {
      $variables["items"] = LinksSettingType::normalize($variables["items"]);
    }
    
    function ui_suite_bootstrap_preprocess_links(array &$variables): void {
      $variables["links"] = LinksSettingType::normalize($variables["links"]);
    }
    
    ...
    

    I think it is OK to have such preprocess because they are:

    • one line only, no business
    • not targeting patterns
  • 🇫🇷France pdureau Paris

    Before we merge, I would like to discuss a bit about the links data structure.

    We decided, and implemented, that:

    • this data structure will be the same as the one in menu.html.twig, with only one property more: link_attributes, because it is the more capable
    • we transform pager, mini pager, links and breadcrumb structure to the expected data structure

    However, #3227824: Move the linkset functionality from the decoupled menus contributed module to core's system module tell us about an IETF's proposed linkset format already used by JSON API to avoid creating too many drupalism.

    Example:

    {
      "linkset":
        [
          { "anchor": "http://example.net/bar",
            "next": [
                  {"href":     "http://example.com/foo",
                   "type":     "text/html",
                   "hreflang": [ "en" , "de" ],
                   "title":    "Next chapter",
                   "title*":   [ { "value": "nachstes Kapitel" , "language" : "de" } ]
                  }
            ]
          }
        ]
    }

    However, expected properties are not easily mappable:

    • attributes: HTML attributes for the menu item. >> ???
    • below: The menu item child items. >> The linkset media type is not hierarchical. LinksetController::toLinkTargetObjects is creating a weird hierarchy structure instead
    • title: The menu link title. >> title
    • link_attributes: HTML attributes for the link >> All properties except the ones defined. See LinksetController::processCustomLinkAttributes
    • url: The menu link URL as string >> href

    And additional properties, sometimes used by components authors, are not mappable at all:

    • localized_options: Menu link localized options. >> ???
    • is_expanded: TRUE if the link has visible children within the current menu tree. >> ???
    • is_collapsed: TRUE if the link has children within the current menu tree that are not currently visible. >> ???
    • in_active_trail: TRUE if the link is in the active trail. >> ???
  • 🇩🇪Germany Christian.wiedemann

    Christian.wiedemann made their first commit to this issue’s fork.

  • Status changed to Fixed about 1 year ago
  • Issue was unassigned.
  • Status changed to Needs work about 1 year ago
  • 🇫🇷France pdureau Paris

    hi,

    We have a little issue with previews, so i am reopening the ticket.

    In this setting type, the data we are storing in the config is not the same as the data we are sending to the component.

    For example:

    We have a the PatternSettingTypeInterface::settingsPreprocess() method which is doing this job of converting.

    However, sometimes, we don't want this conversion. We want the raw data. For example, when we take it from the preview in the definition YML file.

    Today, there is no data returned, empty array, so I am proposing this fix:

    --- a/src/Plugin/ComplexSettingTypeBase.php
    +++ b/src/Plugin/ComplexSettingTypeBase.php
    @@ -95,7 +95,7 @@ abstract class ComplexSettingTypeBase extends PatternSettingTypeBase implements
           $this->provider = $instance;
           return $instance->getData($value['configuration'][$provider_id]['config'] ?? []);
         }
    -    return [];
    +    return $value;
       }
     
       /**
    

    Is it too naive?

  • 🇫🇷France pdureau Paris

    Other issue.

    Those 2 imports are missing from LinksSettingType :

    use Drupal\Core\Url;
    use Drupal\Core\Template\Attribute;

    So, this hook is not working properly:

    function template_preprocess_menu(array &$variables): void {
      $variables["items"] = LinksSettingType::normalize($variables["items"]);
    }
  • Assigned to pdureau
  • 🇫🇷France pdureau Paris

    Other feedback.

    While working on 📌 Bootstrap 5: Convert menu, breadcrumb and pagers to the links prop type Needs review , we found some changes to do useful for https://www.drupal.org/project/menu_link_attributes integration (but not specific to this module, those changes may also be useful for other menu API integration).

    I will propose a MR

  • 🇫🇷France pdureau Paris

    Other subject.

    To define out links data structure, we have added link_attributes property to the Drupal menu data structure (see menu.html.twig).

              $item["options"] = $url->getOptions();
              if (isset($options["attributes"])) {
                $item["link_attributes"] = new Attribute($options["attributes"]);
              }
    

    Why not injecting directly options property? There are some stuff which can be useful in a template:

    'query': An array of query key/value-pairs (without any URL-encoding) to append to the URL.
    'fragment': A fragment identifier (named anchor) to append to the URL. Do not include the leading '#' character.
    'absolute': Defaults to FALSE. Whether to force the output to be an absolute link (beginning with http:). Useful for links that will be displayed outside the site, such as in an RSS feed.
    'attributes': An associative array of HTML attributes that will be added to the anchor tag if you use the \Drupal\Core\Link class to make the link.
    'language': An optional language object used to look up the alias for the URL. If $options['language'] is omitted, it defaults to the current language for the language type LanguageInterface::TYPE_URL.
    'https': Whether this URL should point to a secure location. If not defined, the current scheme is used, so the user stays on HTTP or HTTPS respectively. TRUE enforces HTTPS and FALSE enforces HTTP.

    language object must be resolved as a string

  • Issue was unassigned.
  • Status changed to Needs review 12 months ago
  • 🇫🇷France pdureau Paris

    Tested with:

  • Status changed to Fixed 11 months ago
  • 🇫🇷France pdureau Paris

    Shipped in 2.2-alpha1

  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024