<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/field/BaseField.kt<|end_filename|>
package me.ibrahimsn.lib.internal.field

import android.view.View
import me.ibrahimsn.lib.api.field.Field
import me.ibrahimsn.lib.internal.core.Config
import me.ibrahimsn.lib.api.rule.Ruler

abstract class BaseField<T : View> : Field {

    abstract val view: T

    internal abstract val config: Config

    override val id: Int get() = view.id

    override var isValid: Boolean = false

    override var error: Ruler? = null

    override var onValidationChangeListener: ((Field) -> Unit)? = null

    override fun validate(): Field {
        this.isValid = true
        this.error = null
        config.rulers.forEach {
            if (!it.validate(getValue())) {
                this.error = it
                this.isValid = false
                return this
            }
        }
        return this
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/core/Parser.kt<|end_filename|>
package me.ibrahimsn.lib.internal.core

import android.content.Context
import android.content.res.Resources
import android.content.res.XmlResourceParser
import androidx.annotation.XmlRes
import me.ibrahimsn.lib.api.rule.Ruler
import me.ibrahimsn.lib.internal.Constants
import me.ibrahimsn.lib.internal.Constants.FIELD_TAG
import me.ibrahimsn.lib.internal.Constants.ID_ATTR

internal class Parser(context: Context, @XmlRes res: Int) {

    private val parser: XmlResourceParser = context.resources.getXml(res)

    fun parse(): Map<Int, Config> {
        val items: MutableMap<Int, Config> = mutableMapOf()
        var eventType: Int?

        do {
            eventType = parser.next()
            if (eventType == XmlResourceParser.START_TAG && parser.name == FIELD_TAG) {
                getFieldConfig(parser).let {
                    items[it.id] = it
                }
            }
        } while (eventType != XmlResourceParser.END_DOCUMENT)

        return items
    }

    private fun getFieldConfig(parser: XmlResourceParser): Config {
        var fieldId = Constants.INVALID_RES
        val rulers = mutableListOf<Ruler>()

        for (index in 0 until parser.attributeCount) {
            try {
                when (parser.getAttributeName(index)) {
                    ID_ATTR -> {
                        fieldId = parser.getAttributeResourceValue(index, 0)
                    }
                    Ruler.Required.ATTR -> {
                        rulers.add(
                            Ruler.Required(parser.getAttributeBooleanValue(index, false))
                        )
                    }
                    Ruler.Email.ATTR -> {
                        rulers.add(
                            Ruler.Email(parser.getAttributeBooleanValue(index, false))
                        )
                    }
                    Ruler.PhoneNumber.ATTR -> {
                        rulers.add(
                            Ruler.PhoneNumber(parser.getAttributeBooleanValue(index, false))
                        )
                    }
                    Ruler.MinSize.ATTR -> {
                        rulers.add(
                            Ruler.MinSize(parser.getAttributeIntValue(index, 0))
                        )
                    }
                    Ruler.MaxSize.ATTR -> {
                        rulers.add(
                            Ruler.MaxSize(parser.getAttributeIntValue(index, 0))
                        )
                    }
                    Ruler.Regex.ATTR -> {
                        rulers.add(
                            Ruler.Regex(parser.getAttributeValue(index))
                        )
                    }
                }
            } catch (e: Resources.NotFoundException) {
                // ignored
            }
        }
        return Config(fieldId, rulers)
    }
}

<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/field/TextInputLayoutField.kt<|end_filename|>
package me.ibrahimsn.lib.internal.field

import com.google.android.material.textfield.TextInputLayout
import me.ibrahimsn.lib.internal.core.Config
import me.ibrahimsn.lib.internal.watcher.FocusOutWatcher
import me.ibrahimsn.lib.internal.watcher.TextChangeWatcher

internal data class TextInputLayoutField(
    override val view: TextInputLayout,
    override val config: Config
) : BaseField<TextInputLayout>() {

    private val textChangeWatcher = view.editText?.let {
        TextChangeWatcher(it) {
            val isValid = this.isValid
            val validated = validate()

            if (validated.isValid && isValid != validated.isValid) {
                onValidationChangeListener?.invoke(validated)
                clearError()
            }
        }
    }

    private val focusOutWatcher = view.editText?.let {
        FocusOutWatcher(it) {
            if (!validate().isValid) {
                onValidationChangeListener?.invoke(this)
            }
        }
    }

    override fun getValue(): Any? {
        return view.editText?.text
    }

    override fun setValue(value: Any?) {
        view.editText?.setText(value.toString())
    }

    override fun setError(error: String?) {
        view.isErrorEnabled = true
        view.error = error
    }

    override fun clearError() {
        view.isErrorEnabled = false
        view.error = null
    }

    override fun onAttached() {
        textChangeWatcher?.register()
        focusOutWatcher?.register()
    }

    override fun onDetached() {
        textChangeWatcher?.unregister()
        focusOutWatcher?.unregister()
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/watcher/FocusOutWatcher.kt<|end_filename|>
package me.ibrahimsn.lib.internal.watcher

import android.view.View
import android.view.ViewTreeObserver

internal class FocusOutWatcher(
    override val view: View,
    private inline val onFocusOut: ((Int) -> Unit)
) : BaseWatcher<ViewTreeObserver.OnGlobalFocusChangeListener>() {

    override var listener: ViewTreeObserver.OnGlobalFocusChangeListener? =
        ViewTreeObserver.OnGlobalFocusChangeListener { oldFocus, _ ->
            if (oldFocus == view) {
                onFocusOut.invoke(view.id)
            }
        }

    override fun register() {
        view.viewTreeObserver.addOnGlobalFocusChangeListener(listener)
    }

    override fun unregister() {
        view.viewTreeObserver.removeOnGlobalFocusChangeListener(listener)
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/api/rule/Ruler.kt<|end_filename|>
package me.ibrahimsn.lib.api.rule

import android.util.Patterns

sealed class Ruler : Rule {

    class Required(override val param: Boolean) : Ruler() {

        override fun validate(data: Any?): Boolean {
            return !param || !data?.toString().isNullOrEmpty()
        }

        companion object {
            const val ATTR = "required"
        }
    }

    class MinSize(override val param: Int) : Ruler() {

        override fun validate(data: Any?): Boolean {
            return data.toString().isEmpty() || data.toString().length >= param
        }

        companion object {
            const val ATTR = "minSize"
        }
    }

    class MaxSize(override val param: Int) : Ruler() {

        override fun validate(data: Any?): Boolean {
            return data.toString().isEmpty() || data.toString().length <= param
        }

        companion object {
            const val ATTR = "maxSize"
        }
    }

    class Email(override val param: Boolean) : Ruler() {

        override fun validate(data: Any?): Boolean {
            return !param || data.toString().isEmpty() || (data != null && Patterns.EMAIL_ADDRESS
                .matcher(data.toString()).matches())
        }

        companion object {
            const val ATTR = "email"
        }
    }

    class PhoneNumber(override val param: Boolean) : Ruler() {

        override fun validate(data: Any?): Boolean {
            return !param || data.toString().isEmpty() || (data != null && Patterns.PHONE
                .matcher(data.toString()).matches())
        }

        companion object {
            const val ATTR = "phoneNumber"
        }
    }

    class Regex(override val param: String) : Ruler() {

        override fun validate(data: Any?): Boolean {
            return data.toString().isEmpty() || data.toString().matches(param.toRegex())
        }

        companion object {
            const val ATTR = "regex"
        }
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/FreyaForm.kt<|end_filename|>
package me.ibrahimsn.lib

import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.EditText
import androidx.appcompat.widget.LinearLayoutCompat
import com.google.android.material.textfield.TextInputLayout
import me.ibrahimsn.lib.internal.Constants
import me.ibrahimsn.lib.internal.core.Parser
import me.ibrahimsn.lib.internal.field.EditTextField
import me.ibrahimsn.lib.api.field.Field
import me.ibrahimsn.lib.internal.field.TextInputLayoutField
import me.ibrahimsn.lib.api.rule.Ruler
import me.ibrahimsn.lib.internal.core.Config

class FreyaForm @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayoutCompat(context, attrs, defStyleAttr) {

    /**
     * Holds form fields in conjunction to their view id's
     */
    private val fields: MutableMap<Int, Field> = LinkedHashMap()

    /**
     * Holds field configs in conjunction to their view id's
     */
    private var fieldConfigs = mapOf<Int, Config>()

    /**
     * Provides all of the form field values
     */
    val values get() = fields.mapValues {
        it.value.getValue()
    }

    /**
     * Listens for validation change of the whole form
     */
    var onValidationChangeListener: ((Boolean) -> Unit)? = null

    /**
     * Listens for validation change of each form field
     */
    var onFieldValidationChangeListener: ((Field) -> Unit)? = null

    /**
     * Listens for field errors after form validation
     */
    var onErrorListener: ((List<Ruler>) -> Unit)? = null

    init {
        obtainStyledAttributes(attrs, defStyleAttr)
        orientation = VERTICAL
    }

    /**
     * Obtains styled view attributes
     *
     * @param attrs Any attribute values in the given AttributeSet
     * @param defStyleAttr The default style specified by defStyleAttr and defStyleRes
     */
    private fun obtainStyledAttributes(attrs: AttributeSet?, defStyleAttr: Int) {
        val typedArray = context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.Freya,
            defStyleAttr,
            0
        )

        try {
            val configRes = typedArray.getResourceId(
                R.styleable.Freya_config,
                Constants.INVALID_RES
            )

            fieldConfigs = Parser(context, configRes).parse()
        } catch (e: Exception) {
            // ignore
        } finally {
            typedArray.recycle()
        }
    }

    /**
     * Listens global layout changes to make sure the layout is completely rendered
     */
    private val onGlobalLayoutListener = object: ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            handleViewChildren(this@FreyaForm)
            viewTreeObserver.removeOnGlobalLayoutListener(this)
        }
    }

    /**
     * Listens for form field validation changes
     */
    private val onFieldValidationListener: ((Field) -> Unit) = { field: Field ->
        onFieldValidationChangeListener?.invoke(field)
    }

    /**
     * Validates the whole form
     *
     * @return Is the form actually valid
     */
    fun validate(): Boolean {
        var isValid = true
        val errors = mutableListOf<Ruler>()

        fields.values.forEach { field ->
            field.validate()
            if (!field.isValid) {
                isValid = false
                field.error?.let { ruler ->
                    errors.add(ruler)
                }
            }
        }
        if (errors.isNotEmpty()) {
            onErrorListener?.invoke(errors.toList())
        }
        return isValid
    }

    /**
     * Iterates and separates form fields
     *
     * @param group layout container view
     */
    private fun handleViewChildren(group: ViewGroup) {
        for (i in 0 until group.childCount) {
            when (val child = group.getChildAt(i)) {
                is TextInputLayout -> handleTextInputLayout(child)
                is EditText -> handleEditText(child)
                is ViewGroup -> handleViewChildren(child)
            }
        }
    }

    /**
     * Handles TextInputLayout fields to setup
     *
     * @param input TextInputLayout view to handle
     */
    private fun handleTextInputLayout(input: TextInputLayout) {
        fieldConfigs[input.id]?.let { config ->
            val field = TextInputLayoutField(input, config)
            field.onValidationChangeListener = onFieldValidationListener
            field.onAttached()
            fields[input.id] = field
        }
    }

    /**
     * Handles EditText fields to setup
     *
     * @param input EditText view to handle
     */
    private fun handleEditText(input: EditText) {
        fieldConfigs[input.id]?.let { config ->
            val field = EditTextField(input, config)
            field.onValidationChangeListener = onFieldValidationListener
            field.onAttached()
            fields[input.id] = field
        }
    }

    /**
     * Setup data to pre-fill form fields
     *
     * @param values Map instance that holds field data in conjunction to their view id's
     */
    fun setup(values: Map<Int, Any?>) {
        post {
            values.forEach {
                if (it.value != null) {
                    fields[it.key]?.setValue(it.value)
                }
            }
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
        fields.values.forEach {
            it.onDetached()
        }
    }
}


<|start_filename|>app/src/main/java/me/ibrahimsn/freya/MainActivity.kt<|end_filename|>
package me.ibrahimsn.freya

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import me.ibrahimsn.lib.FreyaForm
import me.ibrahimsn.lib.api.rule.Ruler

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val freya = findViewById<FreyaForm>(R.id.freya)
        val submit = findViewById<Button>(R.id.submit)

        /*
         * Listens if the whole form is valid
         */
        freya.onValidationChangeListener = {
            Log.d(TAG, "Is form valid: $it")
        }

        /*
         * Listens for validation changes of any form field
         */
        freya.onFieldValidationChangeListener = {
            when (it.error) {
                is Ruler.Required -> {
                    it.setError("This field is required.")
                }
                is Ruler.Email -> {
                    it.setError("Please enter a valid email address.")
                }
                is Ruler.PhoneNumber -> {
                    it.setError("Please enter a valid phone number.")
                }
                is Ruler.MinSize -> {
                    it.setError("This field must contain at least ${it.error?.param} characters.")
                }
                is Ruler.MaxSize -> {
                    it.setError("This field must contain at most ${it.error?.param} characters.")
                }
                is Ruler.Regex -> {
                    it.setError("Please provide a valid data.")
                }
            }
        }

        /*
         * Listens for form validation errors after validate()
         */
        freya.onErrorListener = {
            Log.d(TAG, "Validation errors: $it")
        }

        /*
         * Pre-fills form fields
         */
        freya.setup(
            mapOf(
                R.id.inputUsername to "Thomas"
            )
        )

        submit.setOnClickListener {
            Log.d(TAG, "Is form valid: ${freya.validate()} values: ${freya.values}")
        }
    }

    companion object {
        private const val TAG = "mainActivity"
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/util/DefaultTextWatcher.kt<|end_filename|>
package me.ibrahimsn.lib.internal.util

import android.text.TextWatcher

internal abstract class DefaultTextWatcher : TextWatcher {

    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        // can be overridden
    }

    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        // can be overridden
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/api/rule/Rule.kt<|end_filename|>
package me.ibrahimsn.lib.api.rule

interface Rule {

    val param: Any?

    fun validate(data: Any?): Boolean
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/field/EditTextField.kt<|end_filename|>
package me.ibrahimsn.lib.internal.field

import android.widget.EditText
import me.ibrahimsn.lib.internal.core.Config
import me.ibrahimsn.lib.internal.watcher.FocusOutWatcher
import me.ibrahimsn.lib.internal.watcher.TextChangeWatcher

internal data class EditTextField(
    override val view: EditText,
    override val config: Config
) : BaseField<EditText>() {

    private val textChangeWatcher = TextChangeWatcher(view) {
        val isValid = this.isValid
        val validated = validate()

        if (validated.isValid && isValid != validated.isValid) {
            onValidationChangeListener?.invoke(validated)
            clearError()
        }
    }

    private val focusOutWatcher = FocusOutWatcher(view) {
        if (!validate().isValid) {
            onValidationChangeListener?.invoke(this)
        }
    }

    override fun getValue(): Any? {
        return view.text
    }

    override fun setValue(value: Any?) {
        view.setText(value.toString())
    }

    override fun setError(error: String?) {
        view.error = error
    }

    override fun clearError() {
        if (view.error != null) view.error = null
    }

    override fun onAttached() {
        textChangeWatcher.register()
        focusOutWatcher.register()
    }

    override fun onDetached() {
        textChangeWatcher.unregister()
        focusOutWatcher.unregister()
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/watcher/Watcher.kt<|end_filename|>
package me.ibrahimsn.lib.internal.watcher

internal interface Watcher {

    fun register()

    fun unregister()
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/watcher/TextChangeWatcher.kt<|end_filename|>
package me.ibrahimsn.lib.internal.watcher

import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
import me.ibrahimsn.lib.internal.util.DefaultTextWatcher

internal class TextChangeWatcher(
    override val view: EditText,
    private inline val onTextChanged: ((Int) -> Unit)
) : BaseWatcher<TextWatcher>() {

    override var listener: TextWatcher? = object: DefaultTextWatcher() {
        override fun afterTextChanged(p0: Editable?) {
            onTextChanged.invoke(view.id)
        }
    }

    override fun register() {
        view.addTextChangedListener(listener)
    }

    override fun unregister() {
        view.removeTextChangedListener(listener)
    }
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/core/Config.kt<|end_filename|>
package me.ibrahimsn.lib.internal.core

import me.ibrahimsn.lib.api.rule.Ruler

internal data class Config(
    val id: Int,
    val rulers: List<Ruler>
)


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/api/field/Field.kt<|end_filename|>
package me.ibrahimsn.lib.api.field

import me.ibrahimsn.lib.api.rule.Ruler

interface Field {

    val id: Int

    var isValid: Boolean

    var error: Ruler?

    var onValidationChangeListener: ((Field) -> Unit)?

    fun getValue(): Any?

    fun setValue(value: Any?)

    fun setError(error: String?)

    fun clearError()

    fun validate(): Field

    fun onAttached()

    fun onDetached()
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/watcher/BaseWatcher.kt<|end_filename|>
package me.ibrahimsn.lib.internal.watcher

import android.view.View

internal abstract class BaseWatcher<T> : Watcher {

    abstract val view: View

    abstract var listener: T?
}


<|start_filename|>lib/src/main/java/me/ibrahimsn/lib/internal/Constants.kt<|end_filename|>
package me.ibrahimsn.lib.internal

internal object Constants {

    const val INVALID_RES = -1
    const val FIELD_TAG = "formField"
    const val ID_ATTR = "id"
}
