Best Practice to wrap ContentCollectionItems with markup

Hi everybody,

I’ve made my own ContentCollection.yaml element:

'Vendor.SitePackage:myContentCollection':
  superTypes:
    'Neos.Neos:ContentCollection': true
  ui:
    label: 'Hauptinhalt'

This one will replace the default childnode main on some Neos.Neos:Dokument .yaml:

'VENDOR.SitePackage:Page':
  superTypes:
    'Neos.Neos:Document': true
  childNodes:
    main:
      type: 'VENDOR.SitePackage:myContentCollection'

Now if I use myContentCollection I would like to wrap every item in that collection with an additional
< section >myItem< /section >

Currently the only possibility I found so far is to enhance every NodeType’s fusion this way:

prototype(Neos.NodeTypes:Text) {
  // Section stuff
  @context.myParent = ${q(node).parent().get(0).nodeData.nodeType.name}
  presentAsSection = ${myParent == 'VENDOR.SitePackage:myContentCollection' ? true : false }
  sectionAttributes = ${q(node).property('bgColor') == true ? 'bgColorGray' : ''}
}

and enhance all templates like this:

<f:if condition="{presentAsSection}">
    <f:then>
        <section{f:if(condition:sectionAttributes, then:' class="{sectionAttributes}"')}>
            <f:render section="content" arguments="{_all}"/>
        </section>
    </f:then>
    <f:else>
        <f:render section="content" arguments="{_all}"/>
    </f:else>
</f:if>

<f:section name="content">
    <div>
        <div{attributes -> f:format.raw()}>
            {neos:contentElement.editable(property: 'text')}
        </div>
    </div>
</f:section>

This does the job. But is there a possibility to do it in only one place for every item no mather which NodeType it is? And thereby respect the settings the user does for bgColor.

Since NEOS is built up that extremly object orientated, I believe there is a way to get this working somehow, but I just can’t find it.

Any idea?

Hi Erich

We do it with a set of configuration, fusion objects and an eel helper. Maybe this can be an inspiration for you. Just pick out the parts that fits your needs or the whole thing. I shortened it a bit because we use some fusion object in between that i just left out. So the code is not tested like it is, but the concept works more that 50 Neos websites already:

Our nodetypes fusion objects are all based on Neos.Neos:ContentComponent:

prototype(Vendor.Package:Content.Foo) < prototype(Neos.Neos:ContentComponent) {
  ...
  renderer = afx`...`
   # or you prefer fluid templates to render you nodes: renderer = Neos.Fusion:Template
}

We “enhanced” that prototype like this to let it wrap a div with class “container” around each content element:

prototype(Neos.Neos:ContentComponent) {
  renderer.@process.wrapContentElement = Neos.Fusion:Tag {
    tagName = 'div'
    attributes.class = 'container'
    content = ${value}

    @if.shallWrapContainer = ${Internezzo.Neos.ContentCollection.shallWrapContainer(node)}
  }
}  

With the eel-helper in the if-condition we control which elements are getting wrapped:

<?php
namespace Internezzo\Neos\Eel\Helper;

use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\ProtectedContextAwareInterface;
use Neos\Flow\Annotations as Flow;

class ContentCollectionHelper implements ProtectedContextAwareInterface
{
    /**
     * Return true if the current node shall be wrapped with a container
     *
     * @param NodeInterface $node
     * @return string
     */
    public function shallWrapContainer(NodeInterface $node)
    {
        $contentCollection = (new FlowQuery(array($node)))->parent('[instanceof Neos.Neos:ContentCollection]')->get(0);
        if (!$contentCollection instanceof NodeInterface) {
            return false;
        }
        $collectionNodeName = $contentCollection->getName();
        /** @var NodeInterface $contentCollectionParent */
        $contentCollectionParent = (new FlowQuery(array($contentCollection)))->parent()->get(0);
        $configuration = array_change_key_case($contentCollectionParent->getNodeType()->getFullConfiguration()['childNodes'], CASE_LOWER)[$collectionNodeName];
        if (!array_key_exists('options', $configuration)) {
            return false;
        }
        if (!array_key_exists('wrapContainer', $configuration['options'])) {
            return false;
        }
        return  $configuration['options']['wrapContainer'];
    }

    /**
     * All methods are considered safe
     *
     * @param string $methodName
     * @return boolean
     */
    public function allowsCallOfMethod($methodName)
    {
        return true;
    }
}

Final part is that we add an option “wrapContainer” to all ContentCollections where we want the wrapping:

'Vendor.Package:Document.Foo':  
  childNodes:
    main:
      type: 'Neos.Neos:ContentCollection'
      options:
        wrapContainer: true

You might also use an Augmenter like this https://fusionpen.punkt.de/fusionpen/743f0a87b223c04babe43a677822602b9a2a4df7.html

Hey,

great!!!

Both solutions gave me the hints that I needed. Peters solution takes the main part:
I added a custom eel helper as explained in here: https://docs.neos.io/cms/manual/extending-neos-with-php-flow/custom-eel-helpers with minor adaptions to Peters helper. Did the adaption in the yaml files to add the ‘wrapContainer: true’. But in my case I used this adaption for my fusion code:

prototype(Neos.Neos:Content) {
    attributes.class = ${q(node).property('indentIt') == true ? 'contentIndent' : ''}

    // Section stuff
    @process.wrapContentElement = Neos.Fusion:Tag {
        tagName = 'section'
        attributes.class = ${q(node).property('bgColor') == true ? 'bgColorGray' : ''}
        content = ${value}

        @if.wrapWithSection = ${Vendor.site.CollectionHelper.wrapWithSection(node)}
    }
}

Thank you very much for inspiring me.

And thanks Sebastian for your hint. This I can use in another project where I’ve never thought about the augmenter.

1 Like