-
-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Controlled Components #148
base: master
Are you sure you want to change the base?
Conversation
I like the idea indeed I have a couple of questions
|
Because the native control mutates the value of We could re-mutate the value of
The actual DSL wouldn't change much. These would only expose a value and a change handler. There is more code to maintain because of the custom controls. For a few reasons, I don't view this as a problem:
In general I would expect an MVU framework to control values like this, so I would expect this to go in the main package. |
Text input is one of areas that will change before 1.0 release AvaloniaUI/Avalonia#3538. As pointed by AvaloniaUI/Avalonia#2076 (comment) it's still "work in progress" |
Good catch. It doesn't look like they've figured out which public API changes are slated yet. |
How can we implement this when controls are nested? For example, controlling the IsExpanded state of an Expander involves messing with a nested ToggleButton somewhere in the Expander's VisualChildren. By the time OnIsExpandedChange is raised, the property is already mutated. In this case I would be looking to override the Expander's ToggleButton? Or re-writing its Template? Edit: Here's a gist with a more generalizable pattern for controlling properties. This class intercepts uncontrolled change events, resets the value to the old value, and raises the new value with the user's onChange handler. (In this case the ToggleButton's IsOpen property doesn't get reset. I'm not sure how to access the binding property here.) If we wanted to put this in the framework (instead of extending individual controls) we could explore adding a 'ControlledProperty' concept to the AttrBuilder. These 'ControlledProperty' functions would take a value and a change handler, and then the framework would be responsible for controlling the value (in a way similar to the above gist). |
I added a new kind of Attr, |
Thanks for the patience! This seems like an interesting addition to FuncUI. Let me rephrase the goal of controlled views/properties, please correct me if I'm missing something. The current value of a control should always come from state. If the user (for example) enters text in a text box its value does not change. Instead the
There are a few things What properties should be controllable?From on top of my head What is with
|
Sorry I didn't see this reply for a bit! Thanks for checking it out. Change handlers
The intermediate value is passed to Take the value unalteredThis is how most components will work most of the time: ControlledTextBox.controlledOnChange (fun e -> e.Text |> SetTextMsg |> dispatch) Map/filter the valueThis is how a component can implement a filter/tunnel pattern: // Filter: remove all vowels
let withoutVowels = String.filter (fun char -> Regex.IsMatch(string char, @"[^aeiouAEIOU]"))
ControlledTextBox.controlledOnChange (fun e -> e.Text |> withoutVowels |> SetTextMsg |> dispatch) Ignore the value and do something else, or nothingThis is how a component can implement other arbitrary responses: ControlledTextBox.controlledOnChange (fun _ -> IncrementCounterMsg |> dispatch) ComboBox (and other 'selected item' controls)
This seems like a good time to throw an exception.
These should only have one or the other being controlled at any given time. We could expose either or both as controlled variants and throw an exception when both are present? This is one of several problems with MVVM, which is why I like this framework 😄 AlternativesI think it's absolutely worth asking Avalonia maintainers about this feature. It's very important to making a functional UI approach work. It's similar in some ways to OneWay bindings. The difference is that establishing an Avalonia binding doesn't prevent other changes to a given property. |
This pull request contains a proof-of-concept for 'controlled components', a feature where FuncUI is the single source of truth for a component's value.
What is this
In Avalonia, controls usually maintain their own internal state via AvaloniaProperty getters and setters. This internal state gets updated by either user input or setters in code. In FuncUI, the value of a control is dictated by state. We can combine these two by making the FuncUI state 'the single source of truth', so that the content of the Avalonia control always reflects the provided state value.
Motivation
When Avalonia controls have their own internal state, they can easily become de-synced with the state provided by the MVU framework. Consider the existing CounterApp example:
This app only dispatches a change event if the control's value successfully parses as a number. If not, the Avalonia control's state becomes out-of-sync with the Counter state. With a controlled component, the input field's text will always reflect state as given by FuncUI. This has immediate benefits:
Implementation
By default, Avalonia controls will mutate their own values in response to user input. To implement controlled components, we need to defer mutation until the value comes back via the framework. As a proof-of-concept, this PR contains a ControlledTextBox control which overrides the destructive methods of Avalonia's existing TextBox control.
All mutation happens in a single method, which is invoked by the framework only after it receives a new value from the view function. The app author receives a synthesized event with text set to a kind of 'staged' change. Then, the app author can decide if they want to accept, reject, or modify this value before putting it into state.
I would like to implement Controlled variants for all user-input components. I implemented this proof-of-concept with the TextBox, which has a lot of different kinds of user input, as well as other kinds of state (the selection and the caret position remain 'uncontrolled'). Other controls probably involve less code.
Next steps
Prior art
Controlled components in React