fuse-open / fuselibs

Fuselibs is the Uno-libraries that provide the UI framework used in Fuse apps
https://npmjs.com/package/@fuse-open/fuselibs
MIT License
176 stars 72 forks source link

UX Let nodes for local data and expressions #680

Open Sunjammer opened 7 years ago

Sunjammer commented 7 years ago

Motivation:

UX is a language about construction of objects and establishment of their relationships. As UX documents increase in complexity the depth of these relationships has the side effect of excessive code duplication and verbosity, which has implications for productivity and readability. UX is not a DAG, even if XML pretty much implies that it is, because you can cross reference and databind across any and all boundaries, and this too has implications for verbosity and code dupe. Since we can't/shouldn't eliminate this ability, It should be our goal to reduce it and make it readable, and this needs a language addition for pure data.

UX has ux:Property for receiving data from a source external to the class in which it was defined, but ux:Property is not ideal for establishing a tree-local data source or transform. I like the proposed Let syntax:

<Let ux:Key="name" ux:Value="expression"/>

@Duckers edit: This syntax is preferred, see below: <Let name="expression" /> I've edited the inline code examples to reflect this

Where Let can be used anywhere and appends its results to the local data context.

Examples:

Animation drivers

<Change icon1.Opacity="1" Delay="0.1" Duration="0.1" Easing="QuadraticInOut" DelayBack="0"/>
<Change icon2.Opacity="1" Delay="0.1" Duration="0.1" Easing="QuadraticInOut" DelayBack="0"/>
<Change icon3.Opacity="1" Delay="0.1" Duration="0.1" Easing="QuadraticInOut" DelayBack="0"/>

These are three lines out 10 and the duplication is obvious. With the proposed <Let ux:Key="progress" ux:Value="0"/> syntax we can clean this up tremendously:

<Let iconOpacity="0"/>
<StackPanel>
    <Icon Opacity="{iconOpacity}"/>
    <Icon Opacity="{iconOpacity}"/>
    <Icon Opacity="{iconOpacity}"/>
</StackPanel>
<Trigger>
    <Change iconOpacity.Value="1" Delay="0.1" Duration="0.1" Easing="QuadraticInOut" DelayBack="0"/>
</Trigger>

We have a single source of truth for animation progress of icon opacities, one which we can scrub in Studio from a single selected node, and one that we can do single timelines for instead of multiple parallel ones. This really shortens the amount of UX markup needed.

Expression reuse and composability

<Let name="'Johnny'"/>
<Let formattedName="toUpper({name})"/>
<DockPanel>
    <StackPanel Dock="Top">
        <TextInput Value="{name}"/>
        <Text Value="{formattedName}"/>
    </StackPanel>
    <Text Value="Let's do this again: {formattedName}"/>
</DockPanel>

@duckers note: As Let is weak typed, it can not assume the content is string. String literals must be double quoted:

This moves the expression/transform into a reusable node, rather than having to copy/paste expressions around the codebase. It also allows for composability for users less comfortable writing single long expressions.

Tree-local vs Class property

Since Let appends to the data context, it does not need to be declared in a class, nor is it visible to parent nodes. This makes Let a versatile, tree-local data source.

<Panel>
    <Let foo="bar"/>
    <Text Value="{foo}"/> <!-- valid -->
</Panel>
<Text Value="{foo}"/> <!-- invalid -->

Let becomes the go-to utility for establishing and contextualizing data as part of a UX subtree, replacing many scenarios where JavaScript is the only solution but without the overkill.

Duckers commented 7 years ago

I propose this simplified syntax:

     <Let name="expression" />

for example

     <Let foo="{bar} / 2" />`

which will be automatically expanded by the UX compiler to

    <Let Key="foo" Value="{bar} / 2" />
Sunjammer commented 7 years ago

I like that too 👍

Duckers commented 7 years ago

Actually, lets do:

     <Let foo="1" />

expands to

     <Let ux:Name="foo" Value="1" />

Where ux:Name is also implicitly setting a Name string property on the Let instance (using the [UXName] attribute). This in turn allows you to use this syntax for animation:

    <Change foo.Value="10" Duration="1" />
Duckers commented 7 years ago

And if we are super lazy, we can also add a UX compiler feature that implicitly appends .Value on Change targets if it finds a property it doesn't recognize. However, keep in mind that this runs the risk of name collisions, e.g. what if you want to animate the data named Duration?

mortoray commented 7 years ago

We should be able to do the <Let ux:Name="foo" Value="1"/> syntax without introducing new UX compiler features. It might be worthwhile to do that first.

I've done variants of this in test cases before.

mortoray commented 7 years ago

This feature requires lots of testing with different types. The problem I've have in my previous attempts (usually in tests), is that Let is untyped, which causes a problem for marshalling. Conversion from Value is fine, but when using Change with Value as a target we have a problem: Change needs to know what type is changing. This applies to Set, and most cases of 2-way binding as well.

I don't see that we can get away with just Let and will likely require LetFloat, LetString, etc. This may play into what syntax we actually want.

yupferris commented 7 years ago

The implementation of this likely depends on https://github.com/fusetools/fuselibs-public/issues/740

mortoray commented 7 years ago

It appears I can get it working with weak types despite #740, but the weaknesses shown there become a lot more relevant with Let availabe. Other than the constant arrays, I've found workarounds for the other forms. Combined with models/contexts though most use-cases won't notice the limitations I think.