¯\_(ツ)_/¯

thunder@home:~$

This is my home blog, mostly to share some useful info or code snippets
~ 2 mins

Hello, gophers!

On one of the projects, I had to create some dynamic inputs which are grouped into an array. Here is my Form example:

<form action="" method="POST">
  <div class="form-row row mt-3">
    <div class="col">
      <label><strong>Storage configuration</strong></label>
      <button type="button" class="btn btn-inverse-success storage-add"><i class="mdi mdi-plus"></i> Add</button>
    </div>
  </div>
  <div id="clone-keeper">
    <div class="form-row row mt-3 clone-container">
      <div class="col-2">
        <label class="control-label" for="storage_size_0"><strong>Size</strong></label>
        <input class="form-control" name="storage[0][size]" id="storage_size_0" placeholder="Enter Disk Size" required />
      </div>
      <div class="col-3">
        <label class="control-label" for="storage_profile_0"><strong>Profile</strong></label>
        <select class="form-control storage-profile" name="storage[0][profile]" id="storage_profile" required />
          <option value="">Select Storage Profile</option>
          <option value="SSD">SSD</option>
          <option value="HDD">HDD</option>
        </select>
      </div>
      <div class="col">
        <label class="control-label" for="storage_mount_0"><strong>Mount</strong></label>
        <input class="form-control" name="storage[0][mount]" id="storage_mount_0" placeholder="Enter Mount point on target instance" required />
      </div>
      <div class="col-1">
        <label class="control-label d-block" for="storage_remove_0"><strong>&nbsp;</strong></label>
        <button type="button" class="w-100 storage-remove btn btn-inverse-danger" name="storage_remove" id="storage_remove_0" title="Delete this block">
          <i class="mdi mdi-delete-forever"></i>
        </button>
      </div>
    </div>
  </div>
</form>

The script I’m using for cloning you can find here: github.com/rajneeshgautam/jquery-dynamicform . I had to modify and cleanup this script to meet my needs, but main logic is the same.

Form items cloner initialization code:

$('button.storage-add').cloneData({
  mainContainerId: 'clone-keeper', // Main container Should be ID
  cloneContainer: 'clone-container', // Which you want to clone
  removeButtonClass: 'storage-remove', // Remove button for remove cloned HTML
  removeConfirm: true, // default true confirm before delete clone item
  removeConfirmMessage: 'Are you sure want to delete?', // confirm delete message
  minLimit: 1, // Default 1 set minimum clone HTML required
  maxLimit: 5, // Default unlimited or set maximum limit of clone HTML
  defaultRender: 1, // Default 1 render clone HTML
});

Now, golang’s ParseForm method do not parse such fields into array, it returns such names as a string.

So, I wrote parsing methods for such inputs:

// ParseFormCollection is a helper method to parse an array of Form items.
// Eg, if Form field name like `inputfield[0][name1]`...
func ParseFormCollection(r *http.Request, typeName string) (result []map[string]string) {
  r.ParseForm()
  for key, values := range r.Form {
    re := regexp.MustCompile(typeName + "\\[([0-9]+)\\]\\[([a-zA-Z]+)\\]")
    matches := re.FindStringSubmatch(key)
    if len(matches) >= 3 {
      index, _ := strconv.Atoi(matches[1])
      for index >= len(result) {
        result = append(result, map[string]string{})
      }
      result[index][matches[2]] = values[0]
    }
  }
  return
}

Usage is simple:

storages := ParseFormCollection(req, "storage")

With 3 cloned fields blocks we’ll get following collection of maps:

[map[mount:/ profile:SSD size:32] map[mount:/var profile:SSD size:100] map[mount:/usr/lib profile:HDD size:200]]

This was dumped with fmt.Printf("%+v", storages).

That is it.

As a Bonus, here is a code snippet to parse input maps with names like host[prefix], host[domain], etc.

// ParseFormCollection is a helper method to parse Form "groupped" items with square bracketing.
// Eg, if Form field name like `inputfield[name1]`...
func ParseFormMap(r *http.Request, typeName string) (result map[string]string) {
  result = make(map[string]string)
  r.ParseForm()
  for key, values := range r.Form {
    re := regexp.MustCompile(typeName + "\\[([a-zA-Z]+)\\]")
    matches := re.FindStringSubmatch(key)
    fmt.Printf("%#v\n", matches)
    if len(matches) >= 2 {
      result[matches[1]] = values[0]
    }
  }
  return
}

Happy golanging!

Thank You For Reading