{{ post.title }}
{{ post.date | date('F j, Y') }}
{{ post.date | date('F j, Y') }}
{{ prop.field }} ({{ prop.type }}) — from {{ prop.source }}
{% endfor %} ``` See [cms.schema.inheritedProperties()](docs/schemas/twig#inheritedproperties) for more details. ## Complete Schema Example ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://www.totalcms.co/schemas/article.json", "id": "article", "type": "object", "description": "News articles and blog posts with categories and tags.", "category": "Content", "inheritFrom": ["base-content"], "formgrid": "id id date\ntitle title category\ncontent content content", "properties": { "content": { "type": "string", "field": "styledtext", "label": "Content" }, "category": { "type": "string", "field": "select", "label": "Category", "options": [ {"label": "News", "value": "news"}, {"label": "Tutorial", "value": "tutorial"} ] }, "tags": { "$ref": "https://www.totalcms.co/schemas/properties/list.json", "label": "Tags", "field": "list" } }, "required": ["id", "content"], "index": ["id", "category"] } ``` ## See Also - [Schema Validation](docs/schemas/validation) - Validation rules for schema properties - [Form Grid Layout](docs/schemas/formgrid) - Arranging fields in the admin form - [Collection Settings](docs/collections/settings) - Collection-level configuration - [cms.schema](docs/schemas/twig) - Twig functions for accessing schemas --- ## Schema Validation This document covers JSON Schema validation keywords that can be used in the **Extra Schema Definitions** field when setting up properties in a schema. ## Overview Total CMS uses JSON Schema (Draft 2020-12) for data validation. In addition to the basic schema properties (type, field, label, etc.), you can add validation constraints to ensure data quality and consistency. **Where to add these:** In the Schema admin interface, when editing a property, use the **Extra Schema Definitions** field to add validation keywords as JSON. > **Important:** Validation keywords like `pattern`, `minLength`, `maxLength`, `minimum`, `maximum`, etc. must be added in the **Extra Schema Definitions** section of a property — not in the property's Settings. The Settings section controls display and behavior options, while Extra Schema Definitions is where all JSON Schema validation rules go. ## Required Fields By default, required string and array fields are automatically validated: - **Strings**: Cannot be empty (`""`) - **Arrays** (list, gallery, deck): Cannot be empty (`[]`) - **Files/Images**: Validated on the frontend (must have a file uploaded) You can override this behavior by explicitly setting `minLength` or `minItems`. > **Conditional required?** The schema's top-level `required` array is a hard data invariant enforced on every save. If you need a field that is only required when another field has a particular value (e.g. "email required only when notifications toggle is on"), use the form-level `settings.required` option paired with `settings.visibility` instead. See [Required (Form-Level)](../fields/all-fields.md#required-form-level) in the All Fields docs. ## String Validation ### minLength / maxLength Enforce minimum and maximum string length. ```json { "minLength": 3, "maxLength": 100 } ``` **Example - Username field:** ```json { "username": { "type": "string", "field": "text", "label": "Username", "minLength": 3, "maxLength": 20 } } ``` **Use cases:** - Usernames (3-20 characters) - Product names (minimum 5 characters) - Short descriptions (maximum 200 characters) - Passwords (minimum 8 characters) ### pattern Validate strings against a regular expression. ```json { "pattern": "^[a-zA-Z0-9_-]+$" } ``` **Example - Slug field:** ```json { "slug": { "type": "string", "field": "text", "label": "URL Slug", "pattern": "^[a-z0-9-]+$" } } ``` **Common patterns:** - **Alphanumeric**: `^[a-zA-Z0-9]+$` - **URL-safe slug**: `^[a-z0-9-]+$` - **Hex color**: `^#[0-9A-Fa-f]{6}$` - **Phone (US)**: `^\\d{3}-\\d{3}-\\d{4}$` ### format Use built-in format validators. ```json { "format": "email" } ``` **Available formats:** - `email` - Email address - `uri` / `url` - URL - `date` - Date (YYYY-MM-DD) - `time` - Time (HH:MM:SS) - `date-time` - ISO 8601 date-time - `ipv4` / `ipv6` - IP address **Example:** ```json { "website": { "type": "string", "field": "url", "label": "Website", "format": "uri" } } ``` ## Number Validation ### minimum / maximum Enforce minimum and maximum numeric values. ```json { "minimum": 0, "maximum": 100 } ``` **Example - Percentage field:** ```json { "discount": { "type": "number", "field": "number", "label": "Discount %", "minimum": 0, "maximum": 100 } } ``` ### exclusiveMinimum / exclusiveMaximum Like minimum/maximum, but excludes the boundary value. ```json { "exclusiveMinimum": 0, "exclusiveMaximum": 100 } ``` **Example - Temperature (must be above 0, not equal to 0):** ```json { "temperature": { "type": "number", "field": "number", "label": "Temperature (°C)", "exclusiveMinimum": 0 } } ``` ### multipleOf Ensure number is a multiple of a specified value. ```json { "multipleOf": 5 } ``` **Example - Quantity (in packs of 5):** ```json { "quantity": { "type": "integer", "field": "number", "label": "Quantity", "multipleOf": 5, "minimum": 5 } } ``` ## Array Validation ### minItems / maxItems Enforce minimum and maximum array length. ```json { "minItems": 1, "maxItems": 10 } ``` **Example - Tags field:** ```json { "tags": { "$ref": "https://www.totalcms.co/schemas/properties/list.json", "label": "Tags", "minItems": 1, "maxItems": 5 } } ``` **Use cases:** - Tags (1-5 items) - Gallery (minimum 3 images) - Options (maximum 10 choices) - Team members (minimum 2 members) ### uniqueItems Ensure all items in an array are unique. ```json { "uniqueItems": true } ``` **Example - Category tags:** ```json { "categories": { "$ref": "https://www.totalcms.co/schemas/properties/list.json", "label": "Categories", "uniqueItems": true } } ``` **Note:** The `list` property type already has `uniqueItems: true` by default. ## Enum (Allowed Values) Restrict values to a specific set of allowed options. ```json { "enum": ["draft", "published", "archived"] } ``` **Example - Status field:** ```json { "status": { "type": "string", "field": "select", "label": "Status", "enum": ["draft", "published", "archived"], "options": [ {"value": "draft", "label": "Draft"}, {"value": "published", "label": "Published"}, {"value": "archived", "label": "Archived"} ] } } ``` **Use cases:** - Predefined statuses - Size options (S, M, L, XL) - Priority levels (low, medium, high) - Content types ## Combining Validations You can combine multiple validation keywords for comprehensive data validation. **Example - Product SKU:** ```json { "sku": { "type": "string", "field": "text", "label": "Product SKU", "minLength": 8, "maxLength": 12, "pattern": "^[A-Z]{3}-[0-9]{4,8}$" } } ``` **Example - Price field:** ```json { "price": { "type": "number", "field": "number", "label": "Price (USD)", "minimum": 0.01, "maximum": 999999.99, "multipleOf": 0.01 } } ``` **Example - Feature list:** ```json { "features": { "$ref": "https://www.totalcms.co/schemas/properties/list.json", "label": "Product Features", "minItems": 3, "maxItems": 10, "uniqueItems": true } } ``` ## Unique Values ### unique Ensure that a property's value is unique across all objects in the collection. ```json { "unique": true } ``` **Example - Email field:** ```json { "email": { "$ref": "https://www.totalcms.co/schemas/properties/email.json", "label": "Email Address", "unique": true } } ``` **Example - Username field:** ```json { "username": { "type": "string", "field": "text", "label": "Username", "unique": true, "minLength": 3, "maxLength": 20 } } ``` **Important requirements:** - **Must be indexed**: Unique properties must be included in the schema's `index` array for performance - **Case-sensitive**: Uniqueness validation is case-sensitive (e.g., "test@example.com" and "TEST@example.com" are considered different) - **Empty values allowed**: Multiple objects can have empty/null values for a unique field - **Update behavior**: When updating an object, you can keep the same value, but cannot change to a value used by another object **Use cases:** - User emails (prevent duplicate registrations) - Usernames (ensure unique login identifiers) - Product SKUs (avoid inventory conflicts) - Slug fields (ensure unique URLs) **Error message:** When a duplicate value is detected, users will see an error message like: ``` Email must be unique. The value 'john@example.com' already exists in another object. ``` **Schema setup example with index:** ```json { "id": "user", "type": "object", "properties": { "id": { "type": "string", "label": "ID", "field": "id" }, "email": { "$ref": "https://www.totalcms.co/schemas/properties/email.json", "label": "Email", "unique": true }, "username": { "type": "string", "label": "Username", "field": "text", "unique": true } }, "required": ["id", "email", "username"], "index": ["id", "email", "username"] } ``` **Note:** If you mark a property as unique but don't include it in the index, you'll receive a helpful error message prompting you to add it to the index. ## Disabling Automatic Required Validation If you want to allow empty values for a required field, explicitly set: **Allow empty strings (for required fields):** ```json { "minLength": 0 } ``` **Allow empty arrays (for required fields):** ```json { "minItems": 0 } ``` **Example - Optional notes on a required field:** ```json { "notes": { "type": "string", "field": "textarea", "label": "Additional Notes", "minLength": 0, "maxLength": 500 } } ``` ## Error Messages When validation fails, Total CMS will display clear error messages to users: - **minLength/maxLength**: "String must be at least X characters" or "String exceeds maximum length of X" - **minimum/maximum**: "Value must be at least X" or "Value must not exceed X" - **pattern**: "Value does not match required format" - **enum**: "Value must be one of: X, Y, Z" - **minItems/maxItems**: "Array must contain at least X items" or "Array exceeds maximum of X items" ## Best Practices 1. **Be specific**: Use validation to enforce your data requirements clearly 2. **Provide context**: Use `help` text to explain validation requirements to users 3. **Balance strictness**: Don't over-validate - allow reasonable flexibility 4. **Test thoroughly**: Verify validation works as expected in the admin interface 5. **Consider UX**: Validation should help users, not frustrate them ## Common Validation Patterns ### Email with length limits ```json { "email": { "$ref": "https://www.totalcms.co/schemas/properties/email.json", "label": "Email Address", "maxLength": 255 } } ``` ### Phone number (US format) ```json { "phone": { "$ref": "https://www.totalcms.co/schemas/properties/phone.json", "label": "Phone Number", "pattern": "^\\d{3}-\\d{3}-\\d{4}$" } } ``` ### Postal code (US ZIP) ```json { "zipcode": { "type": "string", "field": "text", "label": "ZIP Code", "pattern": "^\\d{5}(-\\d{4})?$" } } ``` ### Percentage (0-100) ```json { "completion": { "type": "integer", "field": "range", "label": "Completion %", "minimum": 0, "maximum": 100 } } ``` ### Required gallery (minimum 3 images) ```json { "gallery": { "$ref": "https://www.totalcms.co/schemas/properties/gallery.json", "label": "Product Images", "minItems": 3, "maxItems": 20 } } ``` ### Unique username with validation ```json { "username": { "type": "string", "field": "text", "label": "Username", "unique": true, "minLength": 3, "maxLength": 20, "pattern": "^[a-z0-9_-]+$" } } ``` ### Unique email address ```json { "email": { "$ref": "https://www.totalcms.co/schemas/properties/email.json", "label": "Email Address", "unique": true, "maxLength": 255 } } ``` ## Reference For complete JSON Schema documentation, see: - [JSON Schema Validation Specification](https://json-schema.org/draft/2020-12/json-schema-validation.html) - [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) --- ## Form Grid Layout The `formgrid` setting in a schema controls how form fields are arranged in the admin dashboard. It uses CSS Grid to create multi-column layouts, allowing you to organize fields in rows and columns. ## Basic Concept Think of your form as a spreadsheet. Each row in your `formgrid` definition becomes a row in your form, and each word in that row represents a column. Fields are placed into named "cells" that you define. ``` row 1: [field1] [field2] row 2: [field3] [field3] <- field3 spans both columns row 3: [field4] [ . ] <- empty cell on the right ``` ## Basic Syntax In the schema form, the `formgrid` field is a textarea where: - Each line represents a row in the grid - Property names are separated by spaces ### Simple Two-Column Layout ``` title date author category content content ``` This creates: ``` +------------------+------------------+ | title | date | +------------------+------------------+ | author | category | +------------------+------------------+ | content | +------------------+------------------+ ``` ## Spanning Multiple Columns To make a field span multiple columns, repeat its name across those columns: ``` id id title title date author content content ``` This creates: ``` +------------------+------------------+ | id | <- spans both columns +------------------+------------------+ | title | <- spans both columns +------------------+------------------+ | date | author | +------------------+------------------+ | content | <- spans both columns +------------------+------------------+ ``` ## Empty Cells Use a period (`.`) to create an empty cell: ``` active . id id name category ``` This creates: ``` +------------------+------------------+ | active | (empty) | +------------------+------------------+ | id | +------------------+------------------+ | name | category | +------------------+------------------+ ``` ## Section Dividers Use `---` on its own line to add a horizontal divider: ``` name email --- address city state zip ``` This creates: ``` +------------------+------------------+ | name | email | +------------------+------------------+ | ───────────────────────────────── | <- visual divider +------------------+------------------+ | address | city | +------------------+------------------+ | state | zip | +------------------+------------------+ ``` ## Section Headers Use `--- My Header` or the legacy `---Title Here---` syntax to add a styled section heading: ``` id id name name --- URL Setup url url slug slug --- Dashboard Setup category sortBy ``` Both `--- My Header` (new) and `---Title Here---` (still supported) produce the same result: ``` +------------------+------------------+ | id | +------------------+------------------+ | name | +------------------+------------------+ | URL Setup | <- styled heading +------------------+------------------+ | url | +------------------+------------------+ | slug | +------------------+------------------+ | Dashboard Setup | <- styled heading +------------------+------------------+ | category | sortBy | +------------------+------------------+ ``` ## Fieldsets Use `[[ ]]` to group related fields inside a styled fieldset container. The text after `[[` on the same line is an optional legend: ``` id id name name --- [[ My Legend field1 field2 field3 field4 ]] ``` This creates: ``` +------------------+------------------+ | id | +------------------+------------------+ | name | +------------------+------------------+ | ───────────────────────────────── | <- divider +--────────────────────────────────── + | My Legend | <- fieldset with legend | +------------------+---------------+| | | field1 | field2 || | +------------------+---------------+| | | field3 | field4 || | +------------------+---------------+| +--────────────────────────────────── + ``` Fields named inside a `[[ ]]` block render inside that styled fieldset and leave the outer grid. The interior is laid out as its own mini-grid using the same row syntax. Dividers (`---`) and headers work inside a fieldset too: ``` id id --- [[ Contact Details first_name last_name --- email phone ]] address address ``` **Important notes:** - Fieldsets are one level deep — no nesting fieldsets inside other fieldsets - The reserved area prefix `formgrid-fieldset-N` should not be used as a field name - If the legend is omitted (e.g. `[[` with nothing after it), the fieldset renders without a legend ## Three or More Columns The grid automatically sizes based on the row with the most columns: ``` id id id first middle last address address address city state zip content content content ``` This creates a three-column layout: ``` +------------+------------+------------+ | id | +------------+------------+------------+ | first | middle | last | +------------+------------+------------+ | address | +------------+------------+------------+ | city | state | zip | +------------+------------+------------+ | content | +------------+------------+------------+ ``` ## Important Rules ### Every Property Must Be Included All properties defined in your schema must appear somewhere in the formgrid. Missing properties will cause the form layout to break. If your schema has `title`, `date`, and `content` properties, this formgrid is **incorrect**: ``` title date ``` The `content` property is missing and must be added. ### Syntax Errors Break the Layout The formgrid parser is strict. Common issues include: - Missing properties - Typos in property names - Inconsistent column counts without proper spanning - Invalid characters in property names ### Valid Property Name Characters Property names in the grid must follow CSS identifier rules: - Start with a letter, underscore, or hyphen - Followed by letters, digits, hyphens, or underscores - The special `.` character is reserved for empty cells ## Real-World Examples ### Blog Post Schema ``` id id title title draft featured date author image image categories categories tags tags summary summary content content media media gallery gallery extra extra updated created ``` ### User Authentication Schema ``` active active id id image image name name email email password password groups loginCount expiration created maxLoginCount lastlogin ``` ### Email Template with Dividers ``` active . id id name category description description --- to from toName fromName replyTo . cc bcc --- subject subject bodyHtml bodyHtml bodyText bodyText ``` ### Collection Settings with Named Sections ``` id id name name schema schema ---URL Setup--- url url prettyUrl prettyUrl ---Dashboard Setup--- category labelPlural sortBy labelSingular ---Public Access--- publicOperations publicOperations groups groups ``` ## Tips 1. **Start simple**: Begin with a single-column layout, then add complexity 2. **Use sections**: Break long forms into logical sections with headers or dividers 3. **Test your layout**: After making changes, preview the form in the admin dashboard 4. **Match column counts**: Ensure each row uses the same total number of cells (using spanning or `.` for empty cells) 5. **Keep related fields together**: Group related fields on the same row or in the same section --- ## Twig Schemas Reference The schema adapter provides access to schema definitions — the blueprints that define object structure for collections. ## Listing Schemas ### list() Get all accessible schemas (filtered by edition restrictions). ```twig {% for schema in cms.schema.list() %}{{ schema.id }} — {{ schema.description }}
{% endfor %} ``` ### reserved() Get all accessible built-in (reserved) schemas. ```twig {% for schema in cms.schema.reserved() %}{{ schema.id }}
{% endfor %} ``` ### custom() Get all accessible custom schemas (Pro edition only). ```twig {% if cms.edition.getIsPro() %} {% for schema in cms.schema.custom() %}{{ schema.id }} — {{ schema.description }}
{% endfor %} {% endif %} ``` ### byCategory() Get schemas grouped by their category. ```twig {% for category, schemas in cms.schema.byCategory() %}{{ schema.description }}
{# Access schema properties #} {% for name, prop in schema.properties %}{{ name }}: {{ prop.type }}
{% endfor %} ``` ### forCollection() Get the schema assigned to a specific collection. ```twig {% set schema = cms.schema.forCollection('my-blog') %}This collection uses the {{ schema.id }} schema
``` ## Schema Properties ### inheritedProperties() Get all inherited properties for a schema, including the source schema each property comes from. ```twig {% set inherited = cms.schema.inheritedProperties('blog') %} {% for prop in inherited %}{{ prop.field }} ({{ prop.type }}) — inherited from {{ prop.source }}
{% endfor %} ``` **Returns:** Array of objects with keys: - `source` — schema ID the property is inherited from - `field` — property field name - `type` — property type - `definition` — full property definition array ## Deck Compatibility ### isDeckCompatible() Check if a schema is compatible with deck usage. Decks require specific property types. ```twig {% if cms.schema.isDeckCompatible('blog') %}This schema can be used in decks
{% endif %} ``` ### deckIncompatibleTypes() Get the list of property types in a schema that are incompatible with deck usage. ```twig {% set incompatible = cms.schema.deckIncompatibleTypes('blog') %} {% if incompatible|length > 0 %}Incompatible types: {{ incompatible|join(', ') }}
{% endif %} ``` --- ============================================================ # Fields ============================================================ ============================================================ # Fields / Field Types ============================================================ ## All Field Settings These settings can be applied to **any field type**. They control universal behaviors like hiding fields and conditional visibility. ## Autogen The `autogen` setting automatically generates a field's value from other fields using template variables. This works on any text-based field type (text, textarea, hidden, email, url, etc.). When used on an **ID field**, the result is automatically slugified (lowercased, hyphens for spaces). When used on any other field, the result is kept as-is with no transformation. ```json { "autogen": "${firstname} ${lastname}" } ``` The value updates automatically whenever a referenced field changes. ### Template Variables Use `${fieldname}` to reference any other property in the same form. There are also special built-in variables: * **now** - Current timestamp in milliseconds * **timestamp** - Current date/time in compact ISO format (e.g., 20230815T143056) * **uuid** - UUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000) * **uid** - Short random 7-character alphanumeric string * **uid-N** - Random alphanumeric string of N characters (e.g., `uid-12`) * **oid** - Object ID counter (increments per object in collection) * **oid-00000** - Zero-padded Object ID * **currentyear** - Full 4-digit year * **currentyear2** - 2-digit year * **currentmonth** - Zero-padded month (01-12) * **currentday** - Zero-padded day (01-31) ### Examples **Full name from first and last:** ```json { "fullname": { "type": "string", "field": "text", "label": "Full Name", "settings": { "autogen": "${firstname} ${lastname}" } } } ``` Generates: `John Smith` (not slugified since it's a text field) **Display title with date:** ```json { "displayTitle": { "type": "string", "field": "text", "label": "Display Title", "settings": { "autogen": "${title} (${currentyear})" } } } ``` Generates: `My Article (2025)` **Hidden computed field:** ```json { "slug": { "type": "string", "field": "text", "label": "URL Slug", "settings": { "autogen": "${title}", "hide": true } } } ``` **For ID-specific autogen features** (slugification, uniqueness checking, OID padding), see the [ID Settings](id.md) documentation. ## Calc (Computed Fields) The `calc` setting evaluates a math expression to compute a field's value from other number fields. The field becomes **read-only** automatically since its value is derived. ```json { "calc": "${price} * ${quantity}" } ``` The value recalculates in real time whenever a referenced field changes. ### Field References Use `${fieldname}` to reference any other property in the same form. For referencing fields inside deck items from the parent object, use dot notation: `${deckProperty.fieldName}`. ### Operators * `+` Addition * `-` Subtraction * `*` Multiplication * `/` Division (division by zero returns 0) * `%` Modulo * `()` Parentheses for grouping * Unary `-` for negative numbers ### Functions **Math functions:** * **round(x)** - Round to nearest integer * **round(x, precision)** - Round to N decimal places (e.g., `round(${total}, 2)` for cents) * **floor(x)** - Round down * **ceil(x)** - Round up * **abs(x)** - Absolute value * **min(a, b, ...)** - Minimum value * **max(a, b, ...)** - Maximum value **Aggregate functions** (for deck item fields): * **sum(${deck.field})** - Sum all values * **avg(${deck.field})** - Average of all values * **count(${deck.field})** - Number of items * **min(${deck.field})** - Minimum value * **max(${deck.field})** - Maximum value ### Clamping with min/max Use `min` and `max` settings alongside `calc` to clamp the computed result: ```json { "settings": { "calc": "${subtotal} - ${discount}", "min": 0 } } ``` This ensures the result never drops below 0, even if the discount exceeds the subtotal. ### Examples **Line item total (within a deck item):** ```json { "lineTotal": { "type": "number", "field": "price", "label": "Line Total", "settings": { "calc": "${unitPrice} * ${quantity}" } } } ``` **Subtotal from deck items:** ```json { "subtotal": { "type": "number", "field": "price", "label": "Subtotal", "settings": { "calc": "sum(${items.lineTotal})" } } } ``` Sums the `lineTotal` field from every item in the `items` deck property. **Tax calculation:** ```json { "tax": { "type": "number", "field": "price", "label": "Tax", "settings": { "calc": "round(sum(${items.lineTotal}) * ${taxRate} / 100, 2)" } } } ``` **Grand total with discount and tax:** ```json { "grandTotal": { "type": "number", "field": "price", "label": "Grand Total", "settings": { "min": 0, "calc": "sum(${items.lineTotal}) - ${discount} + round((sum(${items.lineTotal}) - ${discount}) * ${taxRate} / 100)" } } } ``` **Average rating from reviews:** ```json { "avgRating": { "type": "number", "field": "number", "label": "Average Rating", "settings": { "calc": "round(avg(${reviews.rating}))" } } } ``` **Item count:** ```json { "totalItems": { "type": "number", "field": "number", "label": "Total Items", "settings": { "calc": "count(${items.id})" } } } ``` ### Order of Operations When an object has both deck-item-level and object-level calc fields, they are processed in order: 1. **Deck item calcs first** - e.g., `lineTotal = ${unitPrice} * ${quantity}` within each item 2. **Object-level calcs second** - e.g., `grandTotal = sum(${items.lineTotal})` using already-computed values On the frontend, this happens naturally through the event system. On the backend (API saves, imports), the same ordering is enforced explicitly. ### Important Notes - **Read-only:** Calc fields are automatically set to read-only on the frontend. Users cannot manually override computed values. - **Number fields:** Calc is designed for number fields (`number`, `price`). While it can be applied to other field types, the result is always numeric. - **Missing fields:** Any referenced field that is missing or non-numeric is treated as `0`. - **Backend evaluation:** Calc expressions are re-evaluated server-side on every object save, ensuring consistency even for API-driven updates. ## Hide Field The `hide` setting allows you to completely hide a field from the admin form while still storing its data. This is useful for fields that should be managed programmatically or set via defaults rather than through the admin interface. ```json { "hide": true } ``` When `hide` is set to `true`, the field will have the `cms-hide` CSS class added, which hides it from view. ### Common Use Cases **System-managed fields:** ```json { "processedAt": { "type": "string", "field": "datetime", "label": "Processed At", "settings": { "hide": true } } } ``` **Fields with autogen values that shouldn't be edited:** ```json { "slug": { "type": "string", "field": "text", "label": "URL Slug", "settings": { "autogen": "${title}", "hide": true } } } ``` **Metadata fields:** ```json { "version": { "type": "number", "field": "number", "label": "Version", "default": 1, "settings": { "hide": true } } } ``` ### Important Notes - **Data Storage:** Hidden fields still store data in the object. They are just not visible in the admin form. - **Default Values:** Hidden fields typically should have a `default` value or be populated programmatically. - **CSS Class:** The field receives the `cms-hide` class. You can customize visibility with CSS if needed. ## Conditional Visibility The `visibility` setting controls when a field is displayed in forms based on the value of another field. ```json { "visibility": { "watch": "fieldName", "value": "expectedValue", "operator": "==" } } ``` **Properties:** - **`watch`** (required) - The name of the field to watch for changes - **`value`** (required) - The value(s) to compare against. Can be a single value or an array of values - **`operator`** (optional) - The comparison operator to use (default: `==`) ### Supported Operators **Equality Operators:** - `==` - Equals (default) - `!=` - Not equals **Numeric Operators:** - `>` - Greater than - `<` - Less than - `>=` - Greater than or equal - `<=` - Less than or equal **Array Operators:** - `in` - Current value is in the expected value array - `not_in` - Current value is not in the expected value array **Special Operators (for array fields like checkbox, multiselect):** - `empty` - Field array is empty - `not_empty` - Field array has at least one value ### Examples **Show field when another field has a specific value:** ```json { "visibility": { "watch": "linkType", "value": "custom", "operator": "==" } } ``` **Hide field when another field is checked:** ```json { "visibility": { "watch": "useDefaultDescription", "value": "1", "operator": "!=" } } ``` **Show field when value is NOT in a list:** ```json { "visibility": { "watch": "userRole", "value": ["guest", "basic"], "operator": "not_in" } } ``` **Show field when multiselect contains a value:** ```json { "visibility": { "watch": "contentTypes", "value": "gallery", "operator": "in" } } ``` **Show field based on numeric comparison:** ```json { "visibility": { "watch": "orderTotal", "value": "100", "operator": ">=" } } ``` **Show field when array field is not empty:** ```json { "visibility": { "watch": "categories", "value": null, "operator": "not_empty" } } ``` **Match multiple values (OR logic):** ```json { "visibility": { "watch": "deliveryMethod", "value": ["standard", "express", "overnight"], "operator": "==" } } ``` Field is visible if `deliveryMethod` matches ANY value in the array. ### Visibility Mode By default, when a condition is not met the field collapses entirely (`display: none`). The optional `mode` key changes this behavior: - **`"hide"`** (default) — field is collapsed and removed from the layout when the condition is not met. - **`"disable"`** — field remains visible but is greyed-out and non-interactive. Its value is preserved and still saved. The field still counts as visible, so other fields that watch it will read its current value normally. ```json { "visibility": { "watch": "plan", "value": "free", "operator": "==", "mode": "disable" } } ``` Use `"disable"` when you want the user to see a field's current value but prevent editing it under certain conditions. ### Default Behavior Fields with a `visibility` setting are **hidden by default** until the condition is met. This ensures fields appear only when they should, even on initial form load. ## Required (Form-Level) The `required` setting marks a field as required in the **form only**, separately from the schema's top-level `required` array. Pair it with `visibility` to make a field required only when another field has a particular value. ```json { "required": true } ``` ### How it differs from schema-level `required` Total CMS has two distinct ways to mark a field as required, and they are intentionally separate: | Where | What it does | When it fires | |---|---|---| | Schema's top-level `required: ["foo"]` array | Hard data invariant — the object cannot be saved without a value | Server-side on every save (form, API, CLI, import) | | Field's `settings.required: true` | Adds the `required` HTML attribute to the input | Client-side HTML5 validation when the form is submitted | Use the schema's top-level `required` when a value must always exist on every record. Use `settings.required` when the requirement is conditional — typically driven by another field's value via `visibility`. > **Important:** If a field is listed in the schema's `required` array AND has `settings.required: true`, the schema-level rule still wins on save. For a truly conditional field, leave it out of the schema's `required` array entirely — otherwise saves will be rejected when the field has no value, defeating the conditional behavior. ### Conditional required (toggle pattern) The common use case is "this field is only required when a toggle is set." Combine `settings.required` with `settings.visibility`: ```json { "enableNotifications": { "type": "boolean", "field": "toggle", "label": "Send Email Notifications" }, "notificationEmail": { "type": "string", "field": "email", "label": "Notification Email", "settings": { "required": true, "visibility": { "watch": "enableNotifications", "value": "1", "operator": "==" } } } } ``` When `enableNotifications` is on, the email field is visible and the browser enforces the required attribute on submit. When the toggle is off, the field is hidden and the form's visibility system strips the required attribute so an empty value doesn't block submission. Because `notificationEmail` is not listed in the schema's `required` array, saves succeed regardless of toggle state. ### Visibility integration When a field has both `required` and `visibility`, the form automatically: - Strips the `required` HTML attribute when the field is hidden - Restores it when the field becomes visible - Skips client-side validation entirely for hidden fields This works transparently — no extra configuration beyond setting both `required` and `visibility` on the field. ## Validation All fields support validation rules like `pattern`, `minLength`, `maxLength`, `minimum`, `maximum`, `enum`, and more. These are added in the **Extra Schema Definitions** section of a property, not in Settings. For full details and examples, see [Schema Validation](../schemas/validation.md). ## Reference Schema: `totalcms` Total CMS ships a builtin **reference schema** named `totalcms` that demonstrates every field type and its settings in a single valid schema file. It lives at `resources/schemas/totalcms.json`, with a companion `resources/schemas/totalcms-item.json` backing its `card` and `deck` examples. The schema is registered and resolvable (you can open it in the schema editor and it is visible to schema tooling), but it is **reference-only** — you cannot create a collection from it. Use it as a copy-paste catalog when authoring your own schemas: find the field type you want, copy the property definition, and adapt it. --- ## Card Fields A **card** is a single-instance deck. It renders the properties of another schema as inline sub-fields and stores the collected values as a nested object on the parent. Use it to group related settings (sitemap config, auth options, integration credentials, etc.) without the deck UX of add/remove/duplicate. The card's shape is defined by a separate schema, referenced via `schemaref`. This keeps the nested structure reusable across collections and makes the parent schema easy to read. ## How It Differs From a Deck | | Deck | Card | |---|---|---| | Cardinality | Many items | Exactly one | | UI | Modal dialog with add/remove/duplicate | Inline sub-fields, no buttons | | Stored as | Object keyed by item IDs | Single nested object keyed by property name | | Schema | `schemaref` defines item shape | `schemaref` defines the card's shape | If you want a single named-object grouping, use a card. If you want a list of named objects, use a deck. ## Basic Usage ```json { "sitemap": { "$ref" : "https://www.totalcms.co/schemas/properties/card.json", "field" : "card", "label" : "Sitemap Settings", "schemaref" : "https://www.totalcms.co/schemas/sitemap-meta.json" } } ``` The referenced schema (`sitemap-meta.json` here) defines the sub-fields the card will render. Its properties become the card's properties at save time. ## Storage Format A card stores as a single nested object keyed by sub-property name: ```json { "sitemap": { "enabled" : true, "frequency" : "weekly", "priority" : 0.7 } } ``` In Twig, access values with dot notation: ```twig {% if object.sitemap.enabled %}Welcome…
", "de": "Willkommen…
", "ar": "مرحبا…
" } } ``` ## Reading Localized Values in Twig ### Direct array access The simplest case: just look up the locale key on the field value. ```twig {{ post.title.en_US }} {{ post.title.de }} ``` This is the lightest path when you know the locale exists and you don't need fallback. ### The `cms.locale.*` helpers When you want a deterministic fallback chain, use the helper: ```twig {{ cms.locale.text(post.title, 'de') }} {# localizedtext + localizedtextarea #} {{ cms.locale.styledtext(post.body, 'de') }} {# localizedstyledtext #} ``` `cms.locale.text()` handles both *localizedtext* and *localizedtextarea* content (they're both plain strings). Use `cms.locale.styledtext()` for *localizedstyledtext* — the HTML renders as-is since Total CMS ships with Twig autoescape disabled. #### Page-scoped locale If you omit the locale argument, both helpers fall back to whatever `cms.locale.get()` returns — the active intl/CakePHP locale. Set it once at the top of a page with `cms.locale.set()` and every subsequent `text()` / `styledtext()` call follows: ```twig {% do cms.locale.set('de') %}Welcome to our site.
{% endblock %} ``` ### Partials (`partials/`) Reusable fragments — navigation, footer, cards. Included via `{% include %}`. ### Macros (`macros/`) Reusable Twig functions for repeated rendering patterns. ## Template Data When a builder page route matches, the template receives: ### `page` The full page object from the collection: ```twig {{ page.title }} {# Page Title #} {{ page.description }} {# Meta description #} {{ page.route }} {# URL pattern #} {{ page.template }} {# Template name #} {{ page.image }} {# Page image (used for og:image / hero) #} {{ page.status }} {# HTTP status code #} {{ page.data.hero }} {# Custom JSON data — see Page Data #} ``` ### `params` Extracted URL parameters from dynamic routes: ```twig {# Route: /products/{id} — URL: /products/widget-x #} {{ params.id }} {# "widget-x" #} {# Route: /blog/{category}/{slug} — URL: /blog/tech/my-post #} {{ params.category }} {# "tech" #} {{ params.slug }} {# "my-post" #} ``` Use params to fetch collection data: ```twig {% set product = cms.data.object('products', params.id) %}{{ object.summary }}
{{ post.summary }}