# Componente

# Introducción

La principal funcionalidad del componente form, es generar la estructura de un formulario, es decir, que generará los campos y un botón de "submit" con función configurable de guardado en DB. También será el encargado aplicar las validaciones.

# Uso

El componente espera recibir en la entrada de la capa de datos un arreglo con la siguiente estructura.

  [
    {
      "type": "IrTextField",
      "props": {
        "key": "/_0005",
        "readonly": false,
        "label": "Input text de ejemplo",
        "visible": true,
        "width": 2
        ... //Demás propiedades del campo a mostrar
      }
    },
    {
      "type": "IrWysiwyg",
      "props": {
        "key": "/0006",
        "readonly": true,
        "label": "Wysiwyg Ejemplo",
        "visible": true,
        "width": 2
        ... //Demás propiedades del campo a mostrar
      }
    },
    ...
  ]

# Ejemplo

# Propiedades

Propiedad Tipo Default Descripción
campos Array

Array de componentes a renderizar en el formulario. Esta propiedad será reemplazada por la respuesta de la base de datos en caso de que se defina la propiedad fcn. En todos los campos podrán establecerse algunas propiedades comunes para todos al estar dentro de un IrForm. Una de ellas será width, en la cual se determinará el ancho del campo en cantidad de columnas. La estructura del formulario se separará en columnas, la cantidad total de estas estará dado por el width de mayor valor que haya en todos los campos. Otra será visible de tipo Boolean, esta determina si el campo será visible o no, de no serlo, no será tenido en cuenta para las validaciones del IrForm. Por ultimo la propiedad label de tipo String, determinará un label para dicho campo del form. Los campos tendrán también la posibilidad de mostrarse dentro de secciones, cuando las mismas se encuentren definidas (props sections o sections_fcn del formulario). Para esto se agregará a los campos la propiedad section, la cual debe coincidir con la propiedad key de alguna de las secciones. De no estar definida la prop section del campo y estar definidas secciones en el formulario, el campo se mostrará al inicio del mismo. Si se envía la propiedad section del campo definida con un valor que no exista en las secciones, este no se mostrará.

width Integer

Width es una subpropiedad de campos, está sirve para determinar el ancho en columnas que va a tener un campo. La estructura del formulario se separará en columnas, la cantidad total de estas estará dado por el width de mayor valor que haya en todos los campos. Se tomará por default en caso de no existir width definido por breakpoint en el breakpoint actual.

width-[breakpoint] Integer

Width es una subpropiedad de campos, está sirve para determinar el ancho en columnas que va a tener un campo. La estructura del formulario se separará en columnas, la cantidad total de estas estará dado por el width-[breakpoint] de mayor valor que haya en todos los campos. La variable breakpoint puede variar entre los valores xs, sm, md, lg y xl. Estos breakpoints se encuentran definidos aquí. Esta propiedad sirve para definir el ancho por breakpoint de pantalla, para mejorar la adaptación mobile. Seleccionando un ancho distinto por cada tipo de pantalla. En los casos xs y sm se tomara por default el máximo ancho para los campos que no tengan el width definido para ese breakpoint.

hide_btn Boolean

false

El valor hide_btn = true, ocultará el botón de submit

readonly Boolean

false

El valor readonly = true, ocultará el botón de submit y mostrará todos los campos en modo solo lectura, independientemente de como esté establecido en la propiedad campos

filter Object

{}

Objeto con la estructura
  {
    propname: valueFiltered
  }

Donde propname será reemplazado por una propiedad de los campos y valueFiltered será reemplazado por el valor esperado de esta propiedad de los campos. Una vez definida correctamente la propiedad filter con esta estructura, solo se mostraran en el formulario los campos establecidos como visibles que tengan en sus propiedades al propname determinado y que el valor de esa propiedad coincida con el valueFiltered determinado.

Si la propiedad filter tiene valor {}, se mostraran todos los campos visibles

campos_fcn String

Al estar definida esta propiedad, el componente realizará una consulta a la base de datos, que deberá retornar el valor de la propiedad campos. El valor de la propiedad campos será reemplazada por la respuesta de la base de datos. Se enviará como parámetros a la función lo definido en la propiedad args

La definición completa de la obtención de los campos se epecifica aquí.

args_campos_fcn Array

[]

Array de argumentos que serán enviados a la base de datos en caso de estar definida la propiedad fcn

flat Boolean

false

Quita la elevación del formulario (sin sombra).
comportamientos Array [] Se explica aquí su estructura.
campos_fcn_refresh String

undefined

Nombre de la función que se ejecutará si se detecta un cambio en el valor de un campo enlazado. Se explica mejor aquí.

args_campos_fcn_refresh String

undefined

Argumentos de la función que se ejecutará si se detecta un cambio en el valor de un campo enlazado. Se explica mejor aquí.

refresh_timer Object

undefined

Determina, en caso que el formulario tenga una función definida en la propiedad campos_fcn_refresh, la cantidad de veces y cada cuanto tiempo se ejecutará esta función para actualizar el formulario automáticamente. Esta propiedad debe tener la siguiente estructura:

  {
    repeat_time: 3000,
    loops: 4,
    type: 'update',
    freeze_screen: false
  }

repeat_time indica cada cuantos milisegundos se refresca. loops indica la cantidad de loops (-1 = infinito). type indica el tipo de refresco (update o replace). freeze_screen indica si se detiene la pantalla mientras se realiza la actualización del formulario.

sections Array

[]

Determina secciones desplegables que contendrán campos. La estructura de esta propiedad es un array de objetos de secciones, los cuales tienen las siguientes props.

  {
    "name": "Frutas",
    "key": "section_frutas"
    'is-default-open': true,
  }

name Indica el titulo de sección que se mostrará en el desplegable. key Identificador univoco para la sección el cual servirá para identificar que campos contiene. is-default-open Indica si la sección se mostrará desplegada al renderizar el formulario.

sections-attrs Object
```js
{
  flat: true,
  accordion: true,
  hover: true,
  multiple: true
}
```

Determina los atributos de los desplegables, estos son componentes v-expansion-panels de vuetify y sus propiedades válidas pueden verse aquí. Excepto la prop value, dado a que se utiliza internamente para la propiedad is-default-open de la sección

forceSubmitRefresh Boolean false

En caso de existir un valor para campos_fcn_refresh, cada vez que se ejecute un fcn_executer, se refresca el formulario con la campos_fcn_refresh si forceSubmitRefresh se envía en true.

containerClasses string false

Clases de estilo que se aplicarán al contenedor del formulario, es decir al objeto que contiene los campos del mismo.

containerStyle string false

Estilo CSS que se aplicará al contenedor del formulario, es decir al objeto que contiene los campos del mismo.

# Código fuente

<template>
  <ir-form-bone
    v-bind="$attrs"
    v-on="formBoneActions"
  >
    <div
      slot-scope="{
        loadingUpdate,
        columnasControl,
        form,
        muestraCampo,
        conditionalSlot,
        flat,
        buttonsForm,
        handleFormClick,
        id_resolver_dlg,
        dialogAttrs,
        continueAction,
        hasError,
        toggleLoading,
        changeLinkedValue,
        refreshParent,
        id_resolver,
        saveDraftFcn,
        loadingRefresh,
        sections,
        sectionsAttrs
      }"
      style="position: relative"
    >
      <ir-dialog
        v-model="showDialog"
        :json_resolver="{ id_resolver: id_resolver_dlg }"
        v-bind="dialogAttrs"
        @confirm="continueAction"
        @refresh-parent="refreshParent"
      />
      <v-overlay
        :value="isQueryHappening"
        z-index="99"
      >
        <v-progress-circular
          indeterminate
          :size="45"
          color="white"
        />
      </v-overlay>
      <section>
        <v-form
          ref="formulario"
          v-model="valid"
          @submit.prevent=""
        >
          <v-card
            class="form--container"
            :elevation="flat ? 0 : ''"
          >
            <dynamicComponentLoader
              :loading="loadingUpdate || loadingRefresh"
            />
            <ir-error
              v-if="hasError"
              principal="Error"
              :titulo="{
                size: 'large',
                titulo: 'Disculpe.',
                type: 'IrTitulo',
                visible: true
              }"
              descripcion="Something went wrong. The administrator has already been notified of this inconvenience."
            />
            <v-card-text
              v-else-if="!loadingUpdate"
              :class="containerClasses"
              :style="topBotButtons(buttonsForm, loadingRefresh)"
            >
              <div
                v-if="$vuetify.breakpoint.smAndDown"
                class="mb-6"
                :style="setGridColumns(columnasControl)"
              >
                <template v-for="(campo, i) in form">
                  <div
                    v-if="muestraCampo(campo.props)"
                    :key="i"
                    :style="setFieldWidth(campo.props, columnasControl)"
                    data-cy="irform-wrapper"
                  >
                    <ir-component-resolver
                      :json_resolver="{
                        id_usuario: $iclstore.state.id_usuario,
                        id_resolver: id_resolver
                      }"
                      :dev_components="[campo]"
                      @change-linked-value="changeLinkedValue"
                      @saveDraft="saveDraftFcn"
                      @toggle-loading="toggleLoading"
                      @refresh-parent="refreshParent"
                      @close-dialog="(e) => $emit('close-dialog', e)"
                      @refresh="refreshParent"
                    />
                  </div>
                </template>
              </div>
              <div
                v-if="$vuetify.breakpoint.mdAndUp"
                :style="setGridColumns(columnasControl)"
              >
                <template v-for="(campo, i) in form">
                  <div
                    v-if="muestraCampo(campo.props) && !campo.props.section"
                    :key="i"
                    :style="setFieldWidth(campo.props, columnasControl)"
                    data-cy="irform-wrapper"
                  >
                    <ir-component-resolver
                      :json_resolver="{
                        id_usuario: $iclstore.state.id_usuario,
                        id_resolver: id_resolver
                      }"
                      :dev_components="[campo]"
                      :is-focused="isFocused(campo, i, form)"
                      @change-linked-value="changeLinkedValue"
                      @saveDraft="saveDraftFcn"
                      @toggleLoading="toggleLoading"
                      @focus="changeFocus(campo)"
                      @refresh-parent="refreshParent"
                      @close-dialog="(e) => $emit('close-dialog', e)"
                      @refresh="refreshParent"
                    />
                  </div>
                </template>
                <div
                  v-if="sections && sections.length > 0"
                  :style="getFullWidth(columnasControl)"
                >
                  <v-divider />
                  <v-expansion-panels v-bind="sectionsAttrs">
                    <v-expansion-panel
                      v-for="{ name, key } in sections"
                      :key="key"
                    >
                      <v-expansion-panel-header>
                        <span class="section-title primaryText--text">{{
                          name
                        }}</span>
                      </v-expansion-panel-header>
                      <v-expansion-panel-content>
                        <div :style="setGridColumns(columnasControl)">
                          <template v-for="(campo, i) in form">
                            <div
                              v-if="
                                muestraCampo(campo.props) &&
                                  campo.props.section === key
                              "
                              :key="i"
                              :style="
                                setFieldWidth(campo.props, columnasControl)
                              "
                              data-cy="irform-wrapper"
                            >
                              <ir-component-resolver
                                :json_resolver="{
                                  id_usuario: $iclstore.state.id_usuario,
                                  id_resolver: id_resolver
                                }"
                                :dev_components="[campo]"
                                :is-focused="isFocused(campo, i, form)"
                                @change-linked-value="changeLinkedValue"
                                @saveDraft="saveDraftFcn"
                                @toggle-loading="toggleLoading"
                                @focus="changeFocus(campo)"
                                @refresh-parent="refreshParent"
                                @close-dialog="(e) => $emit('close-dialog', e)"
                                @refresh="refreshParent"
                              />
                            </div>
                          </template>
                        </div>
                      </v-expansion-panel-content>
                      <v-divider />
                    </v-expansion-panel>
                  </v-expansion-panels>
                </div>
              </div>
              <template
                v-for="({ buttons, position }, i) in buttonsPositions(
                  buttonsForm
                )"
              >
                <ir-form-comportamiento
                  v-if="conditionalSlot"
                  :key="'full-width-' + i"
                  :buttons-form="buttonsForm"
                  :handle-form-click="handleFormClick"
                  :is-query-happening="isQueryHappening"
                  :position="position"
                  :buttons="buttons"
                  :valid="valid"
                  :formulario="$refs.formulario"
                />
              </template>
            </v-card-text>
            <v-card-actions
              v-if="!loadingUpdate && $slots.footer"
              style="display: flex; justify-content: end"
            >
              <slot name="footer" />
            </v-card-actions>
          </v-card>
        </v-form>
      </section>
    </div>
  </ir-form-bone>
</template>

<script>
import {
  formComponentName,
  IRFORM_COMPORTAMIENTOS_POSITIONS
} from '../../../constants'
import { ref, getCurrentInstance } from 'vue'
import Vue from 'vue'
import dynamicComponentLoader from '../../IrComponentResolver/IrComponentLoader.vue'
import IrError from '../../IrError/IrError.vue'
import IrFormBone from './IrFormBone.vue'
import IrFormComportamiento from './IrFormComportamiento.vue'

import { findFirstVisibleField } from './helper.js'

const { TOP, LEFT, FULLWIDTH, BOTTOM, RIGHT } = IRFORM_COMPORTAMIENTOS_POSITIONS

export default {
  name: formComponentName,
  components: {
    dynamicComponentLoader,
    IrError,
    IrFormBone,
    IrFormComportamiento
  },
  props: {
    fieldIdFocused: {
      type: String,
      required: false,
      default: ''
    },
    containerClasses: {
      type: String,
      required: false,
      default: ''
    },
    containerStyle: {
      type: String,
      required: false,
      default: ''
    },
    fluid: {
      type: Boolean,
      default: false
    }
  },
  setup (props, { emit }) {
    const vueInstance = getCurrentInstance()
    const vuetify = vueInstance.proxy.$vuetify
    const iclRouter = Vue.prototype.$iclRouter
    const valid = ref(false)
    const fieldIdFocusedLocal = ref('')
    const showDialog = ref(false)
    const isQueryHappening = ref(false)

    const cerrarDialog = () => {
      showDialog.value = false
    }

    const abrirDialog = () => {
      showDialog.value = true
    }

    const toggleLoadingOverlay = (isLoading) => {
      isQueryHappening.value = isLoading
    }

    const getFullWidth = (maxCols) => {
      return `grid-column: span ${maxCols} / span ${maxCols} !important;`
    }

    // Consulta si el campo está enfocado dependiendo de la propiedad o en su defecto si es el primer campo visible del formulario.
    const isFocused = (campo, index, form) => {
      let comparableFieldId
        if (iclRouter && iclRouter.currentRoute && iclRouter.currentRoute.query && iclRouter.currentRoute.query.focus) {
          comparableFieldId = iclRouter.currentRoute.query.focus
        } else {
          comparableFieldId = fieldIdFocusedLocal.value
          ? fieldIdFocusedLocal.value
          : props.fieldIdFocused
        }
      const isCampoValid = campo.props && campo.props.id
      let isFocused
      if (comparableFieldId)
        isFocused = Boolean(
          comparableFieldId &&
            isCampoValid &&
            campo.props.id === comparableFieldId
        )
      else isFocused = index === findFirstVisibleField(form)
      return isFocused
    }

    const changeFocus = (campo) => {
      if (campo && campo.props && campo.props.id) {
        fieldIdFocusedLocal.value = campo.props.id
      }
    }

    const setGridColumns = (columnasControl) => {
      return {
        gridTemplateColumns: `repeat(${columnasControl}, minmax(0,1fr))`,
        display: 'grid',
        gridGap: '10px'
      }
    }

    const setFieldWidth = (props, maxCols) => {
      const breakpoint = vuetify.breakpoint.name
      const widthByBreakpoint = {
        xs: props['width-xs'],
        sm: props['width-sm'],
        md: props['width-md'],
        lg: props['width-lg'],
        xl: props['width-xl'],
        default: props.width
      }
      const selectedWidth = widthByBreakpoint[breakpoint]
      let columnSpan = maxCols
      if (selectedWidth) columnSpan = selectedWidth
      else if (
        widthByBreakpoint.default &&
        breakpoint !== 'xs' &&
        breakpoint !== 'sm'
      )
        columnSpan = widthByBreakpoint.default
      const gridColumn = `span ${columnSpan} / span ${columnSpan} !important`
      const styles = {
        'grid-column': gridColumn,
        'z-index': '9'
      }
      return Object.entries(styles)
        .map(([property, value]) => `${property}: ${value}`)
        .join('; ')
    }

    const resolveAttr = (attr) => {
      return attr !== undefined ? attr : true
    }

    const topBotButtons = (buttons, loadingRefresh) => {
      let isAnyTopButtons = false
      let isAnyBottomButtons = false
      for (let i = 0; i < buttons.length; i++) {
        let button = buttons[i]
        if (typeof button.visible === 'boolean' && !button.visible) break
        if (!isAnyTopButtons || !isAnyBottomButtons) {
          if (button.position && button.position.top) isAnyTopButtons = true
          if (
            (button.position &&
              !button.position.top &&
              !button.position[FULLWIDTH]) ||
            typeof button.position === 'undefined'
          )
            isAnyBottomButtons = true
        } else break
      }
      let styles = ''
      if (vuetify.breakpoint.mdAndUp) {
        if(props.fluid) styles += 'padding-left: 0px !important; padding-right: 0px !important;'
        if (isAnyTopButtons) styles += 'padding-top: 60px !important;'
        else if(props.fluid)  styles += 'padding-top: 0px !important;'
        if (isAnyBottomButtons) styles += 'padding-bottom: 60px !important;'
        else if (props.fluid) styles += 'padding-bottom: 0px !important;'
        if (loadingRefresh) styles += 'opacity: 0%;'
      } else {
        styles += 'padding:20px 0 0 0 !important;'
      }
      styles += props.containerStyle
      return styles
    }

    const buttonsPositions = (buttons) => {
      let arrButtons = [
        { position: `${TOP} ${LEFT}`, buttons: [] },
        { position: `${TOP} ${RIGHT}`, buttons: [] },
        { position: `${BOTTOM} ${LEFT}`, buttons: [] },
        { position: `${BOTTOM} ${RIGHT}`, buttons: [] },
        { position: `${BOTTOM} ${FULLWIDTH}`, buttons: [] }
      ]
      buttons.forEach((button) => {
        let positionAux = button.position
        if (
          positionAux !== undefined &&
          ((positionAux[TOP] !== undefined &&
            positionAux[LEFT] !== undefined) ||
            (positionAux[0] !== undefined &&
              positionAux[0][TOP] !== undefined &&
              positionAux[0][LEFT] !== undefined))
        ) {
          let arrPosition = Array.isArray(positionAux)
            ? positionAux
            : [positionAux]
          arrPosition.forEach((pos) => {
            let left = pos[LEFT]
            let top = pos[TOP]
            let full = pos[FULLWIDTH]
            let position
            if (full) position = 4
            else position = top && left ? 0 : top ? 1 : left ? 2 : 3
            arrButtons[position].buttons.push(button)
          })
        } else arrButtons[3].buttons.push(button)
      })
      return arrButtons
    }

    const formBoneActions = ref({
      'close-dialog': (e) => emit('close-dialog', e),
      'refresh-parent': () => emit('refresh-parent'),
      updateCampos: (campos) => emit('updateCampos', campos),
      changeCamposValues: (campos) => emit('changeCamposValues', campos),
      refresh: () => emit('refresh'),
      cerrarDialog,
      abrirDialog,
      'toggle-loading-overlay': toggleLoadingOverlay
    })

    return {
      valid,
      showDialog,
      isQueryHappening,
      formBoneActions,
      cerrarDialog,
      abrirDialog,
      toggleLoadingOverlay,
      getFullWidth,
      isFocused,
      changeFocus,
      setGridColumns,
      setFieldWidth,
      resolveAttr,
      topBotButtons,
      buttonsPositions
    }
  }
}
</script>

<style lang="scss" scoped>
.section-title {
  font-weight: 600;
  font-size: 18px;
}
.form--container {
  border: none !important;
}
</style>
Last Updated: 9/24/2025, 6:49:18 PM