Display entity images with thumbnails and 1 large image I want to attach multiple images to custom entity (commerce product),
and display them as follow: 1 large image, and list of thumbnails, user can click on thumbnail thus changing large image.

I found and plan to use the library http://www.elevateweb.co.uk/image-zoom/examples#gallery-lightbox

So my steps:


create subtheme of bootstrap theme
add field "images" to the commerce product entity
write twig file to display images


Is it correct path? or maybe there is better solution? better library? 

With image zoom, I need to get output like this

&lt;img id="img_01" src="small/image1.jpg" data-zoom-image="large/image1.jpg"/&gt;

&lt;div id="gal1"&gt;

  &lt;a href="#" data-image="small/image1.jpg" data-zoom-image="large/image1.jpg"&gt;
    &lt;img id="img_01" src="thumb/image1.jpg" /&gt;
  &lt;/a&gt;

  &lt;a href="#" data-image="small/image2.jpg" data-zoom-image="large/image2.jpg"&gt;
    &lt;img id="img_01" src="thumb/image2.jpg" /&gt;
  &lt;/a&gt;

&lt;/div&gt;
&lt;?php

/**
 * @file
 * Contains \Drupal\mymodule\Plugin\Field\FieldFormatter\SlickMyentityGalleryFormatter.
 */

namespace Drupal\mymodule\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Html;

/**
 * @FieldFormatter(
 *   id = "slick_myentity_gallery",
 *   label = @Translation("Slick Myentity Gallery"),
 *   field_types = {
 *     "image"
 *   }
 * )
 */
class SlickMyentityGalleryFormatter extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'visible_slides' =&gt; 3,
      'image_style' =&gt; 'thumbnail',
      'arrows' =&gt; TRUE,
      'dots' =&gt; TRUE,
      'fade' =&gt; FALSE,
      'standalone_image' =&gt; TRUE,
      'standalone_location' =&gt; 'above',
      'standalone_style' =&gt; 'xlarge',
      'vertical' =&gt; FALSE,
      'lazy_load' =&gt; 'ondemand',
      'hide_single_navigator' =&gt; TRUE
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $element = parent::settingsForm($form, $form_state);
    $field_name = $this-&gt;fieldDefinition-&gt;getName();

    $image_style_entities = \Drupal::entityManager()
      -&gt;getStorage('image_style')
      -&gt;loadMultiple();

    $image_styles = array_map(function($image_style) {
      return $image_style-&gt;label();
    }, $image_style_entities);

    $image_styles = ['_original' =&gt; '- ' . $this-&gt;t('Original') . ' -'] + $image_styles;

    $element['visible_slides'] = [
      '#type' =&gt; 'number',
      '#title' =&gt; $this-&gt;t('Number of visible slides'),
      '#default_value' =&gt; $this-&gt;getSetting('visible_slides'),
      '#min' =&gt; 1,
      '#max' =&gt; 10,
      '#description' =&gt; $this-&gt;t('Number of images visible at once.')
    ];

    $element['image_style'] = [
      '#type' =&gt; 'select',
      '#title' =&gt; $this-&gt;t('Image style'),
      '#default_value' =&gt; $this-&gt;getSetting('image_style'),
      '#options' =&gt; $image_styles,
      '#description' =&gt; $this-&gt;t('Image style to display the images with.')
    ];

    $element['arrows'] = [
      '#type' =&gt; 'checkbox',
      '#title' =&gt; $this-&gt;t('Show arrows'),
      '#default_value' =&gt; $this-&gt;getSetting('arrows'),
      '#description' =&gt; $this-&gt;t('Display the navigation arrows.')
    ];

    $element['dots'] = [
      '#type' =&gt; 'checkbox',
      '#title' =&gt; $this-&gt;t('Paging'),
      '#default_value' =&gt; $this-&gt;getSetting('dots'),
      '#description' =&gt; $this-&gt;t('Display the paging dots.')
    ];

    $element['fade'] = [
      '#type' =&gt; 'checkbox',
      '#title' =&gt; $this-&gt;t('Fade'),
      '#default_value' =&gt; $this-&gt;getSetting('fade'),
      '#description' =&gt; $this-&gt;t('Fade between transitions.')
    ];

    $element['vertical'] = [
      '#type' =&gt; 'checkbox',
      '#title' =&gt; $this-&gt;t('Vertical slider'),
      '#default_value' =&gt; $this-&gt;getSetting('vertical'),
      '#description' =&gt; $this-&gt;t('Display slider vertically instead of horizontally.')
    ];

    $element['lazy_load'] = [
      '#type' =&gt; 'select',
      '#title' =&gt; $this-&gt;t('Lazy load'),
      '#options' =&gt; [
        'none' =&gt; '- ' . $this-&gt;t('None') . ' -',
        'ondemand' =&gt; $this-&gt;t('On demand'),
        'progressive' =&gt; $this-&gt;t('Progressive')
      ],
      '#default_value' =&gt; $this-&gt;getSetting('lazy_load'),
      '#description' =&gt; $this-&gt;t('Lazy load the images to increase page performance.')
    ];

    $element['standalone_image'] = [
      '#type' =&gt; 'checkbox',
      '#title' =&gt; $this-&gt;t('Standalone active image'),
      '#default_value' =&gt; $this-&gt;getSetting('standalone_image'),
      '#description' =&gt; $this-&gt;t('Display the active image separately.')
    ];

    $element['standalone_style'] = [
      '#type' =&gt; 'select',
      '#title' =&gt; $this-&gt;t('Standalone image style'),
      '#default_value' =&gt; $this-&gt;getSetting('standalone_style'),
      '#options' =&gt; $image_styles,
      '#description' =&gt; $this-&gt;t('Image style to display the standalone image with.'),
      '#states' =&gt; [
        'visible' =&gt; [
          "input[name=\"fields[$field_name][settings_edit_form][settings][standalone_image]\"]" =&gt; [
            'checked' =&gt; TRUE
          ]
        ]
      ]
    ];

    $element['standalone_location'] = [
      '#type' =&gt; 'select',
      '#title' =&gt; $this-&gt;t('Standalone image location'),
      '#default_value' =&gt; $this-&gt;getSetting('standalone_location'),
      '#options' =&gt; [
        'above' =&gt; $this-&gt;t('Above'),
        'below' =&gt; $this-&gt;t('Below'),
        'left' =&gt; $this-&gt;t('Left'),
        'right' =&gt; $this-&gt;t('Right')
      ],
      '#description' =&gt; $this-&gt;t('Display slider vertically instead of horizontally.'),
      '#states' =&gt; [
        'visible' =&gt; [
          "input[name=\"fields[$field_name][settings_edit_form][settings][standalone_image]\"]" =&gt; [
            'checked' =&gt; TRUE
          ]
        ]
      ]
    ];

    $element['hide_single_navigator'] = [
      '#type' =&gt; 'checkbox',
      '#title' =&gt; $this-&gt;t('Hide single item carousel'),
      '#default_value' =&gt; $this-&gt;getSetting('hide_single_navigator'),
      '#description' =&gt; $this-&gt;t('If standalone image is displayed and there is only one image, the carousel will be hidden.'),
      '#states' =&gt; [
        'visible' =&gt; [
          "input[name=\"fields[$field_name][settings_edit_form][settings][standalone_image]\"]" =&gt; [
            'checked' =&gt; TRUE
          ]
        ]
      ]
    ];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $summary = parent::settingsSummary();

    if ($this-&gt;getSetting('standalone_image')) {
      $standalone_style = $this-&gt;getSetting('standalone_style');
      if ($standalone_style == '_original') {
        $summary[] = $this-&gt;t('Display standalone image without image style.');
      } else {
        $style = \Drupal::entityManager()-&gt;getStorage('image_style')-&gt;load($standalone_style);
        $summary[] = $this-&gt;t('Display standalone image with %style image style.', [
          '%style' =&gt; $style ? $style-&gt;label() : 'original'
        ]);
      }

      $location = $this-&gt;getSetting('standalone_location');
      $location_labels = [
        'above' =&gt; $this-&gt;t('above'),
        'below' =&gt; $this-&gt;t('below'),
        'left' =&gt; $this-&gt;t('left'),
        'right' =&gt; $this-&gt;t('right')
      ];
      $summary[] = $this-&gt;t('Standalone image position: %position.', [
        '%position' =&gt; $location_labels[$location]
      ]);

      if ($this-&gt;getSetting('hide_single_navigator')) {
        $summary[] = $this-&gt;t('Hide carousel with single image.');
      }
    }

    $image_style = $this-&gt;getSetting('image_style');
    if ($image_style == '_original') {
      $summary[] = $this-&gt;t('Display images without image style.');
    } else {
      $style = \Drupal::entityManager()-&gt;getStorage('image_style')-&gt;load($image_style);
      $summary[] = $this-&gt;t('Display images with %style image style.', [
        '%style' =&gt; $style ? $style-&gt;label() : 'original'
      ]);
    }

    if ($this-&gt;getSetting('vertical')) {
      $summary[] = $this-&gt;t('Display slider vertically.');
    }

    switch ($this-&gt;getSetting('lazy_load')) {
      case 'ondemand':  $summary[] = $this-&gt;t('Use &lt;em&gt;on demand&lt;/em&gt; lazy loading.'); break;
      case 'progressive':  $summary[] = $this-&gt;t('Use &lt;em&gt;progressive&lt;/em&gt; lazy loading.'); break;
    }

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    // Skip if there are no items to display.
    if ($items-&gt;isEmpty()) {
      return [];
    }

    $standalone_html_id = Html::getUniqueId($items-&gt;getName() . '-standalone');
    $carousel_id = Html::getUniqueId($items-&gt;getName() . '-nav');
    $lazy_load = $this-&gt;getSetting('lazy_load');
    $use_lazy_load = $lazy_load != 'none';

    // Retrieve file IDs.
    $fids = [];
    foreach ($items-&gt;getIterator() AS $item) {
      $fids[] = $item-&gt;get('target_id')-&gt;getValue();
    }

    // Load files so we can get their URIs.
    $files = \Drupal::entityManager()-&gt;getStorage('file')-&gt;loadMultiple($fids);

    // Make a list of all URIs.
    $uris = array_map(function($file) {
      return $file-&gt;getFileUri();
    }, $files);

    // Set up the render element.
    $element = [
      '#attached' =&gt; [
        'library' =&gt; [
          'mymodule/slick_myentity_gallery'
        ]
      ]
    ];

    // Create carousel for the standalone image if needed.
    if ($this-&gt;getSetting('standalone_image')) {
      $element['#attributes']['class'][] = 'with-standalone';

      $element['standalone'] = [
        '#theme' =&gt; 'item_list',
        '#items' =&gt; [],
        '#weight' =&gt; 0,
        '#attributes' =&gt; [
          'id' =&gt; $standalone_html_id,
          'class' =&gt; [
            'slick-myentity-gallery',
            'slider-standalone',
            'location-' . $this-&gt;getSetting('standalone_location')
          ],
          'data-slick-settings' =&gt; json_encode([
            'slidesToShow' =&gt; 1,
            'slidesToScroll' =&gt; 1,
            'arrows' =&gt; FALSE,
            'fade' =&gt;  TRUE,
            'asNavFor' =&gt; '#' . $carousel_id,
            'lazyLoad' =&gt; $lazy_load,
            'mobileFirst' =&gt; TRUE
          ])
        ]
      ];

      foreach ($uris AS $uri) {
        $element['standalone']['#items'][] = $this-&gt;getImageItem($uri, $this-&gt;getSetting('standalone_style'), $use_lazy_load);
      }
    }

    // Create the main image carousel.
    $element['carousel'] = [
      '#theme' =&gt; 'item_list',
      '#items' =&gt; [],
      '#weight' =&gt; 1,
      '#attributes' =&gt; [
        'id' =&gt; $carousel_id,
        'class' =&gt; [
          'slick-myentity-gallery',
          'slider-navigation',
          $this-&gt;getSetting('vertical') ? 'vertical' : 'horizontal'
        ],
        'data-slick-settings' =&gt; json_encode([
          'slidesToShow' =&gt; (int) $this-&gt;getSetting('visible_slides'),
          'slidesToScroll' =&gt; 1,
          'asNavFor' =&gt; '#' . $standalone_html_id,
          'dots' =&gt; (bool) $this-&gt;getSetting('dots'),
          'arrows' =&gt; (bool) $this-&gt;getSetting('arrows'),
          'fade' =&gt; (bool) $this-&gt;getSetting('fade'),
          'vertical' =&gt; (bool) $this-&gt;getSetting('vertical'),
          'verticalSwiping' =&gt; (bool) $this-&gt;getSetting('vertical'),
          'focusOnSelect' =&gt; TRUE,
          'lazyLoad' =&gt; $lazy_load,
          'mobileFirst' =&gt; TRUE,
          'rtl' =&gt; \Drupal::languageManager()-&gt;getCurrentLanguage()-&gt;getDirection() == 'rtl',
          'infinite' =&gt; FALSE
        ])
      ]
    ];

    foreach ($uris AS $uri) {
      $element['carousel']['#items'][] = $this-&gt;getImageItem($uri, $this-&gt;getSetting('image_style'), $use_lazy_load);
    }

    // Display the carouse first if the standalone image is visible and
    // is positioned below the carousel or to the right next to it.
    if ($this-&gt;getSetting('standalone_image') &amp;&amp; in_array($this-&gt;getSetting('standalone_location'), ['below', 'right'])) {
      $element['carousel']['#weight'] = -1;
    }

    // Hide navigator if standalone image is displayed and there is only
    // one image.
    if ($this-&gt;getSetting('standalone_image') &amp;&amp; $this-&gt;getSetting('hide_single_navigator') &amp;&amp; $items-&gt;count() == 1) {
      unset($element['carousel']);
    }

    return [$element];
  }

  /**
   * Helper method to generate image for provided style
   * that takes the lazy load setting into account.
   *
   * @param string $uri
   *   The original image uri.
   * @param string $image_style
   *   The image style name.
   * @param bool $lazy_load
   *   If TRUE the lazy slick load image will be returned.
   *
   * @return array
   *   Renderable array for a single image.
   */
  function getImageItem($uri, $image_style, $lazy_load = FALSE) {
    if ($lazy_load) {
      $style = \Drupal::entityManager()
        -&gt;getStorage('image_style')
        -&gt;load($image_style);

      return [
        '#type' =&gt; 'inline_template',
        '#template' =&gt; '&lt;img data-lazy="{{ path }}" /&gt;',
        '#context' =&gt; [
          'path' =&gt; $style ? $style-&gt;buildUrl($uri) : file_create_url($uri)
        ]
      ];
    } else {
      return [
        '#theme' =&gt; 'image_style',
        '#uri' =&gt; $uri,
        '#style_name' =&gt; $image_style
      ];
    }
  }

}


(function($) {

  Drupal.behaviors.slickMyentityGallery = {
    attach: function(context) {
      // Initialize the Slick Carousel on all appropriate elements.
      $('.slick-myentity-gallery[data-slick-settings]', context).once('slick-myentity-gallery-init').each(function() {
        var $this = $(this);
        var settings = $this.data('slick-settings');

        // Adjust behavior if this is a Field API field.
        var $fapi_items = $this.find('.field__item');
        if ($fapi_items.length != 0) {
          $fapi_items.parent().slick( settings );
        } else {
          $this.slick( settings );
        }
      });
    }
  }

})(jQuery);