|
1 | 1 | import 'package:codelessly_api/codelessly_api.dart'; |
2 | 2 | import 'package:flutter/material.dart'; |
3 | 3 | import 'package:flutter/services.dart'; |
| 4 | +import 'package:provider/provider.dart'; |
4 | 5 |
|
5 | 6 | import '../../../codelessly_sdk.dart'; |
6 | 7 | import '../utils/node_state_provider.dart'; |
@@ -271,6 +272,11 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> { |
271 | 272 | } |
272 | 273 |
|
273 | 274 | void onFocusChanged() { |
| 275 | + // A `focused` variable follows the field's focus, so other nodes can |
| 276 | + // style themselves by it (a focus ring drawn around the field). |
| 277 | + if (mounted && widget.node.variables['focused'] != null) { |
| 278 | + _setFocusedVariable(context, widget.node, focusNode.hasFocus); |
| 279 | + } |
274 | 280 | if (!focusNode.hasFocus) { |
275 | 281 | widget.onSubmitted?.call(context, controller.text); |
276 | 282 | } |
@@ -306,9 +312,19 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> { |
306 | 312 | final FormFieldValidator<String> validator = |
307 | 313 | widget.validator ?? validatorModel.validate; |
308 | 314 |
|
| 315 | + // A bound `autoFocus` (e.g. only for a new item on desktop) replaces the |
| 316 | + // design value. Like Flutter's, it applies when the field is first built. |
309 | 317 | final bool autofocus = |
310 | 318 | !widget.settings.isPreview && |
311 | | - (widget.autofocus ?? properties.autoFocus); |
| 319 | + (widget.autofocus ?? |
| 320 | + (node.variables['autoFocus'] == null |
| 321 | + ? null |
| 322 | + : PropertyValueDelegate.getPropertyValue<bool>( |
| 323 | + node, |
| 324 | + 'autoFocus', |
| 325 | + scopedValues: scopedValues, |
| 326 | + )) ?? |
| 327 | + properties.autoFocus); |
312 | 328 |
|
313 | 329 | // Flutter can only obscure a single-line field, and a field that expands |
314 | 330 | // has no line limit, so an obscured field (a password) doesn't expand: |
@@ -451,21 +467,32 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> { |
451 | 467 | scopedValues: scopedValues, |
452 | 468 | settings: settings, |
453 | 469 | ); |
454 | | - final Widget? errorText = buildText( |
455 | | - context, |
| 470 | + // The field is in error only while its error text has something to |
| 471 | + // show: a template or bound value that resolves to empty (or null) means |
| 472 | + // no error, so the error state can follow a variable. |
| 473 | + final String? error = resolveErrorText( |
456 | 474 | node, |
457 | | - property: 'errorText', |
458 | | - defaultValue: decoration.errorText, |
| 475 | + decoration.errorText, |
459 | 476 | scopedValues: scopedValues, |
460 | 477 | settings: settings, |
461 | | - variablesOverrides: widget.variablesOverrides, |
462 | | - color: decoration.errorStyle.fills.first.toFlutterColor(), |
463 | | - fontSize: decoration.errorStyle.fontSize, |
464 | | - letterSpacing: decoration.errorStyle.letterSpacing, |
465 | | - lineHeight: decoration.errorStyle.lineHeight, |
466 | | - fontName: decoration.errorStyle.fontName, |
467 | | - textDecoration: decoration.errorStyle.textDecoration, |
468 | 478 | ); |
| 479 | + final Widget? errorText = error == null |
| 480 | + ? null |
| 481 | + : buildText( |
| 482 | + context, |
| 483 | + node, |
| 484 | + property: 'errorText', |
| 485 | + defaultValue: error, |
| 486 | + scopedValues: scopedValues, |
| 487 | + settings: settings, |
| 488 | + variablesOverrides: widget.variablesOverrides, |
| 489 | + color: decoration.errorStyle.fills.first.toFlutterColor(), |
| 490 | + fontSize: decoration.errorStyle.fontSize, |
| 491 | + letterSpacing: decoration.errorStyle.letterSpacing, |
| 492 | + lineHeight: decoration.errorStyle.lineHeight, |
| 493 | + fontName: decoration.errorStyle.fontName, |
| 494 | + textDecoration: decoration.errorStyle.textDecoration, |
| 495 | + ); |
469 | 496 | final Widget? prefixText = buildText( |
470 | 497 | context, |
471 | 498 | node, |
@@ -594,6 +621,34 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> { |
594 | 621 | ); |
595 | 622 | } |
596 | 623 |
|
| 624 | + /// The error text [node] shows, or null when it has none: a bound |
| 625 | + /// `errorText` variable (or a condition or set value action) takes |
| 626 | + /// precedence over the design [errorText], and a value that substitutes |
| 627 | + /// to an empty string, or a null value, means no error. |
| 628 | + String? resolveErrorText( |
| 629 | + TextFieldNode node, |
| 630 | + String? errorText, { |
| 631 | + required ScopedValues scopedValues, |
| 632 | + required WidgetBuildSettings settings, |
| 633 | + }) { |
| 634 | + final bool bound = node.variables['errorText'] != null; |
| 635 | + final Object? value = bound |
| 636 | + ? PropertyValueDelegate.getPropertyValue<Object>( |
| 637 | + node, |
| 638 | + 'errorText', |
| 639 | + scopedValues: scopedValues, |
| 640 | + ) |
| 641 | + : context.getNodeValue(node.id, 'errorText') ?? errorText; |
| 642 | + if (value == null) return null; |
| 643 | + final String text = PropertyValueDelegate.substituteVariables( |
| 644 | + value.toString(), |
| 645 | + scopedValues: scopedValues, |
| 646 | + nullSubstitutionMode: NullSubstitutionMode.emptyString, |
| 647 | + ); |
| 648 | + if (text.trim().isEmpty || text == 'null') return null; |
| 649 | + return text; |
| 650 | + } |
| 651 | + |
597 | 652 | InputBorder? getInputBorder(InputBorderModel? inputBorder) { |
598 | 653 | if (inputBorder == null) return null; |
599 | 654 | switch (inputBorder.borderType) { |
@@ -674,6 +729,21 @@ class _PassiveTextFieldWidgetState extends State<PassiveTextFieldWidget> { |
674 | 729 | } |
675 | 730 | } |
676 | 731 |
|
| 732 | +/// Writes [focused] to the variable [node] binds its `focused` property to. |
| 733 | +void _setFocusedVariable(BuildContext context, BaseNode node, bool focused) { |
| 734 | + final String? path = node.variables['focused']; |
| 735 | + if (path == null) return; |
| 736 | + final VariableMatch? match = VariableMatch.parse( |
| 737 | + path.wrapWithVariableSyntax(), |
| 738 | + ); |
| 739 | + if (match == null || match.hasPath) return; |
| 740 | + final ValueNotifier<VariableData>? notifier = context |
| 741 | + .read<CodelesslyContext>() |
| 742 | + .findVariableByName(match.name); |
| 743 | + if (notifier == null || notifier.value.value == '$focused') return; |
| 744 | + notifier.value = notifier.value.copyWith(value: focused); |
| 745 | +} |
| 746 | + |
677 | 747 | class _ReactiveIcon extends StatelessWidget { |
678 | 748 | final VoidCallback onTap; |
679 | 749 | final ReactiveIconModel iconModel; |
|
0 commit comments