diff --git a/samples/form.dart b/samples/form.dart new file mode 100644 index 000000000..b8742e065 --- /dev/null +++ b/samples/form.dart @@ -0,0 +1,307 @@ +import 'package:flutter/material.dart'; + +// Sample entity classes +class ClientEntity { + ClientEntity({this.name, this.contacts}); + String name; + List contacts; +} + +class ContactEntity { + ContactEntity({this.email}); + String email; +} + +void main() => runApp(MyApp()); + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State with SingleTickerProviderStateMixin { + static final GlobalKey _formKey = GlobalKey(); + static final GlobalKey _clientKey = + GlobalKey(); + static final GlobalKey _contactsKey = + GlobalKey(); + + 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 + ClientEntity _client = ClientEntity( + name: 'Acme Client', + contacts: [ContactEntity(email: 'test@example.com')]); + + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: Text('Client Form'), + actions: [ + IconButton( + icon: Icon(Icons.cloud_upload), + onPressed: () { + if (!_formKey.currentState.validate()) { + return; + } + + _formKey.currentState.save(); + + var clientState = _clientKey.currentState; + var contactsState = _contactsKey.currentState; + + // If the user never views a tab the state can be null + // in which case we'll use the current value + 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: [ + ClientPage(client: _client, key: _clientKey), + ContactsPage(client: _client, key: _contactsKey), + ], + ), + ), + ), + ); + } +} + +// Display the client's details, currently just their name +class ClientPage extends StatefulWidget { + ClientPage({ + Key key, + @required this.client, + }) : super(key: key); + + final ClientEntity client; + + @override + ClientPageState createState() => ClientPageState(); +} + +class ClientPageState extends State + with AutomaticKeepAliveClientMixin { + ClientPageState({this.client}); + final ClientEntity client; + + @override + bool get wantKeepAlive => true; + + String name; + + @override + Widget build(BuildContext context) { + return FormCard( + children: [ + 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 { + ContactsPage({ + Key key, + @required this.client, + }) : super(key: key); + + final ClientEntity client; + + @override + ContactsPageState createState() => ContactsPageState(); +} + +class ContactsPageState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + List _contacts; + List> _contactKeys; + + @override + void initState() { + super.initState(); + var client = widget.client; + _contacts = client.contacts.toList(); + _contactKeys = client.contacts + .map((contact) => GlobalKey()) + .toList(); + } + + List getContacts() { + List contacts = []; + _contactKeys.forEach((contactKey) { + contacts.add(contactKey.currentState.getContact()); + }); + return contacts; + } + + _onAddPressed() { + setState(() { + _contacts.add(ContactEntity()); + _contactKeys.add(GlobalKey()); + }); + } + + _onRemovePressed(GlobalKey key) { + setState(() { + var index = _contactKeys.indexOf(key); + _contactKeys.removeAt(index); + _contacts.removeAt(index); + }); + } + + @override + Widget build(BuildContext context) { + List items = []; + + for (var i = 0; i < _contacts.length; i++) { + var contact = _contacts[i]; + var contactKey = _contactKeys[i]; + items.add(ContactForm( + contact: contact, + key: contactKey, + onRemovePressed: (key) => _onRemovePressed(key), + )); + } + + items.add(Padding( + padding: const EdgeInsets.all(12.0), + child: RaisedButton( + elevation: 4.0, + color: Theme.of(context).primaryColor, + textColor: Theme.of(context).secondaryHeaderColor, + child: Text('ADD CONTACT'), + onPressed: _onAddPressed, + ), + )); + + return ListView( + children: items, + ); + } +} + +// Displays an individual contact +class ContactForm extends StatefulWidget { + ContactForm({ + Key key, + @required this.contact, + @required this.onRemovePressed, + }) : super(key: key); + + final ContactEntity contact; + final Function(GlobalKey) onRemovePressed; + + @override + ContactFormState createState() => ContactFormState(); +} + +class ContactFormState extends State { + String _email; + + ContactEntity getContact() { + return ContactEntity(email: _email); + } + + @override + Widget build(BuildContext context) { + return FormCard( + children: [ + TextFormField( + initialValue: widget.contact.email, + onSaved: (value) => _email = value.trim(), + decoration: InputDecoration( + labelText: 'Email', + ), + keyboardType: TextInputType.emailAddress, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: FlatButton( + 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 { + FormCard({this.children}); + final List 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, + ), + ), + ), + ); + } +}