Skip to content

source: parent — declare the props a component takes from its call sites #14

Description

@parisek

Follow-up to #13, which introduces source: and implements source: context. This one covers the third binding: props passed by a calling template.

The gap

28 of the 70 components in eprukaz2025 are internal or utility components — button, picture, header, footer, card-teaser. They have no <name>.yaml, no acf.json, and no fields: annotation anywhere. Their contract exists only implicitly, in whatever each call site happens to pass:

{{ component_button({
    title: content.button.title,
    url: content.button.url,
    icon: 'arrow-right',
}) }}

Measured on the most-used component in the project

button, 14 call sites across static/templates/**/*.twig, compared against what button.twig actually reads:

prop passed by read by template reading
title 14 / 14 yes required in practice
url 14 / 14 yes required in practice
target 14 / 14 yes required in practice
icon 14 / 14 yes required in practice
wrapper_classes 8 / 14 yes optional
link 3 / 14 no dead prop
wrapper_id 0 / 14 yes project-wide convention

Three call sites pass link:, which button.twig never readsprice-list, price-list-grid and one more. Leftover from a refactor that split link into url / title / target. Twig discards unknown keys silently, so it has shipped unnoticed.

That is the whole argument: for props there is no such thing as a typo that fails. Rename a prop in the template, miss one of fourteen call sites, and the symptom is an empty spot on a page — possibly weeks later.

The counter-example that shapes the design

wrapper_id is read by button.twig and passed by zero call sites — but it is not dead. It is a project-wide passthrough convention for the Gutenberg anchor, read by 62 of 70 components. A naive "declared but never passed → finding" rule would flag it 62 times and the linter would be turned off within a day.

So the contract must distinguish three states, not two:

  • required — every call site passes it; absence is a defect
  • optional — some pass it; absence is fine
  • inherited — no call site passes it; supplied by the render pipeline (wrapper_id, wrapper_classes from the block wrapper)

A rule that only knows "declared" and "passed" cannot express wrapper_id and will produce noise.

Sketch

name: Button
kind: element
fields:
  title:           { type: text, source: parent, required: true }
  url:             { type: url,  source: parent, required: true }
  target:          { type: text, source: parent }
  icon:            { type: text, source: parent }
  wrapper_classes: { type: text, source: inherited }
  wrapper_id:      { type: text, source: inherited }

No acf.json is produced — zero fields with source: acf, per #13's rule.

Why this is harder than context and should wait

context has exactly one producer (timber_context()), so validation is a static lookup. parent has as many producers as there are call sites, and checking it means parsing component_<name>({…}) invocations across every template — including calls inside loops, conditionals, and {% embed %} blocks, where a static parse gets unreliable fast.

Concretely, before building this: decide whether the linter reports a missing required prop (needs reliable call-site parsing, high false-positive risk) or only an unknown prop passed (the link: case — needs only the declared set, and is where the real value is). The second is much cheaper and catches the defect that actually shipped here.

Recommend doing #13 first, running it on a real project, and only then judging whether call-site parsing earns its cost.

Reference

https://claude.ai/code/artifact/58e4b426-2027-4027-8d77-6c5124c56f90

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions