A lightweight, feature-rich jQuery plugin for managing dynamic form rows with automatic reindexing, nested form support, and comprehensive event handling.
npm install addremrowhtml
`
$3
- ensure jQuery is loaded.
`npm
npm install addremrow
`
then in app.js
a) webpack / laravel-mix
` bash
require('addremrow');
``
b) Vite
` bash
import 'addremrow';
`
$3
`html
`
⚙️ 1. Available Options
$3
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| addBtn | string | '' | CSS selector for the "Add Row" button |
| maxRows | number | 5 | Maximum number of rows allowed |
| startRow | number | 0 | Starting index for row numbering |
| fieldName | string | 'data' | Base name for form fields (e.g., data[0][name]) |
| rowSelector | string | 'rowserial' | CSS class for row containers |
| removeClass | string | 'serial_remove' | CSS class for remove buttons |
| nestedwrapper | string | null | Selector for nested wrappers (for complex forms) |
$3
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| reindexRowName | array | ['name', 'data-bv-field', 'data-bv-for'] | Attributes containing field names to reindex |
| reindexRowID | array | ['id', 'for', 'aria-describedby'] | Attributes containing IDs to reindex |
| reindexRowIndex | array | ['data-index', 'data-id'] | Attributes containing indexes to reindex |
$3
| Option | Type | Parameters | Description |
|--------|------|------------|-------------|
| onAdd | function | (index, event, $row, name) | Called when a row is added |
| onRemove | function | (index, event, $row, name) | Called before removing a row |
| rowTemplate | function | (index, fieldName) | Custom HTML template for rows |
📖 2. Usage Examples
$3
`html
`
$3
`html
`
$3
`html
`
$3
`html
`
🔧 3. Public Methods
$3
`javascript
const plugin = $('#container').addRemRow(options);
// Use methods
plugin.add(); // Add a row
plugin.remove(2); // Remove row at index 2
plugin.getCount(); // Get current row count
plugin.reindexAll(); // Force reindex all rows
`
$3
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| add() | None | Plugin instance | Adds a new row |
| addBatch(dataArray) | dataArray (array) | Plugin instance | Batch add multiple rows with data |
| remove(index) | index (number) | Plugin instance | Removes row at specified index |
| getCount() | None | number | Returns current number of rows |
| reset() | None | Plugin instance | Removes all rows, resets counter |
| reindexAll() | None | Plugin instance | Reindexes all rows |
| setReindexConfig(type, attributes) | type (string): 'name', 'id', or 'index'
attributes (array/string) | Plugin instance | Updates reindexing configuration |
| getConfig() | None | object | Returns current configuration |
| destroy() | None | Plugin instance | Removes all events, resets DOM |
| getRow(index) | index (number) | string/null | Gets HTML of specific row |
| getAllData() | None | array | Returns all row data as array of objects |
| hasRow(index) | index (number) | boolean | Checks if specific row exists |
| getWrapper() | None | jQuery object | Returns wrapper element |
| validateAll(options) | options (object) | validation object | Validates all rows with custom rules |
| clearAll() | None | Plugin instance | Clears all field values |
| disableAll() | None | Plugin instance | Disables all rows and fields |
| enableAll() | None | Plugin instance | Enables all rows and fields |
| setRowData(index, data) | index (number), data (object) | Plugin instance | Sets data for specific row |
| exportJSON() | None | string | Exports all rows as JSON string |
| importJSON(jsonString, clearExisting) | jsonString (string), clearExisting (boolean) | result object | Imports rows from JSON |
| findRowByField(fieldName, value) | fieldName (string), value (any) | array | Finds rows by field value |
| countBy(conditionCallback) | conditionCallback (function) | number | Counts rows matching condition |
| toggleRows(show) | show (boolean) | Plugin instance | Shows/hides all rows |
| filterRows(filterCallback) | filterCallback (function) | Plugin instance | Filters rows based on callback |
| sortRows(fieldName, ascending) | fieldName (string), ascending (boolean) | Plugin instance | Sorts rows by field value |
$3
`javascript
// Initialize
const dynamicForm = $('#formContainer').addRemRow({
addBtn: '#addBtn',
maxRows: 10
});
// Programmatically add row
$('#anotherButton').click(function() {
dynamicForm.add();
});
// Remove specific row
$('#removeThird').click(function() {
dynamicForm.remove(2); // Removes row with index 2
});
// Update reindexing config
dynamicForm.setReindexConfig('name', ['name', 'data-field', 'custom-attr']);
// Get current state
const config = dynamicForm.getConfig();
console.log('Current rows:', config.currentIndex);
console.log('Max rows:', config.maxRows);
// Reset form
$('#resetForm').click(function() {
dynamicForm.reset();
});
// Clean up when leaving page
$(window).on('beforeunload', function() {
dynamicForm.destroy();
});
`
$3
#### Data Management
`javascript
// Get all row data
const allData = plugin.getAllData();
console.log('All row data:', allData);
// Export as JSON
const jsonData = plugin.exportJSON();
console.log('JSON export:', jsonData);
// Import from JSON
const importResult = plugin.importJSON('[{"name":"John","skill":"JS"},{"name":"Jane","skill":"Python"}]', true);
if (importResult.success) {
console.log(Imported ${importResult.count} rows);
}
// Set data for specific row
plugin.setRowData(2, {
name: 'Alice',
skill: 'React'
});
// Batch add multiple rows
plugin.addBatch([
{name: 'Bob', skill: 'Vue'},
{name: 'Charlie', skill: 'Angular'}
]);
`
#### Validation
`javascript
// Validate all rows
const validation = plugin.validateAll({
required: true,
minLength: 2,
maxLength: 50,
customValidation: function($field, rowIndex) {
if ($field.attr('name').includes('email')) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test($field.val())) {
return {
valid: false,
message: 'Invalid email format'
};
}
}
return { valid: true };
}
});
if (!validation.valid) {
console.log('Validation errors:', validation.getAllErrors());
console.log('First error:', validation.getFirstError());
}
`
#### Row Operations
`javascript
// Find rows by field value
const johnRows = plugin.findRowByField('name', 'John');
johnRows.forEach(row => {
console.log(Found John at row ${row.index});
});
// Count rows with specific condition
const jsExperts = plugin.countBy(function($row, index) {
return $row.find('[name*="[skill]"]').val() === 'JavaScript';
});
console.log(JavaScript experts: ${jsExperts});
// Filter rows
plugin.filterRows(function($row, index) {
const skill = $row.find('[name*="[skill]"]').val();
return skill.includes('JS') || skill.includes('JavaScript');
});
// Sort rows by name
plugin.sortRows('name', true); // Ascending
// Toggle visibility
plugin.toggleRows(false); // Hide all rows
plugin.toggleRows(true); // Show all rows
// Get specific row HTML
const row2HTML = plugin.getRow(2);
console.log('Row 2 HTML:', row2HTML);
// Check if row exists
if (plugin.hasRow(3)) {
console.log('Row 3 exists');
}
`
#### Form Control
`javascript
// Clear all form fields
$('#clearBtn').click(function() {
plugin.clearAll();
});
// Disable all fields (read-only mode)
plugin.disableAll();
// Enable all fields
plugin.enableAll();
// Get wrapper for custom operations
const $wrapper = plugin.getWrapper();
$wrapper.addClass('highlighted');
`
#### Advanced Usage
`javascript
// Chain multiple operations
plugin
.clearAll()
.addBatch(sampleData)
.sortRows('name')
.validateAll()
.reindexAll();
// Search and highlight
$('#searchBtn').click(function() {
const query = $('#searchInput').val().toLowerCase();
plugin.filterRows(function($row, index) {
const rowText = $row.text().toLowerCase();
const matches = rowText.includes(query);
$row.toggleClass('highlight-match', matches);
return true; // Show all, but highlight matches
});
});
// Export to CSV
function exportToCSV() {
const data = plugin.getAllData();
let csv = 'Name,Skill\n';
data.forEach(row => {
csv += "${row.name || ''}","${row.skill || ''}"\n;
});
return csv;
}
`
#### Integration with External Libraries
`javascript
// With DataTables
const table = $('#dataTable').DataTable();
const refreshTable = function() {
table.clear();
plugin.getAllData().forEach(row => {
table.row.add([row.name, row.skill, row.email]);
});
table.draw();
};
// With Chart.js
const updateChart = function() {
const data = plugin.getAllData();
const skillCounts = {};
data.forEach(row => {
skillCounts[row.skill] = (skillCounts[row.skill] || 0) + 1;
});
// Update chart data
chart.data.labels = Object.keys(skillCounts);
chart.data.datasets[0].data = Object.values(skillCounts);
chart.update();
};
// Auto-save to localStorage
$(window).on('beforeunload', function() {
const jsonData = plugin.exportJSON();
localStorage.setItem('formData', jsonData);
});
// Load from localStorage on page load
$(document).ready(function() {
const savedData = localStorage.getItem('formData');
if (savedData) {
plugin.importJSON(savedData, true);
}
});
`
$3
`javascript
// Complex operation chain
plugin
.reset() // Clear everything
.addBatch(initialData) // Add initial dataset
.sortRows('skill', true) // Sort by skill
.filterRows(function($row, idx) { // Filter by condition
return $row.find('[name*="[experience]"]').val() > 3;
})
.validateAll({ // Validate
required: true,
customValidation: customValidator
})
.reindexAll(); // Ensure proper indexing
// Quick setup chain
$('#container')
.addRemRow(config)
.addBatch(defaultRows)
.disableAll() // Start disabled
.on('enableForm', function() {
$(this).enableAll(); // Enable on custom event
});
``
🎯 4. Reindexing Logic Explained
$3
The plugin automatically reindexes three types of attributes when rows are added/removed:
1. Name Attributes (reindexRowName):
- Updates name, data-bv-field, data-bv-for attributes
- Pattern: fieldName[index][field] → fieldName[newIndex][field]
2. ID Attributes (reindexRowID):
- Updates id, for, aria-describedby attributes
- Pattern: field_0 → field_1
3. Index Attributes (reindexRowIndex):
- Updates data-index, data-id attributes
- Pattern: data-index="0" → data-index="1"
$3
When using nestedwrapper, the plugin handles two-level reindexing:
Outer (main row):
- id="product_0" → id="product_1"
- name="data[0][name]" → name="data[1][name]"
Inner (nested rows):
- id="product_0_item_0" → id="product_1_item_0"
- name="data[0][items][0][name]" → name="data[1][items][0][name]"
🔌 5. Event Handling
$3
`javascript
$('#container').addRemRow({
// ... other options ...
onAdd: function(index, event, $row, name) {
// Your custom logic
console.log('Row added at index:', index);
// Dispatch custom event
$(document).trigger('rowAdded', {
index: index,
row: $row,
fieldName: name
});
// Example: Initialize datepicker on new fields
$row.find('.datepicker').datepicker();
// Example: Initialize select2
$row.find('.select2').select2();
// Return false to prevent adding (optional)
// return false;
},
onRemove: function(index, event, $row, name) {
// Custom confirmation
const confirmDelete = Swal.fire({
title: 'Are you sure?',
text: "You won't be able to revert this!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!'
});
// Return promise for async operations
return confirmDelete.then((result) => {
return result.isConfirmed;
});
// Or return boolean for sync operations
// return confirm('Delete this row?');
}
});
`
🚀 6. Advanced Usage Patterns
$3
`javascript
// Create reusable configuration
const baseConfig = {
maxRows: 10,
rowSelector: 'dynamic-row',
removeClass: 'remove-dynamic',
reindexRowName: ['name', 'data-validate']
};
// Extend for specific use cases
const productConfig = {
...baseConfig,
fieldName: 'products',
rowTemplate: function(index, name) {
return ... product template ...;
}
};
const userConfig = {
...baseConfig,
fieldName: 'users',
rowTemplate: function(index, name) {
return ... user template ...;
}
};
// Initialize multiple instances
const productForm = $('#products').addRemRow(productConfig);
const userForm = $('#users').addRemRow(userConfig);
`
$3
`javascript
// React component example
class DynamicForm extends React.Component {
componentDidMount() {
this.plugin = $(this.container).addRemRow({
addBtn: this.addButton,
maxRows: this.props.maxRows,
rowTemplate: this.renderRow.bind(this)
});
}
componentWillUnmount() {
this.plugin.destroy();
}
renderRow(index, fieldName) {
return
;
}
render() {
return (
this.container = el}>
);
}
}
`
⚠️ 7. Common Issues & Solutions
$3
Solution: Check your reindexing configuration:
`javascript
// Ensure attributes are included
.setReindexConfig('name', ['name', 'data-field', 'data-bv-field'])
.setReindexConfig('id', ['id', 'for'])
.setReindexConfig('index', ['data-index', 'data-id'])
`
$3
Solution: Set nestedwrapper option:
`javascript
nestedwrapper: '.inner-container',
// And ensure your template has matching structure
`
$3
Solution: Refresh validator after operations:
`javascript
onAdd: function(index, event, $row, name) {
// For Bootstrap Validator
$('#form').bootstrapValidator('addField',
[name="${name}[${index}][field]"]
);
// For jQuery Validate
$('#form').validate().element([name="${name}[${index}][field]"]);
}
`
📝 8. Best Practices
1. Always call destroy() when removing the form from DOM
2. Use reset() instead of manually clearing rows
3. Set appropriate maxRows to prevent unlimited additions
4. Implement onRemove callback for confirmation dialogs
5. Test reindexing with your specific form structure
6. Use getConfig() for debugging when issues arise
📄 9. License & Credits
This plugin is open-source and can be modified for your needs. Include credit to the original author if redistributing.
`javascript
/**
* addRemRow - jQuery Dynamic Form Rows Plugin
* @version 1.0.0
* @license MIT
* @author Your Name
* @requires jQuery 3.0+
*/
``