Part 2: Collections & Dynamic Dropzones
Drive the FAQ from a questions collection and let individual answers become dropzones
At the end of Part 1 the FAQ renders two hard-coded questions. In this part the questions become real data: a questions collection that users manage in the inspector, a hook that enriches each item with IDs, ARIA attributes, and precomputed booleans, and a template loop where each answer is either plain text or a dropzone. This is the same architecture the core Tabs and Table components use for their repeating content.
Define the Questions Collection
Collections live in a collections/ folder at the root of the component, one subfolder per collection. Create the folder and its three files:
com.example.faq/
├── collections/
│ └── questions/
│ ├── info.json
│ ├── properties.json
│ └── defaults.json
├── ...collections/questions/info.json
The collection's own manifest identifies it to Elements. The convention—documented in Collections and followed by every core component—is to extend the component identifier with .collections.{name}:
{
"identifier": "com.example.faq.collections.questions",
"title": "Questions"
}title is the label shown above the collection panel in the inspector.
collections/questions/properties.json
This file describes the fields of each item in the collection, using the same control vocabulary as component properties. Three fields per question: the question itself, a text answer, and a per-item switch to swap that answer for a dropzone:
Walking through it against the collections reference:
Item fields use the key
propertywhere component controls useid—compare this file with the component-levelproperties.jsonfrom Part 1. The core Tabs and Table collections are written exactly this way.useDropzoneis a per-item Switch. The core Table has the same idea on its columns collection (a per-column "Cell Mode" choice between text and dropzone).Everything is
"responsive": false—collection fields are data, not styling, so breakpoint variants make no sense here.
collections/questions/defaults.json
Without defaults, a freshly dropped FAQ would be empty. defaults.json pre-populates the collection so users see the component working immediately:
Each object supplies values for the property names defined above; anything omitted (like useDropzone) falls back to the field's own default.
Register the Collection in properties.json
The collection folder alone does nothing—the component's main properties.json must reference it with a collection control, as described in Collections in properties.json. Replace Part 1's Content group with this—the Style and Behavior groups stay unchanged:
The collection.identifier must match collections/questions/info.json exactly, and the id becomes the property name the data is exposed under: rw.collections.questions in hooks and collections.questions in templates. This registration is shaped just like the core Tabs component's, which registers its collection as { "title": "Tabs", "id": "tabs", "collection": { "identifier": "com.realmacsoftware.tabs.collections.tabs" } } at the top of its Settings group.
Save, and the inspector's Content group now shows a Questions panel where items can be added, edited, reordered, and removed.
[Screenshot: the inspector's Content group with the Questions collection panel showing the three default items]
Prepare Items in hooks.js
We could loop over collections.questions directly in the template, but the core components almost never do—they enrich each item in the hook first, then pass the processed array to the template. Two reasons apply here:
1. Accessibility needs stable IDs. Each question button should carry aria-expanded and point at its answer region via aria-controls; the answer points back with aria-labelledby. That requires a unique pair of element IDs per item. The core Tabs hook builds them by combining the component instance's ID with the item index:
2. Template @if takes exactly one condition. No &&, ||, or comparisons are allowed inside @if(...)—see Combining Conditions. Whether an item's answer is a dropzone depends on two values: the component-level Answers segmented control and the item's own Use Dropzone switch. So we combine them in the hook into a single showDropzone boolean per item, and the template tests only that.
Here are the additions—the classes object and the rw.setRootElement() call are unchanged from Part 1, and the complete file is in the checkpoint at the end of this page:
The hook also destructures answerType from rw.props alongside the style properties, and its final rw.setProps() call now passes the processed array: rw.setProps({ classes, questions: items }).
The interesting decisions:
rw.collections.questionsis the raw item array (rw.collections); the guard(questions || [])keeps the hook safe when the collection is empty.Each processed item keeps its original fields (
...item) and gainsindex, a fallbackquestionlabel, the two element IDs, andshowDropzone.showDropzoneis true when the component-wide Answers control is set to Dropzone or the item's own Use Dropzone switch is on—so Text mode is the default, individual items can opt in, and flipping the segmented control converts every answer at once.The processed array is passed to the template as
questionsvia rw.setProps. The raw data remains available ascollections.questions, but the template only uses the enriched version—the same convention as the core Audio Playlist and Tabs templates.
Render the Loop in index.html
Replace the two hard-coded items with an @each loop. The @if(heading) block from Part 1 stays as it is; everything below it becomes this:
Two things deserve a closer look.
The Dropzone-or-Text Answer
The @if(item.showDropzone) branch is the payoff of the hook work: one precomputed boolean chooses between rendering the item's answer text and emitting an @dropzone. This pattern comes straight from the core Table component, whose body cells do exactly the same per-column switch:
(The Table renders an inline-editable @text in its else branch; our FAQ prints the collection field instead.) For a deeper look at how collections and dropzones interact, see Collection-Driven Dropzones.
Note that the dropzone's name is the static literal "answer" even though it sits inside a loop—dropzone names can't contain template variables. Elements automatically creates a unique dropzone per iteration and keeps its contents attached to the right item, even when items are reordered. See Dropzones Inside Loops.
ARIA Now, Interactivity Next
The button hard-codes aria-expanded="true" because every answer really is visible right now—there is no open/close state yet. The IDs wired through aria-controls and aria-labelledby are the piece worth doing early: they're pure data, so they belong in the hook, and Part 3 will simply rebind aria-expanded (and hide closed answers) with Alpine without touching the IDs again.
Try It Out
Add and reorder questions in the inspector and watch the accordion follow. Then exercise the dropzone logic:
Open one item and flip Use Dropzone on—its text answer is replaced by an empty dropzone. Drop an Image or List component in.
Switch the Behavior group's Answers control to Dropzone—every answer becomes a dropzone, regardless of the per-item switches.
[Screenshot: the FAQ on the canvas with two text answers and one dropzone answer containing an Image component]
If you delete every question the component renders only the heading—Part 4 adds placeholder items for that case, so don't worry about it yet.
Checkpoint: Full Files So Far
At the end of Part 2, info.json is unchanged from Part 1, properties.json differs only by the Content group shown above, and the three collection files are listed in full earlier on this page. The two files that changed substantially are worth checking end to end—Part 3 starts from exactly these.
hooks.js
The complete file—Part 1's hook plus the item preparation:
templates/index.html
The complete file—the heading from Part 1 followed by the loop:
Next Up
The FAQ now has real data, accessible markup, and flexible answers—but every answer is permanently open. Part 3 brings the accordion to life with Alpine.js:
Related Documentation
Last updated
Was this helpful?

