Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## 1.2.2
- fixed rendering issue when both `controller` and `onChange` properties are set
- widget now properly manages disposal of externally provided controllers and focus nodes
- added comprehensive tests for controller disposal scenarios
- added example demonstrating external controller with `onChange` callback

## 1.2.1
- added `initialChips` to set initial chips
- added `onChanged` and renamed old `onChanged` to `onChangedTextField`
Expand Down
77 changes: 77 additions & 0 deletions example/lib/example_on_changed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:chips_input_autocomplete/chips_input_autocomplete.dart';
import 'package:chips_input_autocomplete_example/constants.dart';
import 'package:flutter/material.dart';

/// Example demonstrating the use of ChipsInputAutocomplete with an external
/// controller and onChange callback that triggers setState in parent widget.
///
/// This example specifically tests the fix for the issue where rendering went
/// weird when both controller and onChange callback were set.
class ChipsInputExampleOnChanged extends StatefulWidget {
const ChipsInputExampleOnChanged({super.key});

@override
State<ChipsInputExampleOnChanged> createState() =>
_ChipsInputExampleOnChangedState();
}

class _ChipsInputExampleOnChangedState
extends State<ChipsInputExampleOnChanged> {
final ChipsAutocompleteController _controller = ChipsAutocompleteController();
String _selectedChipsText = '';
int _changeCount = 0;

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'External Controller with onChange',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
const Text(
'This example uses an external controller and onChange callback '
'that triggers setState. This previously caused rendering issues '
'but is now fixed.',
style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
),
const SizedBox(height: 16),
Text('Options: ${options.join(', ')}'),
const SizedBox(height: 8),
ChipsInputAutocomplete(
controller: _controller,
options: options,
decorationTextField: const InputDecoration(
hintText: 'Type a fruit...',
),
onChanged: (chips) {
// This setState call previously caused issues
setState(() {
_selectedChipsText = chips?.join(', ') ?? '';
_changeCount++;
});
},
),
const SizedBox(height: 8),
Text('Selected: $_selectedChipsText'),
Text('Changes: $_changeCount'),
],
),
),
),
);
}
}
5 changes: 5 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:async';
import 'package:chips_input_autocomplete_example/example_basic.dart';
import 'package:chips_input_autocomplete_example/example_form.dart';
import 'package:chips_input_autocomplete_example/example_insert_on_select.dart';
import 'package:chips_input_autocomplete_example/example_on_changed.dart';
import 'package:chips_input_autocomplete_example/example_only_options.dart';
import 'package:chips_input_autocomplete_example/constants.dart';
import 'package:chips_input_autocomplete_example/example_options_async_basic.dart';
Expand Down Expand Up @@ -240,6 +241,10 @@ class _HomePageState extends State<HomePage> {
const SizedBox(
height: 16,
),
const ChipsInputExampleOnChanged(),
const SizedBox(
height: 16,
),
const ChipsInputExampleForm(),
],
),
Expand Down
27 changes: 22 additions & 5 deletions lib/src/chips_input_autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,20 @@ class ChipsInputAutocompleteState extends State<ChipsInputAutocomplete> {
late final ChipsAutocompleteController _chipsAutocompleteController;
late final GlobalKey<FormFieldState<List<String>>> _formFieldKey;
late final FocusNode _focusNode;
bool _isControllerInternal = false;
bool _isFocusNodeInternal = false;
String? _errorText;

@override
void initState() {
super.initState();
_chipsAutocompleteController =
widget.controller ?? ChipsAutocompleteController();
if (widget.controller != null) {
_chipsAutocompleteController = widget.controller!;
_isControllerInternal = false;
} else {
_chipsAutocompleteController = ChipsAutocompleteController();
_isControllerInternal = true;
}
if (widget.options != null) {
_chipsAutocompleteController.options = widget.options!;
}
Expand All @@ -262,13 +269,23 @@ class ChipsInputAutocompleteState extends State<ChipsInputAutocomplete> {
_formFieldKey =
widget.formFieldKey ?? GlobalKey<FormFieldState<List<String>>>();
_chipsAutocompleteController.formFieldKey = _formFieldKey;
_focusNode = widget.focusNode ?? FocusNode();
if (widget.focusNode != null) {
_focusNode = widget.focusNode!;
_isFocusNodeInternal = false;
} else {
_focusNode = FocusNode();
_isFocusNodeInternal = true;
}
}

@override
void dispose() {
_chipsAutocompleteController.dispose();
_focusNode.dispose();
if (_isControllerInternal) {
_chipsAutocompleteController.dispose();
}
if (_isFocusNodeInternal) {
_focusNode.dispose();
}
super.dispose();
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: chips_input_autocomplete
description: "A chips input with autocomplete. Ideal for tagging or categorizing user input. Many options to customize the behavior and appearance."
version: 1.2.1
version: 1.2.2
#homepage:
repository: https://github.com/BruckCode/chips_input_autocomplete
issue_tracker: https://github.com/BruckCode/chips_input_autocomplete/issues
Expand Down
97 changes: 97 additions & 0 deletions test/chips_input_autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,101 @@ void main() {
reason:
'Chip not added \nchipsAutocompleteController.chips: ${chipsAutocompleteController.chips} \nchipsAutocompleteController.text: ${chipsAutocompleteController.text}');
});

// Test 3: External controller with onChange should not cause disposal issues
testWidgets(
'Widget with external controller and onChange callback handles rebuilds correctly',
(WidgetTester tester) async {
final chipsAutocompleteController = ChipsAutocompleteController();
int onChangedCallCount = 0;

await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) {
return ChipsInputAutocomplete(
controller: chipsAutocompleteController,
createCharacter: kCreateCharacter,
onChanged: (chips) {
onChangedCallCount++;
// Trigger a rebuild in the parent widget
setState(() {});
},
);
},
),
),
));

// Simulate typing 'test,' to trigger chip addition and onChange
await tester.enterText(find.byType(TextFormField), 'test$kCreateCharacter');
await tester.pump();

// Verify the chip was added
expect(chipsAutocompleteController.chips.contains('test'), true);
expect(onChangedCallCount, 1);

// Simulate adding another chip to verify controller is still functional
await tester.enterText(find.byType(TextFormField), 'test2$kCreateCharacter');
await tester.pump();

// Verify the second chip was added
expect(chipsAutocompleteController.chips.contains('test2'), true);
expect(onChangedCallCount, 2);
});

// Test 4: External controller should not be disposed when widget is disposed
testWidgets('External controller should not be disposed when widget is disposed',
(WidgetTester tester) async {
final chipsAutocompleteController = ChipsAutocompleteController();

await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ChipsInputAutocomplete(
controller: chipsAutocompleteController,
),
),
));

// Verify the widget is present
expect(find.byType(ChipsInputAutocomplete), findsOneWidget);

// Remove the widget from the tree
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: SizedBox(),
),
));

// Verify the controller is still functional after widget disposal
expect(() => chipsAutocompleteController.chips, returnsNormally);
expect(() => chipsAutocompleteController.addChip('test'), returnsNormally);
expect(chipsAutocompleteController.chips.contains('test'), true);

// Clean up
chipsAutocompleteController.dispose();
});

// Test 5: Internal controller should be disposed when widget is disposed
testWidgets('Internal controller should be disposed when widget is disposed',
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: ChipsInputAutocomplete(),
),
));

// Verify the widget is present
expect(find.byType(ChipsInputAutocomplete), findsOneWidget);

// Remove the widget from the tree
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: SizedBox(),
),
));

// Widget disposal should not throw any errors
// (Internal controller is properly disposed)
});
}