[SOLVED] Simple content doesn't save


(George Dimitriadis) #1

Hello,

I am still going through the basics, I am now a bit familiar with rendering Neos.Neos:ContentCollection data in my project and I’m trying to get things simpler.

So, instead of having a Neos.Neos:ContentCollection, I am trying to render a simple text field of type Neos.NodeTypes:Text. So far I have managed to have the field in the page, but any changes done to it cannot be saved.

Here’s my setup:

NodeTypes.yaml

 'Neos.NodeTypes:Page':
   childNodes:
     'text':
       type: 'Neos.NodeTypes:Text'

Root.fusion

page = Neos.Neos:Page {
    body {
        content {
            text = Neos.NodeTypes:Text {
                nodePath = 'text'
            }
        }
    }
 }

template.html

{namespace neos=Neos\Neos\ViewHelpers}

    {content.text -> f:format.raw()}

The code above is minimal, cut off from the full setup. So, what happens now is when I type anything in the text box, I am getting “Saving …” message at the top right and then nothing, everything looks as if I did no changes and I can’t Apply changes. If I reload, the text field is empty again.

What am I doing wrong ?

Thanks in advance for any help !


(Benjamin Klix) #2

Hello @dimitriadisg,

the problem is, that Neos doesn’t get the right Node on which it should save the contents to. All content elements need to be wrapped so that changes are updated correctly. If you only add a childnode the way you did, you add it with it’s default content but changes are stored to the parent node.

To fix this you could do one of the following:

page = Neos.Neos:Page {
    body {
        content {
            collection = Neos.Fusion:Collection {
                collection = ${q(node).children('text')}
                itemName = 'node' # do not use an other name here as this is used for wrapping. See second example.
                itemRenderer = Neos.Neos:ContentCase # you can also use Neos.NodeTypes:Text in your case
            }
        }
    }
}

or

page = Neos.Neos:Page {
    body {
        content {
            text = Neos.NodeTypes:Text {
                @context.node = ${q(node).children('text').get(0)}
                node = ${node}
            }
        }
    }
}

and then output it as you already did.

To get content/nodes editable, you need the Neos.Neos:ContentElementWrapping defined in Neos.Neos:Content (if you do not define it yourself). Please ask again, if you have any further questions.


(George Dimitriadis) #3

Hello @BenjaminK,

thanks for your helpful reply, I am about to try it now. The second solution looks simpler and to the point, is there a best practice as to which I should choose the first solution ?

  1. If I get it right, using the first solution would end up with less code if I had more fields other than “text”, which is going to be the case probably, but using the second solution would give me many lines of copy-paste, is that correct ?

  2. Something irrelevant, how do you markup the code like that ? I can’t find an option in the editor.

  3. Also, could you explain to me or point me to a documentation as to what @context syntax does ?

Thanks a lot once again !

EDIT:
4. I tried both solutions, but when I do edits there is a problem. The changes are saved properly, but the page preview in the middle doesn’t update when I reload the page on the browser. The front-end loads the new value properly though. The backend page preview will update only when I do some change in the template, how can I fix this functionality to always update the preview ?


(Benjamin Klix) #4

Hey @dimitriadisg,

To 1.:
I always use the first example as i think it is more flexible. You can do things like collection = ${q(node).children('image,text')} with it and then simply switch the order if needed by passing in the order you want ('text,image' here to first output the text). I don’t know why you would need many lines of copy-paste in the second example, but as said i’d prefer the first.

To 2.:
You could have a look at Neos.Neos:ContentElementWrapping in Fusion and the corresponding class \Neos\Neos\Fusion\ContentElementWrappingImplementation. All NodeTypes, that inherit from Neos.Neos:Content use this for wrapping (see Neos.Neos:Content (@process.contentElementWrapping)).
Alternatively you could also use the neos:contentElement.wrap-ViewHelper to wrap parts of your template to be wrapped for another node.

To 3.:
Variables prefixed with @context. are added to the current fusion context. That means, that you access them also an all fusion childs until they get overridden. Further information on the docs can be found here: http://neos.readthedocs.io/en/stable/CreatingASite/Fusion/InsideFusion.html#default-context-variables

For 4.:
Could you add some more of your code so i can check?

Edit:
Sounds like you have some caching issues. Try to adjust your @cache data in your fusion to correctly cache the values. See http://neos.readthedocs.io/en/stable/CreatingASite/ContentCache.html?highlight=%40cache for further information


(George Dimitriadis) #5

Thanks for your information @BenjaminK !

About point 2, I’m sorry what I meant to ask is “how to format the text as code here in the forum ?” .

I turned the cache to uncached, with this example code from the reference link you sent me

root {
    @cache {
        mode = 'uncached'
        context {
            1 = 'node'
            2 = 'documentNode'
        }
    }
}

However, I believe there may be a better way, because probably I will need cache on production. So, do you know if I can flush the cache upon saving ? The ContentCollection does it, so I should be able to do it too, right ?

I tested the same code in neos.demo as well, and I’m having the same problem.

Also, something interesting is that I am getting the uncached, correct data in my inline-text field if I do changes in a ContenctCollection element as well, that’s how I figured that the ContentCollection flushes the cache.

Thanks once again in advance !

EDIT:
About point 4, well, not much I can add more, the only change I did was to apply your code. Here it is:
Root.fusion

page = Neos.Neos:Page {
    // ....
    body {
        templatePath = 'resource://path/to/my/templates/Default.html'
        // ...

        content {
                // ....
                text = Neos.Fusion:Collection {
                    collection = ${q(node).children('text')}
                    itemName = 'node'
                    itemRenderer = Neos.NodeTypes:Text
                }
            }
            // .....
        }
}

Default.html

{namespace neos=Neos\Neos\ViewHelpers}

    
    {content.text -> f:format.raw()}
     

NodeTypes.yaml

'Neos.NodeTypes:Page':
  childNodes:
    'text':
      type: 'Neos.NodeTypes:Text'
    # .....

(George Dimitriadis) #6

Ha, I found it !
I was right, ContentCollection uses this cache configuration (which unfortunately I don’t understand what it does) :

@cache {
     mode = 'cached'

     entryIdentifier {
             collection = ${node}
         }

         entryTags {
              1 = ${'DescendantOf_' + node.identifier}
              2 = ${'Node_' + node.identifier}
         }

         maximumLifetime = ${q(node).context({'invisibleContentShown': true}).children().cacheLifetime()}
}

And adding that into my text declaration did the trick. So, I guess it’s a good idea for me to extend Neos.Fusion:Collection to an object with this cache configuration and use this instead where needed.

Thanks for your help once again !


(George Dimitriadis) #7

Hi again @BenjaminK,

I keep getting stuck on minor issues. I am trying to make a prototype out of your example code, is there a way to automatically fetch the type of the child node in question and tell the itemRenderer to use that ?
This is my prototype so far:

prototype(Vendor.Site:Content) < prototype(Neos.Fusion:Collection) {
    itemName = 'node'
    nodePath = '-filled-by-the-user'
    collection = ${q(node).children(this.nodePath)}
    @cache {
        mode = 'cached'

        entryIdentifier {
                collection = ${node}
            }

            entryTags {
                1 = ${'DescendantOf_' + node.identifier}
                2 = ${'Node_' + node.identifier}
            }

            maximumLifetime = ${q(node).context({'invisibleContentShown': true}).children().cacheLifetime()}
        }
}

Which is then used like this:

            text = Vendor.Site:Content {
                nodePath = 'text'
                itemRenderer = Neos.NodeTypes:Text
            }

Now, I would like to skip that itemRenderer definition and let my prototype do it for me, so I tried this:

itemRenderer = collection.type

but of course I am getting an error, I didn’t expect it to be that easy. So, what’s the way to do it ? The collection already points to the child node, so I should be able to get its type.

Thanks in advance once again !

PS: This lack of code styling is killing me, how do you style your code in this forum ?


(Benjamin Klix) #8

Hi @dimitriadisg,

to use the renderer for the current rendered NodeType (of the child nodes) you could simply use

itemRenderer = Neos.Neos:ContentCase

which will then use the prototype of the current element (for example Neos.NodeTypes:Text) to render the element.

For little adjustments you could also do sth. like this:

itemRenderer = Neos.Neos:ContentCase {
    custom {
        @position = 'start'
        condition = ${'your-condition-here'}
        # Use a custom "Preview" Renderer for the selected Nodes/NodeTypes
        type = ${q(node).property('_nodeType.name') + 'Preview'}
    }
    # And this is the default case that is used to render the current nodetype
    # of the element (already declared for Neos.Neos:ContentCase):
    default {
        @position = 'end'
        condition = TRUE
        type = ${q(node).property('_nodeType.name')}
    }
} 

# Now you can use custom templates inside this collection
# with new prototypes like this: (note the "Preview" as declared above)
prototype(Neos.NodeTypes:TextPreview) < prototype(Neos.NodeTypes:Text) {
    templatePath = 'resource://Your.Custom/Preview/Path.html'
}

If you have questions on this, just ask again :wink:

PS: To style your code here leave a blank line before and after your code and then start every line of code with 4 spaces.


(George Dimitriadis) #9

Hi @BenjaminK,

ah yes, I can use ContentCase of course, that did the trick.

Thanks for the adjustments, I get the idea, but taking this behaviour one step further got me stuck again.

Now, while all this configuration for a ‘Text’ is clear, I would like to return to my more general problem, like I mentioned in my first post :

I am still going through the basics, I am now a bit familiar with rendering Neos.Neos:ContentCollection data in my project and I’m trying to get things simpler.

So, instead of having a Neos.Neos:ContentCollection, I am trying to render a simple text field of type Neos.NodeTypes:Text.

So, we managed to get the simple Text to be loaded without being wrapped in a ContentCollection, but what happens with all the other fields that a ContentCollection can render ? I don’t see how I can apply this solution with a menu for example, or even my custom prototype that I had made to be able to render a block of multiple text fields.

Specifically, in order to render a custom content block, I have the following structure:

# NodeTypes.yaml
'Neos.NodeTypes:Page':
  childNodes:
    'heroText':
      type: 'Neos.Neos:ContentCollection'
      constraints:
        nodeTypes:
          '*': FALSE
          'Vendor.Site:HeroText': TRUE

'Vendor.Site:HeroText':
  superTypes:
    'Neos.Neos:Content': TRUE
  ui:
    label: 'Hero Text'
    group: 'general'
    inspector:
      groups:
        image:
          label: 'Image'
          icon: 'icon-image'
          position: 1
  properties:
    headline:
      type: string
      defaultValue: 'Enter headline'
      ui:
        label: 'Headline'
        inlineEditable: TRUE
    subheadline:
      type: string
      defaultValue: 'Enter subheadline'
      ui:
        label: 'Subheadline'
        inlineEditable: TRUE
    text:
      type: string
      ui:
        label: 'Text'
        reloadIfChanged: TRUE

Then in fusion

// HeroText.fusion
prototype(Vendor.Site:HeroText) < prototype(Neos.Neos:Content) {
    templatePath = 'resource://Vendor.Site/Private/Templates/FusionObjects/HeroText.html'

    headline = ${q(node).property('headline')}
    subheadline = ${q(node).property('subheadline')}
    text = ${q(node).property('text')}
}

// Root.fusion
page = Neos.Neos:Page {
    body {
        content {
            heroText = Neos.Neos:ContentCollection {
                nodePath = 'heroText'
            }
        }
    }
}

Then I render it properly in my .html files, I believe you get the idea. Everything works well with this approach, but like I said, I want to get rid of the dynamic rendering of contentCollection. I want the field to be there by default and editable right away. But, what I have now is that the user needs to click the “Add” button on the contentCollection tool and select the HeroText and (s)he can also add more than one of these blocks. This is the reason I want to get rid of the default behaviour, but I want to still have consistent configuration options when it comes to text, menu, or custom prototype.

As always, I am grateful for your help !


(Benjamin Klix) #10

Dear @dimitriadisg,

if you use a ContentCollection as a childNode, the editor/user can add as many items as he wants to, so a ContentCollection is always dynamic (you can limit the output if you want, but that’s not the problem here).

To add ChildNodes that are always there and can not be removed or to avoid the creation of further NodeTypes you need to add the ChildNode like in the following example. I will add two childs to demonstrate how to handle it, but for one, this is almost the same:

'Neos.NodeTypes:Page':
  childNodes:
    'heroText':
      type: 'Vendor.Site:HeroText' # This one is important! No collection here.
    'anotherNode':
      type: 'Some.Other:NodeType'

Then in your fusion file keep your Vendor.Site:HeroText as is and adjust your Page like so:

page = Neos.Neos:Page {
    body {
        content {
            fixedChildren = Neos.Fusion:Collection {
                # Order of the name in .children() defines the order of the output
                collection = ${q(node).children('heroText,anotherNode')}
                itemName = 'node'
                itemRenderer = Neos.Neos:ContentCase
            }
        }
    }
}

Then in your template simply use

{content.fixedChildren -> f:format.raw()}

(George Dimitriadis) #11

Hi again @BenjaminK,

Yes, I figured that would be the way to do it, but this code just shows me an empty line where I can’t do anything (so, not working).

Here’s my current structure based on your suggestions:

# NodeTypes.yaml
'Neos.NodeTypes:Page':
  childNodes:
   'heroText':
      type: 'Vendor.Site:HeroText'
'Vendor.Site:HeroText':
  superTypes:
    'Neos.Neos:Content': TRUE
  ui:
    label: 'Hero Text'
    group: 'general'
    inspector:
      groups:
        image:
          label: 'Image'
          icon: 'icon-image'
          position: 1
  properties:
    headline:
      type: string
      defaultValue: 'Enter headline'
      ui:
        label: 'Headline'
        inlineEditable: TRUE
    subheadline:
      type: string
      defaultValue: 'Enter subheadline'
      ui:
        label: 'Subheadline'
        inlineEditable: TRUE
    text:
      type: string
      ui:
        label: 'Text'
        reloadIfChanged: TRUE

Fusion:

// HeroText.fusion
prototype(Vendor.Site:HeroText) < prototype(Neos.Neos:Content) {
    templatePath = 'resource://Vendor.Site/Private/Templates/FusionObjects/HeroText.html'

    headline = ${q(node).property('headline')}
    subheadline = ${q(node).property('subheadline')}
    text = ${q(node).property('text')}
}

// Root.fusion
page = Neos.Neos:Page {
    body {
        content {
            heroText = Neos.Fusion:Collection {
                collection = ${q(node).children('heroText')}
                itemName = 'node'
                itemRenderer = Neos.Neos:ContentCase
            }
        }
    }
}

Default.html

<f:section name="body">
    {content.heroText -> f:format.raw()}
</f:section>

Vendor.Site/Private/Templates/FusionObjects/HeroText.html

{namespace neos=Neos\Neos\ViewHelpers}
<h2 class="title">
    {neos:contentElement.editable(property: 'headline', tag: 'p', class: 'top')}
    {neos:contentElement.editable(property: 'subheadline', tag: 'p', class: 'bottom')}
</h2>
{neos:contentElement.editable(property: 'text', tag: 'p', class: 'subtitle')}

Am I doing something wrong ? Perhaps the way I render them in my html template ?


(Benjamin Klix) #12

I’m not really sure. I think neos adds an additional div arround all elements on which it adds node specific data (if there isn’t a surrounding html tag already).
What i can say is that a p tag inside a headline won’t work. Try using “small” or sth. else here. And then i would add a surrounding div instead of letting neos add it, just to be sure.
If this doesn’t help i need to test the code by myself. Don’t know what’s wrong here just by reading it.


(George Dimitriadis) #13

Hi @BenjaminK,

Changing p to span or small didn’t help. Though, I don’t know how to surround the fields myself, using neos:contentElement.editable was the only way I could find to render the fields in live-edit mode.

With the contentCollection method I had in 9th post I could properly see the three text fields that the heroText loads properly.

Thanks for bearing with me !


(Benjamin Klix) #14

Could you try this in your Root.fusion:

page = Neos.Neos:Page {
    body {
        content {
            # Change heroText to a tag for extra wrapping
            heroText = Neos.Fusion:Tag {
                tagName = 'div'

                # Output the content inside the tag
                content = Neos.Fusion:Collection {
                    collection = ${q(node).children('heroText')}
                    itemName = 'node'
                    itemRenderer = Neos.Neos:ContentCase
                }

                # Add additional wrapping to the tag
                @process.contentElementWrapping = Neos.Neos:ContentElementWrapping
            }
        }
    }
}

I’m not really sure, but it could be the case that the content is not correctly wrapped as it is a direct child of a page. If i find an example i will post you further information on this.


(George Dimitriadis) #15

Hi @BenjaminK,

Nothing changes with this edit unfortunately, but I got some good feedback: I tried the same structure (as in 11th post) in a neos.demo site and it works perfectly as I’d want it in my site. So, I guess I am missing something in my own build.

Weird thing is, on the neos.demo site the heroText displays as custom input, but on my site it displays as ContentCollection under the “Structure” sidebar.

I’ll dig around to see what I’m doing wrong. But in the meanwhile, is there anything else I should run when editing fusion / yaml files ? I figured I have to run flow node:repair when changing the nodeTypes.yaml, is there anything similar thing that could be reading old type from heroText and wrongly treats it as a contentCollection ? Or maybe I am missing something from Neos.Demo ? I don’t use Neos.Demo on my own site.

UPDATE: Woohoo !! I got it working after a cache flush, damn, some of these caches shouldn’t be there in development environment … Anyway, thanks a lot once again @BenjaminK, I’ll now try to implement this with my custom content prototype.

UPDATE 2: Nice, works like a charm ! So, with my prototype now looking like this :

prototype(Vendor.Site:Content) < prototype(Neos.Fusion:Collection) {
    itemName = 'node'
    nodePath = '-filled-by-the-user'
    collection = ${q(node).children(this.nodePath)}
    itemRenderer = Neos.Neos:ContentCase

    @cache {
        mode = 'cached'

        entryIdentifier {
            collection = ${node}
        }

        entryTags {
            1 = ${'DescendantOf_' + node.identifier}
            2 = ${'Node_' + node.identifier}
        }

        maximumLifetime = ${q(node).context({'invisibleContentShown': true}).children().cacheLifetime()}
    }
}

I can render content as well as custom content (originating from Neos.Neos:Content) like my HeroText, like so:

heroText = Vendor.Site:Content {
    nodePath = 'heroText'
}

Maybe later I’ll look for a way to make this even more magic and load the variable name directly from the nodeTypes.yaml so that I don’t have to define all children nodes both in yaml and fusion, but I’ll leave it like this for now until I’m more familiar with the best practices :slight_smile:

Cheers, I guess I can mark this as solved (eh, no idea how to do that though, just edit the title ?) !


Overriding the default value of childNodes
(George Dimitriadis) #16

Hello @BenjaminK,

sorry for bothering you again with this raw-content rendering thing, I hope you can help me out once again.

The problem now is that wrapping the element with a contentCollection throws away my ability to customize it directly. Specifically, I need to change the templatePath for an instance of the same contentElement, but the content is rendered through the collection, so I am a bit confused on how I can change the templatePath.

In this case, I have one menu that I want to render twice on my page. In the example below, I am using mainMenu and footerMenu which actually load the same childNode:

Root.fusion

mainMenu = Vendor.Site:Content {
    nodePath = ${q(site).find('mainMenu').property('_path')}
}

footerMenu = Vendor.Site:Content {
    nodePath = ${q(site).find('mainMenu').property('_path')}
}

Now, the next step is to tell fusion that I want a different template for the mainMenu, but Vendor.Site:Content is actually a collection that renders my element, so specifying a templatePath there doesn’t work. A dirty way I figured out to do it is like this (I didn’t actually expect this to work, but it does haha) :

mainMenu = Vendor.Site:Content {
    prototype(Neos.NodeTypes:Menu) {
        templatePath = 'resource://Vendor.Site/Private/Templates/FusionObjects/Menu.html'
    }
    nodePath = ${q(site).find('mainMenu').property('_path')}
}

What I would like to do instead of that dirty solution is something like this:

mainMenu = Vendor.Site:Content {
    templatePath = 'resource://Vendor.Site/Private/Templates/FusionObjects/Menu.html'
    nodePath = ${q(site).find('mainMenu').property('_path')}
}

// This is actually pseudocode, not a working example
prototype(Vendor.Site:Content) < prototype(Neos.Fusion:Collection) {
    // ...
    collection = ${q(node).children(this.nodePath)}
    // Here I need to tell fusion that the content in the loaded collection
    // to override the templatePath if specified
    // ... cache configuration etc here
}

Do you have any tips on how I can make this work the right way ? Also, it would be nice to make this work for all kind of contents, not just the Menu type, otherwise it doesn’t make sense to add it in my Content prototype.

Thanks a lot in advance !


(Benjamin Klix) #17

Hey @dimitriadisg,

sorry for the late reply. Thought i already answered.

Using nested prototypes is just fine and is also the way you should change properties if you need to.
So whenever you need to change a child node of a content type simply use it as you already did:

prototype(Vendor.Prefix:ChildContent) {
    childVar = '123'
}

prototype(Vendor.Prefix:ChildContent2) {
    child2Var = '123_2'
}

prototype(Vendor.Prefix:Content) {
    prototype(Vendor.Prefix:ChildContent2) {
        child2Var = '456_2'
    }
}

content = Vendor.Prefix:Content {
    contentVar = 'abc'
    
    prototype(Vendor.Prefix:ChildContent) {
        // Change properties only when inside Content
        childVar = '456
    }
}

(George Dimitriadis) #18

Hello @BenjaminK,

That’s a bit unfortunate, 'cause the nesting gets override conflicts if it’s too deep, similar to how CSS styles get conflicted from multiple rules applied on the same element.

So, I guess I will have to find a way to avoid such nesting.

Thanks for your answer !