You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js Express中Repeater字段的使用与验证错误展示问题

问题描述

我是Node.js Express新手,要实现重复字段的展示、验证并把数据存到MongoDB。现在用中间件做表单验证,非重复字段的错误展示正常,但重复字段有问题:提交后页面刷新,只剩一个重复块,其他都没了,也没法正确显示重复字段的错误。另外想知道能不能把重复字段的错误整理成和提交数据类似的数组形式,求指导。


重复字段HTML代码

<div data-repeater-list="tanent_info">
    <div data-repeater-item>
        <div class="card card-bordered card-stretch">
            <div class="card-inner-group">
                <div class="card-inner">
                    <div class="row">
                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="tanent_name">Tanent Name <span class="mandatory">*</span></label>
                            <div class="form-control-wrap">
                                <input type="text" class="form-control form-control-lg" id="tanent_name" name="tanent_info[tanent_name]" placeholder="Tanent Name">
                            </div>
                        </div>

                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="tanent_phone">Phone <span class="mandatory">*</span></label>
                            <div class="form-control-wrap">
                                <input type="text" class="form-control form-control-lg" id="tanent_phone" name="tanent_info[tanent_phone]" placeholder="Enter your phone number">
                            </div>
                        </div>
                    </div>

                    <div class="row">
                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="tanent_email">Email <span class="mandatory">*</span></label>
                            <div class="form-control-wrap">
                                <input type="email" class="form-control form-control-lg" id="tanent_email" name="tanent_info[tanent_email]" placeholder="Enter Email">
                            </div>
                        </div>
                    </div>

                    <div class="row">
                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="tanent_address">Address <span class="mandatory">*</span></label>
                            <div class="form-control-wrap">
                                <div class="form-control-wrap">
                                    <textarea class="form-control no-resize" name="tanent_info[tanent_address]" id="tanent_address"></textarea>
                                </div>
                            </div>
                        </div>
                    </div>


                    <div class="row">
                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="aadhar_front">Aadhar Card Front</label>
                            <div class="form-control-wrap">        
                                <div class="form-file">            
                                    <input type="file" name="tanent_info[aadhar_front]" class="form-file-input" id="customFile">
                                    <label class="form-file-label" for="customFile">Choose file</label>
                                </div>    
                            </div>
                        </div>

                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="aadhar_back">Aadhar Card Back</label>
                            <div class="form-control-wrap">        
                                <div class="form-file">            
                                    <input type="file" name="tanent_info[aadhar_back]" class="form-file-input" id="customFile">
                                    <label class="form-file-label" for="customFile">Choose file</label>        
                                </div>    
                            </div>
                        </div>
                    </div>

                    <div class="row">
                        <div class="form-group col-md-6"> 
                            <label class="form-label" for="pan_card">Pan Card</label>
                            <div class="form-control-wrap">        
                                <div class="form-file">            
                                    <input type="file" name="tanent_info[pan_card]" class="form-file-input" id="customFile">            
                                    <label class="form-file-label" for="customFile">Choose file</label>        
                                </div>    
                            </div>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        <input data-repeater-delete type="button" value="Remove Tenant" class="btn btn-danger"/>
    </div>
</div>

中间件验证代码

const express = require('express');
const { body } = require("express-validator");

const companyValidation = [
  body("company")
    .notEmpty()
    .withMessage("Please enter company name")
    .isString()
    .withMessage("company name should be string"),
  body("office_number")
    .notEmpty()
    .withMessage("Please select office number"),
  body("agreement_date")
    .notEmpty()
    .withMessage("Please select agreement date"),
  body("expiry_date")
    .notEmpty()
    .withMessage("Please select expiry date"),
  body("rent_amount")
    .notEmpty()
    .withMessage("Please enter rent amount")
    .isInt()
    .withMessage("Rent amount should be number only"),
  body("rent_start")
    .notEmpty()
    .withMessage("Please select rent start date"),
  body("status")
    .notEmpty()
    .withMessage("Please select tenant status"),
  body("security_paid")
    .optional()
    .if(body('security_paid').notEmpty())
    .isInt()
    .withMessage("Security Amount Paid should be number only"),
  body('tanent_info').optional(),
    body('tanent_info.*.tanent_name')
        .if(body('tanent_info').exists())
        .notEmpty()
        .withMessage("Please enter tenant name"),
    body('tanent_info.*.tanent_name')
        .if(body('tanent_info').exists())
        .notEmpty()
        .withMessage("Please enter tenant name"),
    body('tanent_info.*.tanent_name')
        .if(body('tanent_info').exists())
        .notEmpty()
        .withMessage("Please enter tenant name"),
    body('tanent_info.*.tanent_name')
        .if(body('tanent_info').exists())
        .notEmpty()
        .withMessage("Please enter tenant name"),
    body('tanent_info.*.tanent_name')
        .if(body('tanent_info').exists())
        .notEmpty()
        .withMessage("Please enter tenant name"),    
];

module.exports = {
  companyValidation
}

提交的数据

{
  "company": "test company",
  "office_number": "",
  "agreement_date": "",
  "expiry_date": "",
  "rent_amount": "",
  "security_paid": "",
  "status": "",
  "rent_start": "",
  "extention": "",
  "agreement_number": "",
  "agreement_document": "",
  "notes": "",
  "tanent_info": [
    {
      "tanent_name": "tenant1",
      "tanent_phone": "",
      "tanent_email": "",
      "tanent_address": "",
      "aadhar_front": "",
      "aadhar_back": "",
      "pan_card": ""
    },
    {
      "tanent_name": "",
      "tanent_phone": "",
      "tanent_email": "",
      "tanent_address": "",
      "aadhar_front": "",
      "aadhar_back": "",
      "pan_card": ""
    },
    {
      "tanent_name": "tenant 2",
      "tanent_phone": "",
      "tanent_email": "",
      "tanent_address": "",
      "aadhar_front": "",
      "aadhar_back": "",
      "pan_card": ""
    }
  ],
  "submit": ""
}

中间件返回的错误

{
  "errors": [
    {
      "type": "field",
      "value": "",
      "msg": "Please select office number",
      "path": "office_number",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please select agreement date",
      "path": "agreement_date",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please select expiry date",
      "path": "expiry_date",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter rent amount",
      "path": "rent_amount",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Rent amount should be number only",
      "path": "rent_amount",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please select rent start date",
      "path": "rent_start",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please select tenant status",
      "path": "status",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant name",
      "path": "tanent_info[1].tanent_name",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant Phone number",
      "path": "tanent_info[0].tanent_phone",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant Phone number",
      "path": "tanent_info[1].tanent_phone",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant Phone number",
      "path": "tanent_info[2].tanent_phone",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant email",
      "path": "tanent_info[0].tanent_email",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant email",
      "path": "tanent_info[1].tanent_email",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant email",
      "path": "tanent_info[2].tanent_email",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant address",
      "path": "tanent_info[0].tanent_address",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant address",
      "path": "tanent_info[1].tanent_address",
      "location": "body"
    },
    {
      "type": "field",
      "value": "",
      "msg": "Please enter tenant address",
      "path": "tanent_info[2].tanent_address",
      "location": "body"
    }
  ]
}

解决方案

1. 修复表单字段命名问题

当前HTML里的重复字段命名tanent_info[tanent_name]无法正确传递数组结构,Express会覆盖同名字段,导致提交后只剩最后一个重复块。需改成数组追加格式:

<!-- 把每个字段的name改成tanent_info[][字段名],Express会自动解析为数组 -->
<input type="text" class="form-control form-control-lg" id="tanent_name" name="tanent_info[][tanent_name]" placeholder="Tanent Name">

如果使用jQuery Repeater插件,需确保插件生成正确的name格式,或手动配置插件的namePrefix参数。

2. 整理重复字段错误为数组格式

从中间件返回的错误中,path包含数组索引(如tanent_info[1].tanent_name),可通过工具函数整理成和提交数据匹配的数组结构:

function formatTenantErrors(errors) {
  const tenantErrors = [];
  const tenantFieldErrors = errors.filter(e => e.path.startsWith('tanent_info'));
  
  tenantFieldErrors.forEach(err => {
    const match = err.path.match(/tanent_info\[(\d+)\]\.(\w+)/);
    if (match) {
      const index = parseInt(match[1]);
      const field = match[2];
      
      if (!tenantErrors[index]) {
        tenantErrors[index] = {};
      }
      tenantErrors[index][field] = err.msg;
    }
  });
  
  return tenantErrors;
}

在路由中使用该函数处理错误:

const { validationResult } = require('express-validator');

router.post('/submit', companyValidation, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    const formattedErrors = {
      general: errors.array().filter(e => !e.path.startsWith('tanent_info')),
      tenantErrors: formatTenantErrors(errors.array())
    };
    // 回传提交数据和格式化后的错误到模板
    return res.render('form', { data: req.body, errors: formattedErrors });
  }
  
  // 验证通过后执行保存逻辑
});

3. 模板渲染时保留重复块并展示错误

以EJS模板为例,根据提交的tanent_info数组渲染所有重复块,同时绑定错误信息:

<div data-repeater-list="tanent_info">
  <% if (data && data.tanent_info && data.tanent_info.length > 0) { %>
    <% data.tanent_info.forEach((tenant, index) => { %>
      <div data-repeater-item>
        <div class="card card-bordered card-stretch">
          <div class="card-inner-group">
            <div class="card-inner">
              <div class="row">
                <div class="form-group col-md-6"> 
                  <label class="form-label" for="tanent_name_<%= index %>">Tanent Name <span class="mandatory">*</span></label>
                  <div class="form-control-wrap">
                    <input type="text" 
                           class="form-control form-control-lg <%= errors.tenantErrors[index] && errors.tenantErrors[index].tanent_name ? 'is-invalid' : '' %>" 
                           id="tanent_name_<%= index %>" 
                           name="tanent_info[][tanent_name]" 
                           value="<%= tenant.tanent_name || '' %>"
                           placeholder="Tanent Name">
                    <% if (errors.tenantErrors[index] && errors.tenantErrors[index].tanent_name) { %>
                      <div class="invalid-feedback"><%= errors.tenantErrors[index].tanent_name %></div>
                    <% } %>
                  </div>
                </div>
                <!-- 其他字段按同样逻辑处理 -->
              </div>
            </div>
          </div>
        </div>
        <input data-repeater-delete type="button" value="Remove Tenant" class="btn btn-danger"/>
      </div>
    <% }) %>
  <% } else { %>
    <!-- 默认空重复块,使用tanent_info[][字段名]格式 -->
    <div data-repeater-item>
      <!-- 原HTML结构,仅修改name属性 -->
    </div>
  <% } %>
</div>

4. 保存到MongoDB的配置

定义MongoDB Schema时,将tanent_info设为数组类型:

const mongoose = require('mongoose');

const tenantSchema = new mongoose.Schema({
  tanent_name: { type: String, required: true },
  tanent_phone: { type: String, required: true },
  tanent_email: { type: String, required: true },
  tanent_address: { type: String, required: true },
  aadhar_front: String,
  aadhar_back: String,
  pan_card: String
});

const companySchema = new mongoose.Schema({
  company: { type: String, required: true },
  office_number: { type: String, required: true },
  agreement_date: { type: Date, required: true },
  // 其他字段...
  tanent_info: [tenantSchema]
});

module.exports = mongoose.model('Company', companySchema);

路由中执行保存操作:

const Company = require('../models/Company');

// 验证通过分支
const newCompany = new Company(req.body);
newCompany.save()
  .then(() => res.redirect('/success'))
  .catch(err => {
    res.render('form', { data: req.body, saveError: err.message });
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 11:27:00