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:

  1. File-Drop Components (Level 0) - Simple text files with full host access
  2. Configured Components (Level 1) - Directory-based with declared inputs and internal logic
  3. 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 to true)
  • 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_name
  • primary_contact.email
  • primary_contact.phone
  • primary_contact.department
  • primary_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:

  1. Client's Full Legal Name
  2. Total Contract Value
  3. I confirm I have the authority to sign... (from the component)
  4. 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: false only 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 .docx files for full document formatting capabilities
  • Development format: You can use .md files 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.