Building with Components¶
As your templates become more complex, you'll find yourself reusing the same pieces of text or logic—like a standard confidentiality clause or a signature block. Components are reusable, modular pieces of your templates that solve this problem.
If you plan to use definition-linked components (Level 2), read Reusable Data with definitions first.
Why Use Components?
- Reusability: Define a clause once, use it in ten different agreements.
- Modularity: Break down a 50-page document into manageable, named chunks.
- Maintainability: Update a component in one place, and the change applies everywhere.
- Consistency: Ensure standard wording or logic is applied consistently wherever used.
- Encapsulation: Control how components access host data and isolate component internals when needed.
VIBE's Progressive Component System¶
VIBE supports three levels of component complexity, following a "start small, grow big" philosophy:
- File-Drop Components (Level 0) - Simple text files with full host access
- Configured Components (Level 1) - Directory-based with declared
inputsand internal logic - Definition-Linked Components (Level 2) - Components that inherit structure from
definitions
Level 0: File-Drop Components¶
The easiest way to start is with a "File-Drop" component. This is just a standard .md or .docx file that you place in your components directory.
Standalone File Component¶
This is a single .md or .docx file that acts as a reusable snippet. It cannot have its own configuration file. It relies entirely on host passthrough and the host's component_defaults for its data.
Example Structure:
components/
└── signature_block.md
components/signature_block.md:
**Signed:** ___________________
**By:** {{ signer_name }}
**Title:** {{ signer_title }}
**Date:** {{ signing_date }}
Components in a Collection¶
You can group related file-based components into a "collection" by adding a defaults.yml file in their directory. All .md and .docx components in that directory will then share the configuration from that defaults.yml.
Example Structure:
components/
└── legal_clauses/
├── defaults.yml
├── force_majeure.md
└── confidentiality.md
The defaults.yml File¶
The defaults.yml file declares shared configuration for all file-drop components in its directory. Its primary purpose is to declare inputs - the variables that the components expect to receive from the host template.
Why declare inputs?
- Validation: VIBE validates that required inputs are provided when components are used
- Documentation: Makes it clear what data each component needs
- Error messages: Undeclared variables cause "undefined variable" errors during validation
- Component defaults integration: Declared inputs can receive values from the host's
component_defaults
legal_clauses/defaults.yml:
inputs:
# Required inputs - must be provided by host template
GoverningLaw:
type: text
description: The law governing this agreement
Jurisdiction:
type: text
description: Court jurisdiction for disputes
# Optional inputs - can be omitted
ArbitrationClause:
type: bool
required: false
description: Whether to include arbitration language
# Inputs that match component_defaults keys are auto-filled
Kunden:
type: text
description: Customer name (from component_defaults)
Leverantoren:
type: text
description: Supplier name (from component_defaults)
Input field options:
type: Expected data type (text,number,bool,date, etc.)required: Whether the input must be provided (defaults totrue)description: Human-readable explanation of the input's purpose
Note: If a component uses a variable that isn't declared in
inputs, VIBE's validation will report an "undefined variable" error. This helps catch typos and missing data early.
For the complete reference, see Component Defaults Configuration (defaults.yml).
Using File-Drop Components:
You include a component in your main template using the {{ insert() }} function:
<!-- main template.md -->
...main agreement text...
{{ insert('legal_clauses.confidentiality') }}
...appendices...
Level 1: Configured Components¶
For more complex components, you can create a folder with a component.yml file. This lets you formally declare the inputs it expects and even add its own internal questions and computables.
Example Structure:
components/
└── contact_block/
├── component.yml
└── template.md
contact_block/component.yml:
# Declare the data this component needs to receive
inputs:
contact_name:
type: text
description: Full name of the contact person.
contact_email:
type: text
description: Email address of the contact
contact_phone:
type: text
required: false
description: Phone number (optional)
# Add questions that ONLY appear when this component is used
questions:
add_phone_number:
label: Include a phone number for this contact?
type: bool
required: false
preferred_contact_method:
label: Preferred contact method
type: select
options:
- email
- phone
- either
required: false
# Optional: Computed values within the component
computables:
contact_summary:
type: computable
compute: "contact_name + ' (' + contact_email + ')'"
contact_block/template.md:
## Contact Information: {{ contact_name }}
Email: {{ contact_email }}
{% if add_phone_number and contact_phone %}
Phone: {{ contact_phone }}
{% endif %}
Summary: {{ contact_summary }}
Using Configured Components:
{{ insert('contact_block',
contact_name=client_name,
contact_email=client_email,
contact_phone=client_phone) }}
Level 2: Definition-Linked Components¶
The most powerful approach links components to definitions using the uses: directive. This automatically provides all fields from a definition as component inputs, reducing repetition and ensuring consistency.
First, define your data structure in config.yml:
definitions:
person_definition:
full_name:
label: Full Name
type: text
email:
label: Email Address
type: email
required: false
phone:
label: Phone Number
type: text
required: false
Then create a component that uses this definition:
contact_card/component.yml:
# This component inherits all fields from person_definition
uses: person_definition
# You can add additional fields beyond the definition
inputs:
department:
label: Department
type: text
# Component-specific questions
questions:
show_department:
label: Show department in contact card?
type: bool
required: false
contact_card/template.md:
<div class="contact-card">
<h4>{{ full_name }}</h4>
<p>{{ email }}</p>
{% if phone %}<p>Phone: {{ phone }}</p>{% endif %}
{% if show_department %}
<p>Department: {{ department }}</p>
{% endif %}
</div>
Using Definition-Linked Components:
When you use this component, VIBE automatically creates namespaced questions in your host template:
<!-- In your main template -->
{{ insert('contact_card', alias='primary_contact') }}
This creates variables like:
primary_contact.full_nameprimary_contact.emailprimary_contact.phoneprimary_contact.departmentprimary_contact.show_department
You can use these variables in your host template.
Controlling Interview Flow: Ordering Component Questions¶
By default, any internal questions defined within a component will appear at the end of the interview, after all of the main template's questions have been asked. For a more logical user experience, you can control exactly where these questions appear using a special marker in your host template's config.yml.
The Problem: Imagine a component for a signature block that has its own question, like confirm_authority_to_sign. Without ordering, this question would appear last, long after the user has filled out all the main contract details.
The Solution: Use the _insert_questions_* marker in your host template's questions block. The * must be replaced with the alias of the component instance from your insert() call.
Example:
template.md:
...
This agreement is entered into by the undersigned parties.
{{ insert('signature_block', alias='party_a_signature') }}
...
components/signature_block/component.yml:
questions:
confirm_authority_to_sign:
label: I confirm I have the authority to sign on behalf of the party.
type: bool
Host Template config.yml:
# in templates/my_agreement/config.yml
questions:
client_name:
type: text
label: Client's Full Legal Name
contract_value:
type: number
label: Total Contract Value
# --- Place the signature block's questions right here ---
_insert_questions_party_a_signature: null
# This question will now appear AFTER the component's questions
final_review_date:
type: date
label: Date of Final Review
Result: The interview flow will now be:
- Client's Full Legal Name
- Total Contract Value
- I confirm I have the authority to sign... (from the component)
- Date of Final Review
This gives you precise control over the user's journey, allowing you to group related questions together, regardless of whether they come from the main template or a reusable component.
Component Defaults: Reducing Repetition¶
Before diving deeper, it's important to understand component defaults - a powerful feature that lets you set common values for component inputs across your entire template.
In your host template's config.yml:
component_defaults:
# Simple literal values
company_name: Acme Corporation
default_currency: USD
# Variable references with transformations
primary_contact_name: "{{ main_contact_name | title }}"
company_address: "{{ office_street }}, {{ office_city }}"
# Complex expressions with conditionals
party_designation: "{{ 'Company' if is_corporation else 'Individual' }}"
contract_value: "{{ base_amount * quantity }}"
formatted_date: "{{ contract_date | strftime('%B %d, %Y') }}"
How component defaults work:
- Each value is rendered as a Jinja template with access to all host template variables
- If a component declares an input matching a key in
component_defaults, the rendered value is automatically provided - This eliminates repetitive input mappings when the same values are used across multiple components
- Explicit mappings in
insert()calls always override component defaults
Example with defaults:
<!-- Without component defaults, you'd need: -->
{{ insert("contract_clause", company_name=company_name, jurisdiction=jurisdiction) }}
{{ insert("signature_block", company_name=company_name, effective_date=contract_date) }}
<!-- With component defaults, you can simply use: -->
{{ insert("contract_clause") }}
{{ insert("signature_block") }}
Managing Component Data¶
Passing Data to Components:
While file-drop components can access main template variables, it's better practice to explicitly pass data:
{{ insert('signature_block',
signer_name=client_contact_name,
signer_title=client_contact_title) }}
Creating Multiple Instances with Aliases:
When you need multiple instances of the same component, give each insert a unique alias:
### Party A Signature:
{{ insert('signature_block', alias='party_a_sig', signer_name=party_a_signer) }}
### Party B Signature:
{{ insert('signature_block', alias='party_b_sig', signer_name=party_b_signer) }}
Accessing Component Data:
You can access component outputs and internal questions from your host template:
<!-- Access component outputs -->
{{ _components.contact_info.contact_summary }}
<!-- Access component internal questions -->
{{ _components.contact_info.add_phone_number }}
Setting Up Component Sources¶
Configure VIBE to find your components by adding this to your config.yml:
COMPONENT_SOURCES:
- path/to/your/components
- shared/component/library
VIBE will scan these directories for component definitions.
Best Practices for Components¶
Component Design:
- Start simple: Use file-drop for simple snippets, move to configured when you need input control
- Use definition linking for components that represent structured data
- Clear interfaces: Always describe inputs and outputs thoroughly
- Meaningful names: Use descriptive component IDs and input names
Input Management:
- Declare all inputs explicitly in component.yml for configured components
- Use required appropriately: Since inputs are required by default, explicitly mark as
required: falseonly when truly optional - Leverage component_defaults for commonly used values across multiple components
- Provide defaults for optional inputs when sensible
Template Organization:
- Set up component_defaults in your host template config.yml for shared values
- Use explicit aliases when using the same component multiple times
- Group related components in logical directories
File Format Support¶
VIBE components support both Markdown and DOCX formats:
- Primary format: Components use
.docxfiles for full document formatting capabilities - Development format: You can use
.mdfiles during development for easier editing - Mixed usage: Host templates can insert components of either format
- Automatic conversion: VIBE handles format conversions automatically for web preview and DOCX output
Common Issues and Solutions¶
- Missing inputs: Check component.yml input declarations match insert() calls
- Undefined variables: Verify host variables exist, are in component_defaults, or provide defaults
- Alias conflicts: Use explicit aliases when inserting same component multiple times
- Definition not found: Ensure referenced definitions exist in host template's config.yml
This progressive system lets you start with simple text snippets and evolve to sophisticated, reusable components as your needs grow. In the next section, we'll explore how definitions make this even more powerful.