How do I validate the data of a Volto block field and display error messages

I want to validate the value of a url-field (widget: 'url') of a custom block entered by a user. The url should belong to a given domain.
Where do I add such a validation and how do I display a custom error-message if the entered data is invalid?

I found a solution with a custom Deseriallizer that is triggered on save and prevents invalid data from being stored in the database.

from plone.base.interfaces import IPloneSiteRoot
from plone.restapi.behaviors import IBlocks
from plone.restapi.interfaces import IBlockFieldDeserializationTransformer
from zExceptions import BadRequest
from zope.component import adapter
from zope.interface import implementer
from zope.publisher.interfaces.browser import IBrowserRequest


class MyBlockDeserializerBase:
    order = 100
    block_type = "myblock"
    disabled = False

    def __init__(self, context, request):
        self.context = context
        self.request = request

    def __call__(self, block):
        url = block.get("src", "")
        if url and not url.startswith("xyz"):
            errors = [
                {
                    "error": "ValidationError",
                    "message": "URL must start with xyz",
                }
            ]
            raise BadRequest(errors)
        return block


@implementer(IBlockFieldDeserializationTransformer)
@adapter(IBlocks, IBrowserRequest)
class MyBlockDeserializer(MyBlockDeserializerBase):
    """Deserializer for content-types with IBlocks behavior"""


@implementer(IBlockFieldDeserializationTransformer)
@adapter(IPloneSiteRoot, IBrowserRequest)
class MyBlockDeserializerRoot(MyBlockDeserializerBase):
    """Deserializer for site root"""
  <subscriber
      factory=".blocks.MyBlockDeserializerRoot"
      provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer"
      />
  <subscriber
      factory=".blocks.MyBlockDeserializer"
      provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer"
      />

That is good enough for my project but I wonder if there is a machinery that also validates the field on data-entry and displays a message next to the field like in the validation of dexterity-schema fields.

If possible, I'd use HTML5 input type of URL.

That's not really the point because I don't control the widgets html when configuring a blocks field. Also the question applies to any field where data should get validated.

How about using an in field validation in frontend? This can be done in a block data adapters.

Maybe file a UX bug on volto. It's a big missing part of the UX having no validation. Sometimes someone steps up to take on missing functionality but they wouldn't know it's missing unless it's reported.

I had the same idea but could not find an example. All widgets seem to have an error-attribute and the Storybook allows to populate it: @storybook/cli - Storybook The Message is displayed nicely but I did not find a example where that is used in Volto.

My other idea was overriding the widget and customizing the onChangeValue method to do the validation but that seemed overkill and again I could not figure out how a custom error-message can be set.

We do have validations on widgets but are handled using semantic-ui primitives for example: UrlWidget I think these can be handled better like we do in ModalForm i.e using a centralised validation module.
Can be a nice topic for Volto Team meeting!

Suggestion with patch of Volto URLWidget:

Your backend code provides regular expression (and a help message describing the constraint) via 'frontendOptions'

    directives.widget(
        "uri_field",
        frontendOptions={
            "widgetProps": {
                "constraints": "my_regular_expression pipapo",
                "constraints_help": "Please provide a URL with …",
            }
        },
    )

See Forms and widgets – Frontend – Development — Plone Documentation v6.0

Volto widget UrlWidget must be changed to take these additional props into account:
onChangeValue or better onblur checks the widget value against the regular expression and sets new introduced state 'fielderror' which includes help text to explain the error.

setFielderror({msg: constraints_help})

this state needs to be passed to FormFieldWrapper

<FormFieldWrapper {...props} error={fielderror} className="url wide">

Et voilà, the help/error message is displayed like already prepared by FormFieldWrapper.

1 Like