sourceafFormBean::FormField.fan

using afIoc
using afBedSheet::ValueEncoder
using afBedSheet::ValueEncoders

** 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

    ** The '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. 
    ValueEncoder?   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
    


    // ---- 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. 
    ** Any other miscellaneous attributes that should be rendered on the '<input>'. 
    ** Example:
    ** 
    **   attributes = "autocomplete='off'"
    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 length (inclusive) a string should be.
    Int?    maxLength

    ** HTML5 validation attribute.
    ** Sets the minimum value (inclusive) an 'Int' should have.
    Int?    min
    
    ** HTML5 validation attribute.
    ** Sets the maximum value (inclusive) an 'Int' should have.
    Int?    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
    
    ** 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

    @Inject private const   |->Scope|       _scope
    @Inject private const   InputSkins      _inputSkins
    @Inject private const   ValueEncoders   _valueEncoders
    
    @NoDoc // Boring!
    new make(|This| in) { in(this) }
    
    
    ** 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)
    }

    ** 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) {
        skinCtx := SkinCtx() {
            it.bean             = bean
            it.field            = this.field
            it.formBean         = this.formBean
            it.formField        = this
            it._valueEncoders   = this._valueEncoders
        }
        
        inputSkin := inputSkin ?: _inputSkins.find(type ?: "text")
        return inputSkin.render(skinCtx)        
    }
    
    ** 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. 
    virtual Void validate() {
        doHtmlValidation
        
        field.parent.methods
            .findAll { ((Validate?) it.facet(Validate#, false))?.field == field }
            .each    { _scope().callMethod(it, null, [this]) }
    }

    ** Performs basic HTML5 validation.
    virtual Void doHtmlValidation() {
        hasValue := formValue != null && !formValue.isEmpty

        if (required ?: false)
            if (formValue == null || formValue.isEmpty)
                return errMsg = msg("required.msg")
        
        if (hasValue && minLength != null)
            if (formValue.size < minLength)
                return errMsg = msg("minLength.msg", minLength)

        if (hasValue && maxLength != null)
            if (formValue.size > maxLength)
                return errMsg = msg("maxLength.msg", maxLength)

        if (hasValue && type == "number")
            if (formValue.toInt(10, false) == null)
                return errMsg = msg("notNum.msg")
    
        if (hasValue && min != null) {
            if (formValue.toInt(10, false) == null)
                return errMsg = msg("notNum.msg")
            if (formValue.toInt < min)
                return errMsg = msg("min.msg", min)
        }

        if (hasValue && max != null) {
            if (formValue.toInt(10, false) == null)
                return errMsg = msg("notNum.msg")
            if (formValue.toInt > max)
                return errMsg = msg("max.msg", max)
        }

        if (hasValue && pattern != null)
            if (!"^${pattern}\$".toRegex.matches(formValue))
                return errMsg = msg("pattern.msg", pattern)         
    }
}