Problem rendering form validation results

Hi,

Background

I’m trying to create a Package that lets users create comments. That works more or less.

What I try to do

Validating user input that come from a <form> with a Controller/Validator. Validation works insofar as no comment gets created when there is invalid user input.

The problem

If there’s invalid user input showing the validation results / flash messages goes crazy for some reason. No comment gets created and therefore no new comment will be rendered. That’s fine. But the user should get some feedback. Unfortunately the feedback doesn’t get rendered, when the user gets redirected back to the form—and I don’t know why. After refreshing the page containing the form or browsing to anther documentNode and back lets Neos render the messages. But I don’t get when exactly it is the case. If the messages get rendered I can refresh/reload the page and browse to another documentNode and back it the messages are still there and just disappear as magically as they came. (I don’t navigate back and forth in the browsers cache but really reload the page or change documentNodes clicking links)

How I’m trying to do it (a.k.a. the code)

The form
<!--F:FLASHMESSAGES-->
<f:flashMessages as="flashMessages">
	<f:for each="{flashMessages}" as="message">
		<div class="message">
			<div class="header">
				{message.title}
			</div>
			<p>{message}</p>
		</div>
	</f:for>
</f:flashMessages>

<!--F:VALIDATION.RESULTS-->
<f:validation.results>
		<ul>
			<f:for each="{validationResults.flattenedErrors}" as="errors" key="propertyPath">
				<li>{propertyPath}
					<ul>
					<f:for each="{errors}" as="error">
						<li>{error.code}: {error}</li>
					</f:for>
					</ul>
				</li>
			</f:for>
		</ul>
</f:validation.results>

<!--F:FORM-->
<f:form class="ui reply form" action="create" controller="Comment" package="V.S" objectName="comment" id="newCommentForm">

	<f:form.hidden name="feedNode" value="{node.path}" />

	<f:form.textfield property="authorName" id="authorName" placeholder="Name"/>

	<f:form.textfield property="authorEmail" id="authorEmail" placeholder="E-Mail"/>

	<f:form.textarea property="commentContent" id="commentContent" />

	<f:form.button type="submit" class="ui blue labeled submit icon button">
		<i class="icon edit"></i> Publish
	</f:form.button>

</f:form>
The Controller
class CommentController extends ActionController
{
	/**
	 * Creates a new comment
	 *
	 * @Flow\Validate(type="V.S:Comment", value="comment")
	 *
	 * @param NodeTemplate<V.S:Comment> $comment NodeTemplate holding the user input as properties
	 * @param NodeInterface $feedNode The node which contains the comment feed's ContentCollection that will contain the new comment node
	 * @return void
	 */
	public function createAction(NodeTemplate $comment, NodeInterface $feedNode)
	{
        // @var commentServie: creates the comment. It basically modifies some properties of the NodeTemplate and calls createNodeFromTemplate to create a child node in feedNode. Nothing special thats why I omit it here.
		$commentNode = $this->commentService->create($comment, $feedNode);
        // See Neos\Neos\View\FusionView where this method is stolen from ;)
		$commentDocumentNode = $this->getClosestDocumentNode($commentNode);

		$this->addFlashMessage('Success', 'Comment created. Thank you!');

		# Send signal and redirect user
		#------------------------------
		$this->emitCommentCreated($commentNode, $feedNode);
		$this->redirectToNewComment($commentDocumentNode, $commentNode->getName());
	}
	protected function redirectToNewComment(NodeInterface $targetDocumentNode, $anchor)
	{
		# Add the comment name as page anchor to the URI
		$this->uriBuilder->setSection($anchor);

		# Build the URI to the Frontend/NodeController which shows the documentNode
		$uri = $this->uriBuilder->uriFor('show', array('node' => $targetDocumentNode), 'Frontend\Node', 'Neos.Neos');

		$this->redirectToUri($uri);
	}
	protected function emitCommentCreated(NodeInterface $commentNode, NodeInterface $feedNode)
	{
	}
}
The Validator
/**
 * Basically this a modification of GenericObjectValidator to work with nodes.
 */
class CommentValidator extends GenericObjectValidator
{
	/**
	 * @var ValidatorResolver
	 * @Flow\Inject
	 */
	protected $validatorResolver;

	/**
	 * Checks if the given value is valid according to the validator, and returns
	 * the Error Messages object which occurred.
	 *
	 * @param mixed $value The value that should be validated
	 * @return ErrorResult
	 */
	public function validate($value)
	{
		$this->result = new ErrorResult();
		if ($this->acceptsEmptyValues === false || $this->isEmpty($value) === false) {
				if (!is_object($value)) {
					$this->addError('Object expected, %1$s given.', 1241099149, [gettype($value)]);
				} elseif ($this->isValidatedAlready($value) === false) {
					# Create an add propertyValidators
					#---------------------------------
					$this->addPropertyValidator('authorName', $this->validatorResolver->createValidator('String'));
					$this->addPropertyValidator('authorName', $this->validatorResolver->createValidator('NotEmpty'));
					$this->addPropertyValidator('authorName', $this->validatorResolver->createValidator('StringLength',array('minimum' => 3)));
					$this->addPropertyValidator('authorEmail', $this->validatorResolver->createValidator('NotEmpty'));
					$this->addPropertyValidator('authorEmail', $this->validatorResolver->createValidator('EmailAddress'));
					$this->addPropertyValidator('commentContent', $this->validatorResolver->createValidator('String'));
					$this->addPropertyValidator('commentContent', $this->validatorResolver->createValidator('NotEmpty'));
					$this->addPropertyValidator('commentContent', $this->validatorResolver->createValidator('StringLength',array('minimum' => 3)));
					$this->isValid($value);
				}
		}

		return $this->result;
	}

	/**
	 * Load the property value to be used for validation.
	 *
	 * @param object $object
	 * @param string $propertyName
	 * @return mixed
	 */
	protected function getPropertyValue($node, $propertyName)
	{
		return $node->getProperty($propertyName);
	}
}
Routes.yaml
-
  name: 'Add a new comment'
  uriPattern: 'comment/create.html'
  defaults:
    '@package': 'V.S'
    '@controller': 'Comment'
    '@format': 'html'
    '@action': 'create'

The Flow manual states

When validation in the MVC layer happens, it is possible to handle errors correctly. In a nutshell, the process is as follows:

  • if there is a property mapping or validation error, the last page (which usually contains an edit-form) is re-displayed, an error message is shown and the erroneous field is highlighted.

If I submit the form with incorrect values the last page will be rendered but the URI doesn’t fit. Instead of something like http://127.0.0.1:8081/path/to/documentNode.html (where the form is) the URI is: http://127.0.0.1:8081/comment/create.html.
Could this cause the problem? Even if not: how can I avoid this (because it looks really bad).

Screenshot

Here’s a Screenshot that may help understand it: These are all flash messages. The first five were created because validation failed but not rendered. After creating a valid comment all of the messages get rendered. Now I can browse/reload as much as I want and the messages keep appearing for some time. The validation results are missing completely!

Any help is more than welcome :slight_smile:

Edit:

One more thing: If i submited an invalid comment and got redirected to http://127.0.0.1:8081/comment/create.html and go immediately to http://127.0.0.1:8081/neos for logging in into the backend the following happens:

Can someone explain why the error gets rendered in the login screen but not on the page?

Edit 2:

Could this be a cache problem? If I submit invalid value, change something negligible in the fluid layout file, save it and reload, the validation results/flashmessages will get rendered.

Yes, it’s very likely that this is a caching issue. The validation errors and the error action message from the controller get rendered the next time a request comes through - if this is the backend login screen, it gets rendered there.
How do you render your form? Is it rendered by a node type, or just a fluid template rendered by a Fusion Template object (or something else entirely)?

It’s some kind of a combination :smiley: It’s Fluid template rendered by a Fusion Template object which is a child of a Node:

The NodeType protoype
prototype(V.S:CommentFeed) < prototype(Neos.Neos:Content) {
	templatePath = 'resource://V.S/Private/Fusion/Components/CommentFeed/CommentFeed.html'

	commentFeed = Neos.Neos:ContentCollection {
		nodePath = 'feed'
	}

	createCommentForm = V.S:CreateCommentForm
}
CreateCommentForm
prototype(V.S:CreateCommentForm) < prototype(Neos.Fusion:Template) {
	templatePath = 'resource://V.S/Private/Fusion/Components/CreateCommentForm/CreateCommentForm.html'
}

So the form itself is a Fusion object, but its embedded in a Node.

Yep, then it’s the cache. The cache doesn’t know about validation - since you have not added any cache config, your object simply gets embedded in the parent, and unless a node is modified, it does not get flushed.
You could use a Plugin to render your comment form, since you have a controller already - just add an index action, render it with a Plugin object and you should be fine. Also, no redirecting to nodes or anything needed then, because the plugin is rendered as a subrequest and you’ll stay on the same page anyway.

Or could it be possible to just do

    @cache {
            mode = 'uncached'

I tried this. It works.

This sounds interesting. Could you elaborate this a bit, please? Reading the How-To didn’t help much.

Especially the redirecting part seems to be interesting because if validation fails the URL is http://127.0.0.1:8081/comment/create.html which is a really ugly one :smiley:

Would the URL with a plugin subrequest be always http://127.0.0.1:8081/path/to/node.html? That would be great.
(How) Could I add sections/page anchors, e.g. http://127.0.0.1:8081/path/to/node.html#newlyCreatedNode or http://127.0.0.1:8081/path/to/node.html#errorMessage?

Yes, you could also just make the template uncached. However, using a Plugin also has the advantage that you don’t have to take care of redirecting the user back to the last visited document node - the plugin logic will take care of that.
The URL will be http://127.0.0.1:8081/path/to/node?pluginsubrequestdata, where the “pluginsubrequestdata” is a rather ugly string containing the package, controller, action and format of the subrequest being sent. The documentation says it’s possible to remedy that by configuring a route, which I haven’t tried yet.
What exactly is missing for you in the docs? You can keep your current createAction (minus the redirect part) and just add an indexAction (or displayFormAction or however you want to call it), add a node type inheriting from Neos.Neos:Plugin to render your form, and configure the Fusion prototype for it as documented here: http://neos.readthedocs.io/en/latest/ExtendingNeos/CreatingAPlugin.html#configure-fusion

Ah, you mean to implement just the form as a plugin and keep the V.S:CommentFeed as “simple” Content NodeType?
That way thinking how to cache the Comment nodes would become superfluous. :wink:

Yup, that’s what I meant. The commentFeed object needs mode “cached” and the “NodeType_V:S:Comment” (replace with your comment NodeType name) entryTag in order to update properly when a new comment is added or one is modified.

Well, then I’m doing something wrong :sweat_smile:

After hitting the submit button the plugin/controller tries to render V.S/Private/Templates/Comment/Create.html which doesn’t exist, because the createAction doesn’t need to render anything (it just transforms the user input into a node). The result is:

Template could not be loaded. I tried “resource://V.S/Private/Templates/Comment/Create.html”

So I need to redirect/forward to the indexAction nevertheless?

Ah, well. You can either of course redirect back to the indexAction (should work with $this->redirect('index'); or have a template that just displays a success message.

I had the idea to show the form again immediately after the comment was created. This is probably purposeless and it’s easier just to create a Template for the createAction :smiley: But sometimes it simply needs someone to state the obvious, thanks :wink:

I’d like to let user see his new comment/success/error messages directly. How can I add an anchor to the URL the user will be redirected (#successMessage), so that the browser scrolls to the correct position while loading the page?
(Like the UriBuilder->setSection('successMessage') method?) I didn’t found a way to do this for a request/response, but probably I overlooked something?

@beheist Is it possible that there is one drawback using a Plugin?

The rough node tree of my page:

+documentNode
+-main (PrimaryContent)
+-comments (Content)
  +-feed (ContentCollection)
  +-indexAction/createAction (Plugin)
                             (indexAction shows the <form>)
                             (createAction creates a childNode in feed and renders a success message)
  • If the page gets rendered, the Pugin’s indexAction renders the <form>. Everything is fine.
  • I fill in some values into the form and submit it, the page reloads.
  • The createAction gets invoked and creates/saves the childNode and renders the success message, so far so good.
  • But: The newly created childNode of feed doesn’t get rendered (even though feed is a ContentCollection which is cached by default but flushes cache if a childNode changes, so it should be rendered). If I reload the page, it gets rendered.

Is this because Plugin is a subrequest and therefore only changes within the Plugin will be re-rendered and the remaining page will be rendered just as it was when it was rendered the first time?
If I reload the page the new childNode will be rendered, too, because this is a completely new request affecting the whole page (and not just the plugin)?
An attempt to visualize it:

Visiting the page for the first time
+documentNode
+-main (PrimaryContent)
+-comments (Content)
  +-feed (ContentCollection)
  +-indexAction(<form>) (Plugin)

Submitting the <form>
+documentNode                     <-- stays the same, because it's only about the subrequest
+-main (PrimaryContent)           <--    " same here "
+-comments (Content)              <--    " same here "
  +-feed (ContentCollection)      <--    " same here " (and therefore won't render the childNode)
  +-createAction (Plugin)         <-- will be re-rendered because it's a Subrequest

Refreshing the page
+documentNode                     <-- gets re-rendered because the request affect the whole page
+-main (PrimaryContent)           <--    " same here "
+-comments (Content)              <--    " same here "
  +-feed (ContentCollection)      <--    " same here " (that's why the new childNode gets rendered)
  +-indexAction (Plugin)          <--    " same here " (shows therefore indexAction again)