Add Layout Paragraphs to Paragraphs Library

Created on 2 December 2023, about 1 year ago

The idea of storing layout paragraphs in the paragraphs library was discussed in Add "Promote to library" / Paragraphs Library compatibility Fixed and a feature request was raised in Allow adding Layouts (incl. children) to Paragraphs library Active .

While waiting for this development we have implemented a workaround that allows layouts to be stored in the library. The basis of this workaround is to create a new paragraph type called 'section_wrapper'. This wraps a layout paragraph and all its related paragraphs into one high level paragraph that can then be stored in the library. This overcomes the issue of the library only accepting a single top level paragraph and layout paragraphs structure of storing layouts and paragraphs as siblings with structure defined in behavior settings.

The section wrapper paragraph is only used by the system and if correctly configured can be hidden entirely from the user. Some new code is required to handle section wrapping and this is based on the existing code in the layout_paragraphs_library module to provide the 'Promote to Library' button on layout paragraphs.

The steps to use this workaraound are:

  1. Create a new paragraph type called 'section_wrapper'. The name can be changed as long as the code is modified appropriately. Add a paragraph reference field and allow all the paragraph types you want to include in sections. In the 'Manage form display' make sure 'Layout Paragraphs' is selected as the widget for this field and 'Require paragraphs to be added inside a layout' is selected.
  2. Add the paragraph type as a valid paragraph on all paragraph fields that can use paragraphs from the library
  3. Create a custom module to handle the 'promote to library' button. There are two functions:
    /**
     * Implements hook_form_FORM_ID_alter().
     *
     * Alters the layout paragraphs component form to add 'Promote to library'.
     */
    function MY_MODULE_form_layout_paragraphs_component_form_alter(array &$form, FormStateInterface $form_state) {
      /** @var \Drupal\layout_paragraphs\Contracts\ComponentFormInterface $form_object */
      $form_object = $form_state->getFormObject();
      $paragraph = $form_object->getParagraph();
      $paragraph_type = $paragraph->getParagraphType();
    
      // Only applies to paragraph types that allow being promoted to library
      // and act as a layout.
      $allow_library_conversion =
        $paragraph_type->getThirdPartySetting('paragraphs_library', 'allow_library_conversion', FALSE)
        && !$paragraph_type->hasEnabledBehaviorPlugin('layout_paragraphs');
      if ($allow_library_conversion) {
        $form['actions']['promote_to_library'] = [
          '#type' => 'submit',
          '#value' => t('Promote to library'),
          '#submit' => ['MY_MODULE_paragraphs_library_submit'],
          '#name' => 'promote-to-library',
          '#ajax' => [
            'callback' => 'layout_paragraphs_library_ajax',
          ],
          '#attributes' => [
            'class' => [
              'lpb-btn--promote-to-library',
            ],
          ],
          '#weight' => 110,
        ];
        // Fix inline_entity_form compabitility.
        // @see https://www.drupal.org/project/inline_entity_form/issues/2830136
        if (isset($form['actions']['submit']['#ief_submit_trigger'])) {
          $form['actions']['promote_to_library']['#ief_submit_trigger'] = TRUE;
          $form['actions']['promote_to_library']['#ief_submit_trigger_all'] = TRUE;
          array_unshift($form['actions']['promote_to_library']['#submit'], $form['actions']['submit']['#submit'][0]);
        }
      }
    }
    /**
     * Form submit callback for "Promote to library" button.
     */
    function MY_MODULE_paragraphs_library_submit(&$form, FormStateInterface $form_state) {
    
      $tempstore = \Drupal::service('layout_paragraphs.tempstore_repository');
      /** @var \Drupal\layout_paragraphs\Contracts\ComponentFormInterface $form_object */
      $form_object = $form_state->getFormObject();
      $paragraph = $form_object->buildParagraphComponent($form, $form_state);
      $layout_paragraphs_layout = $form_object->getLayoutParagraphsLayout();
      $component = $layout_paragraphs_layout->getComponent($paragraph);
      $component_settings = $component->getSettings();
      $layout_section = $layout_paragraphs_layout->getLayoutSection($paragraph);
      $section_paragraphs = [$layout_section->getEntity()];
      foreach ($layout_section->getComponents() as $section_component) {
        $section_paragraphs[] = $section_component->getEntity();
      }
    
      $section_wrapper = Paragraph::create([
        'type' => 'section_wrapper',
        'field_paragraph_section' => $section_paragraphs,
      ]);
      $section_wrapper->save();
      $form_state->set('original_paragraph', $paragraph);
    
      // Replacing element in the array.
      $library_item = LibraryItem::createFromParagraph($section_wrapper);
      $library_item->save();
    
      // Replace this paragraph with a library reference one.
      $library_paragraph = Paragraph::create([
        'type' => 'from_library',
        'field_reusable_paragraph' => $library_item,
      ]);
      $library_component = $layout_paragraphs_layout->getComponent($library_paragraph);
      $library_component->setSettings($component_settings);
      $form_object->setParagraph($library_component->getEntity());
    
      if (get_class($form_object) == 'Drupal\layout_paragraphs\Form\EditComponentForm') {
        $layout_paragraphs_layout->insertBeforeComponent($paragraph->uuid(), $library_component->getEntity());
        foreach ($section_paragraphs as $section_paragraph) {
          $layout_paragraphs_layout->deleteComponent($section_paragraph->uuid());
        }
        $form_object->setLayoutParagraphsLayout($layout_paragraphs_layout);
        $tempstore->set($form_object->getLayoutParagraphsLayout());
      }
      elseif (get_class($form_object) == 'Drupal\layout_paragraphs\Form\InsertComponentForm') {
        $form_object->setParagraph($library_component->getEntity());
        /** @var \Drupal\layout_paragraphs\Form\InsertComponentForm $form_object */
        $form_object->insertComponent();
        $tempstore->set($form_object->getLayoutParagraphsLayout());
      }
    
    }

    The first routine as the 'Promote to Library' button to layout paragraphs only. The second routine handle the creation of the section wrapper and library item before replacing the current layout with the library item in the UI.

  4. Make section wrapper the only allowed paragraph type that can be added to the library item. This should only be done if you only want sections/layouts in the library. Making the restriction does make the UX simpler as the section wrapper can be pre-selected when creating new library items. Restricting the paragraph types in the library item does currently require the patch #35 in 🐛 Items marked not marked as 'Allow adding to library' can be added to library Needs review .
  5. For the paragraphs library item under 'Manage form display' select 'Paragraphs legacy' as the widget and NOT 'Paragraphs Layout'. If you are only allowing sections/layouts in the library this setting preselects the section wrapper paragraph type and should just show an 'Add section' button. It will work with 'Layout Paragraphs' selected but the library item build will be done in a modal window instead of the main page.
  6. To hide the section wrapper entirely you can use css in an admin theme override stylesheet as follows:
    body.paragraphs-library-add-form {
      .paragraph-type-top {
        display: none;
      }
      .field--type-entity-reference-revisions .fieldset__legend {
        display: none;
        ~ .fieldset__wrapper {
          margin-top: inherit;
        }
      }
    }
    .gin--edit-form {
      .lpb-component-list__item.type-section_wrapper {
        display: none;
      }
    }

    This hides the section wrapper paragraph name from the library item edit form and from the paragraph selection dropdown so section wrapper cannot be selected by the user.

We use this setup on decoupled sites so do not use the 'Manage display' settings which may need some configuration.

📌 Task
Status

Active

Version

2.0

Component

Code

Created by

🇬🇧United Kingdom dippers

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

Comments & Activities

  • Issue created by @dippers
  • 🇬🇧United Kingdom dippers
  • 🇬🇧United Kingdom dippers
  • 🇬🇧United Kingdom dippers
  • I don't know if this is a general error or if it only happens in our implementation and configuration, but we ran into an issue when using this workaround. Saving sections with paragraphs works fine, but when we try to edit the saved paragraphs, ValidReferenceConstraintValidator throws an error saying, "This entity (paragraph: XXXXXX) cannot be referenced."

    I don't have a certain answer why (I'm not a Drupal or PHP dev as such; I'm just tasked with making this work), but I think it's because of the way these paragraphs in Layout Paragraphs are referenced. My workaround for the issue was to use hook_validation_constraint_alter to replace this validator with a copy of it where I can skip if the paragraph is a section_wrapper. Everything is working fine if I do this.

    function [MODULE]_validation_constraint_alter(array &$definitions) {
      $definitions["ValidReference"]['class'] = "Drupal\drupal_cms_validators\Plugin\Validation\Constraint\ValidReferenceConstraint";
    }
    

    It might be important to note that we only want the sections added to the library because we want to limit which paragraph types can be added to specific areas. We use a module called layout_paragraphs_limit to achieve this.

    Did anyone else experience this problem? Is there a better way of fixing it?

  • I don't know if this is a general error or if it only happens in our implementation and configuration, but we ran into an issue when using this workaround. Saving sections with paragraphs works fine, but when we try to edit the saved paragraphs, ValidReferenceConstraintValidator throws an error saying, "This entity (paragraph: XXXXXX) cannot be referenced."

    I don't have a certain answer why (I'm not a Drupal or PHP dev as such; I'm just tasked with making this work), but I think it's because of the way these paragraphs in Layout Paragraphs are referenced. My workaround for the issue was to use hook_validation_constraint_alter to replace this validator with a copy of it where I can skip if the paragraph is a section_wrapper. Everything is working fine if I do this.

    function [MODULE]_validation_constraint_alter(array &$definitions) {
      $definitions["ValidReference"]['class'] = "Drupal\drupal_cms_validators\Plugin\Validation\Constraint\ValidReferenceConstraint";
    }
    

    It might be important to note that we only want the sections added to the library because we want to limit which paragraph types can be added to specific areas. We use a module called layout_paragraphs_limit to achieve this.

    Did anyone else experience this problem? Is there a better way of fixing it?

Production build 0.71.5 2024