From f2c64105732bccc46b6c08814a7808ef54bcc1a9 Mon Sep 17 00:00:00 2001 From: Sean DeNigris Date: Fri, 20 Sep 2024 21:48:10 -0400 Subject: [PATCH] [Enh]: Dynamic Form Validation in GT Bloc Give feedback while user types, showing whether input is valid. --- .../Magritte-GToolkit/MABrTextEditor.class.st | 142 +++++++++++++++++ .../MAElementBuilder.class.st | 148 +++++++++++------- .../Magritte-GToolkit/MAFieldElement.class.st | 5 + .../MAGtInputFieldAptitude.class.st | 12 ++ 4 files changed, 252 insertions(+), 55 deletions(-) create mode 100644 source/Magritte-GToolkit/MABrTextEditor.class.st create mode 100644 source/Magritte-GToolkit/MAFieldElement.class.st create mode 100644 source/Magritte-GToolkit/MAGtInputFieldAptitude.class.st diff --git a/source/Magritte-GToolkit/MABrTextEditor.class.st b/source/Magritte-GToolkit/MABrTextEditor.class.st new file mode 100644 index 00000000..33df64c3 --- /dev/null +++ b/source/Magritte-GToolkit/MABrTextEditor.class.st @@ -0,0 +1,142 @@ +Class { + #name : #MABrTextEditor, + #superclass : #BrEditor, + #instVars : [ + 'elementDescription', + 'builder', + 'errors', + 'header' + ], + #category : #'Magritte-GToolkit' +} + +{ #category : #accessing } +MABrTextEditor class >> elementDescription: anMAElementDescription [ + + ^ self basicNew + elementDescription: anMAElementDescription; + initialize +] + +{ #category : #accessing } +MABrTextEditor class >> onDescription: anMAElementDescription forBuilder: anMAElementBuilder [ + + ^ self basicNew + elementDescription: anMAElementDescription; + builder: anMAElementBuilder; + initialize +] + +{ #category : #accessing } +MABrTextEditor >> builder [ + ^ builder +] + +{ #category : #accessing } +MABrTextEditor >> builder: anObject [ + builder := anObject +] + +{ #category : #accessing } +MABrTextEditor >> elementDescription [ + ^ elementDescription +] + +{ #category : #accessing } +MABrTextEditor >> elementDescription: anObject [ + elementDescription := anObject +] + +{ #category : #accessing } +MABrTextEditor >> errorBorder [ + + ^ BlBorder paint: Color red width: 1 +] + +{ #category : #accessing } +MABrTextEditor >> errors [ + ^ errors ifNil: [ false "MAMultipleErrors new" ] +] + +{ #category : #accessing } +MABrTextEditor >> errors: anObject [ + errors := anObject +] + +{ #category : #accessing } +MABrTextEditor >> hasErrors [ + ^ self errors" collection isNotNil" +] + +{ #category : #accessing } +MABrTextEditor >> header [ + ^ header +] + +{ #category : #accessing } +MABrTextEditor >> header: anObject [ + header := anObject +] + +{ #category : #accessing } +MABrTextEditor >> initialize [ + super initialize. + + self + beEditable; + aptitude: MAGtInputFieldAptitude "BrGlamorousEditorAptitude + BrGlamorousInputFieldSpacingAptitude"; + geometry: (BlRoundedRectangleGeometry cornerRadius: 4); + vFitContent; + hMatchParent; + text: (self builder textUsing: self elementDescription). "BrGlamorousEditableLabelAptitude" "BrGlamorousButtonExteriorAptitude" + + self initializeListeners. + self initializeCompletion +] + +{ #category : #accessing } +MABrTextEditor >> initializeCompletion [ + + self elementDescription propertyAt: #completions ifPresent: [ :comps | + | compStrings compStrat | + compStrings := comps value: self builder object. + compStrat := self builder completionStrategy + completions: (GtPrefixTree withAll: compStrings); + yourself. + (self builder completionControllerClass on: self strategy: compStrat) + showOnTextModification: true; + install ]. +] + +{ #category : #accessing } +MABrTextEditor >> initializeListeners [ + + "Handle typed in text" + self editor + when: BrTextEditorModifiedEvent + do: [ :evt | self validate ]. + + "Handle directly-set text e.g. `text: aText`" + self + when: BrEditorTextChanged + do: [ :evt | self validate ] +] + +{ #category : #accessing } +MABrTextEditor >> validate [ + [ + self elementDescription + writeFromString: self text greaseString + to: self builder memento. + self elementDescription + validate: (self builder memento readUsing: self elementDescription). + self builder showValidationPassFor: self header + ] + on: MAReadError, MAMultipleErrors + do: [ "on: MAMultipleErrors + do: [ :err | + self errors: err. + self border: self errorBorder ]":err | + self errors: true. + self builder showWarningFor: self header ] +] diff --git a/source/Magritte-GToolkit/MAElementBuilder.class.st b/source/Magritte-GToolkit/MAElementBuilder.class.st index 7588b3ae..ae1b99a3 100644 --- a/source/Magritte-GToolkit/MAElementBuilder.class.st +++ b/source/Magritte-GToolkit/MAElementBuilder.class.st @@ -54,7 +54,10 @@ MAElementBuilder >> addInputFieldUsing: aDescription [ self using: aDescription - addInputField: [ self newInputElementUsing: aDescription ] + addInputField: [ :headerElement | + (self newInputElementUsing: aDescription) + header: headerElement; + yourself ] ] { #category : #accessing } @@ -101,8 +104,7 @@ MAElementBuilder >> calendarButtonFor: editor [ do: [ "calendar date: event date. Redundant because a new calendar will be created on next button click":event | editor beEditable; - text: event date mmddyyyy; - acceptEdition. + text: event date mmddyyyy asRopedText. calendar fireEvent: BrDropdownHideWish new ] ]. ^ BrButton new @@ -113,10 +115,17 @@ MAElementBuilder >> calendarButtonFor: editor [ yourself ] +{ #category : #private } +MAElementBuilder >> checkIcon [ + ^ BrGlamorousVectorIcons accept create + background: Color green; + yourself +] + { #category : #accessing } MAElementBuilder >> completionControllerClass [ - ^ completionControllerClass ifNil: [ GtCompletionController ] + ^ completionControllerClass ifNil: [ PeGtCompletionController ] ] { #category : #accessing } @@ -164,13 +173,13 @@ MAElementBuilder >> form [ text: string asRopedText bold; aptitude: BrGlamorousLabelAptitude; yourself ]. - ^ form := BlElement new + ^ form := "BlElement"BrVerticalPane new constraintsDo: [ :c | c vertical fitContent. - c horizontal matchParent ]; - layout: (BlGridLayout horizontal columnCount: 2; cellSpacing: 10); + c horizontal matchParent ]"; + layout: (BlGridLayout horizontal columnCount: 2; cellSpacing: 10)""; addChild: (headerStancil value: 'Field'); - addChild: (headerStancil value: 'Current'); + addChild: (headerStancil value: 'Current');" "addChild: (headerStancil value: 'Original');" yourself. ] @@ -188,29 +197,9 @@ MAElementBuilder >> memento [ { #category : #accessing } MAElementBuilder >> newInputElementUsing: aDescription [ - | editor | - editor := BrEditableLabel new - aptitude: - BrGlamorousEditableLabelAptitude new glamorousRegularFontAndSize; - vFitContent; - hMatchParent; - when: BrEditorAcceptWish do: [ :aWish | - aDescription - writeFromString: aWish text greaseString - to: self memento ]; - text: (self textUsing: aDescription). - - aDescription propertyAt: #completions ifPresent: [ :comps | - | compStrings compStrat | - compStrings := comps value: self object. - compStrat := self completionStrategy - completions: (GtPrefixTree withAll: compStrings); - yourself. - (self completionControllerClass on: editor strategy: compStrat) - showOnTextModification: true; - install ]. - - ^ editor + ^ MABrTextEditor + onDescription: aDescription + forBuilder: self ] { #category : #accessing } @@ -244,6 +233,32 @@ MAElementBuilder >> presenter [ ^ presenter := MABlocContainerPresenter memento: memento ] +{ #category : #accessing } +MAElementBuilder >> showValidationPassFor: headerElement [ + + headerElement + childNamed: #icon + ifFound: [ :oldIcon | + headerElement + replaceChild: oldIcon + with: self checkIcon + as: #icon ] + ifNone: [ headerElement addChild: self checkIcon as: #icon ] +] + +{ #category : #accessing } +MAElementBuilder >> showWarningFor: headerElement [ + + headerElement + childNamed: #icon + ifFound: [ :oldIcon | + headerElement + replaceChild: oldIcon + with: self warningIcon + as: #icon ] + ifNone: [ headerElement addChild: self warningIcon as: #icon ] +] + { #category : #accessing } MAElementBuilder >> textUsing: aDescription [ | valueString | @@ -278,34 +293,42 @@ MAElementBuilder >> toolbar [ { #category : #private } MAElementBuilder >> using: aDescription addInputField: aValuable [ - - | labelElement "diffElement" inputElement | + | labelElement "diffElement" inputElement labelString headerElement | aDescription isVisible ifFalse: [ ^ self ]. - - inputElement := aValuable value. + + labelString := aDescription label. + aDescription isRequired ifTrue: [ labelString := labelString , ' *' ]. + labelElement := BrLabel new - text: aDescription label , ':'; + text: labelString asRopedText bold; addEventHandlerOn: BlClickEvent do: [ :evt | evt target phlow spawnObject: (aDescription read: self object) ]; aptitude: BrGlamorousLabelAptitude. - labelElement constraintsDo: [ :c | + + headerElement := BrHorizontalPane new + fitContent; + margin: (BlInsets all: 3); + addChild: labelElement; + yourself. + + inputElement := (aValuable cull: headerElement) + margin: (BlInsets top: 2 bottom: 10); + yourself. + + labelElement + constraintsDo: [ :c | c vertical fitContent. c horizontal fitContent. - c grid vertical alignCenter ]. - - aDescription isRequired ifTrue: [ self flag: 'unsupported' ]. - - aDescription hasComment ifTrue: [ - self addTooltip: aDescription comment to: labelElement. - self addTooltip: aDescription comment to: inputElement ]. - - "diffElement := BrEditor new - text: inputElement text copy; ""if we don't copy, diff magically changes as input updates" - "aptitude: BrGlamorousEditorAptitude". - - self form addChild: labelElement. - self form addChild: inputElement. - "self form addChild: diffElement." + c grid vertical alignCenter. + c grid horizontal alignRight ]. + + aDescription hasComment + ifTrue: [ self addTooltip: aDescription comment to: headerElement. + self addTooltip: aDescription comment to: inputElement ]. "diffElement := BrEditor new + text: inputElement text copy; ""if we don't copy, diff magically changes as input updates" "aptitude: BrGlamorousEditorAptitude" + + self form addChild: headerElement. + self form addChild: inputElement "self form addChild: diffElement." ] { #category : #accessing } @@ -358,10 +381,14 @@ MAElementBuilder >> visitDateDescription: aDescription [ self using: aDescription - addInputField: [ + addInputField: [ :headerElement | | editor calendarButton | - editor := self newInputElementUsing: aDescription. - calendarButton := self calendarButtonFor: editor. "calendar date: event date. Redundant because a new calendar will be created on next button click" + editor := (self newInputElementUsing: aDescription) + header: headerElement; + yourself. + editor constraintsDo: [ :c | + c minWidth: 100 ]. + calendarButton := self calendarButtonFor: editor. BrHorizontalPane new fitContent; cellSpacing: 5; @@ -387,6 +414,7 @@ MAElementBuilder >> visitDurationDescription: anObject [ { #category : #'visiting-description' } MAElementBuilder >> visitElementDescription: aDescription [ + self addInputFieldUsing: aDescription ] @@ -540,3 +568,13 @@ MAElementBuilder >> visitTokenDescription: anObject [ MAElementBuilder >> visitUrlDescription: anObject [ self visitElementDescription: anObject ] + +{ #category : #accessing } +MAElementBuilder >> warningIcon [ + + ^ BlElement new + geometry: BlTriangleGeometry new beTop; + size: 8@8; + background: Color orange; + yourself. +] diff --git a/source/Magritte-GToolkit/MAFieldElement.class.st b/source/Magritte-GToolkit/MAFieldElement.class.st new file mode 100644 index 00000000..cd296522 --- /dev/null +++ b/source/Magritte-GToolkit/MAFieldElement.class.st @@ -0,0 +1,5 @@ +Class { + #name : #MAFieldElement, + #superclass : #BrVerticalPane, + #category : #'Magritte-GToolkit' +} diff --git a/source/Magritte-GToolkit/MAGtInputFieldAptitude.class.st b/source/Magritte-GToolkit/MAGtInputFieldAptitude.class.st new file mode 100644 index 00000000..29ae6dfe --- /dev/null +++ b/source/Magritte-GToolkit/MAGtInputFieldAptitude.class.st @@ -0,0 +1,12 @@ +Class { + #name : #MAGtInputFieldAptitude, + #superclass : #BrGlamorousInputFieldSpacingAptitude, + #category : #'Magritte-GToolkit' +} + +{ #category : #accessing } +MAGtInputFieldAptitude >> initialize [ + + super initialize. + self add: BrGlamorousEditorAptitude +]