Block Basics

Update The Page Block

In your EDS project, create new git branch yourname-page-model

git checkout -b yourname-page-model

Go to the models/_page.json file.

Replace the contents with:

{
  "models": [
    {
      "id": "page-metadata",
      "fields": [
        {
          "component": "text",
          "valueType": "string",
          "name": "jcr:title",
          "label": "Title"
        },
        {
          "component": "text",
          "valueType": "string",
          "name": "jcr:description",
          "label": "Description"
        },
        {
          "component": "text",
          "valueType": "string",
          "name": "keywords",
          "multi": true,
          "label": "Keywords"
        },
        {
          "component": "aem-tag",
          "label": "AEM Tag Picker",
          "description": "Select an AEM tag",
          "name": "tags",
          "valueType": "string",
          "rootPath": "/content/cq:tags"
        }
      ]
    }
  ]
}

Run the linter. Add and commit the change. Then push the branch.

npm run lint:fix
git add .
git commit -m 'page properties'
git push origin yourname-page-model

Go to a page in your site and open it for editing.

Acces the Page properties

In the address bar of the Universal Editor, and ?ref=yourname-page-model

End of activity


Demo: Update The Header Block

In your EDS project, go to blocks/header/, and open header.js.

Replace the contents with:

import { getMetadata } from '../../scripts/aem.js';
import { loadFragment } from '../fragment/fragment.js';

// media query match that indicates mobile/tablet width
const isDesktop = window.matchMedia('(min-width: 900px)');

function closeOnEscape(e) {
  if (e.code === 'Escape') {
    const nav = document.getElementById('nav');
    const navSections = nav.querySelector('.nav-sections');
    const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
    if (navSectionExpanded && isDesktop.matches) {
      // eslint-disable-next-line no-use-before-define
      toggleAllNavSections(navSections);
      navSectionExpanded.focus();
    } else if (!isDesktop.matches) {
      // eslint-disable-next-line no-use-before-define
      toggleMenu(nav, navSections);
      nav.querySelector('button').focus();
    }
  }
}

function closeOnFocusLost(e) {
  const nav = e.currentTarget;
  if (!nav.contains(e.relatedTarget)) {
    const navSections = nav.querySelector('.nav-sections');
    const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
    if (navSectionExpanded && isDesktop.matches) {
      // eslint-disable-next-line no-use-before-define
      toggleAllNavSections(navSections, false);
    } else if (!isDesktop.matches) {
      // eslint-disable-next-line no-use-before-define
      toggleMenu(nav, navSections, false);
    }
  }
}

function openOnKeydown(e) {
  const focused = document.activeElement;
  const isNavDrop = focused.className === 'nav-drop';
  if (isNavDrop && (e.code === 'Enter' || e.code === 'Space')) {
    const dropExpanded = focused.getAttribute('aria-expanded') === 'true';
    // eslint-disable-next-line no-use-before-define
    toggleAllNavSections(focused.closest('.nav-sections'));
    focused.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true');
  }
}

function focusNavSection() {
  document.activeElement.addEventListener('keydown', openOnKeydown);
}

/**
 * Toggles all nav sections
 * @param {Element} sections The container element
 * @param {Boolean} expanded Whether the element should be expanded or collapsed
 */
function toggleAllNavSections(sections, expanded = false) {
  sections.querySelectorAll('.nav-sections .default-content-wrapper > ul > li').forEach((section) => {
    section.setAttribute('aria-expanded', expanded);
  });
}

/**
 * Toggles the entire nav
 * @param {Element} nav The container element
 * @param {Element} navSections The nav sections within the container element
 * @param {*} forceExpanded Optional param to force nav expand behavior when not null
 */
function toggleMenu(nav, navSections, forceExpanded = null) {
  const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true';
  const button = nav.querySelector('.nav-hamburger button');
  document.body.style.overflowY = (expanded || isDesktop.matches) ? '' : 'hidden';
  nav.setAttribute('aria-expanded', expanded ? 'false' : 'true');
  toggleAllNavSections(navSections, expanded || isDesktop.matches ? 'false' : 'true');
  button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation');
  // enable nav dropdown keyboard accessibility
  const navDrops = navSections.querySelectorAll('.nav-drop');
  if (isDesktop.matches) {
    navDrops.forEach((drop) => {
      if (!drop.hasAttribute('tabindex')) {
        drop.setAttribute('tabindex', 0);
        drop.addEventListener('focus', focusNavSection);
      }
    });
  } else {
    navDrops.forEach((drop) => {
      drop.removeAttribute('tabindex');
      drop.removeEventListener('focus', focusNavSection);
    });
  }

  // enable menu collapse on escape keypress
  if (!expanded || isDesktop.matches) {
    // collapse menu on escape press
    window.addEventListener('keydown', closeOnEscape);
    // collapse menu on focus lost
    nav.addEventListener('focusout', closeOnFocusLost);
  } else {
    window.removeEventListener('keydown', closeOnEscape);
    nav.removeEventListener('focusout', closeOnFocusLost);
  }
}

/**
 * loads and decorates the header, mainly the nav
 * @param {Element} block The header block element
 */
export default async function decorate(block) {
  // load nav as fragment
  const navMeta = getMetadata('nav');
  const navPath = navMeta ? new URL(navMeta, window.location).pathname : '/nav';
  const fragment = await loadFragment(navPath);

  // decorate nav DOM
  block.textContent = '';
  const nav = document.createElement('nav');
  nav.id = 'nav';
  while (fragment.firstElementChild) nav.append(fragment.firstElementChild);

  const classes = ['brand', 'sections', 'tools'];
  classes.forEach((c, i) => {
    const section = nav.children[i];
    if (section) section.classList.add(`nav-${c}`);
  });

  const navBrand = nav.querySelector('.nav-brand');
  const brandLogo = navBrand.querySelector('picture');

  if (brandLogo) {
    // Replace the first section's contents with the authored image wrapped with a link to '/'
    navBrand.innerHTML = `<a href="/" aria-label="Home" title="Home" class="home">${brandLogo.outerHTML}</a>`;
    navBrand.querySelector('img').setAttribute('loading', 'eager');
  }

  const navSections = nav.querySelector('.nav-sections');
  if (navSections) {
    // Remove Edge Delivery Services button containers and buttons from the nav sections links
    navSections.querySelectorAll('.button-container, .button').forEach((button) => {
      button.classList = '';
    });

    navSections.querySelectorAll(':scope .default-content-wrapper > ul > li').forEach((navSection) => {
      if (navSection.querySelector('ul')) navSection.classList.add('nav-drop');
      navSection.addEventListener('click', () => {
        if (isDesktop.matches) {
          const expanded = navSection.getAttribute('aria-expanded') === 'true';
          toggleAllNavSections(navSections);
          navSection.setAttribute('aria-expanded', expanded ? 'false' : 'true');
        }
      });
    });
  }

  // hamburger for mobile
  const hamburger = document.createElement('div');
  hamburger.classList.add('nav-hamburger');
  hamburger.innerHTML = `<button type="button" aria-controls="nav" aria-label="Open navigation">
      <span class="nav-hamburger-icon"></span>
    </button>`;
  hamburger.addEventListener('click', () => toggleMenu(nav, navSections));
  nav.prepend(hamburger);
  nav.setAttribute('aria-expanded', 'false');
  // prevent mobile nav behavior on window resize
  toggleMenu(nav, navSections, isDesktop.matches);
  isDesktop.addEventListener('change', () => toggleMenu(nav, navSections, isDesktop.matches));

  const navWrapper = document.createElement('div');
  navWrapper.className = 'nav-wrapper';
  navWrapper.append(nav);
  block.append(navWrapper);
}

In the same folder, open header.css.

Replace the contents with:

/* header and nav layout */
header .nav-wrapper {
  background-color: var(--background-color);
  width: 100%;
  z-index: 2;
  position: fixed;
}

header nav {
  box-sizing: border-box;
  display: grid;
  grid-template:
    'hamburger brand tools' var(--nav-height)
    'sections sections sections' 1fr / auto 1fr auto;
  align-items: center;
  gap: 0 24px;
  margin: auto;
  max-width: 1248px;
  height: var(--nav-height);
  padding: 0 24px;
  font-family: var(--body-font-family);
}

header nav[aria-expanded='true'] {
  grid-template:
    'hamburger brand' var(--nav-height)
    'sections sections' 1fr
    'tools tools' var(--nav-height) / auto 1fr;
  overflow-y: auto;
  min-height: 100dvh;
}

@media (width >= 900px) {
  header nav {
    display: flex;
    justify-content: space-between;
    gap: 0 32px;
    max-width: 1264px;
    padding: 0 32px;
  }

  header nav[aria-expanded='true'] {
    min-height: 0;
    overflow: visible;
  }
}

header nav p {
  margin: 0;
  line-height: 1;
}

header nav a:any-link {
  color: currentcolor;
}

/* hamburger */
header nav .nav-hamburger {
  grid-area: hamburger;
  height: 22px;
  display: flex;
  align-items: center;
}

header nav .nav-hamburger button {
  height: 22px;
  margin: 0;
  border: 0;
  border-radius: 0;
  padding: 0;
  background-color: var(--background-color);
  color: inherit;
  overflow: initial;
  text-overflow: initial;
  white-space: initial;
}

header nav .nav-hamburger-icon,
header nav .nav-hamburger-icon::before,
header nav .nav-hamburger-icon::after {
  box-sizing: border-box;
  display: block;
  position: relative;
  width: 20px;
}

header nav .nav-hamburger-icon::before,
header nav .nav-hamburger-icon::after {
  content: '';
  position: absolute;
  background: currentcolor;
}

header nav[aria-expanded='false'] .nav-hamburger-icon,
header nav[aria-expanded='false'] .nav-hamburger-icon::before,
header nav[aria-expanded='false'] .nav-hamburger-icon::after {
  height: 2px;
  border-radius: 2px;
  background: currentcolor;
}

header nav[aria-expanded='false'] .nav-hamburger-icon::before {
  top: -6px;
}

header nav[aria-expanded='false'] .nav-hamburger-icon::after {
  top: 6px;
}

header nav[aria-expanded='true'] .nav-hamburger-icon {
  height: 22px;
}

header nav[aria-expanded='true'] .nav-hamburger-icon::before,
header nav[aria-expanded='true'] .nav-hamburger-icon::after {
  top: 3px;
  left: 1px;
  transform: rotate(45deg);
  transform-origin: 2px 1px;
  width: 24px;
  height: 2px;
  border-radius: 2px;
}

header nav[aria-expanded='true'] .nav-hamburger-icon::after {
  top: unset;
  bottom: 3px;
  transform: rotate(-45deg);
}

@media (width >= 900px) {
  header nav .nav-hamburger {
    display: none;
    visibility: hidden;
  }
}

/* brand */
header .nav-brand {
  grid-area: brand;
  flex-basis: 128px;
  font-size: var(--heading-font-size-s);
  font-weight: 700;
  line-height: 1;
}

header nav .nav-brand img {
  width: 128px;
  height: auto;
}

/* sections */
header nav .nav-sections {
  grid-area: sections;
  flex: 1 1 auto;
  display: none;
  visibility: hidden;
}

header nav[aria-expanded='true'] .nav-sections {
  display: block;
  visibility: visible;
  align-self: start;
}

header nav .nav-sections ul {
  list-style: none;
  padding-left: 0;
  font-size: var(--body-font-size-s);
}

header nav .nav-sections ul > li {
  font-weight: 500;
}

header nav .nav-sections ul > li > ul {
  margin-top: 0;
}

header nav .nav-sections ul > li > ul > li {
  font-weight: 400;
}

@media (width >= 900px) {
  header nav .nav-sections {
    display: block;
    visibility: visible;
    white-space: nowrap;
  }

  header nav[aria-expanded='true'] .nav-sections {
    align-self: unset;
  }

  header nav .nav-sections .nav-drop {
    position: relative;
    padding-right: 16px;
    cursor: pointer;
  }

  header nav .nav-sections .nav-drop::after {
    content: '';
    display: inline-block;
    position: absolute;
    top: 0.5em;
    right: 2px;
    transform: rotate(135deg);
    width: 6px;
    height: 6px;
    border: 2px solid currentcolor;
    border-radius: 0 1px 0 0;
    border-width: 2px 2px 0 0;
  }

  header nav .nav-sections .nav-drop[aria-expanded='true']::after {
    top: unset;
    bottom: 0.5em;
    transform: rotate(315deg);
  }

  header nav .nav-sections ul {
    display: flex;
    gap: 24px;
    margin: 0;
  }

  header nav .nav-sections .default-content-wrapper > ul > li {
    flex: 0 1 auto;
    position: relative;
  }

  header nav .nav-sections .default-content-wrapper > ul > li > ul {
    display: none;
    position: relative;
  }

  header nav .nav-sections .default-content-wrapper > ul > li[aria-expanded='true'] > ul {
    display: block;
    position: absolute;
    left: -24px;
    width: 200px;
    top: 150%;
    padding: 16px;
    background-color: var(--light-color);
    white-space: initial;
  }

  header nav .nav-sections .default-content-wrapper > ul > li > ul::before {
    content: '';
    position: absolute;
    top: -8px;
    left: 16px;
    width: 0;
    height: 0;
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
    border-bottom: 8px solid var(--light-color);
  }

  header nav .nav-sections .default-content-wrapper > ul > li > ul > li {
    padding: 8px 0;
  }
}

/* tools */
header nav .nav-tools {
  grid-area: tools;
}

/** 
* WKND customizations to the header 
* 
* These overrides can be incorporated into the provided CSS,
* however they are included discretely in thus tutorial for clarity and ease of addition.
* 
* Because these are added discretely
* - They are added to the bottom to override previous styles.
* - They are wrapped in a header .header.block nav selector to ensure they have more specificity than the provided CSS.
* 
**/

header .header.block nav {
  /* Set the height of the logo image.
     Chrome natively sets the width based on the images aspect ratio */
  .nav-brand img {
    height: calc(var(--nav-height) * .75);
    width: auto;
    margin-top: 5px;
  }
  
  .nav-sections {
    /* Update menu items display properties */
    a {
      text-transform: uppercase;
      background-color: transparent;
      color: var(--text-color);
      font-weight: 500;
      font-size: var(--body-font-size-s);
    
      &:hover {
        background-color: inherit;
      }
    }

    /* Adjust some spacing and positioning of the dropdown nav */
    .nav-drop {      
      &::after {
        transform: translateY(-50%) rotate(135deg);
      }
      
      &[aria-expanded='true']::after {
        transform: translateY(50%) rotate(-45deg);
      }

      & > ul {
        top: 2rem;
        left: -1rem;      
       }  
    }
  }
}

End of activity


In your EDS project, go to blocks/footer/, and open footer.js.

Replace the contents with:

import { getMetadata } from '../../scripts/aem.js';
import { loadFragment } from '../fragment/fragment.js';

/**
 * loads and decorates the footer
 * @param {Element} block The footer block element
 */
export default async function decorate(block) {
  // load footer as fragment
  const footerMeta = getMetadata('footer');
  const footerPath = footerMeta ? new URL(footerMeta, window.location).pathname : '/footer';
  const fragment = await loadFragment(footerPath);

  // decorate footer DOM
  block.textContent = '';
  const footer = document.createElement('div');
  while (fragment.firstElementChild) footer.append(fragment.firstElementChild);

  block.append(footer);
}

In the same folder, open footer.css.

Replace the contents with:

footer {
  background-color: var(--light-color);

  .block.footer {
    border-top: solid 1px var(--dark-color);
    font-size: var(--body-font-size-s);

    a { 
      all: unset;
      
      &:hover {
        text-decoration: underline;
        cursor: pointer;
      }
    }

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border: solid 1px white;
    }

    p {
      margin: 0;
    }

    ul {
      list-style: none;
      padding: 0;
      margin: 0;

      li {
        padding-left: .5rem;
      }
    }

    & > div {
      margin: auto;
      max-width: 1200px;
    }

    .columns > div {
      gap: 5rem;
      align-items: flex-start;

      & > div:first-child {
        flex: 2;
      }
    }

    .default-content-wrapper {
      padding-top: 2rem;
      margin-top: 2rem;
      font-style: italic;
      text-align: right;
    }
  }
}

@media (width >= 900px) {
  footer .block.footer > div {
    padding: 40px 32px 24px;
  }
}

End of demo


Model Definitions, Fields, and Component Types