Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
admin-portal/samples/form_keys.dart
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
314 lines (270 sloc)
7.78 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://hillelcoren.com/2018/06/12/flutter-complex-forms-with-multiple-tabs-and-relationships/ | |
import 'package:flutter/material.dart'; | |
import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; | |
import 'package:invoiceninja_flutter/utils/platforms.dart'; | |
// Sample entity classes | |
class ClientEntity { | |
ClientEntity({this.name, this.contacts}); | |
String name; | |
List<ContactEntity> contacts; | |
} | |
class ContactEntity { | |
ContactEntity({this.email}); | |
String email; | |
} | |
void main() => runApp(MyApp()); | |
class MyApp extends StatefulWidget { | |
@override | |
_MyAppState createState() => _MyAppState(); | |
} | |
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin { | |
static final GlobalKey<FormState> _formKey = | |
GlobalKey<FormState>(debugLabel: '_appState'); | |
static final GlobalKey<ClientPageState> _clientKey = | |
GlobalKey<ClientPageState>(); | |
static final GlobalKey<ContactsPageState> _contactsKey = | |
GlobalKey<ContactsPageState>(); | |
TabController _controller; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = TabController(vsync: this, length: 2); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
// Create a test client to show initially | |
final ClientEntity _client = ClientEntity( | |
name: 'Acme Client', | |
contacts: [ContactEntity(email: 'test@example.com')]); | |
return MaterialApp( | |
home: Scaffold( | |
appBar: AppBar( | |
centerTitle: false, | |
automaticallyImplyLeading: isMobile(context), | |
title: Text('Client Form'), | |
actions: <Widget>[ | |
IconButton( | |
icon: Icon(Icons.cloud_upload), | |
onPressed: () { | |
if (!_formKey.currentState.validate()) { | |
return; | |
} | |
_formKey.currentState.save(); | |
final clientState = _clientKey.currentState; | |
final contactsState = _contactsKey.currentState; | |
// If the user never views a tab the state can be null | |
// in which case we'll use the current value | |
final ClientEntity client = ClientEntity( | |
name: clientState?.name ?? _client.name, | |
contacts: contactsState?.getContacts() ?? _client.contacts, | |
); | |
// Do something with the client... | |
print('Client name: ${client.name}'); | |
}, | |
) | |
], | |
bottom: TabBar( | |
controller: _controller, | |
tabs: [ | |
Tab( | |
text: 'Details', | |
), | |
Tab( | |
text: 'Contacts', | |
), | |
], | |
), | |
), | |
body: Form( | |
key: _formKey, | |
child: TabBarView( | |
controller: _controller, | |
children: <Widget>[ | |
ClientPage(client: _client, key: _clientKey), | |
ContactsPage(client: _client, key: _contactsKey), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
// Display the client's details, currently just their name | |
class ClientPage extends StatefulWidget { | |
const ClientPage({ | |
Key key, | |
@required this.client, | |
}) : super(key: key); | |
final ClientEntity client; | |
@override | |
ClientPageState createState() => ClientPageState(); | |
} | |
class ClientPageState extends State<ClientPage> | |
with AutomaticKeepAliveClientMixin { | |
ClientPageState({this.client}); | |
final ClientEntity client; | |
@override | |
bool get wantKeepAlive => true; | |
String name; | |
@override | |
Widget build(BuildContext context) { | |
super.build(context); | |
return FormCard( | |
children: <Widget>[ | |
TextFormField( | |
initialValue: widget.client.name, | |
onSaved: (value) => name = value.trim(), | |
decoration: InputDecoration( | |
labelText: 'Name', | |
), | |
), | |
], | |
); | |
} | |
} | |
// Displays the list of contacts with a button to add more | |
class ContactsPage extends StatefulWidget { | |
const ContactsPage({ | |
Key key, | |
@required this.client, | |
}) : super(key: key); | |
final ClientEntity client; | |
@override | |
ContactsPageState createState() => ContactsPageState(); | |
} | |
class ContactsPageState extends State<ContactsPage> | |
with AutomaticKeepAliveClientMixin { | |
@override | |
bool get wantKeepAlive => true; | |
List<ContactEntity> _contacts; | |
List<GlobalKey<ContactFormState>> _contactKeys; | |
@override | |
void initState() { | |
super.initState(); | |
final client = widget.client; | |
_contacts = client.contacts.toList(); | |
_contactKeys = client.contacts | |
.map((contact) => GlobalKey<ContactFormState>()) | |
.toList(); | |
} | |
List<ContactEntity> getContacts() { | |
final List<ContactEntity> contacts = []; | |
_contactKeys.forEach((contactKey) { | |
contacts.add(contactKey.currentState.getContact()); | |
}); | |
return contacts; | |
} | |
void _onAddPressed() { | |
setState(() { | |
_contacts.add(ContactEntity()); | |
_contactKeys.add(GlobalKey<ContactFormState>()); | |
}); | |
} | |
void _onRemovePressed(GlobalKey<ContactFormState> key) { | |
setState(() { | |
final index = _contactKeys.indexOf(key); | |
_contactKeys.removeAt(index); | |
_contacts.removeAt(index); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
super.build(context); | |
final List<Widget> items = []; | |
for (var i = 0; i < _contacts.length; i++) { | |
final contact = _contacts[i]; | |
final contactKey = _contactKeys[i]; | |
items.add(ContactForm( | |
contact: contact, | |
key: contactKey, | |
onRemovePressed: (key) => _onRemovePressed(key), | |
)); | |
} | |
items.add(Padding( | |
padding: const EdgeInsets.all(16), | |
child: ElevatedButton( | |
child: Text('ADD CONTACT'), | |
onPressed: _onAddPressed, | |
), | |
)); | |
return ScrollableListView( | |
children: items, | |
); | |
} | |
} | |
// Displays an individual contact | |
class ContactForm extends StatefulWidget { | |
const ContactForm({ | |
Key key, | |
@required this.contact, | |
@required this.onRemovePressed, | |
}) : super(key: key); | |
final ContactEntity contact; | |
final Function(GlobalKey<ContactFormState>) onRemovePressed; | |
@override | |
ContactFormState createState() => ContactFormState(); | |
} | |
class ContactFormState extends State<ContactForm> { | |
String _email; | |
ContactEntity getContact() { | |
return ContactEntity(email: _email); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return FormCard( | |
children: <Widget>[ | |
TextFormField( | |
initialValue: widget.contact.email, | |
onSaved: (value) => _email = value.trim(), | |
decoration: InputDecoration( | |
labelText: 'Email', | |
), | |
keyboardType: TextInputType.emailAddress, | |
), | |
Row( | |
mainAxisAlignment: MainAxisAlignment.end, | |
children: <Widget>[ | |
Padding( | |
padding: const EdgeInsets.only(top: 12.0), | |
child: TextButton( | |
onPressed: () => widget.onRemovePressed(widget.key), | |
child: Text( | |
'Delete', | |
style: TextStyle( | |
color: Colors.grey[600], | |
), | |
), | |
), | |
) | |
], | |
), | |
], | |
); | |
} | |
} | |
// Helper widget to make the form look a bit nicer | |
class FormCard extends StatelessWidget { | |
const FormCard({this.children}); | |
final List<Widget> children; | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.only(left: 12.0, top: 12.0, right: 12.0), | |
child: Card( | |
elevation: 2.0, | |
child: Padding( | |
padding: const EdgeInsets.only( | |
left: 12.0, right: 12.0, top: 12.0, bottom: 18.0), | |
child: Column( | |
children: children, | |
), | |
), | |
), | |
); | |
} | |
} |