Line data Source code
1 : library apptive_grid_form;
2 :
3 : import 'package:apptive_grid_core/apptive_grid_core.dart';
4 : import 'package:apptive_grid_form/translation/apptive_grid_localization.dart';
5 : import 'package:apptive_grid_form/widgets/apptive_grid_form_widgets.dart';
6 : import 'package:flutter/material.dart';
7 : import 'package:flutter/widgets.dart';
8 : import 'package:lottie/lottie.dart';
9 :
10 : export 'package:apptive_grid_core/apptive_grid_core.dart';
11 : export 'package:apptive_grid_form/translation/apptive_grid_localization.dart';
12 : export 'package:apptive_grid_form/translation/apptive_grid_translation.dart';
13 :
14 : /// A Widget to display a ApptiveGrid Form
15 : ///
16 : /// In order to use this there needs to be a [ApptiveGrid] Widget in the Widget tree
17 : class ApptiveGridForm extends StatefulWidget {
18 : /// Creates a ApptiveGridForm.
19 : ///
20 : /// The [formId] determines what Form is displayed. It works with empty and pre-filled forms.
21 2 : const ApptiveGridForm({
22 : Key? key,
23 : required this.formUri,
24 : this.titleStyle,
25 : this.contentPadding,
26 : this.titlePadding,
27 : this.hideTitle = false,
28 : this.onFormLoaded,
29 : this.onActionSuccess,
30 : this.onError,
31 2 : }) : super(key: key);
32 :
33 : /// [FormUri] of the Form to display
34 : ///
35 : /// If you copied the id from a EditLink or Preview Window on apptivegrid you should use:
36 : /// [FormUri..fromRedirect] with the id
37 : /// If you display Data gathered from a Grid you more likely want to use [FormUri..directForm]
38 : final FormUri formUri;
39 :
40 : /// Style for the Form Title. If no style is provided [headline5] of the [TextTheme] will be used
41 : final TextStyle? titleStyle;
42 :
43 : /// Padding of the Items in the Form. If no Padding is provided a EdgeInsets.all(8.0) will be used.
44 : final EdgeInsets? contentPadding;
45 :
46 : /// Padding for the title. If no Padding is provided the [contentPadding] is used
47 : final EdgeInsets? titlePadding;
48 :
49 : /// Flag to hide the form title, default is false
50 : final bool hideTitle;
51 :
52 : /// Callback after [FormData] loads successfully
53 : ///
54 : /// Use this to modify the UI Displaying the Form
55 : /// ```dart
56 : /// ApptiveGridForm(
57 : /// id: [YOUR_FORM_ID],
58 : /// onFormLoaded: (data) {
59 : /// setState(() {
60 : /// title = data.title;
61 : /// });
62 : /// }
63 : /// ),
64 : /// ```
65 : final void Function(FormData)? onFormLoaded;
66 :
67 : /// Callback after [FormAction] completes Successfully
68 : ///
69 : /// If this returns false the default success screen is not shown.
70 : /// This functionality can be used to do a custom Widget or Transition
71 : final Future<bool> Function(FormAction, FormData)? onActionSuccess;
72 :
73 : /// Callback if an Error occurs
74 : ///
75 : /// If this returns false the default error screen is not shown.
76 : /// This functionality can be used to do a custom Widget or Transition
77 : final Future<bool> Function(dynamic)? onError;
78 :
79 2 : @override
80 2 : ApptiveGridFormState createState() => ApptiveGridFormState();
81 : }
82 :
83 : /// [State] for an [ApptiveGridForm]. Use this to access [currentData] to get the most up to date version
84 : class ApptiveGridFormState extends State<ApptiveGridForm> {
85 : FormData? _formData;
86 : late ApptiveGridClient _client;
87 :
88 : dynamic _error;
89 :
90 : final _dataKey = GlobalKey<ApptiveGridFormDataState>();
91 :
92 : /// Returns the data currently being edited
93 4 : FormData? get currentData => _dataKey.currentState?.currentData;
94 :
95 2 : @override
96 : void didChangeDependencies() {
97 2 : super.didChangeDependencies();
98 6 : _client = ApptiveGrid.getClient(context);
99 2 : _loadForm();
100 : }
101 :
102 2 : @override
103 : Widget build(BuildContext context) {
104 2 : return ApptiveGridFormData(
105 2 : key: _dataKey,
106 2 : formData: _formData,
107 2 : error: _error,
108 4 : titleStyle: widget.titleStyle,
109 4 : contentPadding: widget.contentPadding,
110 4 : titlePadding: widget.titlePadding,
111 4 : hideTitle: widget.hideTitle,
112 4 : onActionSuccess: widget.onActionSuccess,
113 4 : onError: widget.onError,
114 2 : triggerReload: _loadForm,
115 : );
116 : }
117 :
118 2 : void _loadForm() {
119 12 : _client.loadForm(formUri: widget.formUri).then((value) {
120 4 : if (widget.onFormLoaded != null) {
121 2 : widget.onFormLoaded!(value);
122 : }
123 4 : setState(() {
124 2 : _formData = value;
125 : });
126 3 : }).catchError((error) {
127 1 : _onError(error);
128 : });
129 : }
130 :
131 1 : void _onError(dynamic error) async {
132 3 : if (await widget.onError?.call(error) ?? true) {
133 2 : setState(() {
134 1 : _error = error;
135 : });
136 : }
137 : }
138 : }
139 :
140 : /// A Widget to display [FormData]
141 : class ApptiveGridFormData extends StatefulWidget {
142 : /// Creates a Widget to display [formData]
143 : ///
144 : /// if [error] is not null it will display a error
145 2 : const ApptiveGridFormData({
146 : Key? key,
147 : this.formData,
148 : this.error,
149 : this.titleStyle,
150 : this.contentPadding,
151 : this.titlePadding,
152 : this.hideTitle = false,
153 : this.onActionSuccess,
154 : this.onError,
155 : this.triggerReload,
156 2 : }) : super(key: key);
157 :
158 : /// [FormData] that should be displayed
159 : final FormData? formData;
160 :
161 : /// Error that should be displayed. Having a error will have priority over [formData]
162 : final dynamic error;
163 :
164 : /// Style for the Form Title. If no style is provided [headline5] of the [TextTheme] will be used
165 : final TextStyle? titleStyle;
166 :
167 : /// Padding of the Items in the Form. If no Padding is provided a EdgeInsets.all(8.0) will be used.
168 : final EdgeInsets? contentPadding;
169 :
170 : /// Padding for the title. If no Padding is provided the [contentPadding] is used
171 : final EdgeInsets? titlePadding;
172 :
173 : /// Flag to hide the form title, default is false
174 : final bool hideTitle;
175 :
176 : /// Callback after [FormAction] completes Successfully
177 : ///
178 : /// If this returns false the default success screen is not shown.
179 : /// This functionality can be used to do a custom Widget or Transition
180 : final Future<bool> Function(FormAction, FormData)? onActionSuccess;
181 :
182 : /// Callback if an Error occurs
183 : ///
184 : /// If this returns false the default error screen is not shown.
185 : /// This functionality can be used to do a custom Widget or Transition
186 : final Future<bool> Function(dynamic)? onError;
187 :
188 : /// Will be called when [formData] should be reloaded
189 : final void Function()? triggerReload;
190 :
191 2 : @override
192 2 : ApptiveGridFormDataState createState() => ApptiveGridFormDataState();
193 : }
194 :
195 : /// [State] for [ApptiveGridFormData]
196 : ///
197 : /// Use this to access [currentData]
198 : class ApptiveGridFormDataState extends State<ApptiveGridFormData> {
199 : FormData? _formData;
200 : late ApptiveGridClient _client;
201 :
202 : final _formKey = GlobalKey<FormState>();
203 :
204 : bool _success = false;
205 :
206 : dynamic _error;
207 :
208 : bool _saved = false;
209 :
210 : /// Returns the current [FormData] held in this Widget
211 1 : FormData? get currentData {
212 2 : if (!_success && !_saved) {
213 1 : return _formData;
214 : } else {
215 : return null;
216 : }
217 : }
218 :
219 2 : @override
220 : void didUpdateWidget(covariant ApptiveGridFormData oldWidget) {
221 2 : super.didUpdateWidget(oldWidget);
222 2 : _updateView();
223 : }
224 :
225 2 : @override
226 : void didChangeDependencies() {
227 2 : super.didChangeDependencies();
228 6 : _client = ApptiveGrid.getClient(context);
229 : }
230 :
231 2 : void _updateView({
232 : bool resetFormData = true,
233 : }) {
234 4 : setState(() {
235 : if (resetFormData) {
236 6 : _formData = widget.formData != null
237 8 : ? FormData.fromJson(widget.formData!.toJson())
238 : : null;
239 : }
240 6 : _error = widget.error;
241 2 : _success = false;
242 2 : _saved = false;
243 : });
244 : }
245 :
246 2 : @override
247 : Widget build(BuildContext context) {
248 2 : return ApptiveGridLocalization(
249 2 : child: Builder(
250 2 : builder: (buildContext) {
251 2 : if (_error != null) {
252 1 : return _buildError(buildContext);
253 2 : } else if (_saved) {
254 1 : return _buildSaved(buildContext);
255 2 : } else if (_success) {
256 2 : return _buildSuccess(buildContext);
257 2 : } else if (_formData == null) {
258 2 : return _buildLoading(buildContext);
259 : } else {
260 4 : return _buildForm(buildContext, _formData!);
261 : }
262 : },
263 : ),
264 : );
265 : }
266 :
267 2 : Widget _buildLoading(BuildContext context) {
268 : return const Center(
269 : child: CircularProgressIndicator(),
270 : );
271 : }
272 :
273 2 : Widget _buildForm(BuildContext context, FormData data) {
274 2 : final localization = ApptiveGridLocalization.of(context)!;
275 2 : return Form(
276 2 : key: _formKey,
277 2 : child: ListView.builder(
278 12 : itemCount: 1 + data.components.length + data.actions.length,
279 2 : itemBuilder: (context, index) {
280 : // Title
281 2 : if (index == 0) {
282 4 : if (widget.hideTitle) {
283 : return const SizedBox();
284 : } else {
285 2 : return Padding(
286 4 : padding: widget.titlePadding ??
287 4 : widget.contentPadding ??
288 2 : _defaultPadding,
289 2 : child: Text(
290 2 : data.title,
291 4 : style: widget.titleStyle ??
292 6 : Theme.of(context).textTheme.headline5,
293 : ),
294 : );
295 : }
296 8 : } else if (index < data.components.length + 1) {
297 1 : final componentIndex = index - 1;
298 1 : return Padding(
299 3 : padding: widget.contentPadding ?? _defaultPadding,
300 3 : child: fromModel(data.components[componentIndex]),
301 : );
302 : } else {
303 8 : final actionIndex = index - 1 - data.components.length;
304 2 : return ActionButton(
305 4 : action: data.actions[actionIndex],
306 2 : onPressed: _performAction,
307 4 : child: Text(localization.actionSend),
308 : );
309 : }
310 : },
311 : ),
312 : );
313 : }
314 :
315 2 : Widget _buildSuccess(BuildContext context) {
316 2 : final localization = ApptiveGridLocalization.of(context)!;
317 2 : return ListView(
318 : padding: const EdgeInsets.all(32.0),
319 2 : children: [
320 2 : AspectRatio(
321 : aspectRatio: 1,
322 2 : child: Lottie.asset(
323 : 'packages/apptive_grid_form/assets/success.json',
324 : repeat: false,
325 : ),
326 : ),
327 2 : Text(
328 2 : localization.sendSuccess,
329 : textAlign: TextAlign.center,
330 6 : style: Theme.of(context).textTheme.headline4,
331 : ),
332 2 : Center(
333 2 : child: TextButton(
334 1 : onPressed: () {
335 2 : widget.triggerReload?.call();
336 1 : _updateView();
337 : },
338 2 : child: Text(
339 2 : localization.additionalAnswer,
340 : ),
341 : ),
342 : )
343 : ],
344 : );
345 : }
346 :
347 1 : Widget _buildSaved(BuildContext context) {
348 1 : final localization = ApptiveGridLocalization.of(context)!;
349 1 : return ListView(
350 : padding: const EdgeInsets.all(32.0),
351 1 : children: [
352 1 : AspectRatio(
353 : aspectRatio: 1,
354 1 : child: Lottie.asset(
355 : 'packages/apptive_grid_form/assets/saved.json',
356 : repeat: false,
357 : ),
358 : ),
359 1 : Text(
360 1 : localization.savedForLater,
361 : textAlign: TextAlign.center,
362 3 : style: Theme.of(context).textTheme.headline4,
363 : ),
364 1 : Center(
365 1 : child: TextButton(
366 1 : onPressed: () {
367 2 : widget.triggerReload?.call();
368 1 : _updateView();
369 : },
370 2 : child: Text(localization.additionalAnswer),
371 : ),
372 : )
373 : ],
374 : );
375 : }
376 :
377 1 : Widget _buildError(BuildContext context) {
378 1 : final localization = ApptiveGridLocalization.of(context)!;
379 1 : return ListView(
380 : padding: const EdgeInsets.all(32.0),
381 1 : children: [
382 1 : AspectRatio(
383 : aspectRatio: 1,
384 1 : child: Lottie.asset(
385 : 'packages/apptive_grid_form/assets/error.json',
386 : repeat: false,
387 : ),
388 : ),
389 1 : Text(
390 1 : localization.errorTitle,
391 : textAlign: TextAlign.center,
392 3 : style: Theme.of(context).textTheme.headline4,
393 : ),
394 1 : Center(
395 1 : child: TextButton(
396 1 : onPressed: () {
397 2 : widget.triggerReload?.call();
398 1 : _updateView(resetFormData: false);
399 : },
400 2 : child: Text(localization.backToForm),
401 : ),
402 : )
403 : ],
404 : );
405 : }
406 :
407 2 : EdgeInsets get _defaultPadding => const EdgeInsets.all(8.0);
408 :
409 2 : void _performAction(FormAction action) {
410 6 : if (_formKey.currentState!.validate()) {
411 10 : _client.performAction(action, _formData!).then((response) async {
412 4 : if (response.statusCode < 400) {
413 7 : if (await widget.onActionSuccess?.call(action, _formData!) ?? true) {
414 4 : setState(() {
415 2 : _success = true;
416 : });
417 : }
418 : } else {
419 : // FormData was saved to [ApptiveGridCache]
420 1 : _onSavedOffline();
421 : }
422 3 : }).catchError((error) {
423 1 : _onError(error);
424 : });
425 : }
426 : }
427 :
428 1 : void _onSavedOffline() {
429 1 : if (mounted) {
430 2 : setState(() {
431 1 : _saved =
432 4 : ApptiveGrid.getClient(context, listen: false).options.cache != null;
433 : });
434 : }
435 : }
436 :
437 1 : void _onError(dynamic error) async {
438 3 : if (await widget.onError?.call(error) ?? true) {
439 2 : setState(() {
440 1 : _error = error;
441 : });
442 : }
443 : }
444 : }
|