@team_yumi/dynamic-form v1.0.5
@team_yumi/dynamic-form
Una librería avanzada de formularios dinámicos con modal bottom sheet para aplicaciones Ionic React, diseñada específicamente para sistemas de gestión de espacios y encuestas dinámicas.
🎯 ¿Para qué sirve la librería?
@team_yumi/dynamic-form es una solución completa para crear formularios dinámicos e interactivos que permiten:
- ✅ Generar formularios desde configuraciones JSON sin necesidad de hardcodear componentes
- ✅ Cargar formularios dinámicamente desde APIs con validación de estado de usuario
- ✅ Manejar dependencias complejas entre preguntas (mostrar/ocultar según respuestas anteriores)
- ✅ Recopilar respuestas con evidencias (comentarios adicionales, archivos adjuntos)
- ✅ Validar completitud automáticamente antes de envío
- ✅ Proveer experiencia de usuario optimizada con modal bottom sheet responsive
- ✅ Soporte para calificación por estrellas con SVGs personalizados
- ✅ Sistema de estadísticas en tiempo real del progreso del formulario
🚀 Características Principales
- 📋 Formularios Dinámicos: Crea formularios complejos desde configuraciones JSON
- 🔄 Carga desde API: Integración con endpoints para formularios dinámicos
- 📱 Bottom Sheet Modal: Modal nativo de Ionic con soporte para breakpoints
- 🎯 TypeScript Completo: Tipado estricto para mejor experiencia de desarrollo
- 🧩 Componentes Modulares: Arquitectura basada en componentes reutilizables
- 🪝 Hook Personalizado:
useFormBottomSheet
para manejo de estado simplificado - 🔗 Dependencias Inteligentes: Mostrar/ocultar preguntas según respuestas anteriores
- ⭐ Rating por Estrellas: Componente de calificación con SVGs personalizados
- 📊 Estadísticas en Tiempo Real: Seguimiento automático del progreso
- 👤 Gestión de Usuario: Integración con datos de usuario y estado de respuestas
- 🛡️ Validaciones Robustas: Sistema de validación completo con reglas de negocio
📦 Instalación
npm install @team_yumi/dynamic-form
Dependencias Peer
npm install react react-dom @ionic/react ionicons @team_yumi/ramen axios
🎯 Uso Básico
1. FormBottomSheet con API (Recomendado)
import React, { useEffect } from 'react';
import { IonApp, IonButton } from '@ionic/react';
import { FormBottomSheet, useFormBottomSheet } from '@team_yumi/dynamic-form';
const App = () => {
const formBottomSheet = useFormBottomSheet({
onFormComplete: (answers, stats, userData) => {
console.log('Formulario completado:', {
answers,
stats,
userData,
});
// Enviar respuestas a tu API
submitToAPI(answers, userData);
},
});
// Abrir automáticamente al renderizar
useEffect(() => {
formBottomSheet.openForm();
}, []);
const submitToAPI = async (answers, userData) => {
try {
await fetch('/api/submit-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers, userData }),
});
} catch (error) {
console.error('Error enviando formulario:', error);
}
};
return (
<IonApp>
<IonButton onClick={formBottomSheet.openForm}>Evaluar Experiencia</IonButton>
<FormBottomSheet
isOpen={formBottomSheet.isOpen}
onDidDismiss={formBottomSheet.closeForm}
// 🆕 Nueva configuración de API
hostUrl="https://api.example.com"
sheetEndpoint="/engagement/sheets/active"
skipEndpoint="/engagement/responses/skip"
sendEndpoint="/engagement/responses/send"
apiKey="your-api-key-here"
// 🆕 Configuración de skip
skipButtonText="Saltear"
skipDelayMinutes={30}
// Configuración visual (opcional - títulos pueden venir del API)
title="Evaluación de Experiencia" // 🔄 Prioridad sobre API
subtitle="Tu opinión es importante" // 🔄 Prioridad sobre API
confirmButtonText="Enviar Evaluación"
cancelButtonText="Cancelar"
icon="star-outline"
size="xl"
// Callbacks
onFormComplete={formBottomSheet.handleFormComplete}
onFormChange={formBottomSheet.handleFormChange}
onFormSkipped={(userData) => {
console.log('Usuario saltó el formulario:', userData);
// Lógica personalizada para cuando se hace skip
}}
onShouldNotShow={() => console.log('Formulario ya completado o salteado')}
// 🆕 Datos del usuario actualizados
getUserData={() => ({
name: 'Juan Pérez',
email: 'juan.perez@example.com',
store: 'STORE-001',
role: 'manager',
businessUnit: 'SM',
country: 'CO',
moduleCode: 'SM-001', // 🆕 Nuevo campo requerido
})}
/>
</IonApp>
);
};
2. FormBottomSheet con Datos Estáticos
import {
FormBottomSheet,
useFormBottomSheet,
RamenQuestionType,
RamenQuestionVisualType,
} from '@team_yumi/dynamic-form';
const StaticFormExample = () => {
const formBottomSheet = useFormBottomSheet({
onFormComplete: (answers, stats) => {
console.log('Respuestas:', answers);
console.log('Estadísticas:', stats);
},
});
const formData = {
questions: [
{
title: '¿Cómo calificarías nuestro servicio?',
required: true,
order: 1,
type: RamenQuestionType.STAR_RATING,
code: 'SERVICE_RATING',
main: true,
visual_type: RamenQuestionVisualType.ICON,
alternatives: [
{
name: '1 estrella - Muy malo',
code: '1',
require_evidence: false,
require_comments: false,
},
{
name: '2 estrellas - Malo',
code: '2',
require_evidence: false,
require_comments: false,
},
{
name: '3 estrellas - Regular',
code: '3',
require_evidence: false,
require_comments: false,
},
{
name: '4 estrellas - Bueno',
code: '4',
require_evidence: false,
require_comments: false,
},
{
name: '5 estrellas - Excelente',
code: '5',
require_evidence: false,
require_comments: false,
},
],
},
{
title: '¿Qué te gustó más?',
required: false,
order: 2,
type: RamenQuestionType.MULTIPLE_SELECTION,
code: 'LIKED_MOST',
main: false,
visual_type: RamenQuestionVisualType.LIST,
alternatives: [
{
name: 'Atención al cliente',
code: 'customer_service',
require_evidence: false,
require_comments: false,
},
{
name: 'Calidad del producto',
code: 'product_quality',
require_evidence: false,
require_comments: false,
},
{
name: 'Tiempo de respuesta',
code: 'response_time',
require_evidence: false,
require_comments: false,
},
{ name: 'Precio', code: 'price', require_evidence: false, require_comments: false },
],
dependencies: [
{
question: 'SERVICE_RATING',
answers: ['4', '5'], // Solo se muestra si calificó 4 o 5 estrellas
},
],
},
{
title: 'Comentarios adicionales',
required: false,
order: 3,
type: RamenQuestionType.COMMENTS,
code: 'ADDITIONAL_COMMENTS',
main: false,
visual_type: RamenQuestionVisualType.TEXT,
alternatives: [],
},
],
overrideAlternatives: [],
};
return (
<>
<IonButton onClick={formBottomSheet.openForm}>Abrir Formulario</IonButton>
<FormBottomSheet
isOpen={formBottomSheet.isOpen}
onDidDismiss={formBottomSheet.closeForm}
formData={formData}
title="Encuesta de Satisfacción"
confirmButtonText="Enviar"
onFormComplete={formBottomSheet.handleFormComplete}
/>
</>
);
};
3. InstrumentContainer (Uso Directo)
import {
InstrumentContainer,
IRamenForm,
IRamenAnswer,
IRamenInstrumentStats,
} from '@team_yumi/dynamic-form';
const DirectFormComponent = () => {
const [answers, setAnswers] = useState<{ [key: string]: IRamenAnswer }>({});
const [stats, setStats] = useState<IRamenInstrumentStats>();
const handleFormChange = (
newAnswers: { [key: string]: IRamenAnswer },
newStats: IRamenInstrumentStats,
question: IRamenQuestion
) => {
setAnswers(newAnswers);
setStats(newStats);
console.log(`Pregunta ${question.code} respondida:`, newAnswers[question.code]);
console.log('Progreso:', `${newStats.totalResolved}/${newStats.total}`);
console.log('¿Completado?', newStats.finished);
};
return (
<div>
<h2>
Progreso: {stats?.totalResolved || 0}/{stats?.total || 0}
</h2>
<InstrumentContainer
questions={formData.questions}
answers={answers}
onChangeHandler={handleFormChange}
/>
<button disabled={!stats?.finished} onClick={() => console.log('Enviar:', answers)}>
{stats?.finished
? 'Enviar Formulario'
: `Faltan ${stats?.totalRequiredRemain || 0} preguntas`}
</button>
</div>
);
};
📋 API Reference Completa
FormBottomSheet Props
Prop | Tipo | Requerido | Valor por defecto | Descripción |
---|---|---|---|---|
isOpen | boolean | ✅ | - | Controla si el modal está abierto |
onDidDismiss | () => void | ✅ | - | Callback cuando el modal se cierra |
formData | IRamenForm | ❌* | undefined | Configuración estática del formulario |
hostUrl | string | ❌* | undefined | URL base del servidor API |
sheetEndpoint | string | ❌* | undefined | Endpoint para cargar formularios (ej: /engagement/sheets/active ) |
skipEndpoint | string | ❌ | undefined | Endpoint para saltar formularios (ej: /engagement/responses/skip ) |
sendEndpoint | string | ❌ | undefined | Endpoint para enviar respuestas (ej: /engagement/responses/send ) |
apiKey | string | ❌* | undefined | API key para autenticación |
title | string | ❌ | 'Formulario' | Título del modal (prioridad sobre API) |
subtitle | string | ❌ | undefined | Subtítulo del modal (prioridad sobre API) |
confirmButtonText | string | ❌ | 'Confirmar' | Texto del botón de confirmación |
cancelButtonText | string | ❌ | 'Cancelar' | Texto del botón de cancelación |
skipButtonText | string | ❌ | 'Saltear' | Texto del botón de skip |
skipDelayMinutes | number | ❌ | 30 | Minutos de retraso para el skip |
onFormComplete | (answers, stats, userData?) => void | ❌ | undefined | Callback cuando se completa el formulario |
onFormChange | (answers, stats, question) => void | ❌ | undefined | Callback cuando cambia una respuesta |
onFormSkipped | (userData?) => void | ❌ | undefined | Callback cuando se hace skip del formulario |
onShouldNotShow | () => void | ❌ | undefined | Callback cuando el formulario no debe mostrarse |
getUserData | () => IUserData \| Promise<IUserData> | ❌ | undefined | Función para obtener datos del usuario |
icon | string | ❌ | undefined | Icono del modal (ej: 'star-outline') |
size | 'xs' \| 's' \| 'm' \| 'l' \| 'xl' | ❌ | 'l' | Tamaño del modal |
className | string | ❌ | undefined | Clase CSS personalizada |
*Nota: Debes proporcionar formData
O (hostUrl
+ sheetEndpoint
+ apiKey
), no ambos.
Tipos de Preguntas Disponibles
1. STAR_RATING - Calificación por Estrellas
{
type: RamenQuestionType.STAR_RATING,
alternatives: [
{ name: '1 estrella', code: '1' },
{ name: '2 estrellas', code: '2' },
{ name: '3 estrellas', code: '3' },
{ name: '4 estrellas', code: '4' },
{ name: '5 estrellas', code: '5' }
]
}
2. MULTIPLE_OPTION - Opción Múltiple (Una selección)
{
type: RamenQuestionType.MULTIPLE_OPTION,
alternatives: [
{ name: 'Opción A', code: 'a' },
{ name: 'Opción B', code: 'b' },
{ name: 'Opción C', code: 'c' }
]
}
3. MULTIPLE_SELECTION - Selección Múltiple
{
type: RamenQuestionType.MULTIPLE_SELECTION,
alternatives: [
{ name: 'Selección 1', code: '1' },
{ name: 'Selección 2', code: '2' },
{ name: 'Selección 3', code: '3' }
]
}
4. DICHOTOMIC - Pregunta Dicotómica (Sí/No)
{
type: RamenQuestionType.DICHOTOMIC,
alternatives: [
{ name: 'Sí', code: 'yes' },
{ name: 'No', code: 'no' }
]
}
5. SHORT_TEXT - Texto Corto
{
type: RamenQuestionType.SHORT_TEXT,
alternatives: [] // No requiere alternativas
}
6. COMMENTS - Comentarios Largos
{
type: RamenQuestionType.COMMENTS,
alternatives: [] // No requiere alternativas
}
Tipos Visuales
Tipo | Descripción | Mejor para |
---|---|---|
TEXT | Campo de texto simple | SHORT_TEXT, COMMENTS |
SELECT | Dropdown/Selector | MULTIPLE_OPTION con muchas opciones |
LIST | Lista vertical de opciones | MULTIPLE_OPTION, MULTIPLE_SELECTION |
ICON | Iconos clickeables | STAR_RATING, DICHOTOMIC |
useFormBottomSheet Hook
const {
isOpen, // boolean - Estado del modal
answers, // object - Respuestas actuales
stats, // IRamenInstrumentStats - Estadísticas
openForm, // function - Abrir formulario
closeForm, // function - Cerrar formulario
handleFormComplete, // function - Manejar completar
handleFormChange, // function - Manejar cambios
} = useFormBottomSheet({
onFormComplete: (answers, stats) => {
// Tu lógica aquí
},
onFormChange: (answers, stats, question) => {
// Tu lógica aquí
},
autoClose: true, // Cerrar automáticamente al completar
});
🔧 Configuración Avanzada
Estructura del Formulario (IRamenForm)
interface IRamenForm {
questions: IRamenQuestion[];
overrideAlternatives: IOverrideAlternative[];
}
interface IRamenQuestion {
title: string; // Título de la pregunta
required: boolean; // Si es obligatoria
order: number; // Orden de aparición
type: RamenQuestionType; // Tipo de pregunta
code: string; // Identificador único
main: boolean; // Si es pregunta principal
visual_type: RamenQuestionVisualType; // Tipo visual
alternatives: IRamenAlternative[]; // Opciones disponibles
dependencies?: IRamenDependency[]; // Dependencias (opcional)
baseAlternativesData?: any[]; // Datos base para alternativas dinámicas
}
interface IRamenAlternative {
name: string; // Texto mostrado
code: string; // Valor interno
require_evidence: boolean; // Si requiere evidencia/archivo
require_comments: boolean; // Si requiere comentarios adicionales
}
interface IRamenDependency {
question: string; // Código de la pregunta de la cual depende
answers: string[]; // Respuestas que activan esta pregunta
filterable?: boolean; // Si cualquier respuesta activa la pregunta
}
Ejemplo de Dependencias Complejas
const formWithDependencies = {
questions: [
{
title: '¿Visitaste nuestra tienda?',
code: 'VISITED_STORE',
type: RamenQuestionType.DICHOTOMIC,
required: true,
alternatives: [
{ name: 'Sí', code: 'yes' },
{ name: 'No', code: 'no' },
],
},
{
title: '¿Cómo calificarías la tienda?',
code: 'STORE_RATING',
type: RamenQuestionType.STAR_RATING,
required: true,
dependencies: [
{
question: 'VISITED_STORE',
answers: ['yes'], // Solo se muestra si visitó la tienda
},
],
alternatives: [
/* ... */
],
},
{
title: '¿Qué podemos mejorar?',
code: 'IMPROVEMENTS',
type: RamenQuestionType.MULTIPLE_SELECTION,
required: false,
dependencies: [
{
question: 'STORE_RATING',
answers: ['1', '2', '3'], // Solo si calificó 3 estrellas o menos
},
],
alternatives: [
{ name: 'Atención al cliente', code: 'service' },
{ name: 'Limpieza', code: 'cleanliness' },
{ name: 'Variedad de productos', code: 'variety' },
],
},
],
overrideAlternatives: [],
};
Integración con API
Formato Esperado de la API
[
{
"userEngagement": {
"hasResponse": false,
"status": null,
"responseId": null
},
"schema": {
"questions": [
{
"title": "¿Cómo calificarías tu experiencia?",
"required": true,
"order": 1,
"type": "STAR_RATING",
"code": "EXPERIENCE_RATING",
"main": true,
"visual_type": "ICON",
"alternatives": [
{
"name": "1 estrella - Muy malo",
"code": "1",
"require_evidence": false,
"require_comments": false
}
]
}
]
}
}
]
Headers Requeridos
const headers = {
apiKey: 'your-api-key-here',
'Content-Type': 'application/json',
};
🎯 Reglas de Negocio Implementadas
1. Gestión de Estados de Encuesta
La librería implementa un sistema inteligente de gestión de estados con soporte completo para skip:
// Estado de userEngagement actualizado
interface UserEngagement {
hasResponse: boolean;
status: 'sent' | 'draft' | null;
responseId: string | null;
isSkipped: boolean; // 🆕 Si la respuesta fue saltada
skipDate: string | null; // 🆕 Fecha cuando se saltó
}
Reglas de Apertura del Formulario:
- ✅
hasResponse: false
→ Formulario se abre normalmente - ✅
hasResponse: true, status: 'draft'
→ Usuario puede continuar completando - ❌
hasResponse: true, status: 'sent'
→ Formulario NO se abre (ya completado) - 🆕
isSkipped: true, skipDate: > fecha actual
→ Formulario NO se abre (skip vigente) - ✅
isSkipped: true, skipDate: null
→ Formulario SÍ se abre - ✅
isSkipped: true, skipDate: <= fecha actual
→ Formulario SÍ se abre (skip expiró) - ❌ Error en API → Formulario NO se abre silenciosamente
Lógica de Skip:
// El skip es válido si la fecha actual es menor o igual a skipDate
const isSkipValid = (skipDate: string) => {
const skipDateTime = new Date(skipDate);
const now = new Date();
return now <= skipDateTime;
};
2. Sistema de Dependencias
Las preguntas pueden depender de respuestas anteriores:
// Ejemplo: Pregunta que solo aparece si calificó 5 estrellas
{
dependencies: [
{
question: 'RATING',
answers: ['5'], // Solo si respondió "5"
},
];
}
// Ejemplo: Pregunta que aparece con cualquier respuesta
{
dependencies: [
{
question: 'ANY_QUESTION',
answers: [],
filterable: true, // Aparece si hay cualquier respuesta
},
];
}
Comportamiento:
- Las preguntas dependientes se ocultan automáticamente si no se cumplen las condiciones
- Las respuestas de preguntas ocultas se resetean automáticamente
- Solo las preguntas visibles cuentan para el cálculo de
totalRequired
3. Validación de Completitud
El sistema calcula automáticamente el progreso:
interface IRamenInstrumentStats {
total: number; // Total de preguntas
totalResolved: number; // Preguntas respondidas
totalRequired: number; // Preguntas requeridas (considerando dependencias)
totalRequiredResolved: number; // Preguntas requeridas completadas
finished: boolean; // Si el formulario está completo
totalRemain: number; // Preguntas pendientes
totalRequiredRemain: number; // Preguntas requeridas pendientes
}
Reglas de Validación:
- Una pregunta se considera "resuelta" si tiene un valor válido
- Para alternativas con
require_comments: true
, debe haber texto enotherText
- Para alternativas con
require_evidence: true
, debe haber archivos enevidences
- Las preguntas de tipo
SHORT_TEXT
yCOMMENTS
se consideran válidas con cualquier texto - El formulario está
finished
cuandototalRequired === totalRequiredResolved
4. Manejo de Evidencias y Comentarios
interface IRamenAnswer {
value: string | string[]; // Respuesta principal
otherText?: string; // Comentarios adicionales
evidences?: any[]; // Archivos/evidencias adjuntas
}
Reglas:
- Si
require_comments: true
,otherText
es obligatorio - Si
require_evidence: true
,evidences
debe tener al menos un elemento - Los comentarios y evidencias se validan automáticamente
5. Reseteo Automático en Dependencias
Cuando una pregunta padre cambia y afecta a preguntas dependientes:
// Si cambio la respuesta de RATING de "5" a "3"
// Todas las preguntas que dependían de RATING === "5"
// se ocultan y sus respuestas se resetean automáticamente
6. Gestión de Datos de Usuario
interface IUserData {
name: string;
email: string;
store: string;
role: string;
businessUnit: string;
country: string;
moduleCode: string; // 🆕 Código del módulo (requerido para skip)
}
Integración:
- Los datos de usuario se cargan al abrir el formulario
- Se incluyen automáticamente en
onFormComplete
- Son requeridos para operaciones de skip (POST al skipEndpoint)
- Útil para auditoría y segmentación de respuestas
- Se usan para construir automáticamente query parameters en sheetEndpoint
7. 🆕 Sistema de Skip Inteligente
La nueva funcionalidad de skip permite a los usuarios posponer formularios:
Configuración requerida:
<FormBottomSheet
hostUrl="https://api.example.com"
sheetEndpoint="/engagement/sheets/active"
skipEndpoint="/engagement/responses/skip" // 🆕 Endpoint para skip
skipDelayMinutes={30} // 🆕 Tiempo de retraso en minutos
onFormSkipped={(userData) => {
// 🆕 Callback de skip
console.log('Usuario hizo skip:', userData);
}}
/>
Payload enviado al skipEndpoint:
{
"sheetId": "60f7b3b3b3b3b3b3b3b3b3b3", // ID del formulario
"userEmail": "juan.perez@company.com",
"userStore": "STORE-001",
"userRole": "admin",
"userName": "Juan Pérez",
"country": "CO",
"businessUnit": "SM",
"moduleCode": "SM-001",
"skipDelayMinutes": 30 // Configurado en props
}
Comportamiento del botón Skip:
- Aparece automáticamente si se configuran
hostUrl
,skipEndpoint
y hayuserData
- Envía POST al skipEndpoint con todos los datos del usuario
- Cierra automáticamente el modal después del skip exitoso
- Ejecuta
onFormSkipped
callback con userData - En caso de error, permite cerrar el modal normalmente
8. 🆕 Envío Automático de Respuestas
Cuando se completa el formulario, las respuestas se pueden enviar automáticamente:
Configuración:
<FormBottomSheet
hostUrl="https://api.example.com"
sendEndpoint="/engagement/responses/send" // 🆕 Endpoint para respuestas
onFormComplete={(answers, stats, userData) => {
console.log('Respuestas enviadas y callback ejecutado');
}}
/>
Payload enviado al sendEndpoint:
{
"sheetId": "60f7b3b3b3b3b3b3b3b3b3b3",
"country": "CO",
"businessUnit": "SM",
"moduleCode": "SM-001",
"userName": "Juan Pérez",
"userEmail": "juan.perez@company.com",
"userStore": "STORE-001",
"userRole": "admin",
"response": {
"SPACE_WAS_SET_UP": {
"value": "NO"
},
"PROBLEM": {
"value": "FALT_ACT"
},
"LOCAL_RATING": {
"value": "3"
},
"ADDITIONAL_COMMENTS": {
"value": "kjkljl"
}
},
"skip": false
}
Flujo de envío:
- Usuario completa formulario y presiona "Confirmar"
- Si
sendEndpoint
está configurado → Envío automático al servidor - Si el envío es exitoso → Log de confirmación
- Si hay error → Log de error pero continúa el flujo
- Ejecuta
onFormComplete
callback original - Cierra el modal automáticamente
Ventajas:
- 🔄 Automático: No requiere lógica adicional en el componente padre
- 🛡️ Resiliente: Continúa funcionando aunque el envío falle
- 🔧 Opcional: Solo se activa si se configura sendEndpoint
- 📊 Completo: Incluye todos los datos del usuario y respuestas
9. 🆕 Títulos y Subtítulos Dinámicos
Los títulos del modal se pueden configurar dinámicamente desde el API:
Prioridad de títulos:
- Props del componente (máxima prioridad)
- Response del sheetEndpoint (si no hay props)
- Valor por defecto ('Formulario' para title)
Response del sheetEndpoint puede incluir:
{
"id": "60f7b3b3b3b3b3b3b3b3b3b3",
"title": "Evaluación de Experiencia del Cliente", // 🆕 Título dinámico
"subtitle": "Tu opinión nos ayuda a mejorar", // 🆕 Subtítulo dinámico
"skippable": true, // 🆕 Si el formulario puede ser saltado
"schema": {
"questions": [...]
},
"userEngagement": {...}
}
Ejemplos de uso:
// ✅ Títulos fijos (prioridad máxima)
<FormBottomSheet
title="Mi Título Fijo"
subtitle="Mi Subtítulo Fijo"
sheetEndpoint="/engagement/sheets/active"
// Aunque el API devuelva title/subtitle, se usan los props
/>
// ✅ Títulos dinámicos del API
<FormBottomSheet
sheetEndpoint="/engagement/sheets/active"
// title y subtitle no especificados
// Se usan los valores del API response
/>
// ✅ Título fijo + subtítulo dinámico
<FormBottomSheet
title="Evaluación"
// subtitle no especificado → usa el del API
sheetEndpoint="/engagement/sheets/active"
/>
Comportamiento:
- getFinalTitle():
title (prop) || dynamicTitle (API) || 'Formulario'
- getFinalSubtitle():
subtitle (prop) || dynamicSubtitle (API) || undefined
- Los títulos dinámicos se resetean al abrir el modal
- Se extraen automáticamente del
response.data[0]
del sheetEndpoint
10. 🆕 Control de Botón Skip (skippable)
El botón "Saltear" se puede controlar dinámicamente desde el API:
Configuración desde sheetEndpoint:
{
"id": "60f7b3b3b3b3b3b3b3b3b3b3",
"skippable": false, // 🆕 false = NO mostrar botón skip
"schema": {...}
}
Lógica de visualización del botón Skip:
const shouldShowSkipButton =
hostUrl && // ✅ URL configurada
skipEndpoint && // ✅ Endpoint configurado
userData && // ✅ Datos de usuario disponibles
currentSheetId && // ✅ ID del formulario disponible
isSkippable; // 🆕 API permite skip
Comportamiento:
skippable: true
→ Botón "Saltear" se muestra (si otras condiciones se cumplen)skippable: false
→ Botón "Saltear" NO se muestraskippable
no definido → Por defectotrue
(backward compatibility)- Reset al abrir modal →
isSkippable
se resetea atrue
Ejemplos de uso:
// Formulario NO skippable (ej: formulario obligatorio)
// Response del API:
{
"skippable": false,
"title": "Formulario Obligatorio",
"schema": {...}
}
// Resultado: Solo botones "Confirmar" y "Cancelar"
// Formulario skippable (ej: encuesta opcional)
// Response del API:
{
"skippable": true,
"title": "Encuesta Opcional",
"schema": {...}
}
// Resultado: Botones "Confirmar", "Saltear" y "Cancelar"
Casos de uso típicos:
- Formularios obligatorios:
skippable: false
(evaluaciones de desempeño) - Encuestas opcionales:
skippable: true
(feedback del cliente) - Formularios temporales:
skippable: true
conskipDelayMinutes
🎨 Personalización Visual
Estilos del Star Rating
El componente de estrellas incluye estilos personalizados:
.star-rating-container {
.star {
&.empty svg {
color: #f0d06b; // Estrella vacía - amarillo tenue
stroke: #f0d06b;
fill: none;
}
&.filled svg {
color: #ffc107; // Estrella llena - dorado
fill: #ffc107;
stroke: #ffc107;
}
&.hovered svg {
color: #ffca28; // Estrella hover - amarillo intermedio
fill: #ffca28;
stroke: #ffca28;
}
}
}
Responsive Design
Los componentes están optimizados para dispositivos móviles:
@media (max-width: 768px) {
.star-rating-container {
gap: 0.5rem;
.star {
padding: 8px;
&:hover {
transform: scale(1.1); // Menor escala en móvil
}
}
}
}
🔧 Nueva Arquitectura de API
Construcción Automática de URLs
La nueva arquitectura separa la configuración para mayor flexibilidad:
// ✅ Nueva forma (recomendada)
<FormBottomSheet
hostUrl="https://api.company.com" // URL base
sheetEndpoint="/engagement/sheets/active" // Endpoint para cargar formulario
skipEndpoint="/engagement/responses/skip" // Endpoint para skip
sendEndpoint="/engagement/responses/send" // Endpoint para enviar respuestas
apiKey="your-api-key"
getUserData={() => ({
country: 'CO',
businessUnit: 'SM',
role: 'admin',
})}
/>
// 🔄 Construye automáticamente:
// GET: https://api.company.com/engagement/sheets/active?country=CO&businessUnit=SM&role=admin
// POST: https://api.company.com/engagement/responses/skip (cuando se hace skip)
// POST: https://api.company.com/engagement/responses/send (cuando se envían respuestas)
Ventajas:
- 📝 URLs dinámicas: Se construyen automáticamente con datos del usuario
- 🔄 Reutilización: Misma configuración para diferentes usuarios/roles
- 🛡️ Tipo safety: TypeScript valida la estructura
- 🧩 Modularidad: Separación clara de responsabilidades
Flujo Completo de Datos
sequenceDiagram
participant U as Usuario
participant F as FormBottomSheet
participant A as API Server
U->>F: Abre formulario
F->>F: getUserData()
F->>A: GET {hostUrl}{sheetEndpoint}?country=CO&businessUnit=SM&role=admin
A->>F: response.data[0] con schema + userEngagement
F->>F: Verifica isSkipped, skipDate
F->>U: Muestra formulario (si procede)
alt Usuario hace skip
U->>F: Presiona "Saltear"
F->>A: POST {hostUrl}{skipEndpoint} con skipPayload
A->>F: 200 OK
F->>F: onFormSkipped(userData)
F->>U: Cierra modal
else Usuario completa formulario
U->>F: Presiona "Confirmar"
F->>A: POST {hostUrl}{sendEndpoint} con responsePayload
A->>F: 200 OK
F->>F: onFormComplete(answers, stats, userData)
F->>U: Cierra modal
end
📊 Ejemplos de Uso Avanzado
Formulario de Evaluación de Local Completo
const LocalEvaluationForm = () => {
const formBottomSheet = useFormBottomSheet({
onFormComplete: async (answers, stats, userData) => {
// Enviar evaluación a la API
const evaluation = {
userId: userData?.email,
store: userData?.store,
rating: answers['LOCAL_RATING']?.value,
improvements: answers['IMPROVEMENTS']?.value,
comments: answers['COMMENTS']?.value,
completedAt: new Date().toISOString(),
};
try {
await submitEvaluation(evaluation);
showSuccessMessage('¡Gracias por tu evaluación!');
} catch (error) {
showErrorMessage('Error al enviar evaluación');
}
},
});
return (
<FormBottomSheet
isOpen={formBottomSheet.isOpen}
onDidDismiss={formBottomSheet.closeForm}
endpoint="https://api.company.com/evaluations/active"
apiKey={process.env.REACT_APP_API_KEY}
title="Evaluación de Mi Local"
subtitle="Tu opinión nos ayuda a mejorar"
confirmButtonText="Enviar Evaluación"
cancelButtonText="Ahora no"
icon="storefront-outline"
size="xl"
onFormComplete={formBottomSheet.handleFormComplete}
onShouldNotShow={() => {
console.log('Usuario ya evaluó este local');
}}
getUserData={() => getAuthenticatedUser()}
/>
);
};
Integración con Estado Global (Redux/Context)
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { submitFormAnswers, selectFormState } from '../store/formSlice';
const GlobalStateForm = () => {
const dispatch = useAppDispatch();
const formState = useAppSelector(selectFormState);
const formBottomSheet = useFormBottomSheet({
onFormComplete: (answers, stats, userData) => {
dispatch(
submitFormAnswers({
answers,
stats,
userData,
timestamp: Date.now(),
})
);
},
onFormChange: (answers, stats, question) => {
// Guardar progreso en tiempo real
dispatch(updateFormProgress({ answers, stats }));
},
});
return (
<FormBottomSheet
{...formBottomSheet}
// ... resto de props
/>
);
};
🔍 Debugging y Troubleshooting
Logs de Debugging
La librería incluye logs útiles para debugging:
// Activar logs detallados
console.log('FormBottomSheet state:', {
isOpen: formBottomSheet.isOpen,
answers: formBottomSheet.answers,
stats: formBottomSheet.stats,
});
Problemas Comunes
El modal no se abre:
- Verificar que
isOpen
seatrue
- Revisar si
userEngagement.status === 'sent'
- Comprobar errores en la llamada a la API
- Verificar que
Dependencias no funcionan:
- Verificar que los códigos de pregunta coincidan exactamente
- Revisar que los valores en
answers
array sean strings
Validación incorrecta:
- Verificar
require_comments
yrequire_evidence
en alternativas - Comprobar que las preguntas
required
estén correctamente marcadas
- Verificar
Archivo de Troubleshooting
Para más detalles, consulta el archivo TROUBLESHOOTING.md
incluido en la librería.
🧪 Testing
# Ejecutar tests
npm run test
# Tests con coverage
npm run test:coverage
# Tests en modo watch
npm run test:watch
Ejemplo de Test
import { render, screen, fireEvent } from '@testing-library/react';
import { FormBottomSheet } from '@team_yumi/dynamic-form';
test('debería renderizar el formulario correctamente', () => {
const mockProps = {
isOpen: true,
onDidDismiss: jest.fn(),
formData: mockFormData,
};
render(<FormBottomSheet {...mockProps} />);
expect(screen.getByText('¿Cómo calificarías nuestro servicio?')).toBeInTheDocument();
});
🚀 Build y Deploy
# Build de la librería
npm run build
# Verificar build
npm run build:check
# Publicar (si tienes permisos)
npm run publish
📈 Roadmap
Próximas Características
- Conditional Logic Builder: Editor visual para dependencias complejas
- Form Templates: Plantillas predefinidas para casos comunes
- Analytics Integration: Métricas automáticas de uso y completitud
- Offline Support: Soporte para formularios sin conexión
- Custom Validations: Validaciones personalizadas por pregunta
- Multi-language: Soporte para múltiples idiomas
- File Upload: Componente nativo para subida de archivos
- Conditional Styling: Estilos dinámicos según respuestas
🤝 Contribuir
- Fork el repositorio
- Crear una branch para tu feature (
git checkout -b feature/nueva-caracteristica
) - Commit tus cambios (
git commit -am 'Agregar nueva característica'
) - Push a la branch (
git push origin feature/nueva-caracteristica
) - Crear un Pull Request
📄 Licencia
MIT © Yumi Team - Cencosud X
🆘 Soporte
- Documentación: Consulta este README y
TROUBLESHOOTING.md
- Issues: Reporta bugs en GitHub Issues
- Team Contact: Contacta al equipo Yumi para soporte técnico
¡Feliz desarrollo! 🎉