Code Monkey home page Code Monkey logo

fusion-afx's Introduction

Neos.Fusion.Afx

JSX inspired compact syntax for Neos.Fusion

This repository is a read-only subsplit of a package that is part of the Neos project (learn more on www.neos.io <https://www.neos.io/>_).

This package provides a fusion preprocessor that expands a compact xml-ish syntax to pure fusion code. This allows to write compact components that do'nt need a seperate template file and enables unplanned extensibility for the defined prototypes because the generated fusion-code can be overwritten and controlled from the outside if needed.

Installation

Neos.Fusion.AFX is available via packagist. Just add "neos/fusion-afx" : "~1.0.0" to the require-section of the composer.json or run composer require neos/fusion-afx.

We use semantic-versioning so every breaking change will increase the major-version number.

Usage

With this package the following fusion code

prototype(Vendor.Site:Example) < prototype(Neos.Fusion:Component) {

    title = 'title text'
    subtitle = 'subtitle line'
    imageUri = 'https://dummyimage.com/600x400/000/fff'

    #
    # The code afx`...` is converted to the fusion code below at parse time.
    # Attention: Currently there is no way to escape closing-backticks inside the Expression.
    #
    renderer = afx`
       <div>
         <h1 @key="headline" class="headline">{props.title}</h1>
         <h2 @key="subheadline" class="subheadline" @if.hasSubtitle={props.subtitle ? true : false}>{props.subtitle}</h2>
         <Vendor.Site:Image @key="image" uri={props.imageUri} />
       </div>
    `
}

Will be transpiled, parsed and then cached and evaluated as beeing equivalent to the following fusion-code

prototype(Vendor.Site:Example) < prototype(Neos.Fusion:Component) {

    title = 'title text'
    subtitle = 'subtitle line'
    imageUri = 'https://dummyimage.com/600x400/000/fff'

    renderer = Neos.Fusion:Tag {
        tagName = 'div'
        content = Neos.Fusion:Join {
            headline = Neos.Fusion:Tag {
                tagName = 'h1'
                content = ${props.title}
                attributes.class = 'headline'
            }
            subheadline = Neos.Fusion:Tag {
                tagName = 'h2'
                content = ${props.subtitle}
                attributes.subheadline = 'subheadline'
                @if.hasSubtitle = ${props.subtitle ? true : false}
            }
            image = Vendor.Site:Image {
                uri = ${props.imageUri}
            }
        }
    }
}

AFX Language Rules

All whitepaces around the outer elements are ignored. Whitepaces that are connected to a newline are considered irrelevant and are ignored.

HTML-Tags (Tags without Namespace)

HTML-Tags are converted to Neos.Fusion:Tag Objects. All attributes of the afx-tag are rendered as tag-attributes.

The following html:

<h1 class="headline" @if.hasHeadline={props.headline ? true : false}>{props.headline}</h1>

Is transpiled to:

Neos.Fusion:Tag {
    tagName = 'h1'
    attributes.class = 'headline'
    content = ${props.headline}
    @if.hasHeadline = ${props.headline ? true : false}
}

If a tag is self-closing and has no content it will be rendered as self closing fusion-tag:.

<br/>

Is transpiled to:

Neos.Fusion:Tag {
    tagName = 'br'
    selfClosingTag = true
}

Fusion-Object-Tags (namespaced Tags)

All namespaced-tags are interpreted as prototype-names and all attributes are passed as top-level fusion-properties.

The following html:

<Vendor.Site:Prototype type="headline" @if.hasHeadline={props.headline ? true : false}>{props.headline}</Vendor.Site:Prototype>

Is transpiled as:

Vendor.Site:Prototype {
    type = 'headline'
    content = ${props.headline}
    @if.hasHeadline= ${props.headline ? true : false}
}

Spread Syntax

To apply multiple properties to a fusion prototype with a single expression afx supports the spread syntax from ES6:

<Vendor.Site:Component {...expression} />

Is transpiled as:

Vendor.Site:Component {
    @apply.spread_1 = ${expression}
}

Spreads can be combined with props and the order of the definition is of props and spreads is preserved, spreads will override previously defined props but are overwritten again by later props.

The order preserving combination of spreads and properties works by only rendering the properties before the first spread as classic fusion properties. Spreads and the following props are transpiled to fusion @apply statements and are thus able to override all props but and are evaluated in the order of definition.

<Vendor.Site:Component title="example" {...data} description="description" {...moreData} />

Is transpiled as:

Vendor.Site:Component {
    title = 'example'
    @apply.spread_1 = ${data}
    @apply.spread_2 = Neos.Fusion:DataStructure {
        description = 'description'
    }
    @apply.spread_3 = ${moreData}

}

This feature is based on the @apply-syntax of fusion and thus will only work in Neos > 4.2.

Tag-Children

The handling of child-nodes below an afx-node is differs based on the number of childNodes that are found.

Single tag-children

If a AFX-tag contains exactly one child this child is rendered directly into the content-attribute. The child is then interpreted as string, eel-expression, html- or fusion-object-tag.

The following AFX-Code:

<h1>{props.title}</h1>

Is transpiled as:

Neos.Fusion:Tag {
    tagName = 'h1'
    content = {props.title}
}

Multiple tag-children

If an AFX-tag contains more than one child the content is are rendered as Neos.Fusion:Join into the content-attribute. The children are interpreted as string, eel-expression, html- or fusion-object-tag.

The following AFX-Code:

<h1>{props.title}: {props.subtitle}</h1>

Is transpiled as:

Neos.Fusion:Tag {
    tagName = 'h1'
    content = Neos.Fusion:Join {
        item_1 = {props.title}
        item_2 = ': '
        item_3 = ${props.subtitle}
    }
}

The @key-property of tag-children inside alters the name of the fusion-attribute to recive render the array-child into. If no @key-property is given index_x is used starting by x=1.

<Vendor.Site:Prototype @children="text">
    <h2 @key="title">{props.title}</h1>
    <p @key="description">{props.description}</p>
</Vendor.Site:Prototype>

Is transpiled as:

Vendor.Site:Prototype {
    text = Neos.Fusion:Join {
        title = Neos.Fusion:Tag {
            tagName = 'h2'
            content  = ${props.title}
        }
        description = Neos.Fusion:Tag {
            tagName = 'p'
            content  = ${props.description}
        }
    }
}

The @path-property of tag-children can be used to render a specific afx-child into the given fusion path instead of beeing included into the content. This allows to render AFX children into different props and to assign Fusion-prototypes to props.

<Vendor.Site:Prototype>
    <h2 @path="title">{props.title}</h1>
    <p @path="description">{props.description}</p>
</Vendor.Site:Prototype>

Is transpiled as:

Vendor.Site:Prototype {
    title = Neos.Fusion:Tag {
        tagName = 'h2'
        content  = ${props.title}
    }
    description = Neos.Fusion:Tag {
        tagName = 'p'
        content  = ${props.description}
    }
}

Meta-Attributes

In general all meta-attributes start with an @-sign.

The @path-attribute can be used to render a child node directly into the given path below the parent Fusion:Object instead of beeing included into the content property.

The @children-attribute defined the property that is used to render the content/children of the current tag into. The default property name for the children is content.

The @key-attribute can be used to define the property name of an item among its siblings if an array is rendered. If no @key is defined index_x is used starting at `x=1.

Attention: @path, @children and @key only support string-values and no expressions.

All other meta attributes are directly added to the generated prototype and can be used for @if or @process statements.

Whitespace and Newlines

AFX is not html and makes some simplifications to the code to optimize the generated fusion and allow a structured notation of the component hierarchy.

The following rules are applied for that:

  1. Newlines and Whitespace-Characters inside a text literal collapse to a single space.
<h1>
    This is a string literal
    with multiple lines
    that shall collapse
    to spaces.
</h1>

Is transpiled as:

Neos.Fusion:Tag {
    tagName = 'h1'
    content = 'This is a string literal with multiple lines that shall collapse to spaces.'
}
  1. Newlines and Whitespace-Characters that are connected to a newline are considered irrelevant and are ignored
<h1>
	{'eelExpression 1'}
	{'eelExpression 2'}
</h1>

Is transpiled as:

Neos.Fusion:Tag {
	tagName = 'h1'
	contents = Neos.Fusion:Join {
		item_1 = ${'eelExpression 1'}
		item_2 = ${'eelExpression 2'}
	}
}
  1. Spaces between Elements on a single line are considered meaningful and are preserved
<h1>
	{'eelExpression 1'} {'eelExpression 2'}
</h1>

Is transpiled as:

Neos.Fusion:Tag {
	tagName = 'h1'
	contents = Neos.Fusion:Join {
		item_1 = ${'eelExpression 1'}
		item_2 = ' '
		item_3 = ${'eelExpression 2'}
	}
}

HTML Comments

AFX accepts html comments but they are not transpiled to any fusion. However if you are converting html to afx it is allowed to have comments inside and you can use comments for disabeling parts of your afx during testing.

foo<!-- comment -->bar

Is transpiled as:

Neos.Fusion:Join {
    item_1 = 'foo'
    item_2 = 'bar'
}

Examples

Rendering of Collections with Neos.Fusion:Collection

For rendering of lists or menus a presentational-component usually will recieve arrays of preprocessed data as prop. To iterate over such an array the Neos.Fusion:Collection can be used in afx.

prototype(Vendor.Site:IterationExample) < prototype(Neos.Fusion:Component) {

    # array {[href:'http://www.example_1.com', title:'Title 1'], [href:'http://example_2.com', title:'Title 2']}
    items = null

    renderer = afx`
        <ul @if.has={props.items ? true : false}>
        <Neos.Fusion:Collection collection={props.items} itemName="item">
            <li @path='itemRenderer'>
                <Vendor.Site:LinkExample {...item} />
            </li>
        </Neos.Fusion:Collection>
        </ul>
    `
}

Augmentation of Child-Components with Neos.Fusion:Augmenter

The Neos.Fusion:Augmenter can be used to add additional attributes to rendered content. This allows some rendering flexibility without extending the api of the component. This is a useful pattern to avoid unneeded tag-wrapping in cases where only additional classes are needed.

prototype(PackageFactory.AtomicFusion.AFX:SliderExample) < prototype(Packagefactory.AtomicFusion:Component) {
  images = ${[]}
  renderer = afx`
     <div class="slider">
        <Neos.Fusion:Collection collection={props.images} itemName="image" iterationName="iteration" @children="itemRenderer">
            <Neos.Fusion:Augmenter class="slider__slide" data-index={iteration.index}>
                <Vendor.Site:ImageExample {...image} />
            </Neos.Fusion:Augmenter>
        </Neos.Fusion:Collection>
     </div>
  `
}

The example iterates over a list of images and uses the Vendor.Site:ImageExample to render each one while the Neos.Fusion:Augmenter adds a class- and data-attribute from outside.

License

see LICENSE file

fusion-afx's People

Contributors

albe avatar comir avatar dfeyer avatar jonnitto avatar kdambekalns avatar lorenzulrich avatar mficzel avatar mhsdesign avatar mstruebing avatar sebobo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

fusion-afx's Issues

Properties with @

If you want to use libraries like Alpine.js, it is not possible to add properties with @. In pure Fusion it works:

tag = Neos.Fusion:Tag {
  attributes {
     '@click' = 'Test'
  }
  content = 'Text'
}

This doesn't work:

renderer = afx`
  <div '@click'='Test'>Text</div>
`

Supported HTML comments, IE conditional comments and CDATA

It should be possible to use HTML comments, IE conditional comments and CDATA blocks in AFX.

Example:

renderer = afx`
    <!-- My comment -->
    <div>Test</div>

    <!--[if lt IE 7 ]>
        Please update your browser.
    <![endif]-->    

    <script>
        <![CDATA[
            var foo = 'bar';
        ]]>
    </script>
`

"keep whitespace" mode

As documented AFX strips whitespaces and newlines and this makes a lot of sense usually.

But sometimes it can be useful or even required to keep the whitespaces, for example when using AFX for email templates, code generators or code snippets.

For those it would be nice to have some kind of "keep whitespace" mode, for example:

emailTemplate = afxkws`
Hello {props.name},

foo bar baz
`

Questions on best practices // add FAQ for common cases (maybe on Docs.Neos.io)

Hey everybody,

I've been trying AFX recently to build a Backend module, and I have stumbled over a few cases which are unclear to me. I'm writing my questions in a kind of FAQ style, so that we can publish this afterwards, and have more and more AFX best practices documented.

How to render a block conditionally?

In Fluid, one can use the <f:if condition="...">...</if> ViewHelper to render a block conditionally.

In my AFX code, I have done the following:

<Neos.Fusion:Value @if.hasChildren={...} @children="value">
   ...
</Neos.Fusion:Value>

Is this a best practice or would you do it differently? Can I attach the @if to an arbitrary DOM node as it is Fusion in the end?

How do you build If/Else cases in AFX?

I'd love if the syntax {myCondition ? <FirstAfxComponent /> : <SecondAfxComponent />} worked, just like React. However, in my understanding, this won't work because everything in {...} is directly interpreted as Eel.

So what I did instead was using Neos.Fusion:Case:

prototype(Sandstorm.NeosAcl:Presentation.NodeList.NodeRow.Acl) < prototype(Neos.Fusion:Case) {
    @context.acl = ${this.acl}

    moreThanThreeAcls {
        condition = ${Array.length(acl) > 3}
        element = Neos.Fusion:Component {
            acl = ${acl}
            renderer = afx`
             ...
            `
        }
    }

    default {
        condition = true
        element = Neos.Fusion:Component {
            acl = ${acl}
            renderer = afx`
                 ...
            `
        }
    }
}

I dislike the above approach because of the following:

  • it's very verbose.
  • we need to pass the acl prop from props to Context, then back from context to Props in the inner Components.

How would you write this idiomatically?

How do you handle "computed state" in a Component?

Example:

prototype(Sandstorm.NeosAcl:Presentation.NodeList.NodeRow) < prototype(Neos.Fusion:Component) {
    # public API
    node = null

    # Internal "API"
    @context.node = ${this.node}
    showLinkUri = Neos.Fusion:UriBuilder {
        action = "show"
        arguments.node = ${node.nodePath}
    }
    renderer = afx`...`

in the above example, the node is a public prop; and I want to generate a certain link with this Node as input. The control flow above is IMHO non-obvious:

  • we need to put the node into context, using this.... notation
  • we need to define a new internal prop, where we use the context value.

I know I could write <...NodeRow showLinkUri.arguments.node={node}> on calling the component, but I feel this exposes too much of the internal behavior of NodeRow to the outside world...

How would you write this idiomatically?
Is there any convention on how to name/build "computed properties"?

Forms

I've tried creating helpers for generating forms, similar to <f:form>. I think this has not been done before, has it? I think it would be super useful as well...

I see one conceptual problem there: For forms to work, we need a "data channel" from the form element to its form fields and back again, to be able to remember the form fields, the bound value, etc etc. Pretty similar to how react's context works.

However, an Neos.Fusion:Component only passes through props and clears all other variables from the Fusion stack (which is generally great of course) -- should we extend it to e.g. keep "private" context variables starting with __?

Menus

How do I build menus in AFX? (Asking because the Menu prototype inherits from Neos.Fusion:Template).

All the best and thanks :)

Sebastian

Simplify AFX @if and @process

Needing to name @if and @process in AFX is cumbersome and harms readability. It would be great it the are generated automatically.

Examples:

<h2 
    @if={something == true}
    @if={2dasdasd}
    title.@if={dasds}
    title.@process={asdas}>
    Lala
</h2>

Cannot use prototype as @process

All of these do not work:

// -> literally "Acme.Com:Processor" as result
<a href={props.href} @process.foo="Acme.Com:Processor">

// -> Expression "Acme.Com:Processor" could not be parsed. Error starting at character 18: ":Processor".
<a href={props.href} @process.foo={Acme.Com:Processor}>

// -> Prop-assignment "@process.foo" was not followed by quotes or braces
<a href={props.href} @process.foo=Acme.Com:Processor>

But this would work in "pure" Fusion:

@process.tracking = Acme.Com:Processor

Leading backslash gets lost

I don't know if this is intended but if not it should be documented I think.

When writing something like:

<span>\o/</span>

It results in

<span>o/</span>

On the other hand

<span>\\o/</span>

results in

<span>\o/</span>

Support inline JS in script tags

While having separate scripts might in general be the preferable way to load some sprinkles of JavaScript, there are still occasions where an inline script is better to show the actual intent and keep the component coupled with its frontend behavior.

For example, let's say you have a button with a click handler doing a very specific action.
I would like to be able to have a simple fusion component containing the button as well as the script tag with the accompanying event binding.

prototype(Vendor:CookieSettingsButton) < prototype(Neos.Fusion:Component) {
    renderer = afx`
        <button id="klaro-manager">
            Open cookie settings
        </button>
        <script>
            document.addEventListener('DOMContentLoaded', () => {
                var button = document.getElementById('klaro-manager');
                button.addEventListener('click', function() {
                    klaro.show();
                });
            });
        </script>
    `
}

renderer from parent prototype can not be fully overridden

prototype(Random.Package:Prototype) < prototype(Random.Package:BasePrototype) {
    renderer = afx`
        <div>
            <div>test</div>
            <div>this should be not red, and there should be no content from the base prototype visible</div>
        </div>
    `
}

prototype(Random.Package:BasePrototype) < prototype(Neos.Fusion:Component) {
    renderer = afx`
        <div style="background: red">
            <div>this is the base prototype content, should not be visible at all</div>
        </div>
    `
}

When we output Random.Package:Prototype we would expect

<div>
    <div>test</div>
    <div>this should be not red, and there should be no content from the base prototype visible</div>
</div>

but instead we get

<div style="background: red">
    divthis is the base prototype content, should not be visible at all
    <div>test</div>
    <div>this should be not red, and there should be no content from the base  prototype visible</div>
</div>

This is most likely because of the auto-generated Neos.Fusion:Tag prototypes in the background and how they are merged, but even completely unsetting the renderer beforehand with renderer > is not helping.

I'm aware of the general "composition over inheritance" stance coming from JSX, but inheritance and overwriting paths should still work like anywhere else in Fusion in combination with AFX.

If this is not considered a bug and if it's completely discouraged to override component render paths in Fusion there should be at least an exception thrown instead of this opaque merging mechanism.

Ability to define Children Prototype

There should be the ability to define the prototype a tag's children a nested into (Neos.Fusion:Array by default).
Currently it's not possible to construct arbitrary data in AFX. For example it is not possible to pass a prop containing a Neos.Fusion:RawArray value like this:

afx`
  <Vendor.Site:Foo @children="bar">
    <Vendor.Site:Foo.Header @key="header" />
    <Vendor.Site:Foo.Body @key="body" />
  </Vendor.Site:Foo @children="bar">
`

Instead Vendor.Site:Foo's bar prop would contain:

bar = Neos.Fusion:Array {
  header = Vendor.Site:Foo.Header
  body = Vendor.Site:Foo.Body
}

I suggest a new meta property like @childrenType to controle the prototype used, which defaults to Neos.Fusion:Array if not set. Also the result should alyways be a value of that type, even if only one child exists, so that the user can always rely on his configuration.

The above example would be written as:

afx`
  <Vendor.Site:Foo @children="bar" @childrenType="Neos.Fusion:RawArray">
    <Vendor.Site:Foo.Header @key="header" />
    <Vendor.Site:Foo.Body @key="body" />
  </Vendor.Site:Foo @children="bar">
`

and would be transpiled to:

bar = Neos.Fusion:RawArray {
  header = Vendor.Site:Foo.Header
  body = Vendor.Site:Foo.Body
}

AFX in Eel Expressions

It would be great to support AFX in expressions, inspired by JSX. Arbitrary AFX could be included in Eel wrapped by some special sequence. The syntactic sugar might not be worth the effort.

Before:

uri = Neos.Fusion:UriBuilder {
    action = 'someMethod'
}
`<a href={props.uri}>Click me</a>

After:

`<a href={afx(<Neos.Fusion:UriBuilder action="someMethod" />)}>Click me</a>

This could then be used with a ternary operator, similar to JSX like this:

Before:

<Button @if.has={something==true}>dasdas</Button>
<Button2 @if.hasNot={something!=true} />

After:

{something==true ? <Button >dasdas</Button> : <Button2 />}

This may result in a structure like this:

Neos.Fusion:Tag {
	_afx_content_afx_child1 = afx`<Button >dasdas</Button>`
	_afx_content_afx_child2 = afx`<Button2 />`
	content = ${something==true ? this._afx_content_afx_child1 : this._afx_content_afx_child2}	
}

AFX in commented Fusion is not ignored

Given the following fusion code:

root = Neos.Fusion:Renderer {
//    renderer = afx`
//      <p>This should be ignored</p>
//    `
    renderer = Neos.Fusion:Value {
        value = '<p>this should be rendered</p>'
    }
}

Expected

<p>this should be rendered</p>

Actual

Exception (#1181575973):

Unexpected closing confinement without matching opening confinement.
Check the number of your curly braces.

Note:

Using multi line comments works as expected:

root = Neos.Fusion:Renderer {
/*
    renderer = afx`
      <p>This should be ignored</p>
    `
*/
    renderer = Neos.Fusion:Value {
        value = '<p>this should be rendered</p>'
    }
}

-> outputs <p>this should be rendered</p>

Note: This issue has been opened in the wrong tracker first (https://github.com/PackageFactory/atomic-fusion-afx/issues/20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.