RFC: Fusion Components: simpler variable flow / `computed` properties, and public component API

Hi :wink:

i have thought lately again of how to make Fusion component rendering easier to write.

Fusion computed properties and component API

As far as i see there are currently a few things that can be improved.

  1. how to archive computed variables? Should i use @process, @context, this or props?
  2. how to declare a public API for your component. Should i use the nested component patter, prefix private paths with _ or use @propTypes?

Fusion patterns and beginners

Over the time some patterns arise, but one needs to master them to become a Fusion pro. For beginners Fusion is often hard to understand as there is no top to bottom control flow like in a scripting language. And without knowing the patters/ actually understanding Fusion, its far to easy to shot oneself into the foot. (by randomly accessing props even though its not a component or thinking this refers to some other component etc…)

Simplicity of Reacts functional components

To tackle the mentioned problems i took the simplicity of Reacts functional components as an example. They offer a simple API for the props, and one can access those props everywhere in the function body and create computed variables and use them everywhere too:

function demo({ variable: "hello" }) {
    const computedExpression = variable + " world"
    const computedJsx = (
        <p>{computedExpression}</p>
    )

    return (
        <>{variable} {computedExpression} {computedJsx}</>
    )
}

Solution? Fusion:FunctionalComponent (with transpilation magic :sparkles:)

I want to have this same simplicity, so one doesnt need to fully understand Fusions context, but can start already building simple frontend components.

Presenting the idea of a FunctionalComponent in Fusion:

all variables starting with $ are ‘magic’ as they are scoped to the component they were written.
(this is just to make regexing/parsing easier xD)

prototype(MHS.Site:Demo) < prototype(MHS.Fusion:FunctionalComponent) (
    // public API
    variable = "hello"
) {
    // private
    computedEEL = ${$variable + " world"}
    computedFusion = afx`
        <p>{$computedEEL}</p>
    `

    renderer = afx`
        {$variable} {$computedEEL} {$computedFusion}
    `
}

Implementation ideas and further specification

To get this running would require two steps, preprocess this Fusion file and transpile it to actual Fusion and implement rendering and context logic in the prototype implementation.

The Fusion below shows how it transpiled could look like:

Note a few specialities:

  • the prefix _123 (which is unique for each functional prototype declaration) is appended to all previously via $ referenced variables. (this is done to make sure the context doesnt interfer with anything else)
  • the property @selfReferencingContext is used by the FunctionalComponent and behaves similar to @context, except one can reference newly defined context variables already.
  • renderer is not considered a variable but more like a return so it is not touched.
prototype(MHS.Site:Demo) < prototype(MHS.Fusion:FunctionalComponent) {

    // public API
    variable = "hello"

    // private
    @selfReferencingContext {
        // hello
        variable_123 = ${this.variable}
        // hello world
        computedEEL_123 = ${variable_123 + " world"}
        // <p>hello world</p>
        computedFusion_123 = afx`
            <p>{computedEEL_123}</p>
        `
    }
    
    
    renderer = afx`
        {variable_123} {computedEEL_123} {computedFusion_123}
    `
}

Usage / Instantiation

it can be used as any other object. The advantage beeing, that private variables are not mutable, due to the random identifier.

foo = MHS.Site:Demo {
    variable = "moin"
}

Private variables not usable from outside the declaration

the following code would not print out the variables, as they have now the unknown prefix _123

foo = MHS.Site:Demo {
    renderer = ${computedEEL + variables}
}

also due to the anonymization the problem the bare use of @context has, that the whole subtree suffers from the pollution is limited as it would be impossible to accidentally reference a randomized identifier. To minimize the pollution, the variables could also be combined in a props like associative array.

Limitations

  • inheritance of those components will be forbidden due to undefined behavior - use composition
  • recursively calling the same component could lead to the context not working as expected so it shouldnt be allowed at first (need to spend some brainpower on this one ;))
  • a simple implementation would only allow one functional component per file and keep them separated from the actual Fusion
  • extending the object is undefined behavior

Further improvements

  • lazy evaluation of the computed variables

  • Runtime type checking can be simply implemented for the API like:

prototype(MHS.Site:Demo) < prototype(MHS.Fusion:FunctionalComponent) (
    variable: string = "hello"
) {
...

I really like the separation of pathes that can be set from outside (public api) and those that are internal. The syntax for those is nice. Also the typed path syntax you suggest at the end is neat.

We could make a distinction between prototypes with external api by looking at the declaration
prototype(Vendor.Site:Example) () {} vs. prototype(Vendor.Site:Example) {} and disallow setting internal pathes in the runtime for the first kind.

What i kind of dislike is the transpilation magic. I would rather have this as a language feature. Also it would be nice when a functional renderer would end up beeing a pure php-function instead of fusion.

1 Like