All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.udash.bootstrap.form.UdashForm.scala Maven / Gradle / Ivy

package io.udash.bootstrap
package form

import com.avsystem.commons._
import com.avsystem.commons.misc.{AbstractCase, AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx}
import io.udash._
import io.udash.bindings.inputs.InputBinding
import io.udash.bindings.modifiers.Binding
import io.udash.bindings.modifiers.Binding.NestedInterceptor
import io.udash.bootstrap.form.UdashForm.{HorizontalLayoutSettings, ValidationTrigger}
import io.udash.bootstrap.utils.BootstrapStyles.ResponsiveBreakpoint
import io.udash.bootstrap.utils.{BootstrapStyles, UdashBootstrapComponent}
import io.udash.css.CssStyle
import io.udash.logging.CrossLogging
import io.udash.properties.seq
import org.scalajs.dom._
import org.scalajs.dom.html.{Form, Input => JSInput}
import scalatags.JsDom.all._

import scala.concurrent.duration.{Duration, DurationLong}
import scala.util.{Failure, Success}

final class UdashForm private(
  formStyle: Option[CssStyle],
  inputValidationTrigger: ValidationTrigger,
  selectValidationTrigger: ValidationTrigger,
  override val componentId: ComponentId
)(content: FormElementsFactory => Modifier)
  extends UdashBootstrapComponent with Listenable with CrossLogging {

  import io.udash.css.CssView._

  override type EventType = UdashForm.FormEvent

  private[form] val validationProperties: MSet[Property[Option[ValidationResult]]] = MSet.empty

  def clearValidationResults(): Unit = {
    validationProperties.foreach(_.set(None))
  }

  override val render: Form =
    form(formStyle)(
      onsubmit :+= { (_: Event) => fire(new UdashForm.FormEvent(this, UdashForm.FormEvent.EventType.Submit)); true },
      content(new FormElementsFactory(nestedInterceptor, inputValidationTrigger, selectValidationTrigger, this))
    ).render
}

object UdashForm {
  final case class FormEvent(override val source: UdashForm, tpe: FormEvent.EventType)
    extends AbstractCase with ListenableEvent
  object FormEvent {
    final class EventType(implicit enumCtx: EnumCtx) extends AbstractValueEnum
    object EventType extends AbstractValueEnumCompanion[EventType] {
      /** Fired on the form submit. */
      final val Submit: Value = new EventType
    }
  }

  /** Decides when the provided property will be validated in order to highlight the input. */
  final class ValidationTrigger(implicit enumCtx: EnumCtx) extends AbstractValueEnum
  object ValidationTrigger extends AbstractValueEnumCompanion[ValidationTrigger] {
    /** The validation is disabled. */
    final val None: Value = new ValidationTrigger
    /** The validation will be triggered on initialization and will be retriggered on each property value change. */
    final val Instant: Value = new ValidationTrigger
    /** The validation will be triggered on each value change. */
    final val OnChange: Value = new ValidationTrigger
    /** The validation will be triggered on `blur` event on the input. */
    final val OnBlur: Value = new ValidationTrigger
    /** The validation will be triggered on the form `submit` event.
     * Notice that the validation won't block the form submit. */
    final val OnSubmit: Value = new ValidationTrigger
  }

  /** Settings for the horizontal form layout.
   * More: Bootstrap Docs.
   *
   * @param labelWidth Width of the label column.
   * @param inputWidth Width of the input column.
   * @param breakpoint Breakpoint of the form's grid.
   * @param labelSize  Size of the label text.
   */
  final case class HorizontalLayoutSettings(
    labelWidth: Int = 4,
    inputWidth: Int = 8,
    breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.Medium,
    labelSize: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None
  ) extends AbstractCase

  /**
   * Creates a standard form with a provided content. 
* Example:
*
   * UdashForm() { factory => Seq(
   *   factory.input.formGroup()(
   * nested => factory.input.textInput(name, validationTrigger = ValidationTrigger.OnBlur)().render,
   * labelContent = nested => Some(span("Name: ", nested(bind(name)))),
   * validFeedback = _ => Some(span("Looks good.")),
   * invalidFeedback = _ => Some(span("Name is too short."))
   * ),
   *   factory.input.formGroup()(
   * nested => factory.input.passwordInput(name, validationTrigger = ValidationTrigger.Instant)().render,
   * labelContent = nested => Some(span("Password: ", nested(bind(name)))),
   * validFeedback = _ => Some(span("Looks good.")),
   * invalidFeedback = _ => Some(span("Name is too short."))
   * )
   * )}.render
   * 
* More: Bootstrap Docs. * * @param inline If true, creates an inline form. * @param inputValidationTrigger Default validation trigger for text inputs in this form. * @param selectValidationTrigger Default validation trigger for selectors like checkboxes or select inputs. * @param componentId An id of the root DOM node. * @param content A factory of the form elements. All elements created with the factory will be cleaned up on the form cleanup. * @return A `UdashForm` component, call `render` to create a DOM element. */ def apply( inline: Boolean = false, inputValidationTrigger: ValidationTrigger = ValidationTrigger.OnBlur, selectValidationTrigger: ValidationTrigger = ValidationTrigger.OnChange, componentId: ComponentId = ComponentId.generate() )(content: FormElementsFactory => Modifier): UdashForm = { new UdashForm( Some(BootstrapStyles.Form.inline).filter(_ => inline), inputValidationTrigger, selectValidationTrigger, componentId )(content) } } final class FormElementsFactory( factoryNestedInterceptor: Binding.NestedInterceptor, inputValidationTrigger: ValidationTrigger, selectValidationTrigger: ValidationTrigger, form: OptArg[UdashForm] = OptArg.Empty ) extends CrossLogging { import io.udash.css.CssView._ /** Use this method to bond the external binding's lifecycle with the lifecycle of the elements created via this factory. */ def externalBinding[T <: Binding](binding: T): T = { factoryNestedInterceptor(binding) binding } /** * Wrapper for disabled elements. * * @param disabled Property indicating if elements are disabled. You can change it anytime. */ def disabled(disabled: ReadableProperty[Boolean] = UdashBootstrap.True)( content: Binding.NestedInterceptor => Modifier ): Modifier = { fieldset( factoryNestedInterceptor((scalatags.JsDom.attrs.disabled := "disabled").attrIf(disabled)) )(content(factoryNestedInterceptor)) } /** Provides input elements factory methods. */ object input { /** * Creates a standard form group for the provided input. It contains a label and validation feedback elements. * The group layout can be organized vertically or horizontally based on the provided options. * You can wrap the result into grid elements in order to create more complex form layout. * * @param horizontal Optional settings for a horizontal layout. If `None`, the layout will be organized vertically. * @param groupId Id of the root element. * @param input The input element. It can be wrapped into `UdashInputGroup` or any other decoration. * Use the provided interceptor to properly clean up bindings inside the content. * @param labelContent Optional label content. * It will be wrapped into `label` element with properly set `for` attribute. * Use the provided interceptor to properly clean up bindings inside the content. * @param validFeedback Optional content of positive validation feedback. * It will be wrapped into `div` element with `valid-feedback` style. * Use the provided interceptor to properly clean up bindings inside the content. * @param invalidFeedback Optional content of negative validation feedback. * It will be wrapped into `div` element with `invalid-feedback` style. * Use the provided interceptor to properly clean up bindings inside the content. * @param helpText Optional content of help text block. * It will be wrapped into `div` element with `form-text text-muted` style. * Use the provided interceptor to properly clean up bindings inside the content. */ def formGroup( horizontal: Option[HorizontalLayoutSettings] = None, groupId: ComponentId = ComponentId.generate() )( input: Binding.NestedInterceptor => Element, labelContent: Option[Binding.NestedInterceptor => Modifier] = None, validFeedback: Option[Binding.NestedInterceptor => Modifier] = None, invalidFeedback: Option[Binding.NestedInterceptor => Modifier] = None, helpText: Option[Binding.NestedInterceptor => Modifier] = None ): UdashBootstrapComponent = { externalBinding(new UdashBootstrapComponent { override val render: Element = horizontal match { case None => val inputEl = input(nestedInterceptor) div(BootstrapStyles.Form.group, groupId)( labelContent.map(content => label(`for` := inputEl.id)(content(nestedInterceptor))), inputEl, validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))), helpText.map(content => div(BootstrapStyles.Form.text, BootstrapStyles.Text.muted)(content(nestedInterceptor))) ).render case Some(HorizontalLayoutSettings(labelWidth, inputWidth, breakpoint, labelSize)) => val inputEl = input(nestedInterceptor) div(BootstrapStyles.Form.group, BootstrapStyles.Grid.row, groupId)( div(BootstrapStyles.Grid.col(labelWidth, breakpoint))( labelContent.map { content => label(`for` := inputEl.id, (BootstrapStyles.Form.colFormLabelSize _).reactiveOptionApply(labelSize))( content(nestedInterceptor) ) } ), div(BootstrapStyles.Grid.col(inputWidth, breakpoint))( inputEl, validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))) ) ).render } override val componentId: ComponentId = groupId }) } /** * Creates a text input with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param property Property which will be synchronised with the input content and validated. * @param debounce Property update timeout after input changes. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided text property */ def textInput( property: Property[String], debounce: Duration = 20 millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, inputId: ComponentId = ComponentId.generate() )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[String] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( TextInput(property, debounce)( inputId, BootstrapStyles.Form.control, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), (BootstrapStyles.Form.size _).reactiveOptionApply(size) ), inputId )) } /** * Creates a password input with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param property Property which will be synchronised with the input content and validated. * @param debounce Property update timeout after input changes. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided password property */ def passwordInput( property: Property[String], debounce: Duration = 20 millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, inputId: ComponentId = ComponentId.generate() )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[String] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( PasswordInput(property, debounce)( inputId, BootstrapStyles.Form.control, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), (BootstrapStyles.Form.size _).reactiveOptionApply(size) ), inputId )) } /** * Creates a number input with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param property Property which will be synchronised with the input content and validated. * @param debounce Property update timeout after input changes. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided number property */ def numberInput( property: Property[Double], debounce: Duration = 20 millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, inputId: ComponentId = ComponentId.generate() )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[Double] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( NumberInput(property.bitransform(_.toString)(_.toDouble), debounce)( inputId, BootstrapStyles.Form.control, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), (BootstrapStyles.Form.size _).reactiveOptionApply(size) ), inputId )) } /** * Creates a range input with a custom bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param value Current value synchronised with the input. * The value should be between `minValue` and `maxValue`. It should be also divisible by `valueStep`. * @param minValue The minimum value for this input, which must not be greater than its maximum (`maxValue` attribute) value. * @param maxValue The maximum value for the input. Must not be less than its minimum (`minValue` attribute) value. * @param valueStep Limit the increments at which a numeric value can be set. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided value property */ def rangeInput( value: Property[Double], minValue: ReadableProperty[Double] = 0d.toProperty, maxValue: ReadableProperty[Double] = 100d.toProperty, valueStep: ReadableProperty[Double] = 1d.toProperty, validationTrigger: ValidationTrigger = selectValidationTrigger, inputId: ComponentId = ComponentId.generate() )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[Double] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( RangeInput(value, minValue, maxValue, valueStep)( inputId, BootstrapStyles.Form.controlRange, BootstrapStyles.Form.customRange, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(value, validationTrigger, factoryNestedInterceptor)(validator) ), inputId )) } /** * Creates a text area with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param property Property which will be synchronised with the input content and validated. * @param debounce Property update timeout after input changes. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided text property */ def textArea( property: Property[String], debounce: Duration = 20 millis, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, inputId: ComponentId = ComponentId.generate() )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[String] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( TextArea(property, debounce)( inputId, BootstrapStyles.Form.control, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(property, validationTrigger, factoryNestedInterceptor)(validator), (BootstrapStyles.Form.size _).reactiveOptionApply(size) ), inputId )) } /** * Creates a file input with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * It creates the whole input compoennt including a label and validation feedback. * * @param selectedFiles This property contains information about files selected by a user. * @param acceptMultipleFiles Accepts more than one file if true. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param inputName Input element name. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param labelContent Required label content. * It will be wrapped into `label` element with properly set `for` attribute. * Use the provided interceptor to properly clean up bindings inside the content. * @param validFeedback Optional content of positive validation feedback. * It will be wrapped into `div` element with `valid-feedback` style. * Use the provided interceptor to properly clean up bindings inside the content. * @param invalidFeedback Optional content of negative validation feedback. * It will be wrapped into `div` element with `invalid-feedback` style. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided files property */ def fileInput( selectedFiles: SeqProperty[File], acceptMultipleFiles: ReadableProperty[Boolean] = UdashBootstrap.False, size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = inputValidationTrigger, inputId: ComponentId = ComponentId.generate() )( inputName: String, inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, labelContent: Binding.NestedInterceptor => Modifier = _ => "", validFeedback: Option[Binding.NestedInterceptor => Modifier] = None, invalidFeedback: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[BSeq[File]] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new UdashBootstrapComponent { private val input = FileInput(selectedFiles, acceptMultipleFiles)( inputName, inputId, BootstrapStyles.Form.customFileInput, inputModifier.map(_.apply(nestedInterceptor)), validationModifier(selectedFiles, validationTrigger, nestedInterceptor)(validator), (BootstrapStyles.Form.size _).reactiveOptionApply(size) ) override val componentId: ComponentId = inputId override val render: Element = div(BootstrapStyles.Form.customFile)( input.render, label(`for` := inputId, BootstrapStyles.Form.customFileLabel)(labelContent(nestedInterceptor)), validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))) ).render }) } /** * Creates a select menu with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param selectedItem Property containing selected element. * @param options SeqProperty of available options. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param itemLabel Provides options' labels. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided item property */ def select[T]( selectedItem: Property[T], options: ReadableSeqProperty[T], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = selectValidationTrigger, inputId: ComponentId = ComponentId.generate() )( itemLabel: T => Modifier, inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[T] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( Select(selectedItem, options)( itemLabel, inputId, BootstrapStyles.Form.customSelect, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(selectedItem, validationTrigger, factoryNestedInterceptor)(validator), factoryNestedInterceptor((BootstrapStyles.Form.size _).reactiveOptionApply(size)) ), inputId )) } /** * Creates a multiple select menu with a default bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * Use `formGroup` if you want to create an input with a label and validation feedback elements. * * @param selectedItems Property containing selected elements. * @param options SeqProperty of available options. * @param size Size of the input. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param itemLabel Provides options' labels. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided items property */ def multiSelect[T, ElemType <: Property[T]]( selectedItems: seq.SeqProperty[T, ElemType], options: ReadableSeqProperty[T], size: ReadableProperty[Option[BootstrapStyles.Size]] = UdashBootstrap.None, validationTrigger: ValidationTrigger = selectValidationTrigger, inputId: ComponentId = ComponentId.generate() )( itemLabel: T => Modifier, inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[BSeq[T]] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new InputComponent( Select(selectedItems, options)( itemLabel, inputId, BootstrapStyles.Form.customSelect, inputModifier.map(_.apply(factoryNestedInterceptor)), validationModifier(selectedItems, validationTrigger, factoryNestedInterceptor)(validator), factoryNestedInterceptor((BootstrapStyles.Form.size _).reactiveOptionApply(size)) ), inputId )) } /** * Creates a checkbox with a custom bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * It also creates a label and validation feedback elements for the checkbox. * * @param property Property which will be synchronised with the checkbox state and validated. * @param validationTrigger Selects the event updating validation state of the input. * @param inputId Id of the input DOM element. * @param groupId Id of the root element. * @param inputModifier Modifiers applied directly to the `input` element. * Use the provided interceptor to properly clean up bindings inside the content. * @param labelContent Optional label content. * It will be wrapped into `label` element with properly set `for` attribute. * Use the provided interceptor to properly clean up bindings inside the content. * @param validFeedback Optional content of positive validation feedback. * It will be wrapped into `div` element with `valid-feedback` style. * Use the provided interceptor to properly clean up bindings inside the content. * @param invalidFeedback Optional content of negative validation feedback. * It will be wrapped into `div` element with `invalid-feedback` style. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided checkbox state property */ def checkbox( property: Property[Boolean], validationTrigger: ValidationTrigger = selectValidationTrigger, inline: ReadableProperty[Boolean] = UdashBootstrap.False, inputId: ComponentId = ComponentId.generate(), groupId: ComponentId = ComponentId.generate() )( inputModifier: Option[Binding.NestedInterceptor => Modifier] = None, labelContent: Option[Binding.NestedInterceptor => Modifier] = None, validFeedback: Option[Binding.NestedInterceptor => Modifier] = None, invalidFeedback: Option[Binding.NestedInterceptor => Modifier] = None, validator: Validator[Boolean] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new UdashBootstrapComponent { private val input = nestedInterceptor(Checkbox(property)( inputId, BootstrapStyles.Form.control, inputModifier.map(_.apply(nestedInterceptor)), validationModifier(property, validationTrigger, nestedInterceptor)(validator) )) override val componentId: ComponentId = groupId override val render: Element = div( groupId, BootstrapStyles.Form.customControl, BootstrapStyles.Form.customCheckbox, input.render.styles(BootstrapStyles.Form.customControlInput), nestedInterceptor(BootstrapStyles.Form.customControlInline.styleIf(inline)), label(`for` := inputId, BootstrapStyles.Form.customControlLabel)( labelContent.map(_.apply(nestedInterceptor)).getOrElse(span("\u00a0")) ), validFeedback.map(content => div(BootstrapStyles.Form.validFeedback)(content(nestedInterceptor))), invalidFeedback.map(content => div(BootstrapStyles.Form.invalidFeedback)(content(nestedInterceptor))) ).render }) } /** * Creates a checkboxes with a custom bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * It also creates a label and validation feedback elements for each checkbox. * * @param selectedItems Property which will be synchronised with the checkbox state and validated. * @param options Seq of available options, one checkbox will be created for each option. * @param validationTrigger Selects the event updating validation state of the input. * @param groupId Id of the root element. * @param inputModifier Modifiers applied directly to the `input` element. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param labelContent Optional label content. * It will be wrapped into `label` element with properly set `for` attribute. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param validFeedback Optional content of positive validation feedback. * It will be wrapped into `div` element with `valid-feedback` style. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param invalidFeedback Optional content of negative validation feedback. * It will be wrapped into `div` element with `invalid-feedback` style. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided checkbox states property */ def checkButtons[T]( selectedItems: seq.SeqProperty[T, _ <: ReadableProperty[T]], options: ReadableSeqProperty[T], inline: ReadableProperty[Boolean] = UdashBootstrap.False, validationTrigger: ValidationTrigger = selectValidationTrigger, groupId: ComponentId = ComponentId.generate() )( inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, validator: Validator[BSeq[T]] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new ButtonsComponent( selectedItems, CheckButtons(selectedItems, options)(_: Seq[(JSInput, T)] => Seq[Node]), BootstrapStyles.Form.customCheckbox, inline, validationTrigger, groupId )(inputModifier, labelContent, validFeedback, invalidFeedback, validator)) } /** * Creates radio buttons with a custom bootstrap styling and an optional validation callback which sets * proper bootstrap classes: `is-valid` and `is-invalid`. * It also creates a label and validation feedback elements for each radio button. * * @param selectedItem Property which will be synchronised with the radio state and validated. * @param options Seq of available options, one button will be created for each option. * @param validationTrigger Selects the event updating validation state of the input. * @param groupId Id of the root element. * @param inputModifier Modifiers applied directly to the `input` element. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param labelContent Optional label content. * It will be wrapped into `label` element with properly set `for` attribute. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param validFeedback Optional content of positive validation feedback. * It will be wrapped into `div` element with `valid-feedback` style. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param invalidFeedback Optional content of negative validation feedback. * It will be wrapped into `div` element with `invalid-feedback` style. * The factory takes item value and index as the arguments. * Use the provided interceptor to properly clean up bindings inside the content. * @param validator Validator for provided item selection property */ def radioButtons[T]( selectedItem: Property[T], options: ReadableSeqProperty[T], inline: ReadableProperty[Boolean] = UdashBootstrap.False, validationTrigger: ValidationTrigger = selectValidationTrigger, groupId: ComponentId = ComponentId.generate() )( inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier] = (_: T, _: Int, _: Binding.NestedInterceptor) => None, validator: Validator[T] = Validator.Default ): UdashBootstrapComponent = { externalBinding(new ButtonsComponent( selectedItem, RadioButtons(selectedItem, options)(_: Seq[(JSInput, T)] => Seq[Node]), BootstrapStyles.Form.customRadio, inline, validationTrigger, groupId )(inputModifier, labelContent, validFeedback, invalidFeedback, validator)) } private class InputComponent(in: InputBinding[_ <: Element], inputId: ComponentId) extends UdashBootstrapComponent { private val input: InputBinding[_ <: Element] = nestedInterceptor(in) override val componentId: ComponentId = inputId override val render: Element = input.render } private class ButtonsComponent[T, SelectedType]( selected: Property[SelectedType], input: (Seq[(JSInput, T)] => Seq[Node]) => InputBinding[_ <: Element], inputDecorationClass: CssStyle, inline: ReadableProperty[Boolean], validationTrigger: ValidationTrigger, groupId: ComponentId )( inputModifier: (T, Int, Binding.NestedInterceptor) => Option[Modifier], labelContent: (T, Int, Binding.NestedInterceptor) => Option[Modifier], validFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier], invalidFeedback: (T, Int, Binding.NestedInterceptor) => Option[Modifier], validator: Validator[SelectedType] ) extends UdashBootstrapComponent { private val inputs = nestedInterceptor(input { inputs => val groupValidationTrigger = Some(Property(0)) inputs.zipWithIndex.map { case ((singleInput, item), idx) => Seq[Modifier]( BootstrapStyles.Form.customControlInput, inputModifier(item, idx, nestedInterceptor), validationModifier(selected, validationTrigger, nestedInterceptor, groupValidationTrigger)(validator) ).applyTo(singleInput) div( singleInput, BootstrapStyles.Form.customControl, inputDecorationClass, nestedInterceptor(BootstrapStyles.Form.customControlInline.styleIf(inline)), labelContent(item, idx, nestedInterceptor).map(label(`for` := singleInput.id, BootstrapStyles.Form.customControlLabel)(_)), validFeedback(item, idx, nestedInterceptor).map(div(BootstrapStyles.Form.validFeedback)(_)), invalidFeedback(item, idx, nestedInterceptor).map(div(BootstrapStyles.Form.invalidFeedback)(_)) ).render } }) override val componentId: ComponentId = groupId override val render: Element = div(groupId, inputs.render).render } private def validationModifier[ArgumentType]( property: ReadableProperty[ArgumentType], validationTrigger: ValidationTrigger, nested: Binding.NestedInterceptor, groupValidationTrigger: Option[Property[Int]] = None // value change on this property should trigger validation )(validator: Validator[ArgumentType]): Modifier = { def groupTrigger(startValidation: () => Any) = new Binding { override def applyTo(t: Element): Unit = { groupValidationTrigger.foreach { p => propertyListeners += p.listen { _ => startValidation() } } } } def startValidation(validationResult: Property[Option[ValidationResult]], triggerGroup: Boolean): Unit = { if (triggerGroup) groupValidationTrigger.foreach { p => p.set(p.get + 1) } validationResult.set(None) validator(property.get).onComplete { case Success(r) => validationResult.set(Some(r)) case Failure(ex) => logger.error("Validation failed.", ex) validationResult.set(None) } } def eventBasedModifiers(validationResult: Property[Option[ValidationResult]]): Modifier = Seq( nested(BootstrapStyles.Form.isValid.styleIf(validationResult.transform(_.contains(Valid)))), nested(BootstrapStyles.Form.isInvalid.styleIf(validationResult.transform(v => v.isDefined && !v.contains(Valid)))), nested(groupTrigger(() => startValidation(validationResult, triggerGroup = false))), nested(new Binding { override def applyTo(t: Element): Unit = form.foreach(_.validationProperties += validationResult) override def kill(): Unit = { super.kill() form.foreach(_.validationProperties -= validationResult) } }) ) validationTrigger match { case ValidationTrigger.None => Seq.empty[Modifier] case ValidationTrigger.Instant => val validationResult = Property[Option[ValidationResult]](None) Seq( nested(new Binding { override def applyTo(t: Element): Unit = { propertyListeners += property.listen({ _ => startValidation(validationResult, triggerGroup = true) }, initUpdate = true) form.foreach(_.validationProperties += validationResult) } override def kill(): Unit = { super.kill() form.foreach(_.validationProperties -= validationResult) } }), nested(BootstrapStyles.Form.isValid.styleIf(validationResult.transform(_.contains(Valid)))), nested(BootstrapStyles.Form.isInvalid.styleIf(validationResult.transform(v => v.isDefined && !v.contains(Valid)))) ) case ValidationTrigger.OnBlur => val validationResult = Property[Option[ValidationResult]](None) Seq( eventBasedModifiers(validationResult), onblur :+= { (_: Event) => startValidation(validationResult, triggerGroup = true) false } ) case ValidationTrigger.OnChange => val validationResult = Property[Option[ValidationResult]](None) Seq( eventBasedModifiers(validationResult), nested(new Binding { override def applyTo(t: Element): Unit = { propertyListeners += property.listen { _ => startValidation(validationResult, triggerGroup = true) } } }) ) case ValidationTrigger.OnSubmit => val validationResult = Property[Option[ValidationResult]](None) Seq( eventBasedModifiers(validationResult), nested(new Binding { override def applyTo(t: Element): Unit = { propertyListeners ++= form.toOpt.map(_.listen { case ev: UdashForm.FormEvent if ev.tpe == UdashForm.FormEvent.EventType.Submit => startValidation(validationResult, triggerGroup = true) }) } }) ) } } } /** Provides grid elements factory methods. */ object grid { def row(content: Modifier*): Modifier = div(BootstrapStyles.Grid.row)(content) def formRow(content: Modifier*): Modifier = div(BootstrapStyles.Grid.formRow)(content) def col(content: Modifier*): Modifier = div(BootstrapStyles.Grid.col)(content) def col(size: Int, breakpoint: ResponsiveBreakpoint = ResponsiveBreakpoint.All)(content: Modifier*): Modifier = div(BootstrapStyles.Grid.col(size, breakpoint))(content) } } object FormElementsFactory { def apply( nestedInterceptor: NestedInterceptor = NestedInterceptor.Identity, inputValidationTrigger: ValidationTrigger = ValidationTrigger.None, selectValidationTrigger: ValidationTrigger = ValidationTrigger.None, form: OptArg[UdashForm] = OptArg.Empty ): FormElementsFactory = new FormElementsFactory(nestedInterceptor, inputValidationTrigger, selectValidationTrigger, form) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy