RFC: Node Templates

Preface

Let’s start off with a simple example. We have an Article node with ‘main’ content collection node. Now in most cases an editor would need to have at least one Text node in that collection. So after he creates an Article, he has to make a few more clicks to create the text node. Wouldn’t it be cool if we could prefill the freshly created node with some boilerplate child nodes?

Currently we have the childNodes functionality, but it presuppose a strong coupling between parent and child nodes. It’s appropriate when child and parent nodes form an integral whole. But it can’t be used for the case of pre-filling nodes with boilerplate content, because it has two serious limitations: 1) the childNodes are fixed and permanent (you can’t move/remove) them; 2) you can’t define nested childNodes (e.g. put the Text node inside main ContentCollection node inside Article node).

Learning from past mistakes

Our previous attempt at solving this issue was great… But it was too imperative. This time we try to do the same thing in a completely different, more declarative way.

The Concept

Not to overburden the low-level concept of the node, we propose to introduce the concept of the node template. Node template represents a nested collection of nodes defined in a declarative way.

The templating would be used when a node is created from Neos UI (in REST service), populating the newly created node with child nodes.

Simple example:

'TYPO3.Neos.NodeTypes:Article':
  template:
    childNodes:
      mainContentCollection: <-- this is just some key for extensibility
        type: 'TYPO3.Neos:ContentCollection' <-- optional
        name: 'main'
        childNodes:
          helloWorldTextNode:
            type: 'TYPO3.Neos.NodeType:Text'
            properties:
              text: 'Hello world!'

When a node template is applied on node creation, the structure of the node tree will be adapted to the structure defined in the template; if a child node with the same name already exists, no second node is added.

If the node with the same name already exists but has a different type or different properties, it will be altered to match the template.

The child nodes array is named, but unnamed arrays are supported, too. It is good practice to use named arrays in packages that might be extended by integrators.

You can use EEL in all properties.

Loops and Conditions

The syntax for creating loops and conditions is inspired by Ansible. Instead of forEach and if we propose to call them withItems and when, because it inspires a more declarative mood. Ansible guys are cool and know how to make the most imperative things feel declarative :slight_smile:

The Wizard

The data context variable can be populated by a wizard that is opened on node creation and can be configured in the node type definition. Please see here for more details.

Complete example

'TYPO3.Neos.NodeTypes:Article':
  template:
    class: (optional, implements NodeTemplateInterface)
    properties:
      title: '${data.title}' ← sets the title property of a given node to data from the wizard
    childNodes:
      boilerplateChildDocument:
        type: 'TYPO3.Neos.NodeTypes:ChildDocument'
        properties:
          title: 'Boilerplate child document node'
      mainContentCollection:
        name: 'main'
        type: 'TYPO3.Neos:ContentCollection' ← optional, as the such node already exists
        childNodes:
          conditionedHelloWorldTextNode:
            type: 'TYPO3.Neos.NodeType:Text'
            properties:
              text: 'Hello world!'
            when: '${data.showHelloWorld}' ← condition example
          multipleTextNodes:
            type: 'TYPO3.Neos.NodeType:Text'
            properties:
              text: '${item + " item text"}' ← `item` points to current item in the `withItems` loop
            withItems: ← loop over array, and create a text node for each
              - 'First'
              - 'Second'
          createRowWithColumnsALaFoundation:
            type: 'TYPO3.Neos.NodeTypes:Row'
            childNodes:
              columnNode:
                type: 'TYPO3.Neos.NodeTypes:Column'
                properties:
                  class: '${'columns small-' + 12 / data.numberOfColumns}'
            repeat: '${data.numberOfColumns}' ← don’t know if this really makes sense
          multipleImageNodesCreatedFromWizardData:
            type: 'TYPO3.Neos.NodeTypes:Image'
            properties:
              asset: '${item.image}' ← sets an Image node’s asset to the data coming from the wizard
              caption: '${item.caption}'
            withItems: '${data.images}' ← loop over array of images

Implementation details

The technical basis for enriching nodes via node templates is the NodeTemplate class that implements the NodeTemplateInterface and that can handle the common configuration options (i.e. childNodes and properties). If you have more complex use cases, you can use an own class that implements the NodeTemplateInterface and configure it via the “class” option (as you can do with node types).

We propose to put this functionality to TYPO3CR package, because this functionality is generic enough to be used outside of Neos. However this functionality will not be triggered by default in CR itself. The CR would just provide the service, which would then be called from Neos’ REST controller, but can also be triggered from user controllers or commands.

~ Thomas and Dmitri

7 Likes

It’s mentioned that it wouldn’t be triggered automatically. However that could be a very common desire, when you create a document node type you’d like to automatically populate it with content from a node template. Would it make sense to allow enabling that in an easy way when using node creation events or is it better to call a service for every custom node creation?

They won’t be triggered from TYPO3CR, but they will be triggered from Neos REST controller, so these templates will only be applied when the action was triggered from the user interface, and not from CR API. E.g. when creating nodes programatically, I would not want to mess with all of the auto-generated templated nodes. But maybe we should add an optional switch to the template, which would trigger creation of Nodes everywhere. Not sure if it’s needed though.

What do you think about the feature in general?

I get the concept and that’s it’s done called in a custom way for Neos. Was thinking about node imports and how they’d use the feature. Suggesting to add a simpel way to use it when using the existing API to create nodes.

Like the feature in general, sounds solid.

Maybe we should add this switch not to nodetype configuration, but to API itself? E.g. there’s a createNodeFromTemplate (the name is confusing, doh!), maybe add on optional parameter to enable the use of templates there.

But frankly I would prefer the more simple and decoupled approach: if you need to apply the template, call the service yourself and be done with it.

Thanks for your feedback! If we get a positive feedback from more team members like @robert or @kdambekalns we could move on with implementation of this feature with @theilm, shouldn’t be too hard to implement.

I agree with @dimaip, I think applying a template should be done in a decoupled way. You could even do it programatically on an existing node, not necessarily only on node creation.

Thinking about decoupling: We discussed via Slack if the template configuration should be inside the node type configuration (and thus coupled to a specific node type) or separated. If we separate it (e.g. in a NodeTemplates.yaml file), we could define several templates for one node type.

Imagine a general document node type “Page”. You could pre-populate the page with a “Landing Page Template” or with a “Image Gallery Template”. Each of them creates different dummy content, perhaps some structure nodes etc.

Decoupling template and node type can pose problems, though, especially when applying a node template that requires a content collection that is not present in the node type. Perhaps we should not introduce too much complexity.

The general concept would not change, though. We could decide on this detail later on probably.

What do you think?

1 Like

How would you choose between different templates? Only programmatically, in own scripts?
The idea of putting template into different file, e.g. NodeTemplates.yaml has its pros and cons.
Among the cons I see the introduction of yet another concept, less cohesive design and no clear gain except for this case of multiple templates per node.

That’s not a problem at all, you define the desired state of the tree, if the node is not already there it would be created, you just have to not leave out its type.

In any case we should try to weight added complexity to value in real usecase and see if it’s worth it.

What does everyone else think?

Wording: A fixed “default content template” could be defined explicitly within the node definition. Only optionally the node definition may refer to a named content template defined say in DefaultContents.yaml. If a node has no reference to a default content definition, but there are definitions available in DefaultContents.yaml, these definitions may show up in the add content node dialog. Allowing the autor to manually add content templates and select e.g. between normal or landing page content.

Ideally, because this a authoring feature, allow authors to select content nodes from an existing node and to save them as default content definitions.

Thank you guys for taking the time to prepare this RFC!

After reading it, I had the same thought like @theilm mentioned: how about separating the Node Template configuration from the Node Type?

  • node type configuration becomes less bulky
  • you could re-use node templates in different node types
  • we might eventually (later, much later) make it possible to define node templates via the user interface

And that last point brings be to the user side: in which concrete situations would an editor like to use node templates, and which further options might we envision for the future? (we don’t have to implement them right now, but consider them at least).

Some scenarios which come to my mind:

An editor creates a new press release. There are three Node Templates (if we at some point decide to rename “node” to “content” in our general vocabulary, they’d be “Content Templates”) defined for the content type “Press Release”: “General News”, “Breaking News” and “Security Bulletin”. Neos will ask the editor which kind of press release she’d like to create and applies the respective template.

A senior editor would like to create a template for a new type of press release (“Product News”). He creates a new press release (maybe in a certain folder, at least without publishing it) and sets up placeholder content elements with some instructions about how to write a Product News press release. When he’s happy with the template, he chooses “create template from this document” in some menu and assigns this new template to the “Press Release” content type. After that, he can safely discard the dummy press release.

Would that be realistic scenarios for you? If so, what would we have to consider now, technically, in order to support these features in the future without breaking backwards compatibility?

3 Likes

Thanks @robert for your thoughts. The scenarios seem realistic to me. I like the senior editor scenario, let’s me think of @christianm’s conent strategy talk on T3CON.

Two things to consider technically:

  • Wizards on node creation need node templates to work. If you allow for different node templates for one node, you have to add the wizard configuration to the node template, not to the node type. I guess that makes sense, as they are closely related anyway. Depending on the template I choose on node creation, the corresponding wizard UI will be shown.
  • If you want to allow setting up templates via UI, you have to link the node template to the node type (not vice versa). In other words: We won’t add a new node type configuration option with the possible node templates. Instead, we would have a node template option that contains all node types this template can be applied to. The YAML configuration could then be extended with configuration stored in the database for example - or even in the CR, allowing for dimension based versions of a template.

That’s the two points I can think of right now, there are probably more. :slight_smile: What do you think, @dimaip?

Just as side note: The Adobe Experience Manager CMS (which also uses JCR) uses a similar (or actually the same) concept of handling such “Node templates”.

See here: https://docs.adobe.com/docs/en/aem/6-1/develop/components/components-basics.html#Component%20Definition

and look for the cq:template or cq:templatePath items in the table. With the cq:template node you can pre-define such content template or with the cq:templatePath you also can reference some already existing content template node.

2 Likes

Hi all!
I wanted to invigorate this discussion, a year later…
During this year, I have taken the time to think it through, and actually almost daily I would hear a use case for this feature.

In the new UI, we want to implement the node creation wizard right from the start, and it would obviously require node templates to work.

It’s way too early to even think about it now before we have at least a concept for editable nodetype definitions.
However, it would be possible to create this feature already now, with a custom template implementation class. But I don’t see it as a core use case for now, since integrators have to define nodetypes via code, and editors have no influence over it.

I don’t mind extracting the templates themselves into an external file. It has two pros: ability to reuse templates across node type and decluttering NodeTypes.yaml.
But clearly the nodetype definition should be the initiator here, it would declare which template and implementation to use.

NodeTypes.yaml:

'TYPO3.Neos.NodeTypes:Article':
  template: 'TYPO3.Neos.NodeTypes:PressRelease'

And in NodeTemplates.yaml:

'TYPO3.Neos.NodeTypes:PressRelease':
  class: (optional, implements NodeTemplateInterface)
  childNodes:
    mainContentCollection: <-- this is just some key for extensibility
      type: 'TYPO3.Neos:ContentCollection' <-- optional
      name: 'main'
      childNodes:
        helloWorldTextNode:
          type: 'TYPO3.Neos.NodeType:Text'
          properties:
            text: 'Hello world!'

OR, a more low-level alternative, similar to old PR:

'TYPO3.Neos.NodeTypes:Article':
  template:
    class: (optional, implements NodeTemplateInterface)
    settings:
      name: 'TYPO3.Neos.NodeTypes:PressRelease'
      someOtherSetting: 'could be used in custom implementation class'

And in NodeTemplates.yaml:

'TYPO3.Neos.NodeTypes:PressRelease':
  childNodes:
    mainContentCollection: <-- this is just some key for extensibility
      type: 'TYPO3.Neos:ContentCollection' <-- optional
      name: 'main'
      childNodes:
        helloWorldTextNode:
          type: 'TYPO3.Neos.NodeType:Text'
          properties:
            text: 'Hello world!'

I like the first, higher-level approach better.
What do you guys think?

1 Like

I somehow would prefer to use a structure inside of the NodeTypes configuration.

  • Iif we want to share a template between different NodeTypes we have mixins and superTypes
  • If we want to write templates separately from other node definitions we can do that by using multiple NodeType.*.yaml files
  • If we want to define NodeTypes via ui or code at some point one mechanism works for nodes and templates

Things I would adjust:

  • Templates should be a dictionary to allow multiple templates per nodeType and to allow overriding
  • Instead of defining a template class how about configuring a service that takes the created node and uses the template configuration to customize it.
  • There should be an option to hide the blank node from the wizard and only use the templates … maybe an empty ‘default’ template in the very basic NodeTypes that can be set to null.

In general i like the template-idea very much.

1 Like

I like the idea of splitting NodeTypes and templates. NodeTypes are overboarding already, I think this will finally move them into unreadable after a year. I haven’t looked into the practical part of this, butI guess it’s very well doable to configure templates separately and just reference them if a connection is needed between NodeTypes and Templates.

I think my point was not clear since i discussed it a while with @dimaip via slack. I hope that i can explain it better now.

My suggestion would be to make the AddNode dialog template-based but keep the current default behavior by adding an empty default-Template for the TYPO3.Neos:Node superType.

#
# define the basic template
#
'TYPO3.Neos:Node':
  templates:
    default:
      class: \TYPO3\Neos\Service\NodeTemplateService
      options: 
        properties: []
        childNodes: []

Below i sketched three use cases i see for that.

#
# add e second item fancy teaser to the addNode dialog
#
'Vendor.Site:Content.Teaser':
  superTypes:
    TYPO3.Neos:Content: true
  templates:
    # default: the default template is inherited
    fancyTeaser:
      class: \TYPO3\Neos\Service\NodeTemplateService
      icon: fancy icon
      label: fancy teaser
      options:
        properties:
          fancy: true
  properties:
    fancy:
      type: boolean
      default: false

#
# add a default slide to the slider node
#
'Vendor.Site:Content.Slider':
  superTypes:
    TYPO3.Neos:ContentCollection: true
    TYPO3.Neos:Content: true
  templates:
    default:
      options:
        childNodes:
          firstSlide:
            type: 'Vendor.Site:Content.Slide'
            properties:
              text: 'Hello world!'    
  constraints:
    childNodes:
	  nodeTypes:
	    'Vendor.Site:Content.Slide': true
	
#
# add a headline to all new documents
#
'Vendor.Site:Document.Page':
  superTypes:
    TYPO3.Neos:Page: true
  templates:
    default:
      options:
        childNodes:
          main:
            firstSlide:
              type: 'TYPO3.NeosNodeTypes:Headline'
              properties:
                text: '<h1>Headline!</h1>'    

For easy use cases this is imho easy enogh to not bloat the nodeTypes. In complex projects i think will have deal with complex nodeTypes. I see no real benefit in adding another configuration type that is so closely related to NodeTypes.

Separate but related topics that are imho not part of this topic are:

  • Optimize the ui of the AddNodeDialog since currently there is much scrolling and the open/close of groups is easyly overlooked
  • Add a second wizard step after selecting a node+template to define initial values

Just to make it clear i know that this are big changes and i am not sure about that at all. The advantage would be that the overall concept stays quite simple.

We have had a LONG discussion with @mficzel.

What we propose:

  • Allow NodeController->createAction to receive another argument with an array of data coming from the to-be implemented node creation wizard (will be implemented in the new UI). This data would be passed to NodeOperations->create and from there a signal would be emitted, smth like “afterNodeCreateFromUi” (naming, duh!), with that data array passed along.
  • Implement Node Templates as an external Flowpack package, listening to afterNodeCreateFromUi signal. The topics brought up in this thread would be discussed in a separate thread, relating to the implementation of that package.

Why? This would allow alternative concepts for processing wizard data to be implemented as 3rd-party packages, without polluting the core.

What do you think?

2 Likes

Sounds great, I am happy to assist thinking through that in detail.
Any feature in separate packages makes me happy :slight_smile:

1 Like

Great proposition @dimaip and @mficzel! Let’s move the complex stuff to a separate package!

Just released a first version of the NodeTemplates package. Comments welcome!
https://packagist.org/packages/flowpack/nodetemplates

1 Like