Skip to content

Instantly share code, notes, and snippets.

@vandorjw
Created March 21, 2023 16:05
Show Gist options
  • Save vandorjw/4294a6cd37ea1b861eb31f9e2322d68e to your computer and use it in GitHub Desktop.
Save vandorjw/4294a6cd37ea1b861eb31f9e2322d68e to your computer and use it in GitHub Desktop.
function Formset(element) {
/*
Dynamic Formset handler for Django formsets.
Events:
* init.formset
* add-form.formset
* remove-form.formset
* renumber-form.formset
*/
if (!(this instanceof Formset)) {
return new Formset(element);
}
var formset = this;
var emptyForm = element.querySelector('.empty-form').firstElementChild;
var formsList = element.querySelector('.forms');
var initialForms = element.querySelector('[name$=INITIAL_FORMS]');
var totalForms = element.querySelector('[name$=TOTAL_FORMS]');
var prefix = initialForms.name.replace(/INITIAL_FORMS$/, '');
function addForm(event) {
// Duplicate empty form.
var newForm = emptyForm.cloneNode(true);
// Update all references to __prefix__ in the elements names.
renumberForm(newForm, '__prefix__', totalForms.value);
// Make it able to delete itself.
newForm.querySelector('[data-formset-remove-form]').addEventListener('click', removeForm);
// Append the new form to the formsList.
formsList.insertAdjacentElement('beforeend', newForm);
element.dispatchEvent(new CustomEvent('add-form.formset', {
detail: {
form: newForm,
formset: formset
}
}));
// Update the totalForms.value
totalForms.value = Number(totalForms.value) + 1;
}
function getForm(target) {
var parent = target.parentElement;
if (parent == document) {
return null;
}
if (parent == formsList) {
return target;
}
return getForm(parent);
}
function renumberForm(form, oldValue, newValue) {
var matchValue = prefix + oldValue.toString()
var match = new RegExp(matchValue);
var replace = prefix + newValue.toString();
['name', 'id', 'for'].forEach(function (attr) {
form.querySelectorAll('[' + attr + '*=' + matchValue + ']').forEach(function (el) {
el.setAttribute(attr, el.getAttribute(attr).replace(match, replace));
});
});
element.dispatchEvent(new CustomEvent('renumber-form.formset', {
detail: {
form: form,
oldValue: oldValue,
newValue: newValue,
formset: formset
}
}));
}
function removeForm(event) {
// Find the form "row": the child of formsList that is the parent of the element
// that triggered this event.
var formToRemove = getForm(event.target);
// Renumber the rows that come after us.
var nextElement = formToRemove.nextElementSibling;
var nextElementIndex = Array.prototype.indexOf.call(formsList.children, formToRemove);
while (nextElement) {
renumberForm(nextElement, nextElementIndex + 1, nextElementIndex);
nextElement = nextElement.nextElementSibling;
nextElementIndex = nextElementIndex + 1;
}
// Remove this row.
formToRemove.remove();
element.dispatchEvent(new CustomEvent('remove-form.formset', {
detail: {
form: formToRemove,
formset: formset
}
}));
// Decrement the management form's count.
totalForms.value = Number(totalForms.value) - 1;
}
element.querySelector('[data-formset-add-form]').addEventListener('click', addForm);
// Allow existing forms to remove themselves.
formsList.querySelectorAll('[data-formset-remove-form]').forEach(
rmBtn => {rmBtn.addEventListener('click', removeForm);
});
element.formset = this;
element.dispatchEvent(new CustomEvent('init.formset', {
detail: {
formset: this
}
}));
this.addForm = addForm;
}
@vandorjw
Copy link
Author

vandorjw commented Mar 21, 2023

modified for .NET

    public class FileUploadPage : PageModel
    {
        ...
        [BindProperty(SupportsGet = true)]
        public List<FileUploadForm> FileUploadFormSet { get; set; }

        public FileUploadForm EmptyForm { get; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            foreach (var from in FileUploadFormSet)
            {
               // do stuff
            }
     }


    public class FileUploadForm
    {
        [Required]
        [Display(Name = "File")]
        [MaxFileSize]
        [ValidFileMimeType(allowedFileTypes: new string[] { ".pdf" })]
        public IFormFile FileUploadField { get; set; }
    }


function Formset(element) {
  /* 
    Dynamic Formset handler for formsets.
  
  Events:
  
    * init.formset
    * add-form.formset
    * remove-form.formset
    * renumber-form.formset
    
  */
  if (!(this instanceof Formset)) {
    return new Formset(element);
  }
  var formset = this;
  var emptyForm = element.querySelector('.empty-form').firstElementChild;
  var formsList = element.querySelector('.forms');

    var totalForms = element.querySelector('[name$=TOTAL_FORMS]');
    var prefix = element.querySelector('[name$=PREFIX]').value;

  function addForm(event) {
    // Duplicate empty form.
    var newForm = emptyForm.cloneNode(true);
    // Update all references to EmptyForm in the elements names.
      renumberForm(newForm, 'EmptyForm', totalForms.value);
    // Make it able to delete itself.
    newForm.querySelector('[data-formset-remove-form]').addEventListener('click', removeForm);
    // Append the new form to the formsList.
    formsList.insertAdjacentElement('beforeend', newForm);
    element.dispatchEvent(new CustomEvent('add-form.formset', {
      detail: {
        form: newForm,
        formset: formset
      }
    }));
    // Update the totalForms.value
    totalForms.value = Number(totalForms.value) + 1;
  }

  function getForm(target) {
    var parent = target.parentElement;
    if (parent == document) {
      return null;
    }
    if (parent == formsList) {
      return target;
    }
    return getForm(parent);
  }

  function renumberForm(form, oldValue, newValue) {
    // .NET has two variations for generating id's and names.
    // for 'id', 'for' and other values, it uses _
    // for 'name' attribute it uses [ ]

    // we need to replace some numbers wrapped in underscores _
    var matchValue = oldValue.toString() === "EmptyForm" ? oldValue.toString() : `${prefix}_${oldValue.toString()}_`;
    var match = new RegExp(matchValue);
    var replace = `${prefix}_${newValue.toString()}_`;

      ['id', 'for', 'data-val-maxfilesize-id', 'aria-describedby'].forEach(function (attr) {
      form.querySelectorAll('[' + attr + '*=' + matchValue + ']').forEach(function (el) {
        el.setAttribute(attr, el.getAttribute(attr).replace(match, replace));
      });
    });

    // we need to replace values between square brackets
    var matchValue = oldValue.toString() === "EmptyForm" ? oldValue.toString() : `${prefix}\\[${oldValue.toString()}\\]`;
    var match = new RegExp(matchValue);
    var replace = `${prefix}\[${newValue.toString()}\]`;
      ['data-valmsg-for', 'name'].forEach(function (attr) {
          form.querySelectorAll('[' + attr + '*=' + matchValue + ']').forEach(function (el) {
              el.setAttribute(attr, el.getAttribute(attr).replace(match, replace));
          });
      });

    element.dispatchEvent(new CustomEvent('renumber-form.formset', {
      detail: {
        form: form,
        oldValue: oldValue,
        newValue: newValue,
        formset: formset
      }
    }));
  }

  function removeForm(event) {
    // Find the form "row": the child of formsList that is the parent of the element
    // that triggered this event.
    var formToRemove = getForm(event.target);
    // Renumber the rows that come after us.
    var nextElement = formToRemove.nextElementSibling;
    var nextElementIndex = Array.prototype.indexOf.call(formsList.children, formToRemove);
    while (nextElement) {
      renumberForm(nextElement, nextElementIndex + 1, nextElementIndex);
      nextElement = nextElement.nextElementSibling;
      nextElementIndex = nextElementIndex + 1;
    }
    // Remove this row.
    formToRemove.remove();
    element.dispatchEvent(new CustomEvent('remove-form.formset', {
      detail: {
        form: formToRemove,
        formset: formset
      }
    }));
    // Decrement the management form's count.
    totalForms.value = Number(totalForms.value) - 1;
  }

  element.querySelector('[data-formset-add-form]').addEventListener('click', addForm);

  // Allow existing forms to remove themselves.
  formsList.querySelectorAll('[data-formset-remove-form]').forEach(
    rmBtn => {rmBtn.addEventListener('click', removeForm);
  });

  element.formset = this;

  element.dispatchEvent(new CustomEvent('init.formset', {
    detail: {
      formset: this
    }
  }));

  this.addForm = addForm;
}

@vandorjw
Copy link
Author

Corresponding HTML

<script src="~/js/formset_manager.js"></script>

<h1>File Upload</h1>
<p>Instructions go here</p>




<form id="documentUploadForm" enctype="multipart/form-data" method="post">

    <input type="hidden" name="TOTAL_FORMS" value="1" />
    <input type="hidden" name="PREFIX" value="FileUploadFormSet" />

    <fieldset disabled class="empty-form" style="display:none">
        <fieldset>
            <div class="form-group row">
                <div class="col-sm-10">
                    <label asp-for="EmptyForm.FileUploadField"></label>
                    <input asp-for="EmptyForm.FileUploadField" type="file" accept=".pdf">
                    <span asp-validation-for="EmptyForm.FileUploadField" class="field-validation-error"></span>
                </div>

                <div class="col-sm-2">
                    <button class="btn-danger btn btn-sm" data-formset-remove-form>Remove</button>
                </div>

            </div>
        </fieldset>
    </fieldset>

    <div class="forms">
    @{
        int i = 0;
        @foreach (var form in Model.FileUploadFormSet)
        {
            <fieldset>
                <div class="form-group row">
                    <div class="col-sm-10">
                        <label asp-for="FileUploadFormSet[i].FileUploadField"></label>
                        <input asp-for="FileUploadFormSet[i].FileUploadField" type="file" accept=".pdf">
                        <span asp-validation-for="FileUploadFormSet[i].FileUploadField" class="field-validation-error"></span>
                    </div>
                </div>
            </fieldset>
            i++;
        }
    }
    </div>

    <fieldset class="controls">
        <button class="btn btn-primary btn-sm" type="button" data-formset-add-form>Add Another File</button>
    </fieldset>

    <hr />

    <input asp-page-handler="Upload" class="btn btn-success" type="submit" value="Upload" />
</form>

<script type="text/javascript">
    new Formset(document.querySelector('#documentUploadForm'));
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment