easy-forms-core 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,6 +108,68 @@ form.schema = {
108
108
  }
109
109
  ```
110
110
 
111
+ ### Slots con inserción por fila (row)
112
+
113
+ Puedes insertar contenido HTML personalizado en posiciones concretas del formulario usando **hijos directos** del componente con el atributo `row`. El índice es 0-based (cada campo o fila del schema cuenta como una posición). Usa `row="-1"` para insertar al final, antes del botón de envío.
114
+
115
+ | Valor | Comportamiento |
116
+ |--------|-----------------|
117
+ | `row="0"` | Antes del primer campo |
118
+ | `row="1"` | Entre el primer y el segundo campo |
119
+ | `row="-1"` | Al final del formulario (antes del submit) |
120
+ | Sin `row` o valor inválido | Se trata como `-1` (al final) |
121
+
122
+ **HTML Vanilla**
123
+
124
+ ```html
125
+ <easy-form id="form">
126
+ <div row="0">Mensaje al inicio</div>
127
+ <div row="2">Mensaje después del segundo campo</div>
128
+ <div row="-1">Mensaje al final</div>
129
+ </easy-form>
130
+ ```
131
+
132
+ ```javascript
133
+ import 'easy-forms-core'
134
+
135
+ const form = document.querySelector('#form')
136
+ form.schema = {
137
+ fields: [
138
+ { type: 'text', name: 'name', label: 'Nombre' },
139
+ { type: 'email', name: 'email', label: 'Email' },
140
+ { type: 'textarea', name: 'message', label: 'Mensaje' }
141
+ ]
142
+ }
143
+ ```
144
+
145
+ **React**
146
+
147
+ ```tsx
148
+ import 'easy-forms-core'
149
+
150
+ function MyForm() {
151
+ return (
152
+ <easy-form schema={{ fields: [...] }}>
153
+ <div row="0">Contenido al inicio</div>
154
+ <div row="-1">Contenido al final</div>
155
+ </easy-form>
156
+ )
157
+ }
158
+ ```
159
+
160
+ **Vue**
161
+
162
+ ```vue
163
+ <template>
164
+ <easy-form :schema="schema">
165
+ <div row="0">Contenido al inicio</div>
166
+ <div row="-1">Contenido al final</div>
167
+ </easy-form>
168
+ </template>
169
+ ```
170
+
171
+ Los slots son solo visuales: los inputs dentro de un slot **no** forman parte del estado ni del envío del formulario. En formularios por pasos (wizard), el índice `row` es relativo al paso actual.
172
+
111
173
  ### React
112
174
 
113
175
  ```tsx
@@ -407,6 +469,7 @@ Puedes sobrescribir cualquier estilo usando las clases CSS del componente. Todas
407
469
  - Formularios anidados
408
470
  - Arrays dinámicos
409
471
  - **Rows (filas horizontales)** - Agrupa campos en filas
472
+ - **Slots (row)** - Inserta contenido HTML en posiciones concretas del formulario
410
473
  - **Datos iniciales** - Carga valores iniciales desde datos externos
411
474
  - Componentes visuales personalizables
412
475
  - Eventos de submit, change y error
@@ -307,6 +307,19 @@ interface FormColors {
307
307
  * Template names available
308
308
  */
309
309
  type TemplateName = 'login' | 'register' | 'otp' | 'contact' | 'password-reset' | 'password-change' | 'profile' | 'checkout' | 'feedback' | 'subscription' | 'booking' | 'review';
310
+ /**
311
+ * Configuración del botón de submit
312
+ */
313
+ interface SubmitButtonConfig {
314
+ /** Si false, no se muestra el botón (submit programático) */
315
+ visible?: boolean;
316
+ /** Texto del botón (default: "Enviar") */
317
+ text?: string;
318
+ /** Ancho del botón: "auto", "100%", "200px", etc. (default: "auto") */
319
+ width?: string;
320
+ /** Alineación: "left" | "center" | "right" (default: "left") */
321
+ align?: 'left' | 'center' | 'right';
322
+ }
310
323
  /**
311
324
  * Schema del formulario
312
325
  */
@@ -314,6 +327,8 @@ interface FormSchema {
314
327
  fields?: Field[];
315
328
  steps?: Step[];
316
329
  initialData?: Record<string, any>;
330
+ /** Configuración del botón de submit (también puede definirse vía atributo submit-button) */
331
+ submitButton?: SubmitButtonConfig;
317
332
  }
318
333
  /**
319
334
  * Componente personalizado para inyección
@@ -346,6 +361,11 @@ declare class EasyForm extends BrowserHTMLElement {
346
361
  private isRendering;
347
362
  private attemptsLock;
348
363
  private lockCountdownInterval;
364
+ /**
365
+ * Plantillas de slots basados en atributo `row` en el light DOM.
366
+ * Se inicializan una sola vez y se clonan en cada render.
367
+ */
368
+ private slotTemplates;
349
369
  static get observedAttributes(): string[];
350
370
  constructor();
351
371
  /**
@@ -387,6 +407,15 @@ declare class EasyForm extends BrowserHTMLElement {
387
407
  */
388
408
  get attemptsStorageKey(): string | null;
389
409
  set attemptsStorageKey(value: string | null);
410
+ /**
411
+ * Configuración del botón de submit (desde atributo o schema)
412
+ */
413
+ get submitButton(): SubmitButtonConfig | null;
414
+ set submitButton(value: SubmitButtonConfig | null);
415
+ /**
416
+ * Obtiene la configuración efectiva del botón submit (atributo > schema > defaults)
417
+ */
418
+ private getSubmitButtonConfig;
390
419
  /**
391
420
  * Se llama cuando el componente se conecta al DOM
392
421
  */
@@ -425,6 +454,16 @@ declare class EasyForm extends BrowserHTMLElement {
425
454
  * Retorna un objeto con los valores preservados
426
455
  */
427
456
  private preserveCurrentValues;
457
+ /**
458
+ * Inicializa las plantillas de slots a partir del light DOM.
459
+ * Cualquier hijo directo que sea HTMLElement se considera slot; si tiene atributo `row` se usa para la posición, si no se inserta al final (-1).
460
+ */
461
+ private initializeSlotTemplates;
462
+ /**
463
+ * Obtiene clones de slots agrupados por índice de fila efectivo.
464
+ * Cualquier valor inválido o fuera de rango se normaliza a -1 (final del formulario).
465
+ */
466
+ private getSlotClonesByRow;
428
467
  /**
429
468
  * Renderiza campos normales
430
469
  */
@@ -515,6 +554,11 @@ declare class EasyForm extends BrowserHTMLElement {
515
554
  * Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado.
516
555
  */
517
556
  getRemainingBlockTimeMs(): number;
557
+ /**
558
+ * Dispara el submit del formulario programáticamente.
559
+ * Útil cuando el botón submit está oculto (visible: false).
560
+ */
561
+ requestSubmit(): void;
518
562
  /**
519
563
  * Limpia todos los valores del formulario
520
564
  */
package/dist/easy-form.js CHANGED
@@ -265,6 +265,13 @@ function getBaseStyles(colors) {
265
265
  .easy-form-submit:active {
266
266
  transform: scale(0.98);
267
267
  }
268
+ .easy-form-submit-wrapper {
269
+ margin-top: 1rem;
270
+ margin-bottom: 0.5rem;
271
+ }
272
+ .easy-form-submit-wrapper .easy-form-submit {
273
+ min-width: 100px;
274
+ }
268
275
  input:not([type="checkbox"]):not([type="radio"]), textarea, select {
269
276
  width: 100%;
270
277
  padding: 0.5rem;
@@ -4756,6 +4763,11 @@ var EasyForm = class extends BrowserHTMLElement {
4756
4763
  this.isRendering = false;
4757
4764
  this.attemptsLock = null;
4758
4765
  this.lockCountdownInterval = null;
4766
+ /**
4767
+ * Plantillas de slots basados en atributo `row` en el light DOM.
4768
+ * Se inicializan una sola vez y se clonan en cada render.
4769
+ */
4770
+ this.slotTemplates = null;
4759
4771
  this.dependencyRenderTimeout = null;
4760
4772
  this.stateManager = new StateManager();
4761
4773
  this.shadow = this.attachShadow({ mode: "open" });
@@ -4772,7 +4784,8 @@ var EasyForm = class extends BrowserHTMLElement {
4772
4784
  "disabled",
4773
4785
  "max-attempts",
4774
4786
  "block-duration-minutes",
4775
- "attempts-storage-key"
4787
+ "attempts-storage-key",
4788
+ "submit-button"
4776
4789
  ];
4777
4790
  }
4778
4791
  /**
@@ -4892,6 +4905,41 @@ var EasyForm = class extends BrowserHTMLElement {
4892
4905
  this.removeAttribute("attempts-storage-key");
4893
4906
  }
4894
4907
  }
4908
+ /**
4909
+ * Configuración del botón de submit (desde atributo o schema)
4910
+ */
4911
+ get submitButton() {
4912
+ const attr = this.getAttribute("submit-button");
4913
+ if (attr) {
4914
+ try {
4915
+ return parseAttributeValue(attr);
4916
+ } catch {
4917
+ return null;
4918
+ }
4919
+ }
4920
+ return null;
4921
+ }
4922
+ set submitButton(value) {
4923
+ if (value && typeof value === "object") {
4924
+ this.setAttribute("submit-button", attributeValue(value));
4925
+ } else {
4926
+ this.removeAttribute("submit-button");
4927
+ }
4928
+ }
4929
+ /**
4930
+ * Obtiene la configuración efectiva del botón submit (atributo > schema > defaults)
4931
+ */
4932
+ getSubmitButtonConfig(schema) {
4933
+ const fromAttr = this.submitButton;
4934
+ const fromSchema = schema?.submitButton;
4935
+ const merged = { ...fromSchema, ...fromAttr };
4936
+ return {
4937
+ visible: merged.visible ?? true,
4938
+ text: merged.text ?? "Enviar",
4939
+ width: merged.width ?? "auto",
4940
+ align: merged.align ?? "left"
4941
+ };
4942
+ }
4895
4943
  /**
4896
4944
  * Se llama cuando el componente se conecta al DOM
4897
4945
  */
@@ -4947,6 +4995,9 @@ var EasyForm = class extends BrowserHTMLElement {
4947
4995
  this.setupAttemptsLock();
4948
4996
  this.updateLockOverlay();
4949
4997
  }
4998
+ if (name === "submit-button" && newValue !== oldValue) {
4999
+ this.render();
5000
+ }
4950
5001
  }
4951
5002
  /**
4952
5003
  * Configura el AttemptsLock según los atributos actuales
@@ -5060,17 +5111,25 @@ var EasyForm = class extends BrowserHTMLElement {
5060
5111
  newFormElement.classList.add("easy-form-disabled");
5061
5112
  }
5062
5113
  if (finalWizardState) {
5063
- this.renderWizard(newFormElement);
5114
+ this.renderWizard(newFormElement, schema);
5064
5115
  } else {
5065
5116
  this.renderFields(newFormElement, schema.fields || []);
5066
- const submitButton = document.createElement("button");
5067
- submitButton.type = "submit";
5068
- submitButton.textContent = "Enviar";
5069
- submitButton.className = "easy-form-submit";
5070
- if (this.disabled || this.loading) {
5071
- submitButton.disabled = true;
5117
+ const submitConfig = this.getSubmitButtonConfig(schema);
5118
+ if (submitConfig.visible) {
5119
+ const submitWrapper = document.createElement("div");
5120
+ submitWrapper.className = "easy-form-submit-wrapper";
5121
+ submitWrapper.style.textAlign = submitConfig.align;
5122
+ const submitButton = document.createElement("button");
5123
+ submitButton.type = "submit";
5124
+ submitButton.textContent = submitConfig.text;
5125
+ submitButton.className = "easy-form-submit";
5126
+ submitButton.style.width = submitConfig.width;
5127
+ if (this.disabled || this.loading) {
5128
+ submitButton.disabled = true;
5129
+ }
5130
+ submitWrapper.appendChild(submitButton);
5131
+ newFormElement.appendChild(submitWrapper);
5072
5132
  }
5073
- newFormElement.appendChild(submitButton);
5074
5133
  }
5075
5134
  const oldForm = this.shadow.querySelector("form");
5076
5135
  if (oldForm && oldForm.parentNode === this.shadow && oldForm !== newFormElement) {
@@ -5153,6 +5212,16 @@ var EasyForm = class extends BrowserHTMLElement {
5153
5212
  * Retorna un objeto con los valores preservados
5154
5213
  */
5155
5214
  preserveCurrentValues() {
5215
+ let currentSchema = null;
5216
+ const templateName = this.template;
5217
+ if (templateName) {
5218
+ currentSchema = this.getSchemaFromTemplate(templateName);
5219
+ } else {
5220
+ currentSchema = this.schema;
5221
+ }
5222
+ if (!currentSchema) {
5223
+ return {};
5224
+ }
5156
5225
  const form = this.shadow.querySelector("form");
5157
5226
  const preservedValues = {};
5158
5227
  if (!form) return preservedValues;
@@ -5160,6 +5229,8 @@ var EasyForm = class extends BrowserHTMLElement {
5160
5229
  for (const input of inputs) {
5161
5230
  const name = input.getAttribute("name");
5162
5231
  if (!name) continue;
5232
+ const belongsToSchema = this.findFieldInSchema(currentSchema, name) !== null;
5233
+ if (!belongsToSchema) continue;
5163
5234
  let value;
5164
5235
  if (input instanceof HTMLInputElement) {
5165
5236
  if (input.type === "checkbox") {
@@ -5190,16 +5261,93 @@ var EasyForm = class extends BrowserHTMLElement {
5190
5261
  }
5191
5262
  return preservedValues;
5192
5263
  }
5264
+ /**
5265
+ * Inicializa las plantillas de slots a partir del light DOM.
5266
+ * Cualquier hijo directo que sea HTMLElement se considera slot; si tiene atributo `row` se usa para la posición, si no se inserta al final (-1).
5267
+ */
5268
+ initializeSlotTemplates() {
5269
+ if (this.slotTemplates !== null) return;
5270
+ const elements = [];
5271
+ for (const child of Array.from(this.children)) {
5272
+ if (child instanceof HTMLElement) {
5273
+ elements.push(child);
5274
+ }
5275
+ }
5276
+ if (elements.length === 0) {
5277
+ this.slotTemplates = [];
5278
+ return;
5279
+ }
5280
+ this.slotTemplates = elements.map((el) => {
5281
+ const raw = el.hasAttribute("row") ? el.getAttribute("row") : null;
5282
+ const parsed = raw != null && raw !== "" ? Number(raw) : NaN;
5283
+ const row = Number.isFinite(parsed) ? parsed : null;
5284
+ return {
5285
+ template: el.cloneNode(true),
5286
+ row
5287
+ };
5288
+ });
5289
+ }
5290
+ /**
5291
+ * Obtiene clones de slots agrupados por índice de fila efectivo.
5292
+ * Cualquier valor inválido o fuera de rango se normaliza a -1 (final del formulario).
5293
+ */
5294
+ getSlotClonesByRow(totalRows) {
5295
+ this.initializeSlotTemplates();
5296
+ const result = /* @__PURE__ */ new Map();
5297
+ if (!this.slotTemplates || this.slotTemplates.length === 0) {
5298
+ return result;
5299
+ }
5300
+ for (const { template, row } of this.slotTemplates) {
5301
+ let effectiveRow = typeof row === "number" ? row : -1;
5302
+ if (!Number.isFinite(effectiveRow)) {
5303
+ effectiveRow = -1;
5304
+ }
5305
+ if (effectiveRow < 0 || effectiveRow >= totalRows) {
5306
+ effectiveRow = -1;
5307
+ }
5308
+ const clone = template.cloneNode(true);
5309
+ const existing = result.get(effectiveRow) ?? [];
5310
+ existing.push(clone);
5311
+ result.set(effectiveRow, existing);
5312
+ }
5313
+ return result;
5314
+ }
5193
5315
  /**
5194
5316
  * Renderiza campos normales
5195
5317
  */
5196
5318
  renderFields(container, fields) {
5197
- for (const field of fields) {
5319
+ if (fields.length === 0) {
5320
+ const slotClones = this.getSlotClonesByRow(0);
5321
+ const endSlots2 = slotClones.get(-1);
5322
+ if (endSlots2 && endSlots2.length > 0) {
5323
+ for (const slotElement of endSlots2) {
5324
+ container.appendChild(slotElement);
5325
+ }
5326
+ }
5327
+ return;
5328
+ }
5329
+ const totalRows = fields.length;
5330
+ const slotClonesByRow = this.getSlotClonesByRow(totalRows);
5331
+ for (let rowIndex = 0; rowIndex < fields.length; rowIndex++) {
5332
+ const slotsForRow = slotClonesByRow.get(rowIndex);
5333
+ if (slotsForRow && slotsForRow.length > 0) {
5334
+ for (const slotElement of slotsForRow) {
5335
+ container.appendChild(slotElement);
5336
+ }
5337
+ slotClonesByRow.delete(rowIndex);
5338
+ }
5339
+ const field = fields[rowIndex];
5198
5340
  const fieldElement = this.renderField(field);
5199
5341
  if (fieldElement) {
5200
5342
  container.appendChild(fieldElement);
5201
5343
  }
5202
5344
  }
5345
+ const endSlots = slotClonesByRow.get(-1);
5346
+ if (endSlots && endSlots.length > 0) {
5347
+ for (const slotElement of endSlots) {
5348
+ container.appendChild(slotElement);
5349
+ }
5350
+ }
5203
5351
  }
5204
5352
  /**
5205
5353
  * Renderiza un campo
@@ -5432,14 +5580,13 @@ var EasyForm = class extends BrowserHTMLElement {
5432
5580
  /**
5433
5581
  * Renderiza wizard
5434
5582
  */
5435
- renderWizard(container) {
5583
+ renderWizard(container, schema) {
5436
5584
  const wizardState = this.stateManager.getWizardState();
5437
5585
  if (!wizardState) return;
5438
5586
  const wizardContainer = document.createElement("div");
5439
5587
  wizardContainer.className = "easy-form-wizard";
5440
5588
  const stepsIndicator = document.createElement("div");
5441
5589
  stepsIndicator.className = "easy-form-wizard-steps";
5442
- const schema = this.schema;
5443
5590
  if (schema?.steps) {
5444
5591
  for (let i = 0; i < schema.steps.length; i++) {
5445
5592
  const stepEl = document.createElement("div");
@@ -5458,12 +5605,7 @@ var EasyForm = class extends BrowserHTMLElement {
5458
5605
  const fieldsContainer = document.createElement("div");
5459
5606
  fieldsContainer.className = "easy-form-wizard-fields";
5460
5607
  const currentFields = this.stateManager.getCurrentStepFields();
5461
- for (const field of currentFields) {
5462
- const fieldElement = this.renderField(field);
5463
- if (fieldElement) {
5464
- fieldsContainer.appendChild(fieldElement);
5465
- }
5466
- }
5608
+ this.renderFields(fieldsContainer, currentFields);
5467
5609
  wizardContainer.appendChild(fieldsContainer);
5468
5610
  const navContainer = document.createElement("div");
5469
5611
  navContainer.className = "easy-form-wizard-nav";
@@ -5524,11 +5666,13 @@ var EasyForm = class extends BrowserHTMLElement {
5524
5666
  }
5525
5667
  navContainer.appendChild(nextButton);
5526
5668
  }
5527
- if (wizardState.currentStep === wizardState.totalSteps - 1) {
5669
+ const submitConfig = this.getSubmitButtonConfig(schema);
5670
+ if (wizardState.currentStep === wizardState.totalSteps - 1 && submitConfig.visible) {
5528
5671
  const submitButton = document.createElement("button");
5529
5672
  submitButton.type = "button";
5530
- submitButton.textContent = "Enviar";
5673
+ submitButton.textContent = submitConfig.text;
5531
5674
  submitButton.className = "easy-form-wizard-next";
5675
+ submitButton.style.width = submitConfig.width;
5532
5676
  if (this.disabled || this.loading) {
5533
5677
  submitButton.disabled = true;
5534
5678
  } else {
@@ -5783,6 +5927,17 @@ var EasyForm = class extends BrowserHTMLElement {
5783
5927
  getRemainingBlockTimeMs() {
5784
5928
  return this.attemptsLock?.getRemainingBlockTimeMs() ?? 0;
5785
5929
  }
5930
+ /**
5931
+ * Dispara el submit del formulario programáticamente.
5932
+ * Útil cuando el botón submit está oculto (visible: false).
5933
+ */
5934
+ requestSubmit() {
5935
+ const form = this.shadow.querySelector("form");
5936
+ if (form && typeof form.requestSubmit === "function") {
5937
+ ;
5938
+ form.requestSubmit();
5939
+ }
5940
+ }
5786
5941
  /**
5787
5942
  * Limpia todos los valores del formulario
5788
5943
  */