# 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 | |
| width | Integer | Width es una subpropiedad de | |
| width-[breakpoint] | Integer | Width es una subpropiedad de | |
| hide_btn | Boolean |
| El valor |
| readonly | Boolean |
| El valor |
| filter | Object |
|
Objeto con la estructura
Donde Si la propiedad filter tiene valor |
| 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 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 |
| flat | Boolean |
| 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
|
| 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.
|
| sections-attrs | Object | | 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 |
| 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>