For the complete documentation index, see llms.txt. This page is also available as Markdown.

Galleries & Resource Collections

Build media components around project resources, folders, and drag-and-drop

Media components live or die on how well they handle someone else's files. You don't know how many images the user will add, what shape they'll be, whether captions exist, or whether one of them turns out to be a video. Elements' resource system absorbs all of that variability before your template runs: the user hands your component a folder, your hooks turn it into a clean array of ready-to-render items, and your template just loops. This guide walks that whole pipeline through a real component, then extends it with patterns for albums, metadata, and switchable layouts.

Resource Fundamentals, Briefly

What are Resources? covers the ground rules, so here is just the working summary. A resource is any file the user has added to their project — image, video, audio, or an entire folder. Your component asks for one by declaring a resource control in properties.json, and Elements delivers an object (not a bare URL) into rw.props and your templates: path, intrinsic dimensions, aspect ratio, alt text, format. Restrict what the control accepts with its accepts/excludes options if your component only handles certain types.

Two facts matter most for gallery-style components:

  • A folder is a single resource value. Assign a folder and the property becomes a container whose resources field is an array of resource objects — one property in the inspector, arbitrarily many files behind it. See Image Resources.

  • Any element can be a drop target. Add rwResourceDropZone="<control id>" to an element in your template — or to your root element from hooks — and users can drag files from Finder or the Resources browser straight onto your component on the canvas.

Gallery is the canonical resource-centric component: one folder in, a responsive thumbnail grid and a full-screen lightbox out. Everything it does flows from a single control in its properties.json{"title": "Resources", "id": "resources", "resource": {}} — with no accepts restriction, because Gallery handles images and videos alike (you'll see how it tells them apart in a moment). The rest of its large inspector is pure presentation: columns, gap, aspect ratio, caption styling, lightbox chrome. The content side is this one dropwell.

The Empty State Is the Drop Target

The first thing a user sees after dropping Gallery on the canvas is nothing — no folder assigned yet. Gallery's main template branches on that case immediately, and makes the placeholder itself the drop zone:

<!-- com.realmacsoftware.gallery/templates/index.html -->
@if(!hasResources)
<div
    rwResourceDropZone="resources"
    class="col-span-full w-full h-64 flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-gray-50 hover:bg-gray-100 transition cursor-pointer text-center px-4 py-6"
>
    <!-- … folder icon SVG … -->
    <p class="text-lg font-medium text-gray-700">
        Drag & drop a folder of resources
    </p>
    <p class="text-sm text-gray-400">or add them via the component inspector</p>
</div>
@else
<div class="{{classes.wrapper}}" x-data>
    @each(image in resources) @include("thumbnail") @endeach
</div>
@endif @includeIf(includeLightbox, template: "lightbox")

The rwResourceDropZone="resources" value matches the resource control's id, so a folder dragged onto the dashed box lands in the resources property and the whole component re-renders as a grid. The instruction and the target are the same element — users are never told to drop somewhere they can't.

Gallery scopes its drop zone to the empty state, but a component that stays droppable after content is assigned can wire the attribute onto its root element from hooks instead. The core Image component does exactly that, passing it through rw.setRootElement's args:

With that in place, dragging a new image anywhere onto the component replaces the current one — no need to hunt for the inspector.

Processing the Folder in Hooks

Templates should loop and print, nothing more. All of Gallery's real work happens in hooks.source.js, which walks the folder's resources array once and enriches every item before the template sees it:

Four moves worth internalising:

  1. hasResources is precomputed. Template @if takes exactly one condition, so the length check lives here and the template tests a plain boolean. Every folder-driven component needs this line.

  2. Every item gets a thumbnail. rw.resizeResource() generates a 400px-wide variant per image and attaches its path to the item as thumbnail. Resizing at render time is impossible — templates can only read — so hooks are where scaled variants are born. For the full retina/srcset/per-breakpoint treatment, see Responsive Images & Media.

  3. Alt text degrades gracefully. alt || caption || author || "" means an image with no alt text still gets something descriptive, and the template never has to think about it.

  4. Format becomes booleans. resource.format distinguishes videos (youtube, vimeo, mp4) from images, but rather than making the template compare strings — which @if can't do — the hook flattens the answer into isVideo, isYouTube, isVimeo, and isMP4 flags the template can test directly.

Finally, rw.setProps({ resources: resources?.resources }) promotes the inner array to a top-level prop, so the template writes @each(image in resources) instead of reaching through the container object.

Rendering the Thumbnails

Back in index.html, the populated branch is one line — @each(image in resources) @include("thumbnail") @endeach — with the per-item markup extracted to an include. Includes share the scope of the template that includes them, so thumbnail.html can use the image loop variable and the classes prop directly:

Notice src="{{image}}" in the image branch: printing a resource object directly outputs its file path, so this renders the original file. The hook's 400px variant is on the same item as {{image.thumbnail}} whenever you want the scaled file instead — attach variants in hooks and each template decides which to print. The {{image::index}} loop variable (see @each) rides along in the Alpine $dispatch calls, telling the lightbox which slide the clicked thumbnail corresponds to.

thumbnailWantsMeta is another precomputed boolean — the hook sets it to thumbnailShowCaption || thumbnailShowAuthor, an OR the template couldn't express itself.

The Lightbox, Gated for the Canvas

The last line of index.html@includeIf(includeLightbox, template: "lightbox") — decides whether the lightbox exists at all. Look back at the hook: includeLightbox is lightboxPreview || rw.project.mode !== "edit". A fixed full-screen overlay would be hostile on the editing canvas, so by default it's only included outside edit mode; the inspector's Lightbox Preview switch opts back in so users can style it. The include itself teleports to the end of the page body and iterates the same processed array a second time:

The video booleans computed in hooks pay off again — each @includeIf swaps in a provider-specific embed template, and images fall through to a plain <img> sized by the resource's own aspect field. The full story of mode gating (and why x-cloak sits inside @if(!edit)) is in Designing the Edit-Mode Experience.

Albums from Nested Folders

A folder's resources array can contain folders. That one fact turns a flat gallery into an album browser: the user assigns a folder of folders, and each subfolder becomes an album with a name, a cover, and its own image set. A folder item carries a resources array of its own instead of an image field, which is exactly how a hook can tell the two apart. Here's a component we'll call com.example.albums, with a single resource control (id: "library"):

One pass builds everything the template needs: the first image in each subfolder becomes the album cover (resized to 600px), and every image gets an 800px display variant plus the same alt-fallback chain Gallery uses. The template renders a cover grid, then a detail section per album — the covers anchor-link down to their sections:

When hasAlbums is false, give the component the same rwResourceDropZone placeholder Gallery uses. The anchor-link detail view is deliberately the simplest sketch that works — to open albums in place instead, reuse the show/hide machinery from Designing the Edit-Mode Experience; the data shape doesn't change, only the markup around album.images. And because the hook filters out empty albums and non-folder items, stray loose files in the user's folder are simply ignored rather than breaking the layout.

Captions & Metadata

Every resource object carries a small, fixed set of descriptive fields. These are the ones documented in the references and used by the core Gallery — treat this as the complete menu:

Field
Available on
Notes

image

Images

Path to the image file

path

All files

Path for non-image files (video, audio, PDF)

width / height

Images

Intrinsic pixel dimensions

aspect

Images

Ratio string such as 16/9 — drops straight into aspect-[…]

alt

Images

Alt text set in the Resources browser

format

All files

File format token, e.g. jpg, mp4, youtube

caption / author

Folder items

Per-item metadata on resources inside a folder

name

Folders & items

The folder's or file's name

Captions and authorship render like any other field — pair them in a <figure> and fall back gracefully when they're empty:

Gallery's alt chain — alt || caption || author || "" — is the pattern to copy: compute the best available description once in hooks so templates never render an image with a missing alt attribute.

That's the extent of what Elements hands you. Richer metadata — camera EXIF, GPS, capture dates — isn't part of the resource object, and the hooks runtime has no module system to pull in a parser at run time. If you need it, bundle an EXIF library into your compiled hooks.js at build time and read the file yourself; Integrating JavaScript Libraries covers the bundling workflow.

Switching Layout Engines

Once your hook owns all class generation (as Gallery's does with its classes object), offering multiple layout engines costs almost nothing: one inspector control, one branch in the hook, zero changes to the template. Here's a two-engine switcher — uniform grid versus CSS-columns masonry — driven by a Segmented control. responsive: false makes it return the raw item value rather than breakpoint-prefixed classes:

The template is engine-agnostic: it prints {{classes.wrapper}}, {{classes.item}}, and {{classes.image}} and never knows which engine is active. Adding a third engine — say a JavaScript-measured masonry for perfectly packed layouts — is another branch in the hook and another segment in the control, with the same markup underneath. That's the property that keeps big media components maintainable as their inspectors grow.

Last updated

Was this helpful?