OndrejKunc / flutter_dynamic_forms

A collection of flutter and dart libraries allowing you to consume complex external forms at runtime.
MIT License
203 stars 59 forks source link

Reset value of element to original json value #71

Open JohnPinto opened 4 years ago

JohnPinto commented 4 years ago

Hi, I'm currently trying to reset the value of a radioButtonGroup from a checkBox, but when I use the expression syntax to set its value, the radioButtonGroup becomes immutable and responds only to the changes in the checkBox.

I'm no sure if I overlooked something in the documentation, or if its something else. My goal is to make the radioButtonGroup, invisible and reset its value to "-1" when the checkBox is unchecked.

Thanks for your time.

OndrejKunc commented 4 years ago

Hi, The current design of the library assumes that each property is either an expression or a value that can be changed by a user. In case of any expression (in your case something like @checkbox.value) it cannot be changed to a different value manually. The reason for this behavior is that at any moment you can be sure that all properties satisfy their expressions and you wouldn't be surprised by any unpredictable states. For example if you had expression x ? 0 : 1 then someone could assign the value 2 manually and that would feel quite inconsistent. One possible solution to this problem is to keep the desired value mutable and use some other property for the expression. In your case, I would solve it by attaching the expression to the radioButtonGroup isVisible property instead, so there would be something like

"isVisible": {
   "expression": "@checkbox.value"
}

In that case your value property on radioButtonGroup stays mutable. Then in your Renderer class you would need to subscribe to the isVisibleChanged Stream and whenever there is a new desired value you would dispatch change event to the the value -1 of the value property.

Hope that helps

Edit: I know it is not a very systematic solution, because this component would always reset its value when it is hidden which makes it difficult to reuse. Another solution is to make a custom component inherited from radioButtonGroup and put an extra property on the model like shouldReset which would handle the reset and you would put the expression there, instead of misusing the isVisible property.

JohnPinto commented 4 years ago

Hi, I've implemented your suggestion of using the isVisibleChanged stream to reset my custom component's value. But I'm currently having a problem with the stream itself, the two states that I'm receiving in the async snapshot from the StreamBuilder are true and null, I never get a false indicating that the form element that I'm trying to reset is not visible.

Even when I'm using the isVisible property, It returns true or null. If I print the output from the properyChanged stream I also have the same outcome, never "isVisible", only null or "value". My problem at the moment is that I want to reset the value when the form element is hidden, but It's resetting when it becomes visible a second time, if I use the .getFormData() the hidden component's value is still being outputted.

Thanks for the previous reply, and sorry to bother you a second time.

OndrejKunc commented 4 years ago

Hi, sorry for the late response.

Firstly, .getFormData simply iterates over all the components that are mutable and collects their values - even if component is not visible.

Are you sure you have correctly set your isVisible expression in your XML/JSON to "@yourCheckboxId.value" so it would change every time you check your checkbox?

Do you think you can provide some minimal reproducible example so I can try it myself? Or at least relevant part of the code (render, XML/JSON...).

JohnPinto commented 4 years ago

Hi, I'm sending the files of the custom components that I've implemented, I'm also sending the json file that I'm using to debug. The behavior happens in every component that I've modified.

Again, sorry to bother you, I imagine that the pandemic must be putting a strain on all of us, so I appreciate you taking some of your time to help me with this :)

custom_components.zip

OndrejKunc commented 4 years ago

Hi,

Thanks for the code! I already see the problem and it is quite an interesting and challenging one. The main issue is in the ReactiveFormRenderer - there is a line: .where((f) => f.isVisible) What this does is that it will not render any child that is no longer visible. So those child renderers will never be called and will not be part of your widget tree. In your case, CRadioButtonGroupRenderer is the child of the form and when it is hidden the StreamBuilder listening to the isVisibleChanged Stream will be disposed and will never receive the false value.

That's the general problem of the renderer classes - they may be added and removed from the widget tree but we need to have this logic in a persistent place. That's when I realized it should not be part of the renderer but rather part of the model class, because there is a single instance of the model that never changes.

What we need to do is to subscribe to the isVisible changes and change the value property. Unfortunately, we cannot subscribe to the Stream in a model constructor, because the Stream is not prepared yet. We would need some lifetime callback on a model which will be called when the Stream is ready - this is something that should be implemented in the future version.

However, there is a workaround - just put this inside your CRadioButtonGroup:

  StreamSubscription<bool> _isVisibleSubscription;

  // We need to call this code from a place where valueChanged Stream is already initialized
  // so this is a workaround before we have a proper lifecycle callback.
  void subscribeToIsVisible() {
    if (_isVisibleSubscription == null) {
      _isVisibleSubscription = isVisibleProperty.valueChanged.listen((visible) {
        if (shouldReset && !visible) {
          if (valueProperty is MutableProperty<String>) {
            var mutableValueProperty = valueProperty as MutableProperty<String>;
            mutableValueProperty.setValue("-1");
          }
        }
      });
    }
  }

And in your renderer just make sure this method is called (this will not be necessary when we have lifecycle callback on the model):

class CRadioButtonGroupRenderer extends FormElementRenderer<CRadioButtonGroup> {
  @override
  Widget render(
      CRadioButtonGroup element,
      BuildContext context,
      FormElementEventDispatcherFunction dispatcher,
      FormElementRendererFunction renderer) {
    element.subscribeToIsVisible();
    return StreamBuilder<List<CRadioButton>>(
      initialData: element.choices,
      stream: element.choicesChanged,
      builder: (context, snapshot) {
        return StreamBuilder(
            stream: MergeStream(
              snapshot.data.map((child) => child.isVisibleChanged),
            ),
            builder: (context, snapshot) {
              return Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                  ),
                  ...element.choices
                      .where((c) => c.isVisible)
                      .map((choice) => renderer(choice, context))
                      .toList(),
                ],
              );
            });
      },
    );
  }
}

I hope this helps.

It is not bothering at all. This is exactly the type of issue that can improve the library! Btw. in my case lack of time is not so much about the pandemic but rather about a newborn child :)