Dripyard Premium Drupal Themes: Handling images from Drupal and Canvas with the same component

Within Twig, we check if the data is from Canvas. This is easy enough with an if statement.To tell Canvas that your content editor needs an image upload widget, you specify a few things in your prop definition.We can’t really explain the whole concept of components here, so this article is geared toward Drupal devs who already know about Single Directory Components (SDCs), and how to create their schemas. If you’re not familiar with all of this, check out the excellent docs on Drupal.org.

Prerequisites

Drupal Canvas is coming! It’ll reach stability by the end of the year, and take center stage in Drupal CMS soon after.

How SDCs receive images from standard Drupal fields

We’re going to create a shared component that can handle images coming from either Drupal fields or Canvas.$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Image
status: stable
group: Dripyard Basic
props:
type: object
properties:
image:
$ref: json-schema-definitions://canvas.module/image
title: Image
type: object
examples:
- src: images/drupalcon-nashville.webp
alt: 'Placeholder'
width: 2000
height: 1500
image_link:
title: Link image (optional)
type: string
format: uri-reference
border_radius:
title: Border radius
type: string
enum:
- small
- medium
- large
width:
title: Image width
type: number
examples: [ 800 ]
alt:
title: Alt text override
type:
- string
- 'null'
description: 'Override the alt text if necessary'
loading:
title: Image loading
description: 'Keep this set to "lazy" unless you are absolutely sure this will always appear "Above the fold" on page load.'
type: string
enum:
- eager
- lazy
meta:enum:
eager: Eager
lazy: Lazy (recommended)
examples: [ lazy ]

{% if image.src %}
{% if width %}
{% set aspect_ratio = image.width / image.height %}
{% set height = (width / aspect_ratio)|round %}
{% endif %}

{{ include('canvas:image',
image|merge({
width: width|default(image.width),
height: height|default(image.height),
}), with_context = false) }}
{% else %}
{{ image }}
{% endif %}

image: {
src: '/path-to-image.jpg',
alt: 'Alt text',
width: 600,
height: 400
}

At Dripyard, we’ve been focused on making our components work seamlessly in both Drupal and Canvas. One of the trickier challenges was creating an image component that supports both systems while following best practices for performance and accessibility. Here’s how we did it.

How SDCs receive images from Drupal Canvas

{# hero.twig #}
{{ image }}

# hero.component.yml
image:
title: Image
type: object

width:
title: Image width
type: number

{# paragraph--hero.html.twig #}
{{ include('dripyard_base:hero', {
image: paragraph.field_image,
}, with_context = false) }}

The schema starts off like this

Creating a shared image component

The simplest solution is above, but what if we want to change the width and height of the image? For example, a hero component might want a maximum image size of 2000px, but a card component might only need 400px.In addition to the width prop, we can pass through a few other useful options.

Create the schema

{% if image.src %}
{% set image_attributes = image_attributes|default(create_attribute()) %}
{% if width %}
{#
Canvas can scale images, but can not currently change the aspect-ratio
of the image. Below, we calculate the original aspect ratio, and then
adjust the height to ensure the aspect ratio does not change. This is
important because browsers will reserve space for the image (based on
width and height attributes) to prevent layout shifts.

This will need to be updated if Canvas gains the ability to change the
image's aspect ratio.
#}
{% set aspect_ratio = image.width / image.height %}
{% set height = (width / aspect_ratio)|round %}
{% endif %}

{% if fetchpriority %}
{% set image_attributes = image_attributes.setAttribute('fetchpriority', fetchpriority) %}
{% endif %}

{{ include('canvas:image',
image|merge({
width: width|default(image.width),
height: height|default(image.height),
alt: alt|default(image.alt),
class: class|default(''),
loading: loading|default('lazy'),
srcset: srcset|default(''),
sizes: sizes|default('auto 100vw'),
attributes: image_attributes,
}), with_context = false) }}
{% else %}
{% if image['#theme'] %}
{{ image|add_suggestion('bare') }}
{% else %}
{{ image }}
{% endif %}
{% endif %}

Let’s create a user facing component for Canvas

When Canvas reaches stability, our themes will be ready to roll. All of them include optimizations like this baked right in, so if you’d rather not build it yourself, grab one of ours.

Check to see if the data is from Canvas and load the correct component

When Canvas sends the data to the component, it looks something like this.# hero.component.yml
image:
title: Image
$ref: json-schema-definitions://canvas.module/image
type: object

The final image-or-media.component.yml looks like this:

But wait! There’s more!

We then load Canvas’s included image component. This utility component handles tasks like setting up responsive images.Within regular Drupal, we map an image field to the SDC’s image prop through a template (which could be block, node, paragraph, etc).$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Image or media
description: Can accept image from Drupal Canvas or an image/media-image from regular Drupal view modes.
status: stable
group: Dripyard Basic
noUi: true
props:
type: object
required:
- image
properties:
image:
title: Image
type: object
width:
title: Image width
type: number
examples: [ 800 ]
alt:
title: Override alt text
type:
- string
- 'null'
class:
title: CSS classes
type: string
loading:
title: Loading
type: string
enum:
- eager
- lazy
examples: [ lazy ]
fetchpriority:
title: Fetch priority
type: string
enum:
- auto
- high
- low
examples: [ auto ]
srcset:
title: srcset attribute
type: string
sizes:
title: sizes attribute
type: string
image_attributes:
title: Image attributes object
type: DrupalCoreTemplateAttribute

canvas-image.component.yml

There’s a lot more we can do

$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Image or media
description: Can accept image from Drupal Canvas or an image/media-image from regular Drupal view modes.
status: stable
group: Dripyard Basic
noUi: true
props:
type: object
required:
- image
properties:
image:
title: Image
type: object

{% if image.src %}
{{ include('canvas:image', image, with_context = false) }}
{% else %}
{{ image }}
{% endif %}

This looks something likeWe declare the width prop.This shared component is meant to be used only by other SDCs (e.g. a card SDC), so will not include the $ref. Note that within a component that invokes this shared component, you’ll still need to have the $ref if you’re using Canvas.{% set classes = [
'canvas-image',
border_radius ? 'canvas-image--radius-' ~ border_radius,
] %}
<div{{ attributes.addClass(classes) }}>
{% if image_link %}
<a class="canvas-image__link" href="{{ image_link }}">
{% endif %}
{{ include('dripyard_base:image-or-media', {
image,
width,
loading,
alt,
}, with_context = false) }}
{% if image_link %}
</a>
{% endif %}
</div>

Conclusion

Just want the code? Click here.But (and this is really important), if the Canvas module does not exist, the json-schema-definition will not resolve and the component will not render.

Similar Posts