Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
"href": "/advanced/control-other-inputs",
"icon": "network-wired"
},
{
"title": "Composed model",
"href": "/advanced/composed-model",
"icon": "box-open"
},
{
"title": "Debugging",
"href": "/advanced/debugging",
Expand Down
40 changes: 40 additions & 0 deletions docs/advanced/composed-model.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Composed forms
description: Building dynamic lists of forms
---

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.

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.

Because `GladeComposedModel` extends `ChangeNotifier`, any dependent widgets automatically rebuild when the model changes.

**Using a composed model**

Composed models behave just like ordinary `GladeModel` objects — they are even provided with the same `GladeModelProvider` and consumed with `GladeFormBuilder`:

```dart
GladeModelProvider(
create: (context) => MyComposedModel([MyFormModel()]),
child: GladeFormBuilder<MyComposedModel>(
builder: (context, composedModel, child) => ...
),
)
```

**Managing contained models**

You can dynamically add or remove form models:

```dart
composedModel.addModel(model);
composedModel.removeModel(model);
```

Whenever any contained form model changes — or a model is added or removed — the entire composed form builder rebuilds to reflect the updated list.

**Rendering multiple identical forms**

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.

![composed-example](/assets/composed-model.gif)
Binary file added docs/assets/composed-model.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions glade_forms/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
## 5.1.0
- **[Add]**: Add `GladeComposedModel` to allow multi-forms creation
- **[Add]**: Add `ComposedExample` to demonstrate `GladeComposedModel` functionality
- **[Add]**: Add `NestedComposedExample` to demonstrate nested `GladeComposedModel` functionality
- **[Add]**: Add `GladeModel.fillDebugMetadata()` method to provide debug metadata as key-value pairs.
- This metadata is displayed in `GladeModelDebugInfo` widget.

Expand Down
64 changes: 64 additions & 0 deletions glade_forms/lib/src/model/glade_composed_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:glade_forms/src/src.dart';

import 'package:glade_forms/src/validator/validator_result.dart';

abstract class GladeComposedModel<M extends GladeModelBase> extends GladeModelBase {
final List<M> _models = [];

/// Returns true if all models are valid.
@override
bool get isValid => models.every((model) => model.isValid);

@override
bool get isValidWithoutWarnings => models.every((model) => model.isValidWithoutWarnings);

@override
bool get isPure => models.every((model) => model.isPure);

/// Returns true if model is not pure.
@override
bool get isDirty => !isPure;

/// Returns true if all models have unchanged inputs.
///
/// Input is unchanged if its value is same as initial value, even if value was updated into initial value.
@override
bool get isUnchanged => models.every((model) => model.isUnchanged);

/// Models that this composed model is currently listening to.
List<M> get models => _models;

@override
List<ValidatorResult<Object?>> get validatorResults => [
for (final e in models) ...e.validatorResults,
];

/// Constructor can take form models to start with.
GladeComposedModel([List<M>? initialModels]) {
if (initialModels != null) {
for (final model in initialModels) {
addModel(model);
}
}
}

/// Adds model to `models` list.
/// Whenever form model changes, it triggers also change on this composed model.
void addModel(M model) {
_models.add(model);
model
..addListener(notifyListeners)
..bindToComposedModel(this);
notifyListeners();
}

/// Removes model from `models` list.
/// Also unregisters from listening to its changes.
void removeModel(M model) {
final _ = _models.remove(model);
model
..removeListener(notifyListeners)
..unbindFromComposedModel(this);
notifyListeners();
}
}
24 changes: 12 additions & 12 deletions glade_forms/lib/src/model/glade_model.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import 'package:flutter/foundation.dart';
import 'package:glade_forms/src/core/core.dart';
import 'package:glade_forms/src/src.dart';
import 'package:glade_forms/src/validator/validator_result.dart';
import 'package:meta/meta.dart';

abstract class GladeModel extends ChangeNotifier {
List<GladeInput<Object?>> _lastUpdates = [];
abstract class GladeModel extends GladeModelBase {
bool _groupEdit = false;

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

@override
bool get isValidWithoutWarnings => inputs.every((input) => input.isValidAndWithoutWarnings);

/// Returns true if any input is not valid.
bool get isNotValid => !isValid;

/// Returns true if all inputs are pure.
///
/// Input is pure if its value is same as initial value and value was never updated.
///
/// Pure can be reset when [setInputValuesAsNewInitialValues] or [resetToInitialValue] on model or its inputs are called.
@override
bool get isPure => inputs.every((input) => input.isPure);

/// Returns true if model is not pure.
@override
bool get isDirty => !isPure;

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

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

List<String> get lastUpdatedInputKeys => _lastUpdates.map((e) => e.inputKey).toList();

/// Formats errors from `inputs`.
String get formattedValidationErrors =>
inputs.map((e) => e.errorFormatted()).where((element) => element.isNotEmpty).join('\n');
Expand All @@ -65,6 +64,7 @@ abstract class GladeModel extends ChangeNotifier {
return '${e.inputKey} - VALID';
}).join('\n');

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

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

_lastUpdates = [input];
lastUpdates = [input];

input.value = value;
notifyListeners();
Expand All @@ -115,9 +115,9 @@ abstract class GladeModel extends ChangeNotifier {
@internal
void notifyInputUpdated(GladeInput<Object?> input) {
if (_groupEdit) {
_lastUpdates.add(input);
lastUpdates.add(input);
} else {
_lastUpdates = [input];
lastUpdates = [input];
notifyDependencies();
notifyListeners();
}
Expand All @@ -138,7 +138,7 @@ abstract class GladeModel extends ChangeNotifier {

/// Notifies dependant inputs about changes.
void notifyDependencies() {
final updatedKeys = _lastUpdates.map((e) => e.inputKey).toSet();
final updatedKeys = lastUpdates.map((e) => e.inputKey).toSet();
for (final input in inputs) {
final updatedKeysExceptInputItself = updatedKeys.difference({input.inputKey});
final union = input.dependencies.map((e) => e.inputKey).toSet().intersection(updatedKeysExceptInputItself);
Expand Down
42 changes: 42 additions & 0 deletions glade_forms/lib/src/model/glade_model_base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:flutter/foundation.dart';
import 'package:glade_forms/src/src.dart';
import 'package:glade_forms/src/validator/validator_result.dart';

abstract class GladeModelBase extends ChangeNotifier {
List<GladeInput<Object?>> lastUpdates = [];
final List<GladeComposedModel> _bindedComposeModels = [];

bool get isValid;

bool get isValidWithoutWarnings;

bool get isPure;

bool get isUnchanged;

List<ValidatorResult<Object?>> get validatorResults;

bool get isNotValid => !isValid;

bool get isDirty => !isPure;

List<String> get lastUpdatedInputKeys => lastUpdates.map((e) => e.inputKey).toList();

/// Binds current model to compose model.
void bindToComposedModel(GladeComposedModel model) {
_bindedComposeModels.add(model);
}

/// Unbinds current model from compose model.
bool unbindFromComposedModel(GladeComposedModel model) {
return _bindedComposeModels.remove(model);
}

@override
void dispose() {
for (final composeModel in _bindedComposeModels) {
composeModel.removeModel(this);
}
super.dispose();
}
}
2 changes: 2 additions & 0 deletions glade_forms/lib/src/model/model.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export 'glade_composed_model.dart';
export 'glade_metadata.dart';
export 'glade_model.dart';
export 'glade_model_base.dart';
Loading