An updated version of this great content can be found in the Neos Docs.
When we (Wilhelm Behncke and Martin Ficzel) started the development of the Sitegeist.Monocle style guide for Neos, we also started evaluating our development processes. We wanted to identify the points of friction and in the end, come up with a better solution for the cooperation of the different web development crafts.
The result of that process, which took us about a year, is the package and the patterns we now call Atomic.Fusion. We believe that this approach can help to structure the web development better, write reusable components, be able to incorporate changes gracefully and of course, have even more fun doing projects with Neos.
The four core principles of Atomic.Fusion we want to explain in the following text are:
- Separation: Strict separation of rendering on one side and data acquisition plus transformation on the other into separate Fusion components which we call mapping components and presentational components.
- Isolation: Presentational components rely solely on the props they are given and are side-effect free.
- Aggregation: Complex presentational components are aggregates of simple presentational components and pass down the props.
- Colocation: Components are collocated with all the CSS and JS code they rely on.
Separation and Isolation of concerns
During web development, we are usually facing either the issue that frontend department already starts working before any backend or editing experience is defined. In other projects backend department is creating the logic first and will output some dummy HTML that later is given to the frontend department for beautification.
This is not generally wrong since someone has to be the first to touch a project but often problems occur if later changes have to be incorporated. In that case it has to be decided which craft is responsible for that change. The reason why this often is hard to decide only exists is because the currently used Fluid templates are mixing various concerns of both crafts:
- HTML structure (frontend)
- Layout (frontend)
- Logic (backend)
- Data acquisition: e.g. fetching of menu-items or content (backend)
- Data transformation: e.g. creating of links and scaling/cropping images (backend)
- Defining the editing experience (backend)
With all those concerns mixed into a single piece of code, it is understandable that in many cases the frontend developers will not be able to make even simple changes after backend department integrated their code and thus the whole template finally is maintained by the backend department.
Atomic.Fusion suggests separating those concerns into components that belong to a single development craft. This allows both crafts to progress and to excel independently. That removes unneeded friction from the development process especially during feedback iterations and raises the appreciation of the other crafts.
Presentational components
A presentational component only cares about rendering and makes no assumptions about where the data originates from, what parts will be inline editable or how URIs are generated. Instead, it defines an explicit API via its fusion properties and uses the data the it receives via this API as props inside the renderer.
prototype(Vendor.Site:Component) < prototype(PackageFactory.AtomicFusion:Component) {
# api
title = ''
description = ''
imageUri = null
# renderer
renderer = afx`
<div class="component">
<img src={props.imageUri} alt={props.title} @if.hasImage={props.imageUri ? true : false}/>
<h1>{props.title}</h1>
<p>{props.description}</p>
</div>
`
}
Hint: In this and the following examples you will see us use the AFX`...` domain-specific fusion language which is a shorthand Fusion syntax that is optimized for presentational components. You can find a more detailed explanation in the AFX documentation.
Mapping components
The mapping components are responsible to acquire data and to transform it to fit the needs of the used presentational components API. Mapping components do not make any layout decisions. By passing editables or rendered collections to the presentational components the mapping also defines the editing experience for the editors without modifying the presentational components.
prototype(Vendor.Site:ExampleContent) < prototype(PackageFactory.AtomicFusion:Content) {
renderer = Vendor.Site:Component {
title = PackageFactory.AtomicFusion:Editable {
property = 'title'
}
description = PackageFactory.AtomicFusion:Editable {
property = 'description'
}
imageUri = Neos.Neos:ImageUri {
asset = ${q(node).property('image')}
}
}
}
The distinction between presentational and mapping components has some very unique benefits:
- During development of a presentational component, no assumptions have to be made where the data comes from and what will be editable.
- The same presentational component can be used for editable and non-editable content and different data sources.
- Presentational components have no side effects and can be reliably tested.
Component Aggregation
Another important problem we identified was the lack of reusability on CSS and JS level. Currently often the same JS code and CSS classes are used in multiple places of a project and over time it becomes nearly impossible to change this code without running into unexpected side effects. This finally leads to projects where only new code is added. Over time the code base degenerates more and more until even small changes become problematic.
Presentational components can solve this by moving the reusability from CSS and JS up to the Fusion component. Classes and scripts are used only inside of a single presentational component and are considered an internal implementation detail and not part of the component api. This makes it way easier to implement and to test changes.
Everyday examples for that are slider-components or grid-components that will exist only once inside a project. Other components will not use the classes or scripts from in there. Instead they reuse the whole component via API. That way the component can safely change its internal structure, styles, and scripts. As long as the external component API stays the same no other code has to be adjusted.
Resource Colocation
We experienced that it is very helpful to store the Fusion components together with the CSS and JS code that they rely on. That way we can easily change it and remove obsolete parts without fearing side effects since we know that no other parts of our codebase is using those classes but the Fusion component that we changed and tested.
By colocating presentational components with their resources it becomes very easy to transfer such components between projects or to remove them in total once they become obsolete.
Another important benefit of that approach is that the frontend developers get sole control of the Fusion components folder and have everything they need in there.
In total this structure establishes a strong force against the code degeneration that often occurs over the project lifetime and the best thing is that this is not enforced by rules but by making the easiest solution a good one.
The Atomic.Fusion workflow
Our workflow now starts with the frontend department developing the presentational components for a website. The development and testing of the presentational components occurs directly in Neos with the help of our living style guide Sitegeist.Monocle. That way each component and component-aggregates can be tested without the need for coordination or preparation of the backend.
Later on the backend-developers are handed over the components and use the component API documentation to implement the required data fetching and transformation. During this process the backend usually only looks at the component API wich documents the intended use of the component.
Since the presentational components are not modified by the backend, the frontend developers are still responsible for changes in here, while backend developers are responsible if the data has to be acquired differently or other parts shall become editable. All those changes can be done independently by the respective craft as long as no API changes are involved. But even if the component API has to be modified the coordination between crafts is very effective and precise .
The development process of Atomic.Fusion
The development of Atomic.Fusion was not a single large effort, but more an iterative process over several projects and many small steps. The more important ones are listed below.
Step 1: Atomic.Fusion as Pattern
In the beginning Atomic.Fusion was meant to be just a pattern and “Component” was a name for a Neos.Fusion:Value object that relied solely on its attributes for rendering.
The code we wrote back then looked like this:
prototype(Vendor.Site:Example) < prototype(Neos.Fusion:Value) {
# api
title = ''
description = ''
imageUri = ''
# context
@context {
title = ${this.title}
description = ${this.description}
imageUri = ${this.imageUri}
}
# renderer
value = Neos.Fusion:Tag {
tagName = 'div'
content = Neos.Fusion:Array {
1 = Vendor.Site:Image {
uri = ${imageUri}
}
2 = Neos.Fusion:Tag {
tagName = 'h1'
content = ${title}
}
3 = Neos.Fusion:Tag {
tagName = 'p'
content = ${description}
}
}
}
}
This already allowed to define a side-effect free presentational components with an explicit API in pure fusion but because fusion can only access properties on the same level all attributes had to be added to the @context to become available in the rendering tree. This was not only bloaty but also hard to get for integrators of fe-devs who were not 100% familiar with Fusion.
Step 2: Atomic.Fusion as package
To get rid of the unintuitive and also repetitive @context annotations we introduced the Atomic.Fusion Package that is built around the component prototype. This prototype transfers all the Fusion properties from the top level to a context property we call 'props'. That way props can always be accessed in the whole rendering tree without further code or advanced Fusion concepts.
prototype(Vendor.Site:Example) < prototype(PackageFactory.AtomicFusion:Component) {
# api
title = ''
description = ''
imageUri = ''
# renderer
renderer = Neos.Fusion:Tag {
tagName = 'div'
content = Neos.Fusion:Array {
1 = Vendor.Site:Image {
uri = ${props.imageUri}
}
2 = Neos.Fusion:Tag {
tagName = 'h1'
content = ${props.title}
}
3 = Neos.Fusion:Tag {
tagName = 'p'
content = ${props.description}
}
}
}
}
We used this kind of Atomic.Fusion in several projects and noticed that while we were collaborating effectively with frontend developers who were willing and able to write such Fusion components. We had trouble onboarding more frontend developers due to the verbose and unfamiliar Fusion syntax.
Step 3: AFX and Resource Colocation
To lower the barrier for frontend-developers we decided to transfer the concept of JSX from the ReactJS world to Neos and named that AFX. This was possible because when writing presentational Fusion components you are only using a very specific subset of Fusion namely:
- Creation of tags
- Access of props
- Creation of other components
- Iteration over arrays
With that and JSX in mind it was obvious to create a more compact syntax that looks like HTML and thus is familiar to frontend developers but at the same time enables them to use other components and passing down of props.
prototype(Vendor.Site:Example) < prototype(PackageFactory.AtomicFusion:Component) {
# api
title = ''
description = ''
imageUri = ''
# renderer
renderer = afx`
<div>
<Vendor.Site:Image uri={props.imageUri}>
<h1>{props.title}</h1>
<p>{props.description}</p>
</div>
`
}
It is important to make a distinction between AFX and inline templates. AFX is not a template language. It is transpiled, parsed and cached as Fusion code and thus has the same runtime characteristics as the previous example. Since this is no inline template it is impossible to create invalid HTML because already the parsing of the AFX code would result in a Fusion error. On the same time, AFX applies the same rules as JSX to the code to ignore non-meaningful whitespace and to produce optimized HTML by default.
At this stage, we also decided to move all the style and script resources that are essential for a component directly into the component folder and make the frontend department solely responsible for that part of the project.
Step 4: The Sitegeist.Monocle styleguide
To enable the independent progress of the frontend department it is important to create a way to define and test components without any support of the backend department. To achieve that we use the Sitegeist.Monocle package.
To render the presentational components in the style guides we only have to add the needed default data. Since this data has to evolve with the component it is defined directly in fusion by using the @styleguide annotation.
prototype(Vendor.Site:Example) < prototype(PackageFactory.AtomicFusion:Component) {
# styleguide informations
@styleguide {
title = 'an example component to explain things'
props {
title = 'Headline'
description = 'Lorem ipsum dolor sit amet'
imageUri = 'http://lorempixel.com/400/200/cats/'
}
}
# api
title = ''
description = ''
imageUri = ''
# renderer
renderer = afx`
<div>
<Vendor.Site:Image uri={props.imageUri}>
<h1>{props.title}</h1>
<p>{props.description}</p>
</div>
`
}
This is all that what is needed to render a fusion-prototype in the styleguide. Since no integration step is necessary the frontend department can create and test the component extensively before it is handed over to the backend.
The resulting component code is very readable for frontend and backend developers and straitly defines api, preview data and renderer that define a presentational component without any waste.
Conclusion
Atomic.Fusion helped us to establish an efficient workflow that maintains speed and fun during all phases of the development. Especially the collaboration and communication between backend and frontend developers has been hugely improved. Having an explicit API between them, the crafts can excel independently and respect each other much more than before.
This approach works very well for the larger projects that we usually are working on but we also see huge benefits for smaller projects if reusable, encapsulated components can be easily transferred between projects and used in different manners. The distinct aspects of mapping and rendering always had been in the code and we mainly suggest to separate them wich allows much easier reusing and adjustment of components .
We will keep working on that topic and while we consider the Atomic.Fusion approach finished and stable we can imagine many packages and tools on top of it that will make Neos development even more efficient and fun.
Links
- Neos-Slack #project-atomic-fusion / #project-styleguide
- Github Atomic.Fusion - Fusion prototypes fror writing atomic fusion code
- Github Atomic.Fusion.AFX - The AFX fusion-dsl that ports the concepts of React JSX to Neos
- Github Sitegeist.Monocle - A living style guide in Neos that can render presentational components directly
Image Attribution:
Header Image: NASA Goddard Space Flight Center - Flickr: Magnificent CME Erupts on the Sun - August 31, CC BY 2.0