Props & PropTypes

Props System

Define schema-level editable fields with reusable prop building blocks, widget types, and conditional logic.

Props System

Props are the atomic unit of the Tagixo builder schema. Every field that appears in a module's properties drawer — a text input, a color picker, a toggle, a repeater of slides — is backed by a PHP class that extends AbstractProp. Props carry their own key, label, default value, widget identifier, and any widget-specific configuration. The builder serializes the full set of Props for a module into a JSON metadata payload that the Vue frontend uses to render the drawer. When a user saves, the values flow back as a JSON object keyed by prop key, and those values are what your module's Blade template receives at render time.

Architecture: Prop vs PropType

Before diving into individual classes, it is worth being clear on the boundary:

  • A Prop is one field. TextProp, SelectProp, ToggleProp — each maps to a single widget in the drawer.
  • A PropType is a reusable group of Props (often with CSS generation logic). TypographyPropType, SpacingPropType, BackgroundPropType bundle multiple Props under a named section.

You compose both in a ModuleDefinition. The content() method takes Props directly. The design() and advanced() methods typically take PropTypes. You can also use Props inside a custom PropType's schema() array.

ModuleDefinition
├── content(TextProp, ToggleProp, RepeaterProp, ...)    ← raw Props
├── design(TypographyPropType, SpacingPropType, ...)    ← PropTypes
└── advanced(ToggleProp, SelectProp, ...)               ← Props or PropTypes

The core base class for all Props is Ccast\Tagixo\Core\Props\AbstractProp.

Built-in Props Reference

The table below lists every built-in Prop, its JSON storage type, and the widget identifier the Vue frontend dispatches on. These are available in the Ccast\Tagixo\Core\Props\ namespace unless noted otherwise.

Class Widget ID Stored as Primary use
TextProp text string Single-line text, labels, URLs
TextareaProp textarea string Multi-line plain text
EditorProp editor string (HTML) Rich text via Tiptap
NumberProp number int|float Numeric inputs with optional min/max/step
SliderProp slider int|float Range slider with configurable bounds and unit
SizeProp size {value, unit} Dimension fields: px, em, rem, %
ColorProp color string (CSS color) Color picker, supports hex, rgb, rgba, CSS variables
SelectProp select string Dropdown from a fixed option set
MultiSelectProp multi-select string[] Multi-select from a fixed option set
ToggleProp toggle bool Boolean on/off switch
MediaProp media {id, url, alt} Image/video from the media gallery
IconProp icon string (icon key) Icon picker from registered Blade Icons packs
FontPickerProp font-picker string (font family) Google Fonts or system font picker
RepeaterProp repeater array of objects Ordered list of structured items with sub-fields
ModelSelectProp model-select int|string (ID) Select from a registered Eloquent model's records
CodeEditorProp code-editor string Raw code with syntax highlighting
LinkProp link {href, target, rel} URL + target + rel as a structured object

Specialized internal Props exist for complex built-in PropTypes — FourWaysProp (spacing), BoxShadowProp, CssDeclarationsProp, TabsProp — but these are consumed by their owning PropType and rarely referenced directly in module definitions.

TextProp

Renders a single-line <input type="text">. The value is stored as a plain string. Use for titles, button labels, URLs as plain strings, or any short text that should not parse to HTML.

TextProp::make('cta_label')
    ->setLabel('Button label')
    ->default('Get started')
    ->placeholder('e.g. Sign up free')
    ->helpText('Appears on the primary button.');

TextareaProp

Multi-line plain text. Does not parse HTML. Use for descriptions, alt text, or any content where newlines matter but rich formatting is not needed.

TextareaProp::make('summary')
    ->setLabel('Summary')
    ->rows(4);

EditorProp

Embeds the Tiptap rich-text editor. The value is an HTML string. Content tokens ({{year}}, {{site.name}}) and inline links work inside editor output. Do not mix rich editor output with additional CSS string manipulation in PHP — treat it as opaque HTML.

EditorProp::make('body')
    ->setLabel('Body content')
    ->default('<p>Describe your product here.</p>');

NumberProp

Integer or float input. Supports min(), max(), step(), and unit() display suffix (display only — the unit is not stored in the value).

NumberProp::make('columns')
    ->setLabel('Column count')
    ->min(1)->max(6)->step(1)
    ->default(3);

SliderProp

A drag slider for numeric values. Identical storage format to NumberProp. Use when a continuous range is more intuitive than a text input. Chain min(), max(), step(), unit().

SliderProp::make('opacity')
    ->setLabel('Opacity')
    ->min(0)->max(100)->step(1)->unit('%')
    ->default(100);

SizeProp

A combined number + unit control. Stored as {value: float, unit: string}. Built-in units: px, %, em, rem, vw, vh. Use allowedUnits() to restrict choices. Call ->none() to add a "none" option that sets the CSS to nothing.

SizeProp::make('max_width')
    ->setLabel('Max width')
    ->allowedUnits(['px', '%', 'rem'])
    ->default(['value' => 800, 'unit' => 'px']);

When rendering, convert with your own helper or destructure directly:

$maxWidth = $props['max_width']['value'] . $props['max_width']['unit'];

ColorProp

A color picker with hex, rgb, rgba, and CSS variable support. The stored value is always a CSS-ready string (#1a2b3c, rgba(0,0,0,0.5), var(--tgx-primary)). Pair with variablePicker('color') to allow selection from global color variables.

ColorProp::make('accent')
    ->setLabel('Accent color')
    ->variablePicker('color')
    ->default('#0ea5e9');

SelectProp

A dropdown backed by a static options array. Options are set via options() as an associative array [value => label].

SelectProp::make('layout')
    ->setLabel('Layout')
    ->options([
        'grid'     => 'Grid',
        'list'     => 'List',
        'masonry'  => 'Masonry',
    ])
    ->default('grid');

ToggleProp

A boolean switch. Default is false unless overridden. Often paired with showWhen() on dependent Props.

ToggleProp::make('show_badge')
    ->setLabel('Show badge')
    ->default(false);

MediaProp

Opens the media gallery picker. The value is a structured object: {id: int, url: string, alt: string}. Use type() to constrain to image or video. At render time, use $props['hero_image']['url'] directly in Blade.

MediaProp::make('hero_image')
    ->setLabel('Hero image')
    ->type('image');

IconProp

A searchable icon picker drawing from all registered Blade Icons packs. The stored value is the icon key as a string (e.g. heroicon-o-star). Render with <x-dynamic-component :component="$props['icon']" /> or resolve via the icon registry.

IconProp::make('feature_icon')
    ->setLabel('Icon')
    ->default('heroicon-o-check-circle');

RepeaterProp

A sortable list of structured sub-items. Each item stores the complete set of child prop values as an object. See the Repeater deep-dive section below.

ModelSelectProp

Populates a dropdown from an Eloquent model's records. Pass the model class and optionally configure label/value columns.

ModelSelectProp::make('category_id')
    ->setLabel('Category')
    ->model(\App\Models\Category::class)
    ->labelColumn('name')
    ->valueColumn('id');

The model must be reachable at schema-build time. Dynamic filtering (e.g. "only categories where type = blog") is not supported out of the box — use a custom Prop backed by a Vue component that accepts an API endpoint if you need filtered lazy-loaded options.

The Fluent API

Every Prop built with AbstractProp shares the same base fluent interface. Method chains return $this so they compose naturally.

Core setters

Method Type Description
make(string $key) static Construct the Prop and bind its storage key. The key is the field name in the saved JSON.
setLabel(string $label) fluent Human-readable label shown above the widget in the drawer.
default(mixed $value) fluent Initial value when the module is first inserted or when the field is reset.
placeholder(string $text) fluent Ghost text shown when the field is empty (text-family Props only).
helpText(string $text) fluent Small description shown below the field. Plain text; do not use {{ }} syntax — it will be evaluated by Vue and crash on undefined vars.
required() fluent Marks the field as required in the drawer UI (visual only — not enforced server-side).

Conditional visibility

Method Signature Description
showWhen(field, value, op?) string, mixed, string Show this Prop only when field matches value. Optional operator: = (default), !=, >, <, >=, <=.
hideWhen(field, value, op?) string, mixed, string Inverse of showWhen.

showWhen and hideWhen are drawer UX features only. The values are still stored and passed to the module renderer regardless of visibility. Never rely on them for security or business-rule enforcement.

ToggleProp::make('has_badge')->setLabel('Show badge')->default(false),

TextProp::make('badge_text')
    ->setLabel('Badge text')
    ->showWhen('has_badge', true),

ColorProp::make('badge_color')
    ->setLabel('Badge color')
    ->showWhen('has_badge', true),

Variable picker

->variablePicker(string $type)

Adds a variable-picker button next to the field. When the user selects a global variable, the stored value becomes a token reference (e.g. var(--tgx-primary) for a color). Supported types: color, spacing, text, link.

ColorProp::make('brand_color')
    ->variablePicker('color')
    ->default('#0ea5e9');

sub() — inline sub-props

Some Props accept nested sub-level props via ->sub([...]). This is used internally by composite Props like SizeProp. For custom Props, prefer RepeaterProp with explicit child fields over inventing nested sub() structures.

withConfig() — raw widget config merge

Escape hatch for sending arbitrary keys into the widget config payload:

->withConfig(['rows' => 6, 'monospace' => true])

Use this when a built-in widget supports options that aren't exposed as named fluent methods.

Organizing Props: Groups and Sections

Within the content drawer, Props appear in declaration order. To group them under labeled collapsible sections, wrap them with a PropGroup:

use Ccast\Tagixo\Core\Props\PropGroup;

ModuleDefinition::make()->content(
    PropGroup::make('Content')
        ->props([
            TextProp::make('title')->setLabel('Title'),
            TextareaProp::make('description')->setLabel('Description'),
        ]),

    PropGroup::make('Call to action')
        ->props([
            ToggleProp::make('show_cta')->setLabel('Show CTA')->default(true),
            TextProp::make('cta_text')->setLabel('Button text')->showWhen('show_cta', true),
            LinkProp::make('cta_link')->setLabel('Button link')->showWhen('show_cta', true),
        ]),
);

Groups collapse independently. Collapsed state is persisted per-module-instance in the drawer. Keep groups focused — five to eight Props per group is a reasonable upper bound before the UX degrades.

Use PropGroup for logical groupings of content Props, not for design tabs. Design tab organization is controlled by PropTypes and their tab() return value, not by groups.

RepeaterProp In Depth

RepeaterProp stores an ordered array of objects. Each item in the array has a _id key (generated at insertion, used for VNode keying) plus whatever keys your child Props define. Items are sortable by drag-and-drop in the drawer.

Defining child fields

Pass child Props to fields(). These are the same Prop classes used everywhere else.

RepeaterProp::make('features')
    ->setLabel('Feature rows')
    ->fields([
        IconProp::make('icon')->setLabel('Icon')->default('heroicon-o-check'),
        TextProp::make('text')->setLabel('Text'),
        ColorProp::make('icon_color')->setLabel('Icon color')->default('#16a34a'),
    ]);

Constraints

->minItems(int $n)   // Enforce a minimum count (prevents deletion below n)
->maxItems(int $n)   // Disable add-item button above n
->sortable(bool)     // Default true; pass false to remove drag handles
->addLabel(string)   // Text for the "Add item" button

JSON shape

{
  "features": [
    { "_id": "abc123", "icon": "heroicon-o-check", "text": "Fast deploy", "icon_color": "#16a34a" },
    { "_id": "def456", "icon": "heroicon-o-shield-check", "text": "Secure by default", "icon_color": "#2563eb" }
  ]
}

Do not reference _id in your Blade template logic. It is a Vue-layer concern for diff keying. Use it only as a loop key if you need one.

Nested repeaters

RepeaterProp fields can include another RepeaterProp — for example, a tab list where each tab has a list of bullet points. Keep nesting depth to two levels maximum; three-deep repeaters become impractical in the drawer and fragile to JSON migrations.

Rendering a repeater

@foreach($props['features'] as $feature)
    <div class="feature-row">
        <x-dynamic-component :component="'{{ $feature['icon'] }}'" />
        <span>{{ $feature['text'] }}</span>
    </div>
@endforeach

Never change repeater item key names in production without a database migration that rewrites the stored JSON. The key name is the sole link between stored data and rendered output. Renames silently produce empty values.

Props at Render Time

When the PageRenderer renders a module, it resolves the stored JSON for that module instance and merges in the declared defaults for any missing keys. The merged result is what your Blade template receives as $props.

This means:

  • A newly inserted module always has the declared default() values, even if the user never opened the drawer.
  • A module saved before a new Prop was added gets that Prop's default() value on next render.
  • Removing a Prop from a module definition does not delete its stored value — the key is silently ignored.
{{-- resources/views/tagixo/modules/my-card.blade.php --}}
<div class="card" style="{{ $props['bg_color'] ? 'background:' . $props['bg_color'] : '' }}">
    <h3>{{ $props['title'] }}</h3>
    @if($props['show_body'])
        <div class="card-body">{!! $props['body'] !!}</div>
    @endif
</div>

Always use {!! !!} (unescaped) when rendering EditorProp values — the stored value is HTML. Use {{ }} (escaped) for TextProp and TextareaProp values, which are plain strings.

Custom Props

Build a custom Prop when none of the built-in widget types fit. A typical trigger: a dual-handle range picker, a structured JSON editor, a gradient stop builder, or any widget with its own Vue component.

Scaffold

php artisan make:tagixo-prop GradientBuilderProp

Default output: app/Tagixo/Props/GradientBuilderProp.php. Relocate via config('tagixo.scaffolding.path').

Required overrides

<?php

namespace App\Tagixo\Props;

use Ccast\Tagixo\Core\Props\AbstractProp;

class GradientBuilderProp extends AbstractProp
{
    protected string $direction = 'to right';

    public function widget(): string
    {
        // Must match the key registered in your Vue widget dispatcher
        return 'gradient-builder';
    }

    public function widgetConfig(): array
    {
        return array_merge($this->baseConfig(), [
            'direction' => $this->direction,
        ]);
    }

    // Custom fluent setter
    public function direction(string $direction): static
    {
        $this->direction = $direction;
        return $this;
    }
}

baseConfig() populates the standard keys all widgets expect: key, label, default, helpText, variablePicker, showWhen. Always spread it first and merge your widget-specific keys on top.

Vue widget contract

The widget key returned from widget() must be registered in the drawer's widget dispatcher (DynamicPropType.vue routes by this key). If using the custom-props distribution pattern from your plugin bundle:

  1. Build a Vue component that receives config (the widgetConfig() payload) and emits update:modelValue with the new value.
  2. Register it under the same key string in your plugin's frontend entry point.
  3. Include your plugin's compiled JS in the page via the Tagixo plugin asset pipeline.

If your custom Prop can reuse an existing widget identifier (e.g. slider, select, number) with different defaults or constraints, return that existing identifier and you need zero Vue code.

Registration

Props are not registered individually. They travel with the PropType or module definition that instantiates them. If your custom Prop is shared across many PropTypes, those PropTypes go into your plugin's service provider — the Prop classes come along for the ride.

Prop Design Checklist

Keep these rules in mind when composing module schemas:

  • One Prop, one value. Do not encode multiple logical fields into a single TextProp value (e.g. "199|349|Annual"). Each logical value gets its own Prop.
  • Keys are permanent. Once a module is in production, renaming a prop key requires a data migration against stored JSON.
  • Defaults must render safely. The default() value is what a freshly inserted module shows. It must produce valid, non-broken output with zero further configuration.
  • showWhen is not validation. All prop values reach the server; conditional visibility is display-only. Validate server-side if a value's presence is conditional on another field.
  • Prefer composable schemas over monoliths. A 20-field content section is a sign the module is doing too much. Consider splitting into two modules or using a repeater.
  • Variable picker for design tokens. Any Prop that controls a color or size should offer ->variablePicker('color') / ->variablePicker('spacing') so pages can participate in the site's design system.
  • RepeaterProp for collections, not JSON blobs. If you find yourself storing json_encode(...) in a TextProp, switch to RepeaterProp with typed child Props.

Example: Full Module Content Schema

use Ccast\Tagixo\Core\Props\TextProp;
use Ccast\Tagixo\Core\Props\EditorProp;
use Ccast\Tagixo\Core\Props\MediaProp;
use Ccast\Tagixo\Core\Props\SelectProp;
use Ccast\Tagixo\Core\Props\ToggleProp;
use Ccast\Tagixo\Core\Props\LinkProp;
use Ccast\Tagixo\Core\Props\ColorProp;
use Ccast\Tagixo\Core\Props\RepeaterProp;
use Ccast\Tagixo\Core\Props\IconProp;

ModuleDefinition::make()
    ->content(
        // Hero block
        MediaProp::make('image')->setLabel('Hero image')->type('image'),
        SelectProp::make('image_position')
            ->setLabel('Image position')
            ->options(['left' => 'Left', 'right' => 'Right'])
            ->default('right'),

        TextProp::make('eyebrow')
            ->setLabel('Eyebrow label')
            ->placeholder('e.g. New release'),
        TextProp::make('headline')
            ->setLabel('Headline')
            ->default('Build faster with Tagixo'),
        EditorProp::make('body')
            ->setLabel('Body text'),

        // CTA group
        ToggleProp::make('show_cta')
            ->setLabel('Show call to action')
            ->default(true),
        TextProp::make('cta_text')
            ->setLabel('Button label')
            ->default('Get started')
            ->showWhen('show_cta', true),
        LinkProp::make('cta_href')
            ->setLabel('Button link')
            ->showWhen('show_cta', true),

        // Feature bullets
        RepeaterProp::make('bullets')
            ->setLabel('Feature points')
            ->minItems(0)
            ->maxItems(6)
            ->addLabel('Add feature')
            ->fields([
                IconProp::make('icon')
                    ->setLabel('Icon')
                    ->default('heroicon-o-check-circle'),
                TextProp::make('text')
                    ->setLabel('Feature text'),
                ColorProp::make('icon_color')
                    ->setLabel('Icon color')
                    ->variablePicker('color')
                    ->default('#16a34a'),
            ]),
    );

See Also