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
Demo: Update The Footer Block
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