Extending Components
Building Components From Your Own Data
This is the guide for consuming engines and apps — code that mounts the
component library and wants to render its components from its own data, rather
than from a BOP-fetched deserializer. (To add a new component to the library
itself, see 05_adding_components.md.)
The data shape
Every component declares its data shape once, with data_attributes. That macro
generates two things:
- the
delegatelist the template reads from, and - a nested immutable value record,
Component::Data(a RubyData), reachable asComponent.data.
In production the component is handed a BOP deserializer. Downstream you build a
Component::Data record instead. The component can't tell them apart — both
satisfy the same duck-typed shape — so anything the library renders, you can
render from your own data.
Recommended: .with(...).new(...)
Use the .with builder. It builds the data record and wires it into the
component for you:
GllComponentLibrary::Patterns::TextCardComponent .with(title: category.title, body: category.description, clickable: category_button(category)) .new.with(**attrs) builds the ::Data record and returns the constructed
component, injecting the record under the component's data kwarg — pattern: for
patterns, panel: for panels:
GllComponentLibrary::Patterns::TextCardComponent.with(title: "Hi", body: "...")# => TextCardComponent.new(pattern: TextCardComponent::Data.new(title: "Hi", body: "..."))It works the same for panels, injecting panel::
GllComponentLibrary::Panels::HeroComponent.with(title: "Welcome", subtitle: "...")Partial construction
Set only the fields you need; the rest default to nil. There's no need to pass
every attribute.
GllComponentLibrary::Patterns::TextCardComponent.with(title: "Just a title")Longhand equivalent
.with(...) is sugar for building the record by hand. These are identical:
# with the builderTextCardComponent.with(title:, body:)# longhandTextCardComponent.new(pattern: TextCardComponent.data.new(title:, body:))Reach for the longhand only when you also need to pass a constructor option
(e.g. additional_class:) alongside the inline data — .with covers the data,
the longhand lets you add the rest:
TextCardComponent.new( pattern: TextCardComponent.data.new(title:, body:), additional_class: "featured")Components without .with
A couple of components (Patterns::ImageComponent, Patterns::VideoComponent)
wrap their own input under a non-standard name — they declare
data_attributes ..., to: :image / to: :video and set no data_source. They
are normally rendered through a parent's asset slot, so .with is deliberately
unavailable and raises NotImplementedError. Build their data with .data.new
and pass it as pattern: directly:
GllComponentLibrary::Patterns::VideoComponent.new( pattern: GllComponentLibrary::Patterns::VideoComponent.data.new( autoplay?: true, loop?: true, desktop_video_url: source.desktop_video_url ))Why this way
- One source of truth —
data_attributesnames the shape once; you never hand-roll a struct to feed a component. - Interchangeable — inline
::Datarecords and BOP deserializers are the same duck type, so the component behaves identically whatever the source. - Only set what matters — partial construction keeps call sites to the fields you actually have.