.buildlog/
.history
.svn/
+coverage/
pubspec.lock
# wk: @ToDo: nicht sicher
--- /dev/null
+#! /bin/sh
+flutter test --coverage
+rm -Rf coverage/html/*
+genhtml --show-details --output-directory=coverage/html coverage/lcov.info
--- /dev/null
+dartdoc:
+ linkToSource:
+ root: '.'
+ uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v0.34.0/%f%#L%l%'
-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';
+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
+}
--- /dev/null
+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;
+ }
+}
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) {
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"',
'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"',
},
};
}
--- /dev/null
+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;
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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';
+}
-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, }
-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()}');
}
});
}
/// 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;
}
/// 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].
/// 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');
+ }
}
-import 'package:flutter_bones/flutter_bones.dart';
+// on change: adapt StringHelper.fromString()
enum DataType {
bool, currency, date, dateTime, float, int, reference, string,
}
-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",
},
},
],
};
- 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;
+}
-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);
}
}
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);
+ }
+ }
}
}
-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 {
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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';
+}
-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,
+}
--- /dev/null
+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);
+}
--- /dev/null
+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
+++ /dev/null
-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
--- /dev/null
+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;
+ }
+ }
+ }
+}
+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');
--- /dev/null
+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'));
+ });
+ });
+}
'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'));
});
});
}
--- /dev/null
+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;
+}