fuselibs icon indicating copy to clipboard operation
fuselibs copied to clipboard

ux:Event feature

Open Duckers opened this issue 6 years ago • 12 comments

In order for the FuseJS (1.4) component story to be complete, we need an ux:Event concept that is analogous to ux:Property.

The proposed syntax is:

    <delegate_type ux:Event="event_name" />

Example usage:

    <Handler ux:Event="LoginClicked" />

Where Handler is a special delgate type Fuse.Reactive.Handler that is designed for use in user-defined events raised from javascript.

When used with feature-Models, declaring a component with an event would look like this:

<StackPanel ux:Class="MyApp.LoginForm" Model="Components/LoginForm(this)">
    <Handler ux:Event="LoginClicked" />
    <TextInput Value="{username}" />
    <TextInput Value="{password}" IsPassword="true" />
    <Button Text="Login" Clicked="{login}" />
</StackPanel>

And in LoginForm.js:

export class LoginForm {
    constructor(view) {
        this.view = view
        this.username = ""
        this.password = ""
    }
    login() {
        this.view.LoginClicked({ username: this.username, password: this.password})
    }
}

When the component is used, it would look like:

    <MyApp.LoginForm LoginClicked="{loginClicked}" />

And in JS:

    loginClicked(args) {
       console.log("Login with user: " + args.username + " and password: " + args.password)
    }

Duckers avatar Aug 26 '17 13:08 Duckers

If possible I'd like to somehow see this connected with the existing UserEvent. The reason is because we'll want to handle this as a UX trigger as well.

<OnUserEvent Event="view.LoginClicked">
   ...

Similarly you'd want to raise this event from UX in many simple forwarding cases.

<RaiseUserEvent Event="view.LoginClicked">
   ...

I'd prefer to reuse the existing names rather than introduce parallel names. IF we don't have these features it'll mean apps will still be forced to mix the two differnet models. Having one would be preferred, even when ux:Event is the preferred way of creating events.

mortoray avatar Aug 26 '17 14:08 mortoray

I don't see how this feature is at all related to UserEvents. This is an UX/Uno language-level construct, while UserEvent is a library-level construct.

Raising events from UX is not a high priority imho. If so, it can be done with an UX function call

Duckers avatar Sep 25 '17 14:09 Duckers

After speaking with @sebbert, we realized with the latest models branch we don't need a language-level construct at all. Consider the following, which achieves the desired result with ux:Property:

MyComponent.ux:

<Panel ux:Class="MyComponent" Model="MyComponent">
    <object ux:Property="OnClick" />

    <Clicked Handler="{clicked}" />
</Panel>

MyComponent.js:

export default class MyComponent
{
    OnClick() {
        console.log("Default handler called");
    }

    clicked() {
        this.OnClick("foo", "bar", "baz");
    }
}

MainView.ux:

<App Model="App">
    <StackPanel>
        <MyComponent />
        <MyComponent OnClick="{clickHandler}" />
    </StackPanel>
</App>

App.js:

export default class App {
    clickHandler(...args) {
        console.log(`External click handler called with args: [${args.join(", ")}]`);
    }
}

This actually already works as-is. If we want to make it read better, we could perhaps introduce a lib-level Event object that could be used as the event's property type (eg. <Event ux:Property="OnClick" />), but I'm not sure that's necessary. It's quite clear what's going on here, arguably clearer than the original proposal and doesn't introduce any new concepts.

yupferris avatar Oct 12 '17 11:10 yupferris

@Duckers @mortoray ^

yupferris avatar Oct 12 '17 11:10 yupferris

One downside of the above approach is that there are subtle corner cases about when the event can be raised. Since it's just a property, it can't be raised from a model constructor for example, as at that time the property would not have been set and the default handler would be called. It would seem with the original proposal that events raised from JS would wait until the JS was finished being evaluated (and in the ctor case the object would have its properties etc set as well before the event was actually raised). But this would have its own set of subtle corner cases the user has to be aware of (for example, raising an event and then calling a function would result in the function being called before the event was raised), and admittedly it sounds a bit finicky to rely on (and test!) such rules.

Another downside is its inability to have multiple handlers like Uno/C#'s event construct. I must admit I'm struggling a bit to see why a user might want to attach multiple handlers to an event in a real-world case (even though I can imagine how such UX would look), especially when a user could attach one handler that calls multiple functions and achieve the same result in most cases. I mean, having a UX construct that maps to Uno events sounds like a nice completeness feature perhaps, but I'd argue that our UX language design should be driven by real-world UI use cases, not necessarily what Uno/C# has and UX doesn't. It's possible that those cases exist, but they're not very clear to me at least.

I think I'll play with some use cases and perhaps an implementation of the original proposal and see if that clears anything up in my head. I think we should try it out and evaluate its usefulness before committing to shipping/maintaining it when a simpler pattern for these cases might exist.

yupferris avatar Oct 12 '17 11:10 yupferris

There a few problems with this approach that I can see:

  • We don't like having object as a property type since we can't do any comptile-time or run-time type checking on this object. Using this type has caused us grief in the past when dealing with observables and has resulted in undesirable hacks. It needs to have a proper type for sanity in marshalling.
  • It doesn't work with multiple listeners, as you mentioned. This is not a good limitation. UX code frequently has multiple responders to the same event. It's very common to have something spawn and animation and action in two different timelines. It's also not uncommon in a compound UI to just have two different parts of the compound dialog responding to the same event.
  • It doesn't appear to provide anyway to be used from UX and/or interact with the existing UserEvent system. I should be able to intercept these events with a UX construct, much like OnUserEvent -- if not the exact same trigger with a different property.

mortoray avatar Oct 12 '17 12:10 mortoray

I did some playing around with possible syntax/library-level API's to satisfy the multiple listeners constraint as well as having an explicit type for the event, and basically wound up with what I think is UserEvent/RaiseUserEvent, but with different names :) . Are there actually any cases where UserEvent wouldn't be sufficient here? Perhaps I've missed a detail or two, but the only constraint it doesn't satisfy is one @Duckers had in a discussion earlier, that we may want to raise events in model constructors. However, consider the following:

constructor {
    RaiseSomeEvent(); // <- this suddenly is different than interacting with other UX objects, which we expect aren't ready until after the ctor is finished
    CallSomeFunction(); // <- this actually executes _before_ the event is raised!
}

It would seem to be that the better way to handle that case is give models a way to handle rooted/unrooted lifetime events or similar, and have the same caveat with events that we do with ux:Property values, that they may not be set up in the model constructor.

If UserEvent would be a fine alternative/implementation mechanism, I wouldn't mind exploring some alternative syntax in UX. But I don't really see a good reason to back ux:Event with an Uno event with a special handler type except perhaps you'd want to subscribe to that from Uno code, but in that case I'd rather have a library-level API to do so. One of the things that's still up in the air for preview app support is not actually generating Uno classes from UX, and avoiding specific language features in the implementation would allow us to migrate easier.

yupferris avatar Oct 12 '17 12:10 yupferris

I guess one thing I don't see on RaiseUserEvent is the ability to raise a specific UserEvent object from UX, only a way to raise it with a name key, but I don't think that would be difficult to implement.

yupferris avatar Oct 12 '17 13:10 yupferris

For example, this is basically what I came up with trying to do this with just library-level features:

MyComponent.ux:

<Panel ux:Class="MyComponent" Model="MyComponent" Color="#f00" Margin="20" Height="100">
	<Event ux:Property="OnClick" />
	<EventHandler Event="OnClick" Function="{defaultImpl}" />
	<EventHandler Event="OnClick" Function="{anotherImpl}" />

	<Clicked Handler="{clicked}" />
</Panel>

MyComponent.js:

export default class MyComponent {
	constructor {
		// Don't raise OnClick here! Event might not be created yet (same semantics as anything else in UX, mind you)
	}

	someCreatedLifetimeEvent {
		// Here we can raise the stuff, just call the event object as a function
		this.OnClick("we're loaded yo");
	}

	OnClick() {
		console.log("Default handler called");
	}

	clicked() {
		this.OnClick("foo", "bar", "baz");
	}
}

MainView.ux:

<App Model="App">
	<StackPanel>
		<MyComponent />
		<MyComponent>
			<EventHandler Event="OnClick" Function="{clickHandler}" />
		</MyComponent>
	</StackPanel>
</App>

App.js:

export default class App {
	clickHandler(...args) {
		console.log(`External click handler called with args: [${args.join(", ")}]`);
	}
}

In this case Event is very similar to UserEvent, and EventHandler is an explicit handler object that could sub/unsub at root/unroot time. As far as I can tell this would solve all of the given constraints (minus the ctor one, which I argue against above) and reads fairly well. If Event was UserEvent and EventHandler could hook up to that, it should all fit together quite nicely I think. And again, perhaps ux:Event-like syntax would be nice here, but I don't think it's needed.

yupferris avatar Oct 12 '17 13:10 yupferris

The mechanism by which UserEvent works is probably fine, and a new mechanism should probably integrate with it, but it lacks the high-level syntax we want. For example:

<Panel ux:Class="MyComp">
    <UserEvent ux:Name="myEvent">

You can't do:

<MyComp myEvent="{Handler}"/>

You can only do:

<MyComp>
   <OnUserEvent Event="myEvent" Callback="{Handler}"/>

One of the earlier aversions to UserEvent was also that they aren't available from Uno code -- they aren't really class-level events. Given that UserEvent's actually solve UX-level use-cases, the ux:Event solution is only valuable if it gives us the cleaner syntax and Uno-level events. Of course, the problem with Uno level events is that there is no way to generically intercept them in UX (I'm not sure how that would be solved).

--

If we aren't allowed to use other UX objects in the constructor then I see no reason why event would be any different.

mortoray avatar Oct 12 '17 13:10 mortoray

Your EventHandler looks pretty much the same as OnUserEvent. It only accepts EventName now, but accepting the actual Event object would be a trivial addition I believe.

Adding a Handler would also be simple. I thought it was already there, but it isn't. Other triggers like Clicked already provide this interface.

mortoray avatar Oct 12 '17 13:10 mortoray

After reading this, I still stand by my original proposal, for several reasons:

  • Discoverability & design of least surprise. When you find ux:Property, it is natural to assume ux:Event exists. This is a very frequent piece of user feedback. UserEvent doesn't feel like part of the canon, more like a polyfill to make up for the lack of ux:Event.
  • We already use a lot of Uno events in our API designs, which is a good thing. The key to componentization is to have these low-level constructs at the language level. Components created in UX markup need to make sense on the Uno language level, as an increasingly important use case is using components made in UX markup in a Fuse Views setting (embedded into code bases in other languages, like C#, Swift or Java). I expect the interface to components to be comprised of Uno properties, methods and events, which gives us a fixed set of concepts that need to be mapped.
  • User-defined events in UX should show up in documentation listings as first class events, like the ones defined in Uno.

No matter how we implement it, it is important that from the FuseJS side, raising the event is equivalent to calling a function:

export class MyComponent {
      doSomething() {
            SomethingHappened({foo: 3})
      }
      SomethingHappened(args) { .. default handler goes here .. }
}

Similar to how ux:Property works; if there is a corresponding ux:Event in the markup, the Model system overrides SomethingHappened with a function that intercepts the event before dispatching it to the default handler.

(note that we no longer pass in a view parameter in the Model system, which my original proposal assumed)

This design achieves the following goals:

  • The JavaScript code makes sense and won't crash even if used without a corresponding view. I.e. it can be tested or used with other frameworks. To test that the "event" works, the test environment simply overrides the function, the same way the Model system does.
  • The default handler can optionally map the event to an EventEmitter to make it possible to subscribe to the event also in a more normal javascript fashion

Duckers avatar Oct 18 '17 17:10 Duckers