javascriptoopevent-handlingcomponentsevent-delegation

How to update a form-related (data-)model with every form-data change?


I have the following function, in order to update values from four different input fields:

UPDATED

My Package Class is defined as followed:

class Package {
  constructor(packageID, type, weight, height, description) {
    this.packageID = packageID;
    this.type = type;
    this.weight = weight;
    this.height = height;
    this.description = description;
  }
  getPackageId() {
    return this.packageId;
  }
  getType() {
    return this.type;
  }
  getWeight() {
    return this.weight;
  }
  getHeight() {
    return this.height;
  }
  getDescription() {
    return this.description;
  }/*
  setPackageId(packageId){
    this.packageId= packageId
    return packageId;
  }*/
  setType(type) {
    this.type = type;
    return type;
  }
  setWeight(weight) {
    this.weight = weight;
    return this.weight;
  }
  setHeight(height) {
    this.height = height;
    return this.height;
  }
  setDescription(description) {
    this.description = description;
    return this.description;
  }
}
let inputfieldtype = document.getElementById('selectPackageType');
let inputfieldweight = document.getElementById('floatWeight');
let inputfieldheight = document.getElementById('floatHeight');
let inputfielddescription = document.getElementById('description');

function updateValues() {
  var value = this.value;

  if (value == null) {

    console.log('value of packege is null');

  } else if (value != "" && (inputfieldtype == 'selectPackageType')) {
    type = value;
  } else if (value != "" && (inputfieldweight == 'floatWeight')) {
    weight = value;
  } else if (value != "" && (inputfieldheight == 'floatHeight')) {
    height = value;
  } else if (value != "" && (inputfielddescription == 'description')) {
    description = value;
  }
}
inputfieldweight.addEventListener('change', updateValues);
inputfieldheight.addEventListener('change', updateValues);
inputfielddescription.addEventListener('change', updateValues);
inputfieldtype.addEventListener('change', updateValues);

What I've learned so far is, that the condition of my if statement is not useful, because in all four cases, the string I want to compare it with, is true. My goal is the following: I want to check, which input field has been clicked and I want to save the value of this field into a variable.

Then I want to create a new instance of my Class "Package" and fill their attributes with this values.

//Create instance of package
const package = new Package();

//shoot all the values into one package
function submit() {
  if (typeof package != "undefined") {
    package.packageID = randomID;
    package.type = type;
    package.weight = weight;
    package.height = height;
    package.description = description;
    console.log(package);
  } else {
    console.log(package);
  }
}

Here is the HTML Code

<form autocomplete="off">
  <div class="custom-select">
    <label for="selectPackageType">Type</label>
    <select id="selectPackageType" name="type">
      <option value="1">letter</option>
      <option value="2">package</option>
    </select>
  </div>
</form>
<div class="fancy-input">
  <label for="floatWeight">Weight</label>
  <input id="floatWeight" name="floatWeight" maxlength="8">
</div>
<div class="fancy-input">
  <label for="floatHeight">Height</label>
  <input id="floatHeight" name="floatHeight" maxlength="8">
</div>
<div class="fancy-input">
  <label for="description">Description</label>
  <input id="description" name="description">
</div>
<button onclick="submit()">Submit</button>

Solution

  • For the sake of easy maintenance/refactoring a solution should be implemented both generic and with higher abstraction.

    For the OP's use case one firstly has to slightly refactor the form-markup including the unification of form-control names and Package class property names.

    Speaking of the latter, the OP's Package class implementation introduces getter and setter methods which do not serve any purpose since a) all properties are public anyhow, and b) no protection has been introduced with the setter implementation. Get/set only makes sense for protected data, hence private data where one is in control of how one allows access to or even the alteration of such data. The OP is better off with the most simple Package abstraction that features just public data.

    The creation of a package instance should always happen in relation to its form-node. In order to establish such a relationship, one would make use of a WeakMap instance, where the package-configuration form-node serves as key for its package instance, and the latter has to be updated according to any form-control's value-changes of its related form-node.

    One could introduce exactly two functions, putPackageData which creates and registers the form-related package-instance and patchPackageData which creates a data-patch from the form's current element-values and does assign it to the form-related package-instance.

    One has to make use of e.g. ...

    The next provided example code shows a possible implementation of the just described approach which of cause, due to being generic, patches a package with all current available form-data every time either a single form-control value changes or the form(-node) has to handle a submit event.

    function patchPackageData(evt) {
      // - prevent the form from being submitted ...
      // - but only in case a real event with its own `preventDefault` method was provided.
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault]
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining]
      evt.preventDefault?.();
    
      const { currentTarget: formNode } = evt;
    
      // - create a form-data instance.
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/API/FormData]
      const formData = new FormData(formNode);
    
      // - create an object from all form-data entries.
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries]
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries]
      const dataPatch = Object.fromEntries([...formData.entries()]);
    
      // - get the form-node related package instance.
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/get]
      const package = packageRegistry.get(formNode);
    
      // - assign the data-patch to the package instance.
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign]
      Object.assign(package, dataPatch);
    
      console.log('PATCH package data ...', package);
    }
    function putPackageData(formNode) {
      // - create and register the form-node related package instance.
      // - see ... [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/set]
      packageRegistry.set(
        formNode, new Package({ id: formNode.dataset.packageId }),
      );
      console.log('PUT package data ...', packageRegistry.get(formNode));
    
      // - assign more form-node related data to the newly created package instance.
      patchPackageData({ currentTarget: formNode });
    }
    const packageRegistry = new WeakMap;
    
    document
      // - query every available package configuration related form-node ...
      .querySelectorAll('form[data-name="package-config"]')
    
      // - ... where one for each form-control ...
      .forEach(elmNode => {
    
        // ... does trigger the creation and registration of the form-node related package instance.
        putPackageData(elmNode);
    
        // ... does register the correct handlers with the relevant event-types.
        elmNode.addEventListener('change', patchPackageData);
        elmNode.addEventListener('submit', patchPackageData);
      });
    body { margin: 0; }
    form {
    
      float: left;
      width: 25%;
      padding: 8px;
      font-size: .8em;
    
      label,
      label > span {
    
        display: block;
        width: 100%;
      }
      label > span {
        margin: 2px 0;
      }
      label, fieldset, [type="submit"] {
        margin: 4px 0;
      }
      select, input {
        width: 90%;
      }
    }
    .as-console-wrapper {
      left: auto!important;
      bottom: 0;
      width: 44%;
      min-height: 100%;
    }
    <form data-name="package-config" data-package-id="6dfc38b8-41f0-4b4f-86d8-4aea355aef79" autocomplete="off">
    
      <label>
        <span>Type</span>
    
        <select name ="type">
          <option value="1">letter</option>
          <option value="2">package</option> 
        </select>
      </label>
    
      <fieldset>
        <legend>Measures</legend>
    
        <label>
          <span>Weight</span>
    
          <input name="weight" maxlength="8" />
        </label>
        <label>
          <span>Height</span>
    
          <input name="height" maxlength ="8" />
        </label>
      </fieldset>
    
      <label>
        <span>Description</span>
    
        <input name="description" />
      </label>
    
      <button type="submit">Submit</button>
    </form>
    
    
    <form data-name="package-config" autocomplete="off">
    
      <label>
        <span>Type</span>
    
        <select name ="type">
          <option value="1">letter</option>
          <option value="2" selected>package</option> 
        </select>
      </label>
    
      <fieldset>
        <legend>Measures</legend>
    
        <label>
          <span>Weight</span>
    
          <input name="weight" maxlength="8" />
        </label>
        <label>
          <span>Height</span>
    
          <input name="height" maxlength ="8" />
        </label>
      </fieldset>
    
      <label>
        <span>Description</span>
    
        <input name="description" />
      </label>
    
      <button type="submit">Submit</button>
    </form>
    
    
    <script>
    const isStringValue = value => (typeof value === 'string');
    const isNonEmptyStringValue = value => (isStringValue(value) && !!value.trim());
    
    class Package {
      // #data = {};
    
      constructor(options) {
        const { id, type, weight, height, description } = options;
    
        // Object.assign(this.#data, {
        Object.assign(this, {
    
          id: isNonEmptyStringValue(id) && id || crypto.randomUUID(),
          type: isNonEmptyStringValue(type) && type || 'not configured',
          weight: isNonEmptyStringValue(weight) && weight || 'not provided',
          height: isNonEmptyStringValue(height) && height || 'not provided',
          description: isNonEmptyStringValue(description) && description || 'not provided',
        });
      }/*
    
      get id() {
        return this.#data.id;
      }
    
      get type() {
        return this.#data.type;
      }
      get weight() {
        return this.#data.weight;
      }
      get height() {
        return this.#data.height;
      }
      get description() {
        return this.#data.description;
      }
    
      set type(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.type = value;
        }
        return this.#data.type;
      }
      set weight(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.weight = value;
        }
        return this.#data.weight;
      }
      set height(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.height = value;
        }
        return this.#data.height;
      }
      set description(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.description = value;
        }
        return this.#data.description;
      }
    
      valueOf() {
        return this.#data;
      }
      toString() {
        return JSON.stringify(this.valueOf());
      }
      [Symbol.toPrimitive](hint) {
        return (hint === 'string') && this.toString() || this.valueOf();
      }*/
    }
    </script>

    Edit/Update

    The above solution could be changed to one that creates the package-instance at initialization time (as it already does), but updates the package data selectively according to the most recent form-control value-change.

    For this scenario a Package class implementation with protected data through real getter/setter functionality does make sense.

    function patchPackageData(evt) {
    
      const { target: formControl, currentTarget: formNode } = evt;
      let { name: key, value } = formControl;
    
      const package = packageRegistry.get(formNode);
    
      // update (protected by a package's setter logic).
      package[key] = value;
    
      // bidirectional update (the current package state is the single source of truth).
      formControl.value = package[key];
    
      console.log('PATCH package data ...', { id: package.id, key, value: package[key] });
    }
    
    function postPackageData(formNode) {
    
      const formData = new FormData(formNode);
      const dataPatch = Object.fromEntries([...formData.entries()]);
    
      const package = packageRegistry.get(formNode);
    
      // post/update.
      Object.assign(package, dataPatch);
    
      Object
        .keys(dataPatch)
    
        // bidirectional update.
        .forEach(key => formNode.elements[key].value = package[key]);
    
      console.log('POST package data ...', package.valueOf());
    }
    function putPackageData(formNode) {
      packageRegistry.set(
        formNode, new Package({ id: formNode.dataset.packageId }),
      );
      console.log('PUT package data ...', packageRegistry.get(formNode).valueOf());
    
      postPackageData(formNode);
    }
    const packageRegistry = new WeakMap;
    
    document
      .querySelectorAll('form[data-name="package-config"]')
      .forEach(elmNode => {
    
        putPackageData(elmNode);
    
        elmNode.addEventListener('change', patchPackageData);
        elmNode.addEventListener('submit', evt => evt.preventDefault());
      });
    body { margin: 0; }
    form {
    
      float: left;
      width: 25%;
      padding: 8px;
      font-size: .8em;
    
      label,
      label > span {
    
        display: block;
        width: 100%;
      }
      label > span {
        margin: 2px 0;
      }
      label, fieldset, [type="submit"] {
        margin: 4px 0;
      }
      select, input {
        width: 90%;
      }
    }
    .as-console-wrapper {
      left: auto!important;
      bottom: 0;
      width: 44%;
      min-height: 100%;
    }
    <form data-name="package-config" data-package-id="6dfc38b8-41f0-4b4f-86d8-4aea355aef79" autocomplete="off">
    
      <label>
        <span>Type</span>
    
        <select name ="type">
          <option value="1">letter</option>
          <option value="2">package</option> 
        </select>
      </label>
    
      <fieldset>
        <legend>Measures</legend>
    
        <label>
          <span>Weight</span>
    
          <input name="weight" maxlength="8" value="20" />
        </label>
        <label>
          <span>Height</span>
    
          <input name="height" maxlength ="8" />
        </label>
      </fieldset>
    
      <label>
        <span>Description</span>
    
        <input name="description" value="letter 20/3" />
      </label>
    
      <button type="submit">Submit</button>
    </form>
    
    
    <form data-name="package-config" autocomplete="off">
    
      <label>
        <span>Type</span>
    
        <select name ="type">
          <option value="1">letter</option>
          <option value="2" selected>package</option> 
        </select>
      </label>
    
      <fieldset>
        <legend>Measures</legend>
    
        <label>
          <span>Weight</span>
    
          <input name="weight" maxlength="8" />
        </label>
        <label>
          <span>Height</span>
    
          <input name="height" maxlength ="8" value="5" />
        </label>
      </fieldset>
    
      <label>
        <span>Description</span>
    
        <input name="description" />
      </label>
    
      <button type="submit">Submit</button>
    </form>
    
    
    <script>
    const isStringValue = value => (typeof value === 'string');
    const isNonEmptyStringValue = value => (isStringValue(value) && !!value.trim());
    
    class Package {
      #data = {};
    
      constructor(options) {
        const { id, type, weight, height, description } = options;
    
        Object.assign(this.#data, {
    
          id: isNonEmptyStringValue(id) && id || crypto.randomUUID(),
          type: isNonEmptyStringValue(type) && type || 'not configured',
          weight: isNonEmptyStringValue(weight) && weight || 'not provided',
          height: isNonEmptyStringValue(height) && height || 'not provided',
          description: isNonEmptyStringValue(description) && description || 'not provided',
        });
      }
    
      get id() {
        return this.#data.id;
      }
    
      get type() {
        return this.#data.type;
      }
      get weight() {
        return this.#data.weight;
      }
      get height() {
        return this.#data.height;
      }
      get description() {
        return this.#data.description;
      }
    
      set type(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.type = value;
        }
        return this.#data.type;
      }
      set weight(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.weight = value;
        }
        return this.#data.weight;
      }
      set height(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.height = value;
        }
        return this.#data.height;
      }
      set description(value) {
        if (isNonEmptyStringValue(value)) {
    
          this.#data.description = value;
        }
        return this.#data.description;
      }
    
      valueOf() {
        return this.#data;
      }
      toString() {
        return JSON.stringify(this.valueOf());
      }
      [Symbol.toPrimitive](hint) {
        return (hint === 'string') && this.toString() || this.valueOf();
      }
    }
    </script>