Skip to content

Commit d999570

Browse files
authored
Merge pull request #98 from netglade/story/96-composed-glade-model
Add ComposedGladeModel and related components for multi-forms support
2 parents 9175da6 + 15b65ca commit d999570

17 files changed

Lines changed: 747 additions & 27 deletions

docs.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
"href": "/advanced/control-other-inputs",
5555
"icon": "network-wired"
5656
},
57+
{
58+
"title": "Composed model",
59+
"href": "/advanced/composed-model",
60+
"icon": "box-open"
61+
},
5762
{
5863
"title": "Debugging",
5964
"href": "/advanced/debugging",

docs/advanced/composed-model.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
title: Composed forms
3+
description: Building dynamic lists of forms
4+
---
5+
6+
Composed forms are ideal when you need to manage multiple instances of the same form. The number of forms doesn’t need to be known in advance — new instances can be added or removed at any time.
7+
8+
The core building block of this system is the `GladeComposedModel`, which maintains and manages a list of individual `GladeModel` or even another `GladeComposedModel` instances contained within the composed form.
9+
10+
Because `GladeComposedModel` extends `ChangeNotifier`, any dependent widgets automatically rebuild when the model changes.
11+
12+
**Using a composed model**
13+
14+
Composed models behave just like ordinary `GladeModel` objects — they are even provided with the same `GladeModelProvider` and consumed with `GladeFormBuilder`:
15+
16+
```dart
17+
GladeModelProvider(
18+
create: (context) => MyComposedModel([MyFormModel()]),
19+
child: GladeFormBuilder<MyComposedModel>(
20+
builder: (context, composedModel, child) => ...
21+
),
22+
)
23+
```
24+
25+
**Managing contained models**
26+
27+
You can dynamically add or remove form models:
28+
29+
```dart
30+
composedModel.addModel(model);
31+
composedModel.removeModel(model);
32+
```
33+
34+
Whenever any contained form model changes — or a model is added or removed — the entire composed form builder rebuilds to reflect the updated list.
35+
36+
**Rendering multiple identical forms**
37+
38+
Since all form models within a composed model share the same type, it's often helpful to automatically generate the same form widget for each model. This is where `GladeComposedListBuilder` comes in. It iterates through all models and builds the appropriate form widget for each one.
39+
40+
![composed-example](/assets/composed-model.gif)

docs/assets/composed-model.gif

97.6 KB
Loading

glade_forms/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
## 5.1.0
2+
- **[Add]**: Add `GladeComposedModel` to allow multi-forms creation
3+
- **[Add]**: Add `ComposedExample` to demonstrate `GladeComposedModel` functionality
4+
- **[Add]**: Add `NestedComposedExample` to demonstrate nested `GladeComposedModel` functionality
25
- **[Add]**: Add `GladeModel.fillDebugMetadata()` method to provide debug metadata as key-value pairs.
36
- This metadata is displayed in `GladeModelDebugInfo` widget.
47

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:glade_forms/src/src.dart';
2+
3+
import 'package:glade_forms/src/validator/validator_result.dart';
4+
5+
abstract class GladeComposedModel<M extends GladeModelBase> extends GladeModelBase {
6+
final List<M> _models = [];
7+
8+
/// Returns true if all models are valid.
9+
@override
10+
bool get isValid => models.every((model) => model.isValid);
11+
12+
@override
13+
bool get isValidWithoutWarnings => models.every((model) => model.isValidWithoutWarnings);
14+
15+
@override
16+
bool get isPure => models.every((model) => model.isPure);
17+
18+
/// Returns true if model is not pure.
19+
@override
20+
bool get isDirty => !isPure;
21+
22+
/// Returns true if all models have unchanged inputs.
23+
///
24+
/// Input is unchanged if its value is same as initial value, even if value was updated into initial value.
25+
@override
26+
bool get isUnchanged => models.every((model) => model.isUnchanged);
27+
28+
/// Models that this composed model is currently listening to.
29+
List<M> get models => _models;
30+
31+
@override
32+
List<ValidatorResult<Object?>> get validatorResults => [
33+
for (final e in models) ...e.validatorResults,
34+
];
35+
36+
/// Constructor can take form models to start with.
37+
GladeComposedModel([List<M>? initialModels]) {
38+
if (initialModels != null) {
39+
for (final model in initialModels) {
40+
addModel(model);
41+
}
42+
}
43+
}
44+
45+
/// Adds model to `models` list.
46+
/// Whenever form model changes, it triggers also change on this composed model.
47+
void addModel(M model) {
48+
_models.add(model);
49+
model
50+
..addListener(notifyListeners)
51+
..bindToComposedModel(this);
52+
notifyListeners();
53+
}
54+
55+
/// Removes model from `models` list.
56+
/// Also unregisters from listening to its changes.
57+
void removeModel(M model) {
58+
final _ = _models.remove(model);
59+
model
60+
..removeListener(notifyListeners)
61+
..unbindFromComposedModel(this);
62+
notifyListeners();
63+
}
64+
}

glade_forms/lib/src/model/glade_model.dart

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
import 'package:flutter/foundation.dart';
2-
import 'package:glade_forms/src/core/core.dart';
2+
import 'package:glade_forms/src/src.dart';
33
import 'package:glade_forms/src/validator/validator_result.dart';
44
import 'package:meta/meta.dart';
55

6-
abstract class GladeModel extends ChangeNotifier {
7-
List<GladeInput<Object?>> _lastUpdates = [];
6+
abstract class GladeModel extends GladeModelBase {
87
bool _groupEdit = false;
98

109
/// Returns true if all inputs are valid.
10+
@override
1111
bool get isValid => inputs.every((input) => input.isValid);
1212

13+
@override
1314
bool get isValidWithoutWarnings => inputs.every((input) => input.isValidAndWithoutWarnings);
1415

15-
/// Returns true if any input is not valid.
16-
bool get isNotValid => !isValid;
17-
1816
/// Returns true if all inputs are pure.
1917
///
2018
/// Input is pure if its value is same as initial value and value was never updated.
2119
///
2220
/// Pure can be reset when [setInputValuesAsNewInitialValues] or [resetToInitialValue] on model or its inputs are called.
21+
@override
2322
bool get isPure => inputs.every((input) => input.isPure);
2423

2524
/// Returns true if model is not pure.
25+
@override
2626
bool get isDirty => !isPure;
2727

2828
/// Returns true if all inputs are unchanged.
2929
///
3030
/// Input is unchanged if its value is same as initial value, even if value was updated into initial value.
31+
@override
3132
bool get isUnchanged => inputs.where((input) => input.trackUnchanged).every((input) => input.isUnchanged);
3233

3334
ValidationTranslator<Object?> get defaultValidationTranslate => (error, key, devMessage, dependencies) => devMessage;
@@ -44,8 +45,6 @@ abstract class GladeModel extends ChangeNotifier {
4445
/// By default equals to [inputs].
4546
List<GladeInput<Object?>> get allInputs => inputs;
4647

47-
List<String> get lastUpdatedInputKeys => _lastUpdates.map((e) => e.inputKey).toList();
48-
4948
/// Formats errors from `inputs`.
5049
String get formattedValidationErrors =>
5150
inputs.map((e) => e.errorFormatted()).where((element) => element.isNotEmpty).join('\n');
@@ -65,6 +64,7 @@ abstract class GladeModel extends ChangeNotifier {
6564
return '${e.inputKey} - VALID';
6665
}).join('\n');
6766

67+
@override
6868
List<ValidatorResult<Object?>> get validatorResults => inputs.map((e) => e.validatorResult).toList();
6969

7070
/// Returns true if model has any debug metadata.
@@ -106,7 +106,7 @@ abstract class GladeModel extends ChangeNotifier {
106106
void updateInput<INPUT extends GladeInput<T?>, T>(INPUT input, T value) {
107107
if (input.value == value) return;
108108

109-
_lastUpdates = [input];
109+
lastUpdates = [input];
110110

111111
input.value = value;
112112
notifyListeners();
@@ -115,9 +115,9 @@ abstract class GladeModel extends ChangeNotifier {
115115
@internal
116116
void notifyInputUpdated(GladeInput<Object?> input) {
117117
if (_groupEdit) {
118-
_lastUpdates.add(input);
118+
lastUpdates.add(input);
119119
} else {
120-
_lastUpdates = [input];
120+
lastUpdates = [input];
121121
notifyDependencies();
122122
notifyListeners();
123123
}
@@ -138,7 +138,7 @@ abstract class GladeModel extends ChangeNotifier {
138138

139139
/// Notifies dependant inputs about changes.
140140
void notifyDependencies() {
141-
final updatedKeys = _lastUpdates.map((e) => e.inputKey).toSet();
141+
final updatedKeys = lastUpdates.map((e) => e.inputKey).toSet();
142142
for (final input in inputs) {
143143
final updatedKeysExceptInputItself = updatedKeys.difference({input.inputKey});
144144
final union = input.dependencies.map((e) => e.inputKey).toSet().intersection(updatedKeysExceptInputItself);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:glade_forms/src/src.dart';
3+
import 'package:glade_forms/src/validator/validator_result.dart';
4+
5+
abstract class GladeModelBase extends ChangeNotifier {
6+
List<GladeInput<Object?>> lastUpdates = [];
7+
final List<GladeComposedModel> _bindedComposeModels = [];
8+
9+
bool get isValid;
10+
11+
bool get isValidWithoutWarnings;
12+
13+
bool get isPure;
14+
15+
bool get isUnchanged;
16+
17+
List<ValidatorResult<Object?>> get validatorResults;
18+
19+
bool get isNotValid => !isValid;
20+
21+
bool get isDirty => !isPure;
22+
23+
List<String> get lastUpdatedInputKeys => lastUpdates.map((e) => e.inputKey).toList();
24+
25+
/// Binds current model to compose model.
26+
void bindToComposedModel(GladeComposedModel model) {
27+
_bindedComposeModels.add(model);
28+
}
29+
30+
/// Unbinds current model from compose model.
31+
bool unbindFromComposedModel(GladeComposedModel model) {
32+
return _bindedComposeModels.remove(model);
33+
}
34+
35+
@override
36+
void dispose() {
37+
for (final composeModel in _bindedComposeModels) {
38+
composeModel.removeModel(this);
39+
}
40+
super.dispose();
41+
}
42+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
export 'glade_composed_model.dart';
12
export 'glade_metadata.dart';
23
export 'glade_model.dart';
4+
export 'glade_model_base.dart';

0 commit comments

Comments
 (0)