This package requires an ALTCHA HMAC secret for the captcha field. The secret is used to sign and verify ALTCHA challenges.
Sitegeist:
PaperTiger:
CPX:
Altcha:
secret: '<your-random-hmac-secret>'PaperTiger supports two form modes:
standard: the form is rendered uncached, so on submit the server re-renders the page with values + validation errors.async: the form is rendered cached; JavaScript submits to the JSON endpoint and updates the UI with the returned errors/result.
PaperTiger uses Flow validators (for example NotEmpty, StringLength, RegularExpression) to validate submitted values.
On submit, PaperTiger validates each field via a schema:
- The field
SchemaProviderbuilds aSchemaDefinition(target type + validators) - The schema converts the raw submitted value to the target type (via Flow PropertyMapper), for example:
<input type="date">submitsYYYY-MM-DDas a string, but the schema converts it to\DateTimeImmutable
- Validation runs on the converted value (if conversion worked). If conversion fails, validators run on the raw value.
- If a value is empty, PaperTiger only runs the required validator and skips all other validators.
This keeps server validation consistent with browser inputs and avoids errors from validators that expect a specific PHP type.
For text fields you can optionally override the inline validation messages in the inspector under the Validation tab.
- Enable
Use custom messagein the corresponding group (Required, Length, Pattern) - Fill in the message fields
These overrides are applied on the schema level (by Flow error code) so they work the same for standard and async.
Fields can optionally define a custom browser popup message (native HTML validation).
This is configured in the inspector under Validation -> Browser validation (popup) and is used via setCustomValidity(...).
PaperTiger collects all validation errors as a list of { fieldName, message }.
- Field specific errors are attached to the field name (for example
email,message,upload) - General (form-level) errors use the special field name
__general - A field can have multiple errors; PaperTiger renders all errors for that field
Custom inline messages are implemented by overriding validator errors by their error code. That means:
- Built-in Flow validators work out of the box (PaperTiger maps the relevant Flow error codes internally)
- Custom validators should use stable, unique integer error codes in
addError(...)if you want to override them later
If no markup changes are needed, use CSS.
Example:
@layer components {
.papertiger-field__control {
@apply border border-brand/20 bg-brand-grey/45 px-24 py-16;
}
}Async mode also toggles papertiger-field--submitError on invalid field containers briefly when submit is blocked by validation. You can use this class for custom animations.
Async mode also toggles papertiger-form--submitLoading on the <form> while a submit request is in-flight, and disables button[type="submit"].
Example:
.papertiger-form--submitLoading {
opacity: 0.65;
pointer-events: none;
}
.papertiger-field--submitError {
animation: papertiger-jiggle 0.32s ease-in-out;
}
@keyframes papertiger-jiggle {
0% { transform: translateX(0); }
25% { transform: translateX(-4px); }
50% { transform: translateX(4px); }
75% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}Async mode renders its error/message markup via the configured shared components as well (for example your custom error / message components).
Async client-side validation is extendable: you can register additional JS validators (by validationId) via registerAsyncValidator(...), and you can add additional PHP async validation rule providers via the Sitegeist.PaperTiger.CPX.asyncValidation.ruleProviders setting.
If you want your own outer wrapper, use FormFactory to build the form and pass it into your project component.
Example:
return ContentContainerFactory::create(
$context,
FormBuilder::create(
$context->neos->getEditable($context->node, 'headline', true),
$this->formFactory->create($context),
),
);If you want to change shared field markup, replace the shared components.
fieldContainerlabelerrorinputtextareaselectuploadcheckboxradiobuttondatemessage
These components must keep the same PaperTiger props.
Example project settings:
Sitegeist:
PaperTiger:
CPX:
components:
fieldContainer: 'Vendor\Shared\Components\Block\FormBuilder\Fields\FieldContainer\FieldContainer'
label: 'Vendor\Shared\Components\Block\FormBuilder\Fields\Label\Label'
error: 'Vendor\Shared\Components\Block\FormBuilder\Fields\Error\Error'
message: 'Vendor\Shared\Components\Block\FormBuilder\Fields\Message\Message'
input: 'Vendor\Shared\Components\Block\FormBuilder\Fields\Input\Input'
button: 'Vendor\Shared\Components\Block\FormBuilder\Fields\Button\Button'Example custom label component:
from "Sitegeist.PaperTiger.CPX/Label/LabelProps.cpx" import { LabelProps }
export component Label {
label: LabelProps
render
<label
for={label.inputId}
class="papertiger-field__label"
>
<span>{label.label}</span>
{!label.isRequired ? <span>(optional)</span> : null}
</label>
}
If the inner field markup is fine, import the PaperTiger component and wrap it.
Example custom input component:
from "Sitegeist.PaperTiger.CPX/Field/InputField/InputField.cpx" import { InputField }
from "Sitegeist.PaperTiger.CPX/Field/InputField/InputFieldProps.cpx" import { InputFieldProps }
from "../InvalidIcon/InvalidIcon.cpx" import { InvalidIcon }
export component Input {
field: InputFieldProps
render
<>
<InputField field={field} />
<InvalidIcon />
</>
}
This is the easiest way to add small extras like an icon.
Use this only if the other options are not enough.
Example:
'Vendor.Site:Content.Form.Field.Text.SingleLine':
superTypes:
'Sitegeist.PaperTiger.CPX:Mixin.Field.Text.SingleLine': trueThis gives your project its own field types.
In CPX, you can also import PaperTiger props as structs.
Example:
from "Sitegeist.PaperTiger.CPX/Field/InputField/InputFieldProps.cpx" import { InputFieldProps }
export component Input {
field: InputFieldProps
render
<input
type={field.type}
id={field.fieldContainer.inputId}
name={field.name}
/>
}
Validation is mapped per field node type.
For every field type, a schema provider builds the schema for that field. A schema does not validate by itself — it only composes validators.
If you want a custom validation, the typical flow is:
- Add inspector properties via a mixin (options + optional custom message)
- Add the mixin to your field node type
- Implement a schema provider that reads the properties and adds validators
- Implement the validator and use stable error codes
- Map your field node type to your schema provider via settings
Example mixin that adds a checkbox + textarea for a custom server-side error:
'Vendor.Site:Mixin.Validation.MyRule':
abstract: true
ui:
inspector:
groups:
form-validation-my-rule:
label: 'Validation - My rule'
icon: icon-check
tab: form-validation
position: 90
properties:
myRuleEnabled:
type: boolean
defaultValue: false
ui:
label: 'Enable rule'
inspector:
group: form-validation-my-rule
position: 10
useCustomMyRuleMessage:
type: boolean
defaultValue: false
ui:
label: 'Use custom message'
inspector:
group: form-validation-my-rule
position: 20
myRuleMessage:
type: string
defaultValue: null
ui:
label: 'Rule error'
inspector:
group: form-validation-my-rule
position: 30
editor: 'Neos.Neos/Inspector/Editors/TextAreaEditor'
hidden: 'ClientEval:node.properties.useCustomMyRuleMessage ? false : true'
editorOptions:
rows: 3'Vendor.Site:Field.MyCustomField':
superTypes:
'Sitegeist.PaperTiger.CPX:Mixin.Field.Text.SingleLine': true
'Vendor.Site:Mixin.Validation.MyRule': true<?php
declare(strict_types=1);
namespace Vendor\Site\Domain\Validation\Schema;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use PackageFactory\Neos\ComponentEngine\NeosContext;
use Sitegeist\PaperTiger\CPX\Domain\Validation\Schema\AbstractFieldSchemaProvider;
use Sitegeist\PaperTiger\CPX\Domain\Validation\SchemaInterface;
use Vendor\Site\Domain\Validation\Validator\MyRuleValidator;
final class MyCustomFieldSchemaProvider extends AbstractFieldSchemaProvider
{
public function build(NeosContext $context, Node $fieldNode): ?SchemaInterface
{
$schema = $this->createSchema('string');
$enabled = $context->nodes->getBoolValue($fieldNode, 'myRuleEnabled') ?? false;
if ($enabled) {
$schema->validatorWithId('myRule', MyRuleValidator::class, [
'useCustomMessage' => $context->nodes->getBoolValue($fieldNode, 'useCustomMyRuleMessage') ?? false,
'message' => $context->nodes->getStringValue($fieldNode, 'myRuleMessage'),
]);
}
return $schema;
}
}Async mode uses a JS validator registry. If you add a custom validator via validatorWithId('myRule', ...), you should also
register a JS validator with the same id:
import { registerAsyncValidator } from "Sitegeist.PaperTiger.CPX/AsyncForm/validatorRegistry";
registerAsyncValidator("myRule", (value, rule) => {
// You can read rule.options to access the schema validator options (if needed)
// rule.message is already prepared by the backend descriptor
return true;
});If no JS validator is registered for a validationId, that rule is ignored on the client (server-side validation still runs).
<?php
declare(strict_types=1);
namespace Vendor\Site\Domain\Validation\Validator;
use Neos\Flow\Validation\Validator\AbstractValidator;
final class MyRuleValidator extends AbstractValidator
{
public const int ERROR_INVALID = 1744800002;
protected $supportedOptions = [
'useCustomMessage' => [false, 'Whether to use the custom message', 'boolean', false],
'message' => [null, 'Custom message', 'string', false],
];
protected function isValid($value): void
{
if ($value === null || $value === '') {
return;
}
if (!is_string($value)) {
$this->addError('The value must be a string.', 1744800001);
return;
}
if ($value === 'forbidden') {
$useCustom = (bool)$this->options['useCustomMessage'];
$message = $useCustom && is_string($this->options['message']) && $this->options['message'] !== ''
? (string)$this->options['message']
: 'The submitted value is invalid.';
$this->addError($message, self::ERROR_INVALID);
}
}
}Add your field node type to the schema provider mapping, for example in Settings.yaml:
Sitegeist:
PaperTiger:
CPX:
validation:
schemaProviders:
'Vendor.Site:Field.MyCustomField': 'Vendor\Site\Domain\Validation\Schema\MyCustomFieldSchemaProvider'The resolver will use that provider whenever a field node is of that type.
This is the full “field” flow (NodeType -> renderer/CPX -> schema -> validator).
You can base it on an existing PaperTiger field mixin:
'Vendor.Site:Field.MyCustomField':
superTypes:
'Sitegeist.PaperTiger.CPX:Mixin.Field.Text.SingleLine': true
ui:
label: 'My custom field'
icon: icon-magicRenderer:
<?php
declare(strict_types=1);
namespace Vendor\Site\NodeTypes\Field\MyCustomField;
use PackageFactory\ComponentEngine\ComponentInterface;
use PackageFactory\Neos\ComponentEngine\Integration\ContentNodeRendererInterface;
use PackageFactory\Neos\ComponentEngine\NeosContext;
final class MyCustomFieldRenderer implements ContentNodeRendererInterface
{
public function renderAsContent(NeosContext $context): ComponentInterface
{
// Typically: read node properties, create PaperTiger props, return your CPX component.
return \Vendor\Site\Components\Form\MyCustomField::create(/* ... */);
}
}CPX (example):
export component MyCustomField {
render <input type="text" />
}
Use a SchemaProvider to map node properties into Flow validators (or your own validators).
See “Full Flow (Mixin -> Schema -> Validator)” above for a complete example.
If you want async mode to reliably match server errors to a specific rule, always add validators with a stable validationId
using validatorWithId(). The server submit response will include that validationId (when available), and the async validation
descriptor is derived from the schema validators as well.