]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
Improvements of the model
authorHamatoma <author.hamatoma.de>
Thu, 1 Oct 2020 00:09:28 +0000 (02:09 +0200)
committerHamatoma <author@hamatoma.de>
Thu, 1 Oct 2020 00:10:43 +0000 (02:10 +0200)
29 files changed:
.gitignore
Coverage.sh [new file with mode: 0755]
dartdoc_options.yaml [new file with mode: 0644]
lib/flutter_bones.dart
lib/src/helper/settings.dart
lib/src/helper/string_helper.dart [new file with mode: 0644]
lib/src/helper/validators.dart
lib/src/model/button_model.dart [new file with mode: 0644]
lib/src/model/checkbox_model.dart [new file with mode: 0644]
lib/src/model/combobox_model.dart [new file with mode: 0644]
lib/src/model/empty_line_model.dart [new file with mode: 0644]
lib/src/model/field_model.dart
lib/src/model/model_base.dart
lib/src/model/model_types.dart
lib/src/model/module/user_model.dart
lib/src/model/module_model.dart
lib/src/model/page_model.dart
lib/src/model/section_model.dart
lib/src/model/text_field_model.dart [new file with mode: 0644]
lib/src/model/text_model.dart [new file with mode: 0644]
lib/src/model/widget_model.dart
lib/src/widget/raised_button_bone.dart [new file with mode: 0644]
lib/src/widget/simple_form.dart [new file with mode: 0644]
lib/src/widget/simpleform.dart [deleted file]
lib/src/widget/view.dart [new file with mode: 0644]
test/helpers/settings_test.dart
test/helpers/string_helper_test.dart [new file with mode: 0644]
test/helpers/validators_test.dart
test/model/model_test.dart [new file with mode: 0644]

index 95b551c429fca61f6da31d64a89559ab2e19464f..77fcaacafac68cf6861fd06a00389ce4a629954e 100644 (file)
@@ -8,6 +8,7 @@
 .buildlog/
 .history
 .svn/
+coverage/
 pubspec.lock
 
 # wk: @ToDo: nicht sicher
diff --git a/Coverage.sh b/Coverage.sh
new file mode 100755 (executable)
index 0000000..fa9044d
--- /dev/null
@@ -0,0 +1,4 @@
+#! /bin/sh
+flutter test --coverage
+rm -Rf coverage/html/*
+genhtml --show-details --output-directory=coverage/html coverage/lcov.info
diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml
new file mode 100644 (file)
index 0000000..afb6933
--- /dev/null
@@ -0,0 +1,4 @@
+dartdoc:
+  linkToSource:
+    root: '.'
+    uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v0.34.0/%f%#L%l%'
index 82ea4cdab6d7a29fd269af8b043e93843b7f2132..8d645ba22908389b6fa6d79824e04c4cfcf18189 100644 (file)
@@ -1,14 +1,27 @@
-export 'src/widget/simpleform.dart';
+/// Helper definitions for flutter applications.
+///
+/// Allows a model driven design of flutter apps:
+/// The design of the screens is done by a yaml file (or a compatible map).
 export 'src/helper/settings.dart';
+export 'src/helper/string_helper.dart';
 export 'src/helper/validators.dart';
-export 'src/page/page_data.dart';
-export 'src/page/login_page.dart';
-export 'src/page/role_page.dart';
-export 'src/page/user_page.dart';
-export 'src/model/model_types.dart';
-export 'src/model/model_base.dart';
-export 'src/model/widget_model.dart';
+export 'src/model/button_model.dart';
+export 'src/model/checkbox_model.dart';
+export 'src/model/combobox_model.dart';
+export 'src/model/empty_line_model.dart';
 export 'src/model/field_model.dart';
+export 'src/model/model_base.dart';
+export 'src/model/model_types.dart';
 export 'src/model/module_model.dart';
+export 'src/model/page_model.dart';
 export 'src/model/section_model.dart';
-export 'src/model/page_model.dart';
\ No newline at end of file
+export 'src/model/text_field_model.dart';
+export 'src/model/text_model.dart';
+export 'src/model/widget_model.dart';
+export 'src/page/login_page.dart';
+export 'src/page/page_data.dart';
+export 'src/page/role_page.dart';
+export 'src/page/user_page.dart';
+export 'src/widget/raised_button_bone.dart';
+export 'src/widget/simple_form.dart';
+export 'src/widget/view.dart';
index 59b75ca65dece11e7e5c708b7fa6987cad389a68..82523e06c42f9e3ca4e782e6a82666f74fd2c507 100644 (file)
@@ -1,26 +1,57 @@
+import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter/material.dart';
 
 class Settings {
   static Locale locale = Locale('US', 'en');
 
+  static final mapWidgetData = <String, dynamic>{
+    'form.card.padding': '16.0',
+    'form.gap.field_button.height': '16.0',
+  };
+  static Settings _instance;
+
+  final BaseConfiguration widgetConfiguration;
+
+  final BaseLogger logger;
+
+  factory Settings(BaseLogger logger, {BaseConfiguration widgetConfiguration}) {
+    if (_instance == null) {
+      _instance = Settings.internal(
+          widgetConfiguration == null
+              ? BaseConfiguration(mapWidgetData, logger)
+              : widgetConfiguration,
+          logger);
+    }
+    return _instance;
+  }
+
+  Settings.internal(this.widgetConfiguration, this.logger) {
+    BaseConfiguration widgetConfiguration =
+        BaseConfiguration(mapWidgetData, logger);
+  }
+
   /// Sets the locale code.
   /// [locale] the info about localisation
   /// Finding [locale] of an app:  @see https://github.com/flutter/website/blob/master/examples/internationalization/minimal/lib/main.dart
   static setLocale(locale) => Settings.locale = locale;
-  static setLocaleByNames({String country='US', String language='en'}) => Settings.locale = Locale(country, language);
+
+  static setLocaleByNames({String country = 'US', String language = 'en'}) =>
+      Settings.locale = Locale(country, language);
+
   /// Translates a [text] with a given translation [map].
   /// Structure of the [map]: { <English text> : { <language code> : translation } }
   /// return: [text] if no translation has been found, otherwise: the translation from the [map]
-  static String translate(String text, Map<String, Map<String, String>> map, {Map<String, String> placeholders}){
+  static String translate(String text, Map<String, Map<String, String>> map,
+      {Map<String, String> placeholders}) {
     var rc = text;
-    if (map.containsKey(text) && map[text].containsKey(locale.languageCode)){
-      rc =  map[text][locale.languageCode];
+    if (map.containsKey(text) && map[text].containsKey(locale.languageCode)) {
+      rc = map[text][locale.languageCode];
     }
-    if (placeholders != null){
+    if (placeholders != null) {
       placeholders.keys.forEach((key) {
         rc = rc.replaceAll('%{$key}', placeholders[key]);
       });
     }
     return rc;
   }
-}
\ No newline at end of file
+}
diff --git a/lib/src/helper/string_helper.dart b/lib/src/helper/string_helper.dart
new file mode 100644 (file)
index 0000000..35ce393
--- /dev/null
@@ -0,0 +1,46 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class StringHelper {
+  static final regExpTrue = RegExp(r'^(true|yes)$', caseSensitive: false);
+  static final regExpFalse = RegExp(r'^(false|no)$', caseSensitive: false);
+
+  /// Converts a string to a given [dataType].
+  static dynamic fromString(String value, DataType dataType) {
+    dynamic rc;
+    if (dataType == DataType.string) {
+      rc = value;
+    } else if (value != null && value.isNotEmpty) {
+      switch (dataType) {
+        case DataType.bool:
+          if (regExpTrue.firstMatch(value) != null) {
+            rc = true;
+          } else if (regExpFalse.firstMatch(value) != null) {
+            rc = false;
+          } else {
+            rc = null;
+          }
+          break;
+        case DataType.string:
+          rc = value;
+          break;
+        case DataType.currency:
+        case DataType.float:
+          rc = StringUtils.asFloat(value);
+          break;
+        case DataType.date:
+        case DataType.dateTime:
+          rc = StringUtils.stringToDateTime(value);
+          break;
+        case DataType.int:
+        case DataType.reference:
+          rc = StringUtils.asInt(value);
+          break;
+        default:
+          rc = '<StringHelper.fromString(): unknown datatype $dataType>';
+          break;
+      }
+    }
+    return rc;
+  }
+}
index fee37c3b2a84527ae1195ceeadf0f95c64866f10..7bb567b19f7c4a0508852921544756529012f0dd 100644 (file)
@@ -3,14 +3,15 @@ import 'package:flutter_bones/flutter_bones.dart';
 import 'package:meta/meta.dart';
 
 /// Tests whether [input] is an valid email address.
-/// returns null if not empty, otherwise an error message
+/// Returns null if it is a valid email address, otherwise an error message.
 String checkEmail(String input) => Validation.isEmail(input)
     ? null
     : _vt('Not an email address: %{0} Example: joe@example.com', {'0': input});
 
-/// returns null if not empty, otherwise an error message
+/// Tests whether [input] is an integer.
+/// Returns null for an integer otherwise an error message.
 String checkInt(String input) =>
-    Validation.isInt(input) ? null : _vt('Not a number: %{0}', {'0': input});
+    Validation.isInt(input) ? null : _vt('Not an integer: %{0}', {'0': input});
 
 /// Validates an [input] with many [validators].
 String checkMany(String input, List<Function> validators) {
@@ -25,21 +26,20 @@ String checkMany(String input, List<Function> validators) {
   return rc;
 }
 
-/// returns null if not empty, otherwise an error message
+/// Tests whether [input] is a natural number (a non negative integer).
+/// Returns null if it is a nat, otherwise an error message.
 String checkNat(String input) => Validation.isNat(input)
     ? null
     : _vt('Not a not negative number: %{0}', {'0': input});
 
-// Tests whether [input] is a natural number (>= 0).
 /// Tests whether [input] is not empty.
-/// returns null if not empty, otherwise an error message
+/// Returns null if it is not empty, otherwise an error message.
 String checkNotEmpty(String input) {
   return input.isEmpty ? _vt('Please fill in') : null;
 }
 
-// Tests whether [input] is an integer (>= 0).
 /// Tests whether [input] is an valid phone number.
-/// returns null if not empty, otherwise an error message
+/// Returns null if it is a phone number, otherwise an error message.
 String checkPhoneNumber(String input) => Validation.isPhoneNumber(input)
     ? null
     : _vt('Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"',
@@ -55,15 +55,15 @@ class ValidatorTranslations {
     'Please fill in': {
       'de': 'Bitte ausfüllen',
     },
-    'Not a number: %{0}': {
-      'de': 'Keine Zahl: %{0}',
+    'Not an integer: %{0}': {
+      'de': 'Keine ganze Zahl: %{0}',
     },
     'Not an email address: %{0} Example: joe@example.com': {
       'de': 'Keine gültige EMailadresse: %{0} Beispiel: joe@example.com',
     },
     'Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"': {
       'de':
-          'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"',
+      'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"',
     },
   };
 }
diff --git a/lib/src/model/button_model.dart b/lib/src/model/button_model.dart
new file mode 100644 (file)
index 0000000..a8eea1a
--- /dev/null
@@ -0,0 +1,48 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+enum ButtonModelType {
+  cancel,
+  custom,
+  search,
+  store,
+}
+
+typedef ButtonOnPressed = void Function(String name);
+
+class ButtonModel extends WidgetModel {
+  static final regExprOptions = RegExp(r'^(undef)$');
+  String text;
+  String name;
+  final Map<String, dynamic> map;
+  List<String> options;
+  ButtonModelType buttonModelType;
+
+  ButtonModel(SectionModel section, PageModel page, this.map, BaseLogger logger)
+      : super(section, page, WidgetModelType.button, logger);
+
+  VoidCallback onPressed;
+  VoidCallback onLongPressed;
+  ValueChanged<bool> onHighlightChanged;
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${section.name}.$name';
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'buttonType name options text widgetType'.split(' '));
+    name = parseString('name', map, required: true);
+    text = parseString('text', map);
+    buttonModelType =
+        parseEnum<ButtonModelType>('buttonType', map, ButtonModelType.values);
+    buttonModelType ??= ButtonModelType.custom;
+    options = parseOptions('options', map);
+    onPressed = () => logger.error('${fullName()}: missing onPressed');
+    checkOptionsByRegExpr(options, regExprOptions);
+  }
+
+  @override
+  String widgetName() => name;
+}
diff --git a/lib/src/model/checkbox_model.dart b/lib/src/model/checkbox_model.dart
new file mode 100644 (file)
index 0000000..1d88809
--- /dev/null
@@ -0,0 +1,20 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class CheckboxModel extends FieldModel {
+  static final regExprOptions = RegExp(r'^(readonly|disabled|required|undef)$');
+
+  final Map<String, dynamic> map;
+
+  CheckboxModel(
+      SectionModel section, PageModel page, this.map, BaseLogger logger)
+      : super(section, page, map, WidgetModelType.checkbox, logger);
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'name label options toolTip widgetType'.split(' '));
+    super.parse();
+    options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
+  }
+}
diff --git a/lib/src/model/combobox_model.dart b/lib/src/model/combobox_model.dart
new file mode 100644 (file)
index 0000000..b393d5f
--- /dev/null
@@ -0,0 +1,32 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class ComboboxModel extends FieldModel {
+  static final regExprOptions = RegExp(r'^(readonly|disabled|required|undef)$');
+  List<String> texts;
+  List<dynamic> values;
+  FormFieldValidator<String> validator;
+  FormFieldSetter onSaved;
+
+  final Map<String, dynamic> map;
+
+  ComboboxModel(
+      SectionModel section, PageModel page, this.map, BaseLogger logger)
+      : super(section, page, map, WidgetModelType.combobox, logger);
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(
+        map,
+        'name label dataType options texts toolTip widgetType values'
+            .split(' '));
+    super.parse();
+    texts = parseStringList('texts', map);
+    values = dataType == DataType.string
+        ? parseStringList('values', map)
+        : parseValueList('values', map, dataType);
+    options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
+  }
+}
diff --git a/lib/src/model/empty_line_model.dart b/lib/src/model/empty_line_model.dart
new file mode 100644 (file)
index 0000000..abee90f
--- /dev/null
@@ -0,0 +1,27 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class EmptyLineModel extends WidgetModel {
+  static final regExprOptions = RegExp(r'^(unknown)$');
+  List<String> options;
+  bool isRichText;
+  final Map<String, dynamic> map;
+
+  EmptyLineModel(
+      SectionModel section, PageModel page, this.map, BaseLogger logger)
+      : super(section, page, WidgetModelType.emptyLine, logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${section.name}.${widgetName()}';
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'options widgetType'.split(' '));
+    options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
+  }
+
+  @override
+  String widgetName() => 'emptyLine$id';
+}
index adb00925a38c88c6202472ffe7ba61480f5fd495..0fed9f8cbc10a6520e68db746886a66e98a18092 100644 (file)
@@ -1,85 +1,52 @@
-import 'package:flutter/material.dart';
 import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_bones/flutter_bones.dart';
-import 'package:meta/meta.dart';
 
-class FieldModel extends ModelBase {
-  static final regExprOptions = RegExp(r'^(undef|readonly|disabled|password|required}');
-  final SectionModel parent;
+class FieldModel extends WidgetModel {
+  static final regExprOptions =
+      RegExp(r'^(undef|readonly|disabled|password|required)$');
   String name;
   String label;
   String toolTip;
-  FieldModelType fieldModelType;
   DataType dataType;
-  int maxSize;
-  int rows;
-  List<String> options;
-  FormFieldValidator<String> validator;
   FormFieldSetter onSaved;
+  List<String> options;
 
   final Map<String, dynamic> map;
+  var _value;
 
-  FieldModel(this.parent, this.map, BaseLogger logger) : super(logger);
+  FieldModel(SectionModel section, PageModel page, this.map,
+      WidgetModelType fieldModelType, BaseLogger logger)
+      : super(section, page, fieldModelType, logger);
+
+  get value => _value;
+
+  /// Stores the value of the field.
+  /// Conversion is done if [value] is a string and [this.dataType] is not.
+  set value(value) {
+    if (dataType != DataType.string && value.runtimeType == String) {
+      _value = StringHelper.fromString(value, dataType);
+    } else {
+      _value = value;
+    }
+  }
 
   /// Returns the name including the names of the parent
   @override
-  String fullName() => '${parent.fullName()}.$name';
+  String fullName() => '${page.name}.$name';
 
   /// Parses the map and stores the data in the instance.
   void parse() {
-    checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' '));
+    checkSuperfluous(
+        map,
+        'fieldType dataType label maxSize name options rows toolTip '
+            .split(' '));
     name = parseString('name', map, required: true);
     label = parseString('label', map, required: false);
     toolTip = parseString('toolTip', map, required: false);
-    fieldModelType = parseEnum<FieldModelType>(
-        'fieldModelType', map, FieldModelType.values);
-    dataType = parseEnum<DataType>(
-        'dataType', map, DataType.values);
-    maxSize = parseInt('maxSize', map, required: false);
-    rows = parseInt('rows', map, required: false);
-    options = parseOptions('options', map);
+    dataType = parseEnum<DataType>('dataType', map, DataType.values);
   }
 
-  /// Tests the validity of the entries in [optionList]
-  /// Errors will be logged.
-  bool checkOptions() {
-    bool rc = false;
-    options.forEach((element) {
-      if (regExprOptions.firstMatch(element) == null) {
-        logger.error('unbekannte Feldoption $element in ${fullName()}');
-        rc = true;
-      }
-    });
-    return rc;
-  }
-  /// Returns the widget representing the field.
-  Widget widget(Key formKey){
-    Widget rc;
-    switch(fieldModelType){
-      case FieldModelType.text:
-        rc = TextFormField(key: formKey,
-          validator: validator,
-          decoration: InputDecoration(labelText: label),
-          onSaved: onSaved,
-          maxLength: maxSize,
-          maxLines: rows,
-          readOnly: options.contains('readonly'),
-          obscureText: options.contains('password'),
-        );
-        break;
-      case FieldModelType.checkbox:
-        rc = Checkbox(key: formKey,
-          decoration: InputDecoration(labelText: label),
-          onSaved: onSaved,
-          maxLength: maxSize,
-          maxLines: rows,
-          readOnly: options.contains('readonly'),
-          obscureText: options.contains('password'),
-        );
-        break;
-    }
-    return rc;
-  }
+  @override
+  String widgetName() => name;
 }
-
-enum FieldModelType { checkbox, combobox, image, text, }
index ff06252bfaaa017fc7c0d61b9773c59aede92071..b379ef267c221cf71bca6e9ce5a89cffe4ddafdc 100644 (file)
@@ -1,22 +1,37 @@
-import 'package:flutter/material.dart';
 import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_bones/flutter_bones.dart';
 import 'package:meta/meta.dart';
 
 /// Base class of all models.
 abstract class ModelBase {
   BaseLogger logger;
+
   ModelBase(this.logger);
 
+  /// Tests the validity of the entries in [options] comparing with an [regExpr].
+  /// Errors will be logged.
+  bool checkOptionsByRegExpr(List<String> options, RegExp regExpr) {
+    bool rc = false;
+    options.forEach((element) {
+      if (regExpr.firstMatch(element) == null) {
+        logger.error('unbekannte Feldoption $element in ${fullName()}');
+        rc = true;
+      }
+    });
+    return rc;
+  }
+
   /// Tests a [map] for superfluous [keys].
   /// Keys of the [map] that are not listed in [keys] will be logged as errors.
   /// Keys that do not have the type String are logged as errors.
   void checkSuperfluous(Map<String, dynamic> map, List<String> keys) {
     map.keys.forEach((element) {
       if (element.runtimeType != String) {
-        logger.error('wrong key type ${element.runtimeType} in ${fullName()}');
-      } else if (!keys.contains(map[element])) {
-        logger.error('wrong key ${element.runtimeType} in ${fullName()}');
+        logger.error(
+            'wrong attribute data type ${element.runtimeType} in ${fullName()}');
+      } else if (!keys.contains(element)) {
+        logger.error('unknown attribute "${element}" in ${fullName()}');
       }
     });
   }
@@ -27,14 +42,15 @@ abstract class ModelBase {
   /// Fetches an entry from a map addressed by a [key].
   /// An error is logged if [required] is true and the map does not contain the key.
   T parseEnum<T>(String key, Map<String, dynamic> map, List values,
-      {required: bool}) {
+      {bool required = false}) {
     T rc;
     if (!map.containsKey(key)) {
       if (required) {
         logger.error('missing $key in ${fullName()}');
       }
     } else {
-      rc = StringUtils.stringToEnum(map[key], values);
+      final text = map[key] as String;
+      rc = StringUtils.stringToEnum<T>(text, values);
     }
     return rc;
   }
@@ -42,26 +58,26 @@ abstract class ModelBase {
   /// Fetches an entry from a map addressed by a [key].
   /// An error is logged if [required] is true and the map does not contain the key.
   int parseInt(String key, Map<String, dynamic> map, {required: bool}) {
-    int rc;
-    if (!map.containsKey(key)) {
-      if (required) {
-        logger.error('missing $key in ${fullName()}');
-      } else {
-        if (map[key].runtimeType == int) {
-          rc = map[key];
-        } else if (map[key].runtimeType == String) {
-          if (Validation.isInt(map[key])) {
-            rc = StringUtils.asInt(map[key]);
-          } else {
-            logger
-                .error('not an integer: ${map[key]} map[$key] in {fullName()}');
-          }
-        } else {
-          logger.error('not an integer: ${map[key]} map[$key] in {fullName()}');
-        }
-      }
-    }
-    return rc;
+  int rc;
+  if (!map.containsKey(key)) {
+  if (required) {
+  logger.error('missing $key in ${fullName()}');
+  } else {
+  if (map[key].runtimeType == int) {
+  rc = map[key];
+  } else if (map[key].runtimeType == String) {
+  if (Validation.isInt(map[key])) {
+  rc = StringUtils.asInt(map[key]);
+  } else {
+  logger
+      .error('not an integer: ${map[key]} map[$key] in {fullName()}');
+  }
+  } else {
+  logger.error('not an integer: ${map[key]} map[$key] in {fullName()}');
+  }
+  }
+  }
+  return rc;
   }
 
   /// Fetches an entry from a map addressed by a [key].
@@ -76,15 +92,79 @@ abstract class ModelBase {
 
   /// Fetches an entry from a map addressed by a [key].
   /// An error is logged if [required] is true and the map does not contain the key.
-  String parseString(String key, Map<String, dynamic> map, {required: bool}) {
+  String parseString(String key, Map<String, dynamic> map,
+      {bool required = false}) {
     String rc;
     if (!map.containsKey(key)) {
       if (required) {
         logger.error('missing $key in ${fullName()}');
+      }
+    } else {
+      rc = map[key] as String;
+    }
+    return rc;
+  }
+
+  /// Fetches an entry from a map addressed by a [key].
+  /// This entry is splitted by the delimiter given at index 0.
+  /// Example: ";a;b" returns ['a', 'b'].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  List<String> parseStringList(String key, Map<String, dynamic> map,
+      {bool required = false}) {
+    var rc = <String>[];
+    if (!map.containsKey(key)) {
+      if (required) {
+        logger.error('missing $key in ${fullName()}');
+      }
+    } else {
+      final value = map[key] as String;
+      if (value.length < 2 && required) {
+        logger.error(
+            'the string list "$value" do not start with an delimiter, e.g. ";adam;bob"');
       } else {
-        rc = map[key] as String;
+        rc = value.substring(1).split(value[0]);
       }
     }
     return rc;
   }
+
+  /// Fetches an entry from a map addressed by a [key].
+  /// This entry is splitted by the delimiter given at index 0.
+  /// Example: ";a;b" returns ['a', 'b'].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  List<dynamic> parseValueList(String key, Map<String, dynamic> map,
+      DataType dataType,
+      {bool required = false}) {
+    if (dataType == null) {
+      dataType = DataType.string;
+    }
+    final strings = parseStringList(key, map, required: required);
+    final rc = strings.map((item) {
+      var rc2;
+      switch (dataType) {
+        case DataType.int:
+          rc2 = StringUtils.asInt(item);
+          break;
+        default:
+          logger.error('unknown dataType in parseValueList()');
+          rc2 = item;
+      }
+      return rc2;
+    });
+    return rc;
+  }
+
+  /// Tests whether an [object] is a list type.
+  /// Works for JSArray<dynamic>, List
+  static bool isList(Object object) {
+    final name = object.runtimeType.toString();
+    return name.contains('List') || name.contains('Array');
+  }
+
+  /// Tests whether an [object] is a list type.
+  /// Works for _InternalLinkedHashMap, _JsonMap, Map
+  static bool isMap(Object object) {
+    final name = object.runtimeType.toString();
+    return name.contains('Map');
+  }
 }
index d441b53d01a081ad85a0221227467c02fcda8bc4..16667a98f8576cf40c67273e19d53bd652a8d27d 100644 (file)
@@ -1,4 +1,4 @@
-import 'package:flutter_bones/flutter_bones.dart';
+// on change: adapt StringHelper.fromString()
 enum DataType {
   bool, currency, date, dateTime, float, int, reference, string,
 }
index 6416fcd2cc1e50251b8db9472847d7dd27d02ae8..942bb3a953452c43a5dd50049145f981fe601324 100644 (file)
@@ -1,34 +1,42 @@
-import 'package:flutter_bones/flutter_bones.dart';
 import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
 
-class UserModel extends ModuleModel{
-
+class UserModel extends ModuleModel {
   static final model = <String, dynamic>{
     "module": "user",
     "pages": [
       {
         "name": "create",
-        "pageModeType": "create",
+        "pageType": "create",
         "sections": [
           {
-            "sectionModeType": "simpleForm",
-            "fields": [
+            "sectionType": "simpleForm",
+            "children": [
+              {
+                "widgetType": "text",
+                "text": "*_Erfassung eines neuen Benutzers:_*",
+                "options": "rich",
+              },
+              {
+                "widgetType": "emptyLine",
+              },
               {
+                "widgetType": "textField",
                 "name": "user",
-                "fieldModelType": "text",
                 "label": "Benutzer",
                 "options": "required;unique",
               },
               {
+                "widgetType": "textField",
                 "name": "displayname",
                 "label": "Anzeigename",
-                "fieldModelType": "text",
+                "fieldType": "text",
                 "options": "required",
               },
               {
+                "widgetType": "combobox",
                 "name": "role",
                 "label": "Rolle",
-                "fieldModelType": "combobox",
                 "dataType": "reference",
                 "options": "required;undef",
               },
@@ -38,6 +46,10 @@ class UserModel extends ModuleModel{
       },
     ],
   };
-  UserModel(BaseLogger logger): super(model, logger);
 
-}
\ No newline at end of file
+  UserModel(BaseLogger logger) : super(model, logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => name;
+}
index dfacd9fb9ecbe48544a6835335f5fd0906939c54..8b65cb1be00a0c08ccf0ce4c265b0153e87f6d83 100644 (file)
@@ -1,24 +1,49 @@
-import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_bones/flutter_bones.dart';
 import 'package:meta/meta.dart';
-import 'package:dart_bones/dart_bones.dart';
 
 class ModuleModel extends ModelBase {
+  static final regExprOptions = RegExp(r'^(unknown)$');
   final Map<String, dynamic> map;
   String name;
   List<String> options;
-  List<PageModel> pages;
+  @protected
+  final pages = <PageModel>[];
+  @protected
+  final pageMap = <String, PageModel>{};
+
+  ModuleModel(this.map, BaseLogger logger) : super(logger);
+
+  /// Appends a [page] to the instance.
+  void addPage(PageModel page) {
+    pages.add(page);
+    pageMap[page.name] = page;
+  }
 
-  ModuleModel(this.map, BaseLogger logger): super(logger);
   /// Returns the name including the names of the parent
   @override
   String fullName() => name;
 
+  /// Returns a child page given by [name], null otherwise.
+  PageModel pageByName(String name) =>
+      pageMap.containsKey(name) ? pageMap[name] : null;
+
   /// Parses the map and stores the data in the instance.
-  void parse(){
-    checkSuperfluous(map, 'module pages options'.split(' '));
+  void parse() {
+    checkSuperfluous(map, 'module options pages'.split(' '));
     name = parseString('module', map, required: true);
-    pages = PageModel.parseList(this, map['pages'], logger);
+    if (!map.containsKey('pages')) {
+      logger.error('Module $name: missing pages');
+    } else {
+      final item = map['pages'];
+      if (!ModelBase.isList(item)) {
+        logger.error(
+            'curious item in page list of ${fullName()}: ${StringUtils.limitString("$item", 80)}');
+      } else {
+        PageModel.parseList(this, item, logger);
+      }
+    }
     options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
   }
 }
index 1281b737c6bbbc15fd917185da7d5d5050c970bf..35d199623e9646c6f613c8cd6d1d83575a3efe72 100644 (file)
@@ -1,35 +1,56 @@
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_bones/flutter_bones.dart';
-import 'package:meta/meta.dart';
 
 /// Represents one screen of the application.
 class PageModel extends ModelBase {
-  static final regExprOptions = RegExp(r'^(unknown)');
+  static final regExprOptions = RegExp(r'^(unknown)$');
   final ModuleModel module;
-  final Map<String, dynamic>map;
+  final Map<String, dynamic> map;
   String name;
   PageModelType pageModelType;
-  final List<SectionModel>sections = [];
+  final List<SectionModel> sections = [];
   List<String> options;
 
-  PageModel(this.module, this.map, BaseLogger logger): super(logger);
+  PageModel(this.module, this.map, BaseLogger logger) : super(logger);
 
   /// Returns the name including the names of the parent
   @override
-  String fullName() => '${module.fullName()}.$name';
+  String fullName() => '${module.name}.$name';
 
   /// Parses the map and stores the data in the instance.
-  void parse(){
-    checkSuperfluous(map, 'name pageModelType sections options'.split(' '));
+  void parse() {
+    checkSuperfluous(map, 'name options pageType sections'.split(' '));
     name = parseString('name', map, required: true);
-    pageModelType = parseEnum<PageModelType>('fieldTypeInfo', map, PageModelType.values);
+    pageModelType =
+        parseEnum<PageModelType>('pageType', map, PageModelType.values);
     options = parseOptions('options', map);
+    if (!map.containsKey('sections')) {
+      logger.error('missing sections in ${fullName()}');
+    } else {
+      final item = map['sections'];
+      if (!ModelBase.isList(item)) {
+        logger.error('"sections" is not an array in ${fullName()}: '
+            '${StringUtils.limitString(item.toString(), 80)}');
+      } else {
+        SectionModel.parseSections(this, null, item, logger);
+      }
+    }
+    checkOptionsByRegExpr(options, regExprOptions);
   }
+
   /// Returns a list of Pages constructed by the Json like [map].
-  static List<PageModel> parseList(
-      ModuleModel parent, List<Map<String, dynamic>> map, BaseLogger logger) {
-    final rc = map.map((item) => PageModel(parent, item, logger));
-    return rc;
+  static void parseList(
+      ModuleModel module, List<Map<String, dynamic>> map, BaseLogger logger) {
+    for (var item in map) {
+      if (!ModelBase.isMap(item)) {
+        module.logger.error(
+            'curious item in section list of ${module.fullName()}: ${StringUtils.limitString("$item", 80)}');
+      } else {
+        final page = PageModel(module, item, logger);
+        page.parse();
+        module.addPage(page);
+      }
+    }
   }
 }
 
index 70ec50b71482da22c3b0bf51db1ee13242570634..bc8aaa48c2738b5f709e8b9cd53918bceb6c6a2e 100644 (file)
-import 'package:flutter/material.dart';
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_bones/flutter_bones.dart';
-import 'package:meta/meta.dart';
 
 /// A part of a page represented by one widget.
-class SectionModel extends ModelBase {
-  static final regExprOptions = RegExp(r'^(unknown)');
+class SectionModel extends WidgetModel {
+  static final regExprOptions = RegExp(r'^(unknown)$');
   SectionModelType sectionModelType;
-  final PageModel page;
   String name;
-  List<FieldModel> fields;
+  final children = <WidgetModel>[];
+  final buttons = <WidgetModel>[];
   List<String> options;
+  final int no;
   final Map<String, dynamic> map;
-  SectionModel(this.page, this.map, BaseLogger logger) : super(logger);
+
+  SectionModel(this.no, PageModel page, SectionModel section, this.map,
+      BaseLogger logger)
+      : super(section, page, WidgetModelType.section, logger);
 
   /// Returns the name including the names of the parent
   @override
-  String fullName() => '${page.fullName()}.$name';
+  String fullName() =>
+      section == null ? '${page.name}.$name' : '${section.name}.$name';
 
   /// Parses the map and stores the data in the instance.
   void parse() {
-    checkSuperfluous(map, 'name sectionModelType fields options'.split(' '));
-    name = parseString('name', map, required: true);
+    checkSuperfluous(
+        map, 'children fields name options sectionType widgetType'.split(' '));
     sectionModelType = parseEnum<SectionModelType>(
-        'sectionModelType', map, PageModelType.values);
+        'sectionType', map, SectionModelType.values);
+    name = parseString('name', map) ??
+        '${StringUtils.enumToString(sectionModelType)}$no';
     options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
+    if (!map.containsKey('children')) {
+      logger.error('missing children in ${fullName()}');
+    } else {
+      final childrenList = map['children'];
+      if (!ModelBase.isList(childrenList)) {
+        logger.error('"children" is not a list in ${fullName()}: '
+            '${StringUtils.limitString(childrenList.toString(), 80)}');
+      } else {
+        int no = 0;
+        for (var child in childrenList) {
+          no++;
+          if (!ModelBase.isMap(childrenList)) {
+            logger
+                .error('child $no of "children" is not a map in ${fullName()}: '
+                    '${StringUtils.limitString(child.toString(), 80)}');
+          } else {
+            if (!child.containsKey('widgetType')) {
+              logger.error(
+                  'child $no of "children" does not have "widgetType" in ${fullName()}: '
+                  '${StringUtils.limitString(child.toString(), 80)}');
+            } else {
+              final widgetType = StringUtils.stringToEnum<WidgetModelType>(
+                  child['widgetType'].toString(), WidgetModelType.values);
+              WidgetModel widget;
+              switch (widgetType) {
+                case WidgetModelType.checkbox:
+                  widget = CheckboxModel(this, page, child, logger);
+                  break;
+                case WidgetModelType.combobox:
+                  widget = ComboboxModel(this, page, child, logger);
+                  break;
+                case WidgetModelType.textField:
+                  widget = TextFieldModel(this, page, child, logger);
+                  break;
+                case WidgetModelType.button:
+                  widget = ButtonModel(this, page, child, logger);
+                  break;
+                case WidgetModelType.text:
+                  widget = TextModel(this, page, child, logger);
+                  break;
+                case WidgetModelType.emptyLine:
+                  widget = EmptyLineModel(this, page, child, logger);
+                  break;
+                default:
+                  //@ToDo: nested section
+                  logger.error(
+                      'Section: unknown "widgetType" ${child['widgetType']} in ${fullName()}');
+                  break;
+              }
+              if (widget != null) {
+                children.add(widget);
+              }
+            }
+          }
+        }
+        //parseSections(page, this, childrenList, logger);
+      }
+    }
   }
 
-  /// Returns a list of Pages constructed by the Json like [map].
-  static List<SectionModel> parseList(
-      PageModel parent, List<Map<String, dynamic>> map, BaseLogger logger) {
-    final rc = map.map((item) => SectionModel(parent, item, logger));
-    return rc;
+  /// Returns a list of SectionModel constructed by the Json like [map].
+  static void parseSections(PageModel page, SectionModel section,
+      List<Map<String, dynamic>> map, BaseLogger logger) {
+    final rc = <SectionModel>[];
+    int no = 0;
+    for (var item in map) {
+      no++;
+      if (!ModelBase.isMap(item)) {
+        page.logger.error(
+            'curious item in section list of ${page.fullName()}: ${StringUtils.limitString("$item", 80)}');
+      } else {
+        final current = SectionModel(no, page, null, item, logger);
+        current.parse();
+        if (section != null) {
+          section.children.add(current);
+        } else {
+          page.sections.add(current);
+        }
+      }
+    }
   }
+
+  @override
+  String widgetName() => name;
 }
 
 enum SectionModelType {
diff --git a/lib/src/model/text_field_model.dart b/lib/src/model/text_field_model.dart
new file mode 100644 (file)
index 0000000..7b39972
--- /dev/null
@@ -0,0 +1,31 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class TextFieldModel extends FieldModel {
+  static final regExprOptions =
+      RegExp(r'^(readonly|disabled|password|required|unique)$');
+  int maxSize;
+  int rows;
+
+  FormFieldValidator<String> validator;
+
+  FormFieldSetter onSaved;
+
+  final Map<String, dynamic> map;
+
+  TextFieldModel(
+      SectionModel section, PageModel page, this.map, BaseLogger logger)
+      : super(section, page, map, WidgetModelType.textField, logger);
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(
+        map, 'dataType label maxSize name options rows toolTip'.split(' '));
+    super.parse();
+    maxSize = parseInt('maxSize', map, required: false);
+    rows = parseInt('rows', map, required: false);
+    options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
+  }
+}
diff --git a/lib/src/model/text_model.dart b/lib/src/model/text_model.dart
new file mode 100644 (file)
index 0000000..fbb4333
--- /dev/null
@@ -0,0 +1,29 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class TextModel extends WidgetModel {
+  static final regExprOptions = RegExp(r'^(richtext)$');
+  List<String> options;
+  String text;
+  bool isRichText;
+  final Map<String, dynamic> map;
+
+  TextModel(SectionModel section, PageModel page, this.map, BaseLogger logger)
+      : super(section, page, WidgetModelType.text, logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => '${section.name}.text$id';
+
+  /// Parses the map and stores the data in the instance.
+  void parse() {
+    checkSuperfluous(map, 'options text widgetType'.split(' '));
+    text = parseString('text', map, required: true);
+    options = parseOptions('options', map);
+    checkOptionsByRegExpr(options, regExprOptions);
+    isRichText = options.contains('richtext');
+  }
+
+  @override
+  String widgetName() => 'text$id';
+}
index fe688cdb9f44431c9e7a5a5edef3f49578e6ed15..c802b0ccab39350dd2b833474533fcbf33c581f5 100644 (file)
@@ -1,75 +1,29 @@
-import 'package:flutter/material.dart';
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_bones/flutter_bones.dart';
-import 'package:meta/meta.dart';
 
 /// A base class for items inside a page: SectionModel FieldModel TextModel...
 abstract class WidgetModel extends ModelBase {
+  static int lastId = 0;
   final SectionModel section;
   final PageModel page;
-  final Map<String, dynamic> map;
+  final WidgetModelType widgetModelType;
+  int id;
 
-  WidgetModel(this.section, this.page, BaseLogger logger) : super(logger);
-
-  /// Returns the name including the names of the parent
-  @override
-  String fullName() => '${parent.fullName()}.$name';
-
-  /// Parses the map and stores the data in the instance.
-  void parse() {
-    checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' '));
-    name = parseString('name', map, required: true);
-    label = parseString('label', map, required: false);
-    toolTip = parseString('toolTip', map, required: false);
-    fieldModelType = parseEnum<WidgetModelType>(
-        'fieldModelType', map, WidgetModelType.values);
-    dataType = parseEnum<DataType>(
-        'dataType', map, DataType.values);
-    maxSize = parseInt('maxSize', map, required: false);
-    rows = parseInt('rows', map, required: false);
-    options = parseOptions('options', map);
+  WidgetModel(this.section, this.page, this.widgetModelType, BaseLogger logger)
+      : super(logger) {
+    this.id = ++lastId;
   }
 
-  /// Tests the validity of the entries in [optionList]
-  /// Errors will be logged.
-  bool checkOptions() {
-    bool rc = false;
-    options.forEach((element) {
-      if (regExprOptions.firstMatch(element) == null) {
-        logger.error('unbekannte Feldoption $element in ${fullName()}');
-        rc = true;
-      }
-    });
-    return rc;
-  }
-  /// Returns the widget representing the field.
-  Widget widget(Key formKey){
-    Widget rc;
-    switch(fieldModelType){
-      case WidgetModelType.text:
-        rc = TextFormField(key: formKey,
-          validator: validator,
-          decoration: InputDecoration(labelText: label),
-          onSaved: onSaved,
-          maxLength: maxSize,
-          maxLines: rows,
-          readOnly: options.contains('readonly'),
-          obscureText: options.contains('password'),
-        );
-        break;
-      case WidgetModelType.checkbox:
-        rc = Checkbox(key: formKey,
-          decoration: InputDecoration(labelText: label),
-          onSaved: onSaved,
-          maxLength: maxSize,
-          maxLines: rows,
-          readOnly: options.contains('readonly'),
-          obscureText: options.contains('password'),
-        );
-        break;
-    }
-    return rc;
-  }
+  String widgetName();
 }
 
-enum WidgetModelType { checkbox, combobox, image, text, }
+enum WidgetModelType {
+  button,
+  checkbox,
+  combobox,
+  emptyLine,
+  image,
+  section,
+  text,
+  textField,
+}
diff --git a/lib/src/widget/raised_button_bone.dart b/lib/src/widget/raised_button_bone.dart
new file mode 100644 (file)
index 0000000..1506fb4
--- /dev/null
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class RaisedButtonBone extends RaisedButton {
+  final ButtonModel model;
+
+  RaisedButtonBone(this.model,
+      {ValueChanged<bool> onHighlightChanged,
+      ButtonTextTheme textTheme,
+      Color textColor,
+      Color disabledTextColor,
+      Color color,
+      Color disabledColor,
+      Color focusColor,
+      Color hoverColor,
+      Color highlightColor,
+      Color splashColor,
+      Brightness colorBrightness,
+      double elevation,
+      double focusElevation,
+      double hoverElevation,
+      double highlightElevation,
+      double disabledElevation,
+      EdgeInsetsGeometry padding,
+      VisualDensity visualDensity,
+      ShapeBorder shape,
+      Clip clipBehavior: Clip.none,
+      FocusNode focusNode,
+      bool autofocus: false,
+      MaterialTapTargetSize materialTapTargetSize,
+      Duration animationDuration,
+      Widget child})
+      : super(
+            onPressed: model.onPressed,
+            onLongPress: model.onLongPressed,
+            onHighlightChanged: model.onHighlightChanged,
+            textTheme: textTheme,
+            textColor: textColor,
+            disabledTextColor: disabledTextColor,
+            color: color,
+            disabledColor: disabledColor,
+            focusColor: focusColor,
+            hoverColor: hoverColor,
+            highlightColor: highlightColor,
+            splashColor: splashColor,
+            colorBrightness: colorBrightness,
+            elevation: elevation,
+            focusElevation: focusElevation,
+            hoverElevation: hoverElevation,
+            highlightElevation: highlightElevation,
+            disabledElevation: disabledElevation,
+            padding: padding,
+            visualDensity: visualDensity,
+            shape: shape,
+            focusNode: focusNode,
+            autofocus: autofocus,
+            materialTapTargetSize: materialTapTargetSize,
+            animationDuration: animationDuration,
+            child: child);
+}
diff --git a/lib/src/widget/simple_form.dart b/lib/src/widget/simple_form.dart
new file mode 100644 (file)
index 0000000..0b6a712
--- /dev/null
@@ -0,0 +1,23 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
+
+class SimpleForm {
+  static Form simpleForm({@required Key key, @required List<Widget> fields,
+    @required List<Widget> buttons, @required BaseConfiguration configuration} ) {
+    final padding = configuration.asFloat('form.card.padding', defaultValue: 16.0);
+    return Form(
+        key: key,
+        child: Card(
+        margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+    child: Padding(
+    padding:
+    EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
+    child: ListView(
+    children:
+      <Widget>[...fields,
+        SizedBox(height: configuration.asFloat('form.gap.field_button.height', defaultValue: 16.0)),
+        ...buttons]
+    )),
+    ));
+  }
+}
\ No newline at end of file
diff --git a/lib/src/widget/simpleform.dart b/lib/src/widget/simpleform.dart
deleted file mode 100644 (file)
index 3bc5269..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:dart_bones/dart_bones.dart';
-
-class SimpleForm {
-  static Form simpleForm({@required Key key, @required List<Widget> fields,
-    @required List<Widget> buttons, @required BaseConfiguration configuration} ) {
-    final padding = configuration.asFloat('form.card.padding', defaultValue: 16.0);
-    return Form(
-        key: key,
-        child: Card(
-        margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding),
-    child: Padding(
-    padding:
-    EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
-    child: ListView(
-    children:
-      <Widget>[...fields,
-        SizedBox(height: configuration.asFloat('form.gap.field_button.height', defaultValue: 16.0)),
-        ...buttons]
-    )),
-    ));
-  }
-}
\ No newline at end of file
diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart
new file mode 100644 (file)
index 0000000..e022755
--- /dev/null
@@ -0,0 +1,151 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/model/widget_model.dart';
+
+class View {
+  static View _instance;
+  Settings settings;
+  BaseConfiguration widgetConfiguration;
+
+  final BaseLogger logger;
+
+  factory View(BaseLogger logger) {
+    if (_instance == null) {
+      _instance = View.internal(logger);
+      _instance.settings = Settings(logger);
+      _instance.widgetConfiguration = _instance.settings.widgetConfiguration;
+    }
+    return _instance;
+  }
+
+  View.internal(this.logger);
+
+  /// Creates a button from the [model].
+  Widget button(ButtonModel model) {
+    final rc = RaisedButtonBone(
+      model,
+      child: Text(model.text),
+    );
+    return rc;
+  }
+
+  /// Creates a list of buttons from a list of [models].
+  List<Widget> buttonList(List<WidgetModel> models) {
+    final rc = <Widget>[];
+    for (var item in models) {
+      rc.add(button(item));
+    }
+    return rc;
+  }
+
+  /// Creates a checkbox from the [model].
+  Widget checkbox(WidgetModel model) {
+    var rc;
+    return rc;
+  }
+
+  /// Creates an empty line from the [model].
+  Widget emptyLine(EmptyLineModel model) {
+    var rc;
+    return rc;
+  }
+
+  /// Creates image from the [model].
+  Widget image(WidgetModel model) {
+    var rc;
+    return rc;
+  }
+
+  /// Creates a section from the [model].
+  Widget section(WidgetModel model) {
+    var rc;
+    return rc;
+  }
+
+  /// Returns a form with the properties given by the [model]
+  /// [formKey] identifies the form. Used for form validation and saving.
+  Form simpleForm({SectionModel model, Key formKey}) {
+    assert(formKey != null);
+    final padding =
+        widgetConfiguration.asFloat('form.card.padding', defaultValue: 16.0);
+    final children = widgetsOfSection(model.children);
+    final buttons = buttonList(model.buttons);
+    final rc = Form(
+        key: formKey,
+        child: Card(
+          margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+          child: Padding(
+              padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
+              child: ListView(children: <Widget>[
+                ...children,
+                SizedBox(
+                    height: widgetConfiguration.asFloat(
+                        'form.gap.field_button.height',
+                        defaultValue: 16.0)),
+                ...buttons
+              ])),
+        ));
+    return rc;
+  }
+
+  /// Creates a text from the [model].
+  Widget text(TextModel model) {
+    final rc = Text(model.text);
+    return rc;
+  }
+
+  /// Creates a form text field from the [model].
+  Widget textField(TextFieldModel model) {
+    var rc = toolTip(
+        TextFormField(
+          validator: model.validator,
+          decoration: InputDecoration(labelText: model.label),
+          onSaved: (input) => model.value(input),
+        ),
+        model);
+    return rc;
+  }
+
+  /// If a tool tip is defined in [model] the [widget] is completed with that.
+  /// Otherwise [widget] is returned.
+  Widget toolTip(Widget widget, FieldModel model) {
+    Widget rc;
+    if (model.toolTip == null) {
+      rc = widget;
+    } else {
+      // @ToDo: ToolTip
+      rc = widget;
+    }
+    return rc;
+  }
+
+  List<Widget> widgetsOfSection(List<WidgetModel> children) {
+    final rc = <Widget>[];
+    for (var child in children) {
+      switch (child.widgetModelType) {
+        case WidgetModelType.textField:
+          rc.add(textField(child));
+          break;
+        case WidgetModelType.emptyLine:
+          rc.add(emptyLine(child));
+          break;
+        case WidgetModelType.text:
+          rc.add(text(child));
+          break;
+        case WidgetModelType.checkbox:
+          rc.add(checkbox(child));
+          break;
+        case WidgetModelType.combobox:
+          rc.add(text(child));
+          break;
+        case WidgetModelType.image:
+          rc.add(image(child));
+          break;
+        case WidgetModelType.section:
+          rc.add(section(child));
+          break;
+      }
+    }
+  }
+}
index cebeaa20e8d2f065933ba4814d7558600e1ee238..27c7f6ae5d8021065e9444fa137cd8a824e9f8ce 100644 (file)
@@ -1,17 +1,34 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter_bones/flutter_bones.dart';
 import 'package:test/test.dart';
 
 void main() {
-  final map = { 'help' : { 'de' : 'Hilfe'},
-  'wrong name %{0}' : { 'de' : 'falscher name %{0}' }
+  final map = {
+    'help': {'de': 'Hilfe'},
+    'wrong name %{0}': {'de': 'falscher name %{0}'}
   };
+  final logger = MemoryLogger(LEVEL_FINE);
   setUpAll(() => Settings.setLocaleByNames(language: 'en'));
   group('Settings', () {
+    test('basic', () {
+      Settings.setLocale(Locale('de', 'de'));
+      final settings = Settings(logger);
+      expect(Settings.translate('abc', map), equals('abc'));
+      expect(Settings.translate('help', map), equals('help'));
+      expect(
+          Settings.translate('wrong name %{0}', map,
+              placeholders: {'0': 'Joe'}),
+          equals('wrong name Joe'));
+    });
     test('translate-en', () {
       Settings.setLocaleByNames(language: 'en');
       expect(Settings.translate('abc', map), equals('abc'));
       expect(Settings.translate('help', map), equals('help'));
-      expect(Settings.translate('wrong name %{0}', map,  placeholders: {'0' : 'Joe'}), equals('wrong name Joe'));
+      expect(
+          Settings.translate('wrong name %{0}', map,
+              placeholders: {'0': 'Joe'}),
+          equals('wrong name Joe'));
     });
     test('translate-de', () {
       Settings.setLocaleByNames(language: 'de');
diff --git a/test/helpers/string_helper_test.dart b/test/helpers/string_helper_test.dart
new file mode 100644 (file)
index 0000000..8de5891
--- /dev/null
@@ -0,0 +1,43 @@
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('fromString', () {
+    test('fromString-string', () {
+      expect(StringHelper.fromString('abc', DataType.string), equals('abc'));
+      expect(StringHelper.fromString('', DataType.string), equals(''));
+      expect(StringHelper.fromString(null, DataType.string), isNull);
+    });
+    test('fromString-num', () {
+      expect(StringHelper.fromString('123', DataType.int), equals(123));
+      expect(StringHelper.fromString('12.3', DataType.float), equals(12.3));
+      expect(
+          StringHelper.fromString('12.35', DataType.currency), equals(12.35));
+      expect(StringHelper.fromString('1234567', DataType.reference),
+          equals(1234567));
+      expect(StringHelper.fromString('', DataType.int), isNull);
+      expect(StringHelper.fromString(null, DataType.int), isNull);
+    });
+    test('fromString-bool', () {
+      expect(StringHelper.fromString('True', DataType.bool), isTrue);
+      expect(StringHelper.fromString('yEs', DataType.bool), isTrue);
+      expect(StringHelper.fromString('nO', DataType.bool), isFalse);
+      expect(StringHelper.fromString('FALSE', DataType.bool), isFalse);
+      expect(StringHelper.fromString('wrong', DataType.bool), isNull);
+      expect(StringHelper.fromString('', DataType.bool), isNull);
+      expect(StringHelper.fromString(null, DataType.bool), isNull);
+    });
+    test('fromString-datetime', () {
+      expect(StringHelper.fromString('2020.1.2', DataType.date),
+          equals(DateTime(2020, 1, 2)));
+      expect(StringHelper.fromString('2020.1.2-3:44:52', DataType.dateTime),
+          equals(DateTime(2020, 1, 2, 3, 44, 52)));
+      expect(StringHelper.fromString('', DataType.date), isNull);
+      expect(StringHelper.fromString(null, DataType.dateTime), isNull);
+    });
+    test('fromString-error', () {
+      expect(StringHelper.fromString('2020.1.2', null),
+          startsWith('<StringHelper.fromString(): unknown datatype'));
+    });
+  });
+}
index f682a9d0c8df6f884853a8308279e14bf372ac49..3d38eec1fac03f2aba2cc6981ea1f8b8b2c553ee 100644 (file)
@@ -43,11 +43,8 @@ void main() {
               'Not a not negative number: -1'));
     });
     test('checkInt', () {
-      expect(checkNat('123'), isNull);
-      expect(
-          checkNat('-a'),
-          equals(
-              'Not a not negative number: -a'));
+      expect(checkInt('-123'), isNull);
+      expect(checkInt('-a'), equals('Not an integer: -a'));
     });
 });
 }
diff --git a/test/model/model_test.dart b/test/model/model_test.dart
new file mode 100644 (file)
index 0000000..f99000c
--- /dev/null
@@ -0,0 +1,54 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:test/test.dart';
+
+void main() {
+  final logger = MemoryLogger(LEVEL_FINE);
+  group('module', () {
+    test('module', () {
+      final module = Demo1(logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(0));
+      final page = module.pageByName('create');
+      expect(page?.fullName(), equals('demo1.create'));
+      expect(module.fullName(), equals('demo1'));
+    });
+  });
+}
+
+class Demo1 extends ModuleModel {
+  static final model = <String, dynamic>{
+    "module": "demo1",
+    "pages": [
+      {
+        "name": "create",
+        "pageType": "create",
+        "sections": [
+          {
+            "sectionType": "simpleForm",
+            "children": [
+              {
+                "widgetType": "textField",
+                "name": "user",
+                "label": "User",
+                "options": "required;unique",
+              },
+              {
+                "widgetType": "button",
+                "name": "buttonStore",
+                "label": "Save",
+              },
+            ]
+          }
+        ]
+      },
+    ],
+  };
+
+  Demo1(BaseLogger logger) : super(model, logger);
+
+  /// Returns the name including the names of the parent
+  @override
+  String fullName() => name;
+}