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

Aggregate expressions support? #81

Open markphillips100 opened 4 years ago

markphillips100 commented 4 years ago

I've been trying to create a custom grouping of checkbox choice items where each choice component has an integer quantity property and the parent group component also has a quantity property. The requirement I have is for the parent quantity value to be calculated as a sum of all the child choices' individual quantity values. The calculation should be reevaluated each time there is a change to any of the child quantity values.

I also want the calculation to be implicit in the parent parser, a bit like how RequiredValidation defines its expression in the parser. Basically I don't want the expression to be defined in the data.

I tried the following but the expression fires only once which makes sense considering the expression is only dependent on the choices property and not the underlying quantity properties. So my question is, is there support for simple aggregate expressions that can take a list of properties? If not, is there some other way of achieving the desired result?

  @override
  void fillProperties(
    TMultiSelectCheckBoxConstraintsGroup multiSelectCheckBoxConstraintsGroup, 
    ParserNode parserNode, 
    Element parent,
    ElementParserFunction parser,
  ) {
    super.fillProperties(multiSelectCheckBoxConstraintsGroup, parserNode, parent, parser);
    multiSelectCheckBoxConstraintsGroup
    ..quantityProperty = getQuantityExpression(multiSelectCheckBoxConstraintsGroup);
  }

  LazyExpressionProperty<int> getQuantityExpression(FormElement multiSelectCheckBoxConstraintsGroup) {
    return LazyExpressionProperty(
      () {
        final choicesPropertyExpression = multiSelectCheckBoxConstraintsGroup.getExpressionProvider(MultiSelectConstraintsGroup.choicesPropertyName);
        return CustomFunctionExpression<int>(
          [
            DelegateExpression(
              [multiSelectCheckBoxConstraintsGroup.id],
              choicesPropertyExpression,
            ),
          ],
          (parameters) {
            var choices = parameters[0] as List<MultiSelectCheckBoxConstraintsChoice>;
            var total = choices.fold(0, (previous, current) => previous + current.quantity);
            print("quantity total: $total");
            return total;
          },
        );
      }
    );
  }  
}
markphillips100 commented 4 years ago

I managed to get it working with a bit of map/reduce which is simple enough for my use case. Although it would be nice to wrap that up into a simple expression to drive the underlying map/reduce.

class MultiSelectCheckBoxConstraintsGroupParser<TMultiSelectCheckBoxConstraintsGroup extends MultiSelectCheckBoxConstraintsGroup>
    extends MultiSelectConstraintsGroupParser<TMultiSelectCheckBoxConstraintsGroup, MultiSelectCheckBoxConstraintsChoice> {
  @override
  String get name => 'multiSelectCheckBoxConstraintsGroup';

  @override
  FormElement getInstance() => MultiSelectCheckBoxConstraintsGroup();

  @override
  void fillProperties(
    TMultiSelectCheckBoxConstraintsGroup multiSelectCheckBoxConstraintsGroup, 
    ParserNode parserNode, 
    Element parent,
    ElementParserFunction parser,
  ) {
    super.fillProperties(multiSelectCheckBoxConstraintsGroup, parserNode, parent, parser);
    multiSelectCheckBoxConstraintsGroup
    ..quantityProperty = getQuantityExpression(multiSelectCheckBoxConstraintsGroup);
  }

  LazyExpressionProperty<int> getQuantityExpression(MultiSelectCheckBoxConstraintsGroup multiSelectCheckBoxConstraintsGroup) {
    return LazyExpressionProperty(
      () {
        final resultExpression = multiSelectCheckBoxConstraintsGroup.choices
          .map<Expression<Number>>((c) => 
            IntToIntegerExpression(
              DelegateExpression(
                [multiSelectCheckBoxConstraintsGroup.id],
                c.getExpressionProvider(MultiSelectConstraintsGroup.quantityPropertyName),
              ),
            ),
          )
          .reduce((value, next) => ConversionExpression<Number, Integer>(PlusNumberExpression(value, next)));

        return IntegerToIntExpression(resultExpression);
      }
    );
  }  
}
OndrejKunc commented 3 years ago

Hi @markphillips100 , Sorry for the late response, but I didn't have any solution to your problem - until now.

What we need is expression with lambda, something like "sum(children.map((x) => x.quantity))". Unfortunately, it is not possible to easily implement lambda functions in the current version of the expression_language package. It is something I would like to implement in the future, but I think it will take a significant amount of work because I would need to completely change the way how the parser works.

To be honest I am quite impressed you were able to find a solution since there is not much documentation for those cases.

However, I don't like the approach of the RequiredValidation where we define the expression directly on the property - it is difficult to understand and not very reusable.

For this reason, I just released a new version of packages with the possibility of implementing custom expressions, see https://github.com/OndrejKunc/flutter_dynamic_forms/tree/master/packages/expression_language#writing-custom-expressions I also added the "Advanced expression form" screen to the example project: https://github.com/OndrejKunc/flutter_dynamic_forms/blob/master/packages/flutter_dynamic_forms_components/example/lib/advanced_expression_form/advanced_expression_form.dart which should solve your issue.

I solved it by adding two custom expressions

  1. SelectNumberPropertyExpression which is the equivalent of the lambda above "children.map((x) => x.quantity)"
  2. SumNumbersExpression which gets a list of number expressions and returns a sum similar to your solution.

Then you can just declare it directly in the XML/JSON without the need to define custom property: sumNumbers(selectNumberProperty(@sliderCollection.children, \"value\"))

markphillips100 commented 3 years ago

Hi @OndrejKunc . Apologies for my even later late response to your response :-) I'll hopefully revisit this soon so I can try out your changes. Appreciate your efforts.