Add theme template suggestions for block typesApril 1, 2022

Drupalphphookspreprocess

Create a pre-process function to add theme template suggestions for custom block types at block-level.

/**
 * Implements hook_theme_suggestions_HOOK_alter() for form templates.
 * @param array $suggestions
 * @param array $variables
 */
function THEMENAME_theme_suggestions_block_alter(array &$suggestions, array $variables) {
  // Block suggestions for custom block bundles.
  if (isset($variables['elements']['content']['#block_content'])) {
    array_splice($suggestions, 1, 0, 'block__bundle__' . $variables['elements']['content']['#block_content']->bundle());
  }
}

This snippet was extracted from drupal.org forum:

Create a full width divFebruary 27, 2022

css

This tip will help you to create a full width element. It does not matter if the element is inside other element.

.w-full-container {
  left: 50%;
  margin-left: -50vw;
  margin-right: -50vw;
  position: relative;
  right: 50%;
  width: 100vw;
}
<section class="w-full-container">
  This is my full width element
</section>

Convert links entities into objectFebruary 27, 2022

Drupalphphookspreprocess

Create a pre-process to intercept a paragraph and transform a list of LinkItem into an object to use it in the twig template.

The following code uses <my-theme-name>, replace with your theme name.

function <my-theme-name>_preprocess_paragraph(&$variables) {
  $paragraph = $variables['elements']['#paragraph'];
  $paragraph_type = $paragraph->getParagraphType();
  switch ($paragraph_type->id) {
    case 'links_list':
      $menu = $paragraph->field_menu;
      $menu_items = [];
      if (count($menu)) {
        foreach ($menu as $menu_item) {
          /* @var $menu_item DrupallinkPluginFieldFieldTypeLinkItem */
          $values = $menu_item->getValue();
          $link_title = isset($values['title']) ? $values['title'] : '';

            $item = [
              'url' => $menu_item->getUrl()->toString(),
              'text' => $link_title,
              'isExternal' => $menu_item->getUrl()->isExternal(),
            ];
            $menu_items[] = $item;
          }
        }

        $variables['menu_items'] = $menu_items;
        break;
      }
   }
}

And in your twig template you can do something like:

{% if menu_items %}
  {% set menu_markup %}
    <ul class="menu list-unstyled">
      {% set link_attributes = create_attribute().setAttribute('rel', 'nofollow noopener') %}
      {% for link in menu_items %}
        <li>
          <a class="d-flex" href="{{ link.url }}"{{ link.isExternal ? link_attributes: '' }}>
            {{ link.text }}
            {{ arrow_icon }}
          </a>
        </li>
      {% endfor %}
    </ul>
  {% endset %}
{% endif %}

# Print menu list
{{menu_markup}}

Get the parent node by using VanillaJSFebruary 27, 2022

javascriptjs

Some utilities to select elements by using some short-hand and safe functions.

/**
 * Get the node's parent element.
 * @param element HTMLElement
 * @param classOrId
 * @returns {null|HTMLElement}
 */
const getParentNode = (element, classOrId) => {
  try {
    let parent = element.parentNode;
    if (!classOrId) {
      return parent;
    }

    while (parent) {
      if (parent.classList.contains(classOrId) || parent.id === classOrId) {
        return parent;
      }
      parent = parent.parentNode;
    }
  } catch (e) {
    return null;
  }
  return null;
};

This snippet was extracted from StackOverflow answers:

Select elements by using VanillaJSFebruary 27, 2022

javascriptjs

Some utilities to select elements by using some short-hand and safe functions.

/**
* Get Element by using vanillaJS
* @param selector selector like string. e.g: ".my-element", "#my-element"
* @param parent Get the element inside another element
* @returns {null|HTMLElement}
*/
const qs = (selector, parent) => {
  try {
    return parent
      ? parent.querySelector(selector)
      : document.querySelector(selector);
  } catch (e) {
    return null;
  }
};

/**
 * Get Elements by using vanillaJS
 * @param selector selector like string. e.g: ".my-element", "#my-element"
 * @param parent Get the element inside another element
 * @returns {null|*|NodeListOf<*>}
 */
const qsa = (selector, parent) => {
  try {
    return parent
      ? parent.querySelectorAll(selector)
      : document.querySelectorAll(selector);
  } catch (e) {
    return null;
  }
};

/**
 * getElementById short-hand function
 * @param id
 * @returns {HTMLElement}
 */
const byId = (id) => document.getElementById(id);

This snippet was extracted from StackOverflow answers:

usePagination hookFebruary 27, 2022

tstypescriptreacthooks

With this React hook you can manipulate items pagination.

import { Reducer, useEffect, useReducer } from 'react';

export const DEFAULT_ITEMS_PER_PAGE = 8;

export interface PaginationState<ItemsType = any> {
  totalPages: number;
  currentPage: number;
  items: ItemsType[];
  itemsPerPage: number;
}

export enum PaginationStateActionType {
  NEXT_PAGE = 'NEXT_PAGE',
  PREV_PAGE = 'PREV_PAGE',
  GOTO_PAGE = 'GOTO_PAGE',
  SET_ITEMS = 'SET_ITEMS',
}

export interface PaginationStateActionPayload<ItemsType = any> {
  pageNumber?: number;
  newItems?: ItemsType[];
}

export interface PaginationStateAction<ItemsType = any> {
  type: PaginationStateActionType;
  payload?: PaginationStateActionPayload<ItemsType>;
}

const reducer = (state: PaginationState, action: PaginationStateAction) => {
  const {
    currentPage,
    itemsPerPage,
    totalPages,
  } = state;
  const {
    type,
    payload: {
      pageNumber,
      newItems = [],
    } = {},
  } = action;

  const nextPage = type === PaginationStateActionType.NEXT_PAGE
    ? currentPage + 1
    : currentPage - 1;

  switch (type) {
    case PaginationStateActionType.NEXT_PAGE: {
      if (nextPage <= totalPages) {
        return { ...state, currentPage: nextPage };
      }
      return state;
    }
    case PaginationStateActionType.PREV_PAGE: {
      if (nextPage > 0) {
        return { ...state, currentPage: nextPage };
      }
      return state;
    }
    case PaginationStateActionType.GOTO_PAGE: {
      if (pageNumber && pageNumber > 0 && pageNumber <= totalPages) {
        return { ...state, currentPage: pageNumber };
      }
      return state;
    }
    case PaginationStateActionType.SET_ITEMS:
      return {
        ...state,
        items: newItems,
        totalPages: newItems && Math.ceil(newItems.length / itemsPerPage),
        currentPage: 1,
      };
    default:
      return state;
  }
};

const initialPaginationState: PaginationState = {
  currentPage: 1,
  items: [],
  totalPages: 0,
  itemsPerPage: DEFAULT_ITEMS_PER_PAGE,
};

const usePagination = <ItemsType>(
  elements: ItemsType[],
  itemsPerPage: number = DEFAULT_ITEMS_PER_PAGE,
) => {
  const totalPages = elements && Math.ceil(elements.length / itemsPerPage);

  const [state, dispatch] = useReducer<Reducer<
  PaginationState<ItemsType>,
  PaginationStateAction<ItemsType>
  >>(
    reducer,
    {
      ...initialPaginationState,
      totalPages,
      items: elements,
      itemsPerPage,
    },
  );
  const {
    currentPage,
    items,
  } = state;

  const currentPageIndex = currentPage - 1;
  const pageElements = items.slice(currentPageIndex * itemsPerPage, currentPage * itemsPerPage);

  const nextPage = () => dispatch({ type: PaginationStateActionType.NEXT_PAGE });
  const prevPage = () => dispatch({ type: PaginationStateActionType.PREV_PAGE });
  const gotToPage = (pageNumber: number) => dispatch({
    type: PaginationStateActionType.GOTO_PAGE,
    payload: {
      pageNumber,
    },
  });

  useEffect(() => {
    dispatch({
      type: PaginationStateActionType.SET_ITEMS,
      payload: {
        newItems: elements,
      },
    });
  }, [elements]);

  return {
    currentPage,
    currentPageIndex,
    pageElements,
    totalPages,
    isFirstPage: currentPage === 1,
    isLastPage: currentPage === totalPages,

    gotToPage,
    nextPage,
    prevPage,
  };
};

export default usePagination;

useBreakpoint hookFebruary 22, 2022

tstypescriptreacthooks

Useful to know if the current window screen size match in determinate breakpoint

import { useEffect, useState } from 'react';

interface IMediaQuery {
  name: string
  mq: string
  setStateFunction(newState:boolean): void;
  currentState: boolean;
}

const useBreakpoint = () => {
  const [isMobile, setIsMobile] = useState(false);
  const [isTablet, setIsTablet] = useState(false);
  const [isDesktop, setIsDesktop] = useState(false);

  const mediaQueries:IMediaQuery[] = [
    {
      name: 'mobile', mq: '(max-width: 767px)', currentState: isMobile, setStateFunction: setIsMobile,
    },
    {
      name: 'tablet', mq: '(min-width: 768px) and (max-width: 1023px)', currentState: isTablet, setStateFunction: setIsTablet,
    },
    {
      name: 'desktop', mq: '(min-width: 1024px)', currentState: isDesktop, setStateFunction: setIsDesktop,
    },
  ];

  useEffect(() => {
    const mqls:MediaQueryList[] = [];
    const onChange = (event: any) => {
      const { media, matches } = event?.currentTarget as MediaQueryList || event;
      const currentMediaQuery = mediaQueries.find((item) => item.mq === media);
      if (currentMediaQuery) {
        const { currentState, setStateFunction } = currentMediaQuery;
        if (typeof setStateFunction === 'function') {
          if (matches !== currentState) {
            setStateFunction(matches);
          }
        }
      }
    };

    mediaQueries.forEach((item) => {
      const { mq } = item;
      if (mq) {
        const mql = window.matchMedia(mq);
        mql.addEventListener('change', onChange);
        mqls.push(mql);
        onChange(mql);
      }
    });

    return () => {
      mqls.forEach((mql) => mql.removeEventListener('change', onChange));
    };
  });

  return {
    isMobile,
    isTablet,
    isDesktop,
  };
};

export default useBreakpoint;

CRACO config to prevent dynamic js/cssFebruary 21, 2022

javascriptjsreact

Override CRA configuration in order to know the output asset name.

module.exports = {
  webpack: {
    configure: {
      output: {
        // publicPath: process.env.REACT_APP_BASE_URL || '/',
        filename: 'js/my-js.js',
      },
      optimization: {
        runtimeChunk: false,
        splitChunks: {
          chunks() {
            return false
          },
        },
      },
    },
    alias: {
      "@atoms": "components/atoms/",
      "@icons": "images/icons/",
      "@molecules": "components/molecules/",
      "@organisms": "components/organisms/",
      "@pages": "pages/",
      "@templates": "components/templates/",
    },
  },
  plugins: [
    {
      plugin: {
        overrideWebpackConfig: ({ webpackConfig }) => {
          webpackConfig.plugins[5].options.filename = 'css/my-css.css';
          return webpackConfig;
        },
      },
      options: {}
    }
  ],
}

More info

Dev Mode in DrupalFebruary 20, 2022

Drupal

You should create or modify the following files:

<your-site-docroot>sites/local.services.yml

parameters:
  http.response.debug_cacheability_headers: true
  twig.config:
    debug: true
    auto_reload: true
    cache: false
services:
  cache.backend.null:
    class: DrupalCoreCacheNullBackendFactory

<your-site-docroot>sites/default/settings/local.settings.php

$settings['kint_maxLevels'] = '4';
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';

More documentation in:

Library Override in DrupalFebruary 20, 2022

Drupal

<your-site-docroot>themes/custom/<your-theme>/<your-theme>.yml

libraries-override:
  # Replace an entire library.
  core/drupal.collapse: mytheme/collapse

  # Replace an asset with another.
  subtheme/library:
    css:
      theme:
        css/layout.css: css/my-layout.css

  # Remove an asset.
  drupal/dialog:
    css:
      theme:
        dialog.theme.css: false

  # Remove an entire library.
  core/modernizr: false

More documentation in:

Component with Children and Classname interfacesFebruary 20, 2022

tstypescriptreact
import { ReactNode } from 'react';

export type ClassNameType = string | undefined;
export type Children = ReactNode | JSX.Element | JSX.Element[] | string | number | null;

export interface WithChildren {
  children?: Children;
}

export interface WithClassName {
  className?: ClassNameType;
}

export interface DefaultComponent extends WithChildren, WithClassName {
}

Manipulate attributes in twig templateFebruary 19, 2022

Drupaltwig
{% set my_attribute = create_attribute() %}
{%
  set my_classes = [
    'kittens',
    'llamas',
    'puppies',
  ]
%}
<div{{ my_attribute.addClass(my_classes).setAttribute('id', 'myUniqueId') }}>
  {{ content }}
</div>

# Other useful methods
# Add single class: attributes.addClass('<className>')
# Remove Class: attributes.removeClass()
# Remove Attribute: attributes.removeAttribute('<attributeName>')
# Verify if an element has a class: attributes.hasClass('<className>')
# Access to attribute key: attributes.<key>. e.g. attribute.style, attribute['attribute-name']

More documentation in:

Browser detectorFebruary 19, 2022

javascriptjsbrowser

How to detect the current user browser.

((window, navigator) => {
  const isOpera = typeof window.opr !== 'undefined';
  const isFirefox = typeof InstallTrigger !== 'undefined';
  const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification));
  const isIE = /*@cc_on!@*/false || !!document.documentMode;
  const isChromium = window.chrome;
  const isEdge = navigator.userAgent.indexOf('Edg') > -1;
  const vendorName = navigator.vendor;
  const isChrome = isChromium !== null && typeof isChromium !== 'undefined' && vendorName === 'Google Inc.' && isOpera === false && isEdge === false;
  const isBlink = (isChrome || isOpera) && !!window.CSS;
  var isIOSChrome = navigator.userAgent.match("CriOS");

  window.browser = {
    isOpera,
    isFirefox,
    isSafari,
    isIE,
    isEdge,
    isChrome,
    isIOSChrome,
    isBlink,
  };
})(window, navigator || {});

This snippet was extracted from StackOverflow answers:

Load scripts asyncFebruary 19, 2022

javascriptjs
const LOADING_STATE = 'loading';
const LOADED_STATE = 'loaded';
const LOADING_INTERVAL_TIME = 200;
const MAX_LOADING_RETRY = 10;

/**
* Append node to header element
*
* @param element
*/
const appendToHeader = (element) => {
  const headers = document.getElementsByTagName('header');
  const header = headers && headers.length && headers[0];
  if (header) {
    header.appendChild(element);
  }
};

/**
* Verify is a library was loaded before
*
* @param id Library id
* @returns {*|null}
*/
const verifyLibraryLoaded = (id) => {
  const { dataset = {} } = document.documentElement;
  const state = dataset[id];

  return state || null;
};

/**
* Load script in async way
*
* @param source
* @param id
* @returns {Promise<boolean>}
*/
const loadScript = (source, id) => {
  const state = verifyLibraryLoaded(id);

  if (state) {
    if (state === LOADING_STATE) {
      /* Waiting 10 times the library loads */
      return new Promise((resolve, reject) => {
        let counter = 0;
        let intervalId;
        const verify = () => {
          const currentState = verifyLibraryLoaded(id);

          if (currentState === LOADED_STATE) {
            clearInterval(intervalId);
            return resolve(true);
          }
          if (counter === MAX_LOADING_RETRY) {
            reject(false);
          }
          counter++;
        };
        intervalId = setInterval(verify, LOADING_INTERVAL_TIME);
      });
    } else if (state === LOADED_STATE) {
      /* return and execute */
      return Promise.resolve(true);
    }
  }

  const script = document.createElement('script');
  script.src = source;

  if (id) {
    document.documentElement.dataset[id] = LOADING_STATE;
  }

  return new Promise((resolve, reject) => {
    script.addEventListener('error', () => reject(false));
    script.addEventListener('load', () => {
      if (id) {
        document.documentElement.dataset[id] = LOADED_STATE;
      }
      resolve(true);
    });
    appendToHeader(script);
  });
};

Load styles asyncFebruary 19, 2022

javascriptjs
const LOADED_STATE = 'loaded';

/**
* Append node to header element
*
* @param element
*/
const appendToHeader = (element) => {
  const headers = document.getElementsByTagName('header');
  const header = headers && headers.length && headers[0];
  if (header) {
    header.appendChild(element);
  }
};

/**
* Verify is a library was loaded before
*
* @param id Library id
* @returns {*|null}
*/
const verifyLibraryLoaded = (id) => {
  const { dataset = {} } = document.documentElement;
  const state = dataset[id];

    return state || null;
  };

/**
* Load stylesheet in async way
*
* @param source stylesheet url
* @param id resource id
* @returns {Promise<boolean>}
*/
const loadStyles = (source, id) => {
  const state = verifyLibraryLoaded(id);

  if (state && state === LOADED_STATE) {
    return Promise.resolve(true);
  }

  return new Promise((resolve) => {
    const resource = document.createElement('link');
    resource.rel = 'stylesheet';
    resource.href = source;

    resource.addEventListener('load', () => {
      resolve(true);
    });
    appendToHeader(resource);
  });
};