using afIoc::Inject
using afIoc::Scope
** Holds all the meta data required to convert a field on a Fantom object to HTML and back again.
class FormField {
** A link back to the owning 'FormBean' instance.
FormBean formBean
** The Fantom field this 'FormField' represents.
Field field
** The 'Str' value that will be rendered in the HTML form.
** You may set this value before the form is rendered to set a default value.
**
** If the 'formValue' is 'null' then the field value is used instead and converted by 'valueEncoder'.
**
** This 'formValue' is also set during form validation so any user entered values are re-rendered should the form be re-displayed.
Str? formValue
** Used as temporary store when uploading binary data, such as 'Bufs' and 'Files'.
** Contains the value that the form field will be set to.
Obj? formData
** The 'afBedSheet::ValueEncoder' used to convert the field value to and from a 'Str'.
**
** If 'null' then a default 'ValueEncoder' based on the field type is chosen from BedSheet's 'ValueEncoders' service.
Obj? valueEncoder
** The 'InputSkin' used to render the field to HTML.
**
** If 'null' then a default 'InputSkin' is chosen based on the 'type' attribute.
InputSkin? inputSkin
** The error message associated with this field.
**
** Setting this to a non-null value invalidate the form field.
Str? errMsg { set { if (it != null) invalid = true; &errMsg = it } }
** Is this form field invalid?
**
** Setting this to 'false' also clears any 'errMsg'.
Bool invalid { set { if (it == false) errMsg = null; &invalid = it } }
** If 'true' then the field is rendered into the HTML form as normal, but no attempt is made
** to validate the form value or decode it back to a Fantom value.
**
** Useful for rendering static, read only, HTML associated with the field.
Bool? viewOnly
** A general stash, handy for passing data to static validate methods.
[Str:Obj?]? stash
** A static method that performs extra server side validation.
Method? validationMethod
// ---- Html Options ------------------------------------------------------------------------
** HTML attribute.
** The type of input to render.
**
** If 'null' then it defaults to 'text'.
Str? type
** The label to display next to the '<input>'.
**
** If 'null' then it defaults to a human readable version of the field name.
Str? label
** HTML attribute.
** The value to render as a 'placeholder' attribute on the '<input>'.
Str? placeholder
** If non-null an extra '<div>' is rendered after the '<input>' to supply a helpful hint.
** The hint is usually rendered with the 'formBean-hint' CSS class.
Str? hint
** HTML attribute.
** The value to render as a CSS 'class' attribute on the '<input>'.
Str? css
** HTML attribute.
** If true then a disabled attribute is rendered on the '<input>'.
Bool disabled
** HTML attribute.
** The value to render as an 'autocomplete' attribute on the '<input>'.
** See [autocomplete on whatwg]`https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls%3A-the-autocomplete-attribute` and [MDN]`https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete` for valid values.
** Example:
**
** autocomplete = "cc-number"
Str? autocomplete
** HTML attribute.
** Any other miscellaneous attributes that should be rendered on the '<input>'.
** Example:
**
** attributes = "data-foo='bar'"
Str? attributes
// ---- Validation Options ------------------------------------------------------------------------
** HTML5 validation attribute.
** Set to 'true' to mark the input as required.
** If 'null' (the default) then the input is required if the field is non-nullable.
Bool? required
** HTML5 validation attribute.
** Sets the minimum length (inclusive) a string should be.
Int? minLength
** HTML5 validation attribute.
** Sets the maximum value (inclusive). May be an 'Int', 'Date', 'DateTime', or 'Str'.
Int? maxLength
** HTML5 validation attribute.
** Sets the minimum value (inclusive). May be an 'Int', 'Date', 'DateTime', or 'Str'.
Obj? min
** HTML5 validation attribute.
** Sets the maximum value (inclusive) for numbers ('Int') and dates ('Date').
Obj? max
** HTML5 validation attribute.
** Sets a regular expression that the (stringified) value should match.
** Starting '^' and ending '$' characters are implicit and not required.
Regex? pattern
** HTML5 validation attribute.
** Defines the interval for a numeric input.
Int? step
// ---- Select Options ------------------------------------------------------------------------
** Used by the '<select>' renderer.
** Set to 'true' to show a blank value at the start of the options list.
**
** leave as null to use 'OptionsProvider.showBlank' value.
Bool? showBlank
** Used by the '<select>' renderer.
** This is the label to display in the blank option.
**
** leave as null to use 'OptionsProvider.blankLabel' value.
Str? blankLabel
** Used by the '<select>' renderer.
** The 'OptionsProvider' used to supply option values when rendering '<select>' tags.
**
** If 'null' then a default 'OptionsProvider' is chosen based on the field type.
OptionsProvider? optionsProvider
@Inject private const |->Scope| _scope
@Inject private const InputSkins _inputSkins
@Inject private const WebProxy _webProxy
** Create 'FormField' instances via IoC:
**
** syntax: fantom
** formField := scope.build(FormField#, [field, formBean])
**
new make(Field field, FormBean formBean, |This| in) {
this.field = field
this.formBean = formBean
in(this)
}
** Populates this 'FormField' instance with values from the '@HtmlInput' facet (if any) and
** message values.
This populate() {
input := (HtmlInput?) field.facet(HtmlInput#, false)
valueEncoder = _fromObjCache(input?.valueEncoder ?: msg("valueEncoder"))
inputSkin = _fromObjCache(input?.inputSkin ?: msg("inputSkin"))
optionsProvider = _fromObjCache(input?.optionsProvider ?: msg("optionsProvider"))
type = input?.type ?: msg("type" )
label = input?.label ?: msg("label" )
placeholder = input?.placeholder ?: msg("placeholder")
hint = input?.hint ?: msg("hint" )
css = input?.css ?: msg("css" )
disabled = (input?.disabled ?: msg("disabled" )?.toBool) ?: false
autocomplete = input?.autocomplete ?: msg("autocomplete")
attributes = input?.attributes ?: msg("attributes" )
viewOnly = input?.viewOnly ?: msg("viewOnly" )?.toBool
required = input?.required ?: msg("required" )?.toBool
minLength = input?.minLength ?: msg("minLength" )?.toInt
maxLength = input?.maxLength ?: msg("maxLength" )?.toInt
min = input?.min ?: msg("min" ) // don't convert to Int, Date, etc... instead allow values to be passed through to HTML
max = input?.max ?: msg("max" ) // don't convert to Int, Date, etc... instead allow values to be passed through to HTML
pattern = input?.pattern ?: msg("pattern" )?.toRegex
step = input?.step ?: msg("step" )?.toInt
showBlank = input?.showBlank ?: msg("showBlank" )?.toBool
blankLabel = input?.blankLabel ?: msg("blankLabel" )
validationMethod= input?.validationMethod ?: Method.findMethod(msg("validationMethod") ?: "<pod>::<type>.<slot>", false)
return this
}
private Obj? _fromObjCache(Obj? what) {
if (what is Str)
what = Type.find(what)
if (what is Type)
return _webProxy.getObj(what)
return null
}
** Returns a message for the given field. Messages are looked up in the following order:
**
** - '<bean>.<field>.<key>'
** - '<field>.<key>'
** - '<key>'
**
** And the following substitutions are made:
**
** - '${label} -> formField.label'
** - '${value} -> formField.formValue'
** - '${arg1} -> arg1.toStr'
** - '${arg2} -> arg2.toStr'
** - '${arg3} -> arg3.toStr'
**
** The form value is substituted for '${value}' because it is intended for use by validation msgs.
**
** Returns 'null' if a msg could not be found.
Str? msg(Str key, Obj? arg1 := null, Obj? arg2 := null, Obj? arg3 := null) {
formBean.fieldMsg(this, key, arg1, arg2, arg3)
}
** Convenience for 'msg()'.
** Returns a message for the given field. Messages are looked up in the following order:
**
** - '<bean>.<field>.<key>'
** - '<field>.<key>'
** - '<key>'
**
** And the following substitutions are made:
**
** - '${label} -> formField.label'
** - '${value} -> formField.formValue'
** - '${arg1} -> arg1.toStr'
** - '${arg2} -> arg2.toStr'
** - '${arg3} -> arg3.toStr'
**
** The form value is substituted for '${value}' because it is intended for use by validation msgs.
**
** Returns 'null' if a msg could not be found.
@Operator @NoDoc
Str getMsg(Str key, Obj? arg1 := null, Obj? arg2 := null, Obj? arg3 := null) {
msg(key, arg1, arg2, arg3)
}
** Sets the given form field message. Messages are stored in the FormBean under the key:
**
** - '<bean>.<field>.<key>'
**
** 'null' values are removed from the messges map.
@Operator
Void set(Str key, Obj? val) {
msgKey := "${formBean.beanType.name}.${field.name}.${key}"
if (val == null)
formBean.messages.remove(msgKey)
else
formBean.messages.set(msgKey, val.toStr)
}
** Hook to render this field to HTML.
** By default this defers rendering to an 'InputSkin'.
**
** Override to perform custom field rendering.
virtual Str render(Obj? bean := null) {
skinCtx := SkinCtx() {
it.bean = bean
it.field = this.field
it.formBean = this.formBean
it.formField = this
}
inputSkin := inputSkin ?: _inputSkins.find(type ?: "text")
return inputSkin.render(skinCtx)
}
** Converts the given value to a string using the preferred 'ValueEncoder'.
Str toClient(Obj? value) {
strVal := (Str) ((valueEncoder != null) ? valueEncoder->toClient(value) : _webProxy.toClient(field.type, value))
return strVal.toXml
}
** Converts the given client value (string) to a server side object using the preferred 'ValueEncoder'.
**
**
Obj? toValue(Str clientValue) {
((valueEncoder != null) ? valueEncoder->toValue(clientValue) : _webProxy.toValue(field.type, clientValue))
}
** Validates this form field.
** Calls 'doHtmlValidation()' and then any static '@Validate' method that corresponds to this field.
**
** '@Validate' methods may check 'invalid' and 'errMsg' to ascertain if any previous validation failed.
**
** After validation check the value of the 'invalid' and 'errMsg' fields.
virtual Void validate() {
doHtmlValidation
if (validationMethod != null)
_scope().callMethod(validationMethod, null, [this])
// should it be validationMethod AND / OR @Validate methods?
field.parent.methods
.findAll { ((Validate?) it.facet(Validate#, false))?.field == field }
.each { _scope().callMethod(it, null, [this]) }
}
** Performs basic HTML5 validation.
virtual Void doHtmlValidation() {
// formValue should already be trimmed
hasValue := formValue != null && !formValue.isEmpty
if (required ?: false)
if (formValue == null || formValue.isEmpty)
return errMsg = msg("required.msg") ?: "[required.msg]"
if (hasValue && minLength != null)
if (formValue.size < minLength)
return errMsg = msg("minLength.msg", minLength) ?: "[minLength.msg]"
if (hasValue && maxLength != null)
if (formValue.size > maxLength)
return errMsg = msg("maxLength.msg", maxLength) ?: "[maxLength.msg]"
if (hasValue && type == "number")
if (formValue.toInt(10, false) == null)
return errMsg = msg("notNum.msg") ?: "[notNum.msg]"
if (hasValue && min?.toStr?.toInt(10, false) != null) {
if (formValue.toInt(10, false) == null)
return errMsg = msg("notNum.msg") ?: "[notNum.msg]"
if (formValue.toInt < min.toStr.toInt)
return errMsg = msg("min.msg", min) ?: "[min.msg]"
}
if (hasValue && max?.toStr?.toInt(10, false) != null) {
if (formValue.toInt(10, false) == null)
return errMsg = msg("notNum.msg") ?: "[notNum.msg]"
if (formValue.toInt > max.toStr.toInt)
return errMsg = msg("max.msg", max) ?: "[max.msg]"
}
if (hasValue && pattern != null)
if (!"^${pattern}\$".toRegex.matches(formValue))
return errMsg = msg("pattern.msg", pattern) ?: "[pattern.msg]"
}
@NoDoc
override Int hash() { field.hash }
@NoDoc
override Bool equals(Obj? that) { (that as FormField)?.field == field }
@NoDoc
override Str toStr() { "${field} - ${formValue}" }
}