Permalink
Browse files

feature: Improved Form-Handling and databinding between form, field a…

…nd ObservableProperty
  • Loading branch information...
MikeMitterer committed Nov 2, 2017
1 parent e454409 commit 781efc0b04ddc98b0822b498fe05cbe2c71ba02d
@@ -103,6 +103,23 @@ class MaterialTextfield extends MdlComponent with FallbackFormatter {
if(isSelectionSupported) {
_placeTheCursorWhereItWasBefore(selStart);
}
// If a text-field-value changed manually (via Dart-Script)
// the form doesn't get informed about the change so we send a CustomEvent
void _triggerFormUpdate() {
dom.HtmlElement _findForm(final dom.HtmlElement element) {
if(element == null || element.classes.contains("mdl-form")) {
return element;
}
return _findForm(element.parent);
}
dom.HtmlElement formElement = _findForm(element);
formElement?.dispatchEvent(new dom.CustomEvent("MaterialTextfieldChanged",detail: this));
}
_triggerFormUpdate();
focus();
}
_updateClasses();
@@ -24,15 +24,6 @@ part of mdldialog;
class MaterialDatePicker extends MaterialDialog {
static final Logger _logger = new Logger('mdldialog.MaterialDatePicker');
static const String _DEFAULT_CONFIRM_BUTTON = "OK";
static const String _DEFAULT_CANCEL_BUTTON = "Cancel";
/// Text for OK Button
String okButton = _DEFAULT_CONFIRM_BUTTON;
/// Text for Cancel-Button
String cancelButton = _DEFAULT_CANCEL_BUTTON;
/// Initial [DateTime] for this Dialog
DateTime dateTime = new DateTime.now();
@@ -434,10 +425,10 @@ class MaterialDatePicker extends MaterialDialog {
</div>
<div class="mdl-dialog__actions">
<button class="mdl-button"
data-mdl-click="onCancel()">{{cancelButton}}</button>
data-mdl-click="onCancel()" translate='yes'>_('Cancel')</button>
<button class="mdl-button mdl-button--colored"
data-mdl-click="onClose()" autofocus>{{okButton}}</button>
data-mdl-click="onClose()" autofocus translate='yes'>_('OK')</button>
</div>
</div>
</div>
@@ -215,7 +215,7 @@ abstract class MaterialDialog extends Object with TemplateComponent, MdlEventLis
});
idCounter++;
_logger.info("show end (Dialog is rendered, got ID: ${_elementID})!");
_logger.fine("show end (Dialog is rendered, got ID: ${_elementID})!");
}
if(onDialogInit != null) {
@@ -374,7 +374,7 @@ abstract class MaterialDialog extends Object with TemplateComponent, MdlEventLis
void _addBackDropClickListener(final dom.DivElement container) {
container.onClick.listen((final dom.MouseEvent event) {
_logger.info("click on container");
_logger.fine("click on container");
if (event.target == container) {
event.preventDefault();
@@ -24,15 +24,6 @@ part of mdldialog;
class MaterialTimePicker extends MaterialDialog {
static final Logger _logger = new Logger('mdldialog.MaterialTimePicker');
static const String _DEFAULT_CONFIRM_BUTTON = "OK";
static const String _DEFAULT_CANCEL_BUTTON = "Cancel";
/// Text for OK Button
String okButton = _DEFAULT_CONFIRM_BUTTON;
/// Text for Cancel-Button
String cancelButton = _DEFAULT_CANCEL_BUTTON;
/// Initial [DateTime] for this Dialog
DateTime dateTime = new DateTime.now();
@@ -263,10 +254,10 @@ class MaterialTimePicker extends MaterialDialog {
<div class="mdl-dialog__actions">
<button class="mdl-button"
data-mdl-click="onCancel()">{{cancelButton}}</button>
data-mdl-click="onCancel()" translate='yes'>_('Cancel')</button>
<button class="mdl-button mdl-button--colored"
data-mdl-click="onClose()">{{okButton}}</button>
data-mdl-click="onClose()" translate='yes'>_('OK')</button>
</div>
</div>
</div>
@@ -80,7 +80,7 @@ class MaterialTranslate extends MdlComponent {
//- private -----------------------------------------------------------------------------------
void _init() {
_logger.info("MaterialTranslate - init");
_logger.fine("MaterialTranslate - init");
// Recommended - add SELECTOR as class
element.classes.add(_MaterialTranslateConstant.WIDGET_SELECTOR);
@@ -92,7 +92,7 @@ class MaterialTranslate extends MdlComponent {
//return translator.translate(new L10N(_idToTranslate));
});
_logger.info("-> " + _idToTranslate);
//_logger.info("-> " + _idToTranslate);
if (_idToTranslate.isNotEmpty) {
value = _idToTranslate;
@@ -54,18 +54,22 @@ class _TextFieldObserver implements ModelObserver {
final ObservableProperty prop = val;
// MaterialTextfield -> ObservableProperty
_subscriptions.add(
// We use onChange instead of onInput because
// it gives us the chance to cast the input-value to the appropriate
// pro.value (in ObservableProperty)
_textfield.hub.onChange.listen((_)
=> prop.value = _textfield.value)
//_textfield.hub.onChange.listen((_)
// => prop.value = _textfield.value)
// _textfield.onInput.listen((_)
// => prop.value = _textfield.value)
_textfield.onInput.listen((_)
=> prop.value = _textfield.value)
);
// ObservableProperty -> MaterialTextfield
_subscriptions.add(
// prop.toString uses the "Formatter" from ObservableProperty.
// The Formatter can change e.g. null-values to valid strings (or empty strings)
prop.onChange.listen( (final PropertyChangeEvent event)
=> _textfield.value = prop.toString())
);
@@ -32,7 +32,7 @@ class MaterialFormComponentModule extends di.Module {
}
}
enum _MaterialFormState {
enum MaterialFormState {
VALID, INVALID
}
@@ -42,6 +42,12 @@ class FormChangedEvent {
FormChangedEvent(this.currentTarget);
}
/// Callback can be set as a last resort for
/// validation checks
///
/// [originalState] is the status determined by MaterialFormComponent
typedef bool IsFormValidCallback(final MaterialFormState originalState);
/**
* Upgrades mdl-form und does some validation checks.
*
@@ -95,32 +101,36 @@ class MaterialFormComponent extends MdlComponent {
: super(element,injector) {
_init();
}
static MaterialFormComponent widget(final dom.HtmlElement element) => mdlComponent(element,MaterialFormComponent) as MaterialFormComponent;
/// Iterates through all MdlComponent found in this form, checks if all the requirements are fulfilled (set via HTML)
/// Iterates through all MdlComponent in this form,
/// checks if all the requirements are fulfilled (set via HTML)
///
/// If the check returns true the "is-invalid" class is removed
/// Normally it's not necessary to call this function manually. This is done automatically if one of the
/// Normally it's not necessary to call this function manually.
/// This is done automatically if one of the
/// MdlComponents gets an "onChange" event
void updateStatus() {
final bool isFormValid = _isFormValid();
_setFormState(isFormValid ? _MaterialFormState.VALID : _MaterialFormState.INVALID);
_setSubmitButtonState(isFormValid ? _MaterialFormState.VALID : _MaterialFormState.INVALID);
}
@override
void update() => _updateStatus(repeatAfter: new Duration(milliseconds: 100));
/// Returns true if all requirements are fulfilled
bool get isValid => _isFormValid();
/// If focus changes from one field to the other
Stream<FormChangedEvent> get onChange {
if(_onChange == null) {
_onChange = new StreamController<FormChangedEvent>.broadcast(onCancel: () => _onChange = null);
}
return _onChange.stream;
}
/// Called by the framework as last resort before it enables/disables the
/// submit-button
IsFormValidCallback isFormValidCallback = (final MaterialFormState state)
=> state == MaterialFormState.VALID;
// - EventHandler -----------------------------------------------------------------------------
@@ -139,21 +149,19 @@ class MaterialFormComponent extends MdlComponent {
});
_components.forEach((final MdlComponent component) {
eventStreams.add(
// Update internal form-status onInput
component.onInput.listen((final dom.Event event) {
_logger.fine("$component changed!");
_componentChanged(component);
}));
eventStreams.add(
// If focus changes - send FormChangedEvent
component.hub.onChange.listen((final dom.Event event) {
_logger.info("$component changed!");
final bool isFormValid = _isFormValid();
_setFormState(isFormValid ? _MaterialFormState.VALID : _MaterialFormState.INVALID);
_setSubmitButtonState(isFormValid ? _MaterialFormState.VALID : _MaterialFormState.INVALID);
isDirty = true;
element.classes.add(_cssClasses.DIRTY);
_fire(new FormChangedEvent(component));
}));
_fire(new FormChangedEvent(component));
})
);
eventStreams.add(
component.hub.onKeyDown.listen((final dom.KeyboardEvent event) {
@@ -169,42 +177,63 @@ class MaterialFormComponent extends MdlComponent {
);
});
eventStreams.add(
// Send from MaterialTextfield
element.on["MaterialTextfieldChanged"].listen((event) {
event.preventDefault();
final customEvent = event as dom.CustomEvent;
final component = customEvent.detail as MdlComponent;
_logger.fine("${component} changed! (on MaterialTextfieldChanged)");
_componentChanged(component);
})
);
final dom.HtmlElement elementWithAutoFocus = element.querySelector("[autofocus]");
if(elementWithAutoFocus != null) {
elementWithAutoFocus.focus();
}
updateStatus();
_updateStatus(repeatAfter: new Duration(milliseconds: 100));
element.classes.add(_cssClasses.IS_UPGRADED);
}
void _componentChanged(final MdlComponent component) {
isDirty = true;
element.classes.add(_cssClasses.DIRTY);
_updateStatus(repeatAfter: new Duration(milliseconds: 100));
}
bool _isFormValid() {
bool state = true;
var state = MaterialFormState.VALID;
_components.forEach((final MdlComponent component) {
if(component.hub is dom.InputElement) {
if(!(component.hub as dom.InputElement).checkValidity()) {
_logger.fine("Checked ${component.hub.id}");
state = false;
state = MaterialFormState.INVALID;
return;
}
}
});
return state;
return (isFormValidCallback == null ? state : isFormValidCallback(state));
}
void _setFormState(final _MaterialFormState state) {
void _setFormState(final MaterialFormState state) {
if(state == _MaterialFormState.VALID) {
if(state == MaterialFormState.VALID) {
element.classes.remove(_cssClasses.INVALID);
} else {
element.classes.add(_cssClasses.INVALID);
}
}
void _setSubmitButtonState(final _MaterialFormState state) {
void _setSubmitButtonState(final MaterialFormState state) {
_submitButtons.forEach((final MaterialButton button) {
button.enabled = state == _MaterialFormState.VALID;
button.enabled = state == MaterialFormState.VALID;
});
}
@@ -213,6 +242,28 @@ class MaterialFormComponent extends MdlComponent {
_onChange.add(event);
}
}
/// Updates the FormState (Valid/Invalid) and the status of the
/// SubmitButton.
///
/// With [repeatAfter] you can define if you want to repeat this
/// function-call automatically after [repeatAfter]
///
/// This is necessary for form-fields connected via [ObservableProperty] and [MaterialModel]
void _updateStatus({ final Duration repeatAfter }) {
final bool isFormValid = _isFormValid();
_setFormState(isFormValid ? MaterialFormState.VALID : MaterialFormState.INVALID);
_setSubmitButtonState(isFormValid ? MaterialFormState.VALID : MaterialFormState.INVALID);
if(repeatAfter != null) {
// Update Form-State again - just in case something took longer than expected
new Timer(repeatAfter, () {
_logger.fine("Auto-Update form state!");
_updateStatus();
});
}
}
}
/// registration-Helper
Oops, something went wrong.

0 comments on commit 781efc0

Please sign in to comment.