Как обновлять модель (данных), связанную с формой, при каждом изменении данных формы?

У меня есть следующая функция для обновления значений из четырех разных полей ввода:

ОБНОВЛЕНО

Мой класс пакета определяется следующим образом:

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.info('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);

На данный момент я узнал, что условие моего оператора if бесполезно, потому что во всех четырех случаях строка, с которой я хочу его сравнить, истинна. Моя цель следующая: я хочу проверить, какое поле ввода было нажато, и я хочу сохранить значение этого поля в переменную.

Затем я хочу создать новый экземпляр моего класса «Пакет» и заполнить его атрибуты этими значениями.

//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.info(package);
  } else {
    console.info(package);
  }
}

Вот HTML-код

<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>

Где вы определили свой класс Package?

Hossein Abedi 07.07.2024 18:13

я не думаю, что вам нужна функция updatevalue(), поскольку пользователь отправляет форму, а вы не проверяете изменения

Mehdi 07.07.2024 18:53

Пара вещей... Как настроен ваш класс Package? Почему бы вам не разместить все поля ввода в форме и, когда пользователь отправит запрос, не запустить один eventListener на кнопке отправки, а затем собрать введенные поля и передать их в конструктор класса в качестве параметров?

dale landry 07.07.2024 19:40

@dalelandry Я последовал вашим предложениям, и это кажется хорошим решением.

Melle 08.07.2024 01:40

Рад, что мои рекомендации помогли!

dale landry 08.07.2024 04:48
inputfieldtype == 'selectPackageType' никогда не может быть правдой. inputfieldtype — это элемент DOM, а не строка. Должно быть inputfieldtype.id == 'selectPackageType'
Barmar 08.07.2024 18:42

@Melle ... относительно моего позднего ответа, остались ли еще вопросы?

Peter Seliger 09.07.2024 19:04

@Питер, спасибо, что спросил. Конечно, их будет несколько, но сначала мне нужно с этим справиться. К сожалению, это займет много времени, так как я могу работать над этим только один или два часа в день. Надеюсь, StackOwerflow простит мне этот комментарий, не имеющий отношения к теме.

Melle 10.07.2024 14:59

@Мелле ... конечно, не торопись.

Peter Seliger 10.07.2024 15:16
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
9
124
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я изменил свой код на следующее:

/**
 * this class describes the package that should be send or collect
 */

 
 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;
     }
     
    
}

//Generate PackageId 
const generateID = () => Math.random().toString(36).slice(2);
//Global Variables
const randomID = generateID();

const formgetInputfields = document.getElementById("getInputfields");
const inputfieldtype = document.getElementById('selectPackageType');
const inputfieldweight = document.getElementById('floatWeight');
const inputfieldheight = document.getElementById('floatHeight');
const inputfielddescription = document.getElementById('description');

//Create instance of package
const package = new Package();
//shoot all the values into one package
function submit(){  
const type = inputfieldtype.value;
const weight = inputfieldweight.value;
const height = inputfieldheight.value;
const description = inputfielddescription.value;
    console.info(formgetInputfields);
    
  if (typeof package != "undefined") {
      package.packageID = randomID;
      package.type = type;
      package.weight = weight; 
      package.height = height;
      package.description = description;   
      console.info(package);
  }else {
        console.info(package);
  }
}
formgetInputfields.addEventListener("change", submit);

Кажется, это работает.

Изучив хорошо документированные предложения @Peter Selinger, я пришел к выводу, что эта версия работает нормально, но не является лучшим решением. Я хотел использовать классы, потому что я привык их использовать, я не знаком с модулями и это показалось хорошим решением для структурирования моего проекта. Встроенные функции, которые может предложить <form>, кажутся лучшим способом реализации моего проекта.

Melle 12.07.2024 18:33

Я считаю, что первое предложение Питера Селигера, в котором реализованы две функции «putPackageData» и «PatchPacketData», очень хорошее.

Melle 12.07.2024 18:54
Ответ принят как подходящий

Для простоты обслуживания/рефакторинга решение должно быть реализовано как в общем виде, так и с более высокой абстракцией.

Для варианта использования OP сначала необходимо немного реорганизовать разметку формы, включая унификацию имен элементов управления формой и имен свойств класса.

Говоря о последнем, реализация класса Package OP вводит методы получения и установки, которые не служат никакой цели, поскольку а) все свойства в любом случае являются общедоступными и б) с реализацией установки не было введено никакой защиты. Получить/установить имеет смысл только для защищенных данных, то есть для частных данных, где можно контролировать доступ к таким данным или даже их изменение. ОП лучше использовать самую простую Package абстракцию, включающую только общедоступные данные.

Создание экземпляра пакета всегда должно происходить относительно его узла формы. Чтобы установить такую ​​связь, можно использовать экземпляр WeakMap, где узел формы конфигурации пакета служит ключом для его экземпляра пакета, и последний должен быть обновлен в соответствии со значением любого элемента управления формой. -изменения связанного с ним узла формы.

Можно было бы ввести ровно две функции: Package которая создает и регистрирует экземпляр пакета, связанный с формой, и putPackageData которая создает патч данных из текущих значений элементов формы и присваивает его экземпляру пакета, связанному с формой.

Нужно использовать, например. ...

Следующий приведенный пример кода показывает возможную реализацию только что описанного подхода, который из-за своей универсальности исправляет пакет со всеми текущими доступными данными формы каждый раз, когда изменяется одно значение элемента управления формой или форма (-node). должен обрабатывать событие отправки.

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.info('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.info('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>

Редактировать/Обновить

Приведенное выше решение можно изменить на решение, которое создает экземпляр пакета во время инициализации (как это уже происходит), но выборочно обновляет данные пакета в соответствии с самым последним изменением значения элемента управления формой.

Для этого сценария имеет смысл реализовать класс patchPackageData с защищенными данными с помощью реальных функций получения/установки.

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.info('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.info('POST package data ...', package.valueOf());
}
function putPackageData(formNode) {
  packageRegistry.set(
    formNode, new Package({ id: formNode.dataset.packageId }),
  );
  console.info('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>

ок, babysteps: Мне не совсем понятно, откуда берется «formNode» в объекте «const { currentTarget: formNode }». Какую ценность это имеет? Я думаю, он представляет собой саму форму со своими значениями? Но где это определено?

Melle 10.07.2024 20:29

@Melle ... 1/2 ... Поскольку функция-обработчик зарегистрирована в элементе формы, свойство currentTarget переданного события содержит ссылку на сам узел формы, тогда как его target указывает на элемент, вызвавший событие первый (здесь, скорее всего, любой из элементов управления формы). В строке const { currentTarget: formNode } = evt; используется синтаксис деструктуризации присваивания для деструктуризации объекта вместе с псевдонимом (присвоением нового имени переменной).

Peter Seliger 10.07.2024 22:52

@Melle ... 2/2 ... Эта линия равна const formNode = evt.currentTarget.

Peter Seliger 10.07.2024 22:53

class инкапсулированы, а это означает, что переменные и методы не могут использоваться другими классами. Глобальных переменных и функций вне class должно быть мало, если их вообще не должно быть. <form> имеет множество встроенных функций, которые избавляют нас от необходимости писать много кода на JavaScript, а class IMO является излишним. <form> полагается на взаимодействие с пользователем, поэтому необходимы обработчики событий для событий «изменение», «ввод», «отправка», «клик» и т. д. Способ включения обработчиков событий в class сложен, вот пример для всех <button>:

class Example {
  constructor(selector) {
    this.nodeList = document.querySelectorAll(selector);
    for (const node of this.nodeList) {
      node.addEventListener("click", this.onClick.bind(this));
    }
  }
  onClick(event) {
    event.target.textContent = "You clicked me";
  }
}

const x = new Example("button");
    

Подробности прокомментированы в примере.

<!DOCTYPE html>
<html lang = "en">

<head>
  <meta charset = "UTF-8">
  <meta name = "viewport" content = "width=device-width, initial-scale=1">
  <title>Package Shipments</title>
  <style>
    :root {
      font: 5vmin/1 "Segoe UI";
    }
    
    form {
      display: flex;
      flex-flow: row nowrap;
    }
    
    fieldset {
      display: inline-block;
      max-width: fit-content;
      border-radius: 4px;
    }
    
    legend {
      font-size: 1.2rem;
    }
    
    label {
      display: inline-block;
      width: 6ch;
      margin-bottom: 0.5rem;
    }
    
    select,
    input,
    textarea,
    button {
      margin-bottom: 0.5rem;
      font: inherit
    }
    
    select {
      margin-top: 0.5rem;
      padding: 0.15rem 0 0.15rem 0;
    }
    
    .buttons {
      display: flex;
      justify-content: flex-end;
      width: 100%;
    }
    
    button {
      padding: 0.2rem 0.5rem;
      cursor: pointer;
    }
    
    [type = "number"] {
      width: 5ch;
      font-weight: 300;
      font-family: Consolas;
      text-align: right;
    }
    
    [for = "type"] {
      margin-top: 0.5rem;
    }
    
    [for = "notes"] {
      vertical-align: top;
    }
    
    textarea {
      box-sizing: border-box;
      width: 100%;
    }
    
    #views {
      display: inline-flex;
      border: 0;
    }
    
    output {
      display: block;
      max-width: 36ch;
      margin: 1rem 0 0 2ch;
      font-size: 0.6rem;
      font-family: Consolas;
      white-space: pre-wrap;
      word-break: break-word;
      line-height: 1.2;
      color: blue;
    }
    
    #viewResponse {
      max-width: 60ch;
      color: #930;
    }
  </style>
</head>

<body>
  <!-- #1
    A HTMLFormControlsCollection on this <form> includes all form controls:
      <fieldset>
      <select>
      <input>
      <textarea>
      <button>
      <output>
  -->
  <form id = "shipments">
    <fieldset>
      <legend>Packages</legend>
      <label for = "type">Type</label>
      <!-- #2
        Any form control with [required] attribute will interrupt
        a "submit" event when it has no value or if the value does not
        meet criteria.
      -->
      <select id = "type" name = "type" required>
        <option value = "">Choose Type</option>
        <option value = "package">Package</option>
        <option value = "letter">Letter</option>
      </select>
      <br>
      <label for = "weight">Weight</label>
      <!-- #2 -->
      <input id = "weight" name = "weight" type = "number" min = "0" max = "50" required>
      <label for = "weight"> lbs.</label>
      <br>
      <label>Dimensions</label>
      <br>
      <label for = "width">Width</label>
      <!-- #2 -->
      <!-- #3
        Belongs to the "dimensions" HTMLCollection.
      -->
      <input id = "width" name = "dimensions" type = "number" min = "4" max = "36" required>
      <label for = "width"> in.</label>
      <br>
      <label for = "length">Length</label>
      <!-- #2 -->
      <!-- #3 -->
      <input id = "length" name = "dimensions" type = "number" min = "6" max = "36" required>
      <label for = "length"> in.</label>
      <br>
      <label for = "height">Height</label>
      <!-- #2 -->
      <!-- #3 -->
      <input id = "height" name = "dimensions" type = "number" min = "0" max = "36" required>
      <label for = "height"> in.</label>
      <br>
      <label for = "notes">Notes</label>
      <br>
      <textarea id = "notes" name = "notes" cols = "20" rows = "3"></textarea>
      <br>
      <label class = "buttons">
        <!-- #4
          Belongs to the "buttons" HTMLCollection.
        -->
        <button id = "view" name = "buttons" type = "button">View</button>
        <button type = "reset">Clear</button>
        <!-- #4 -->
        <!-- #5
          A <button> with [type = "submit"] or without any type that's
          within a <form> will trigger a "submit" event when clicked.
        -->
        <button name = "buttons">Done</button>
      </label>
    </fieldset>
    <fieldset id = "views">
      <output id = "viewData"></output>
      <output id = "viewResponse"></output>
    </fieldset>
  </form>
  <script>
    /**
     * This gathers data from a <form> that has form controls
     * with [name = "..."]: "type", "weight", "dimensions",
     * "notes", and "buttons". The data will be formatted and it
     * will be assigned a timestamp id when it is sent to a
     * server.
     * @param formID - #id or [name] of the <form>
     */
    class Package {
      constructor(formID) {
        // #6 Reference the <form>
        this.form = document.forms[formID];
        // #7 References to all form controls (see #1)
        this.io = this.form.elements;
        // #8 An array of all form controls with [name = "dimensions"] (see #3)
        this.dim = Array.from(this.io.dimensions);
        // #9 An array of all form controls with [name = "buttons"] (see #4)
        this.btn = Array.from(this.io.buttons);
        // #10 This will store formatted data 
        this.data = {};
        /* #11 
        Register the <select>, <textarea>, and all <input>s
        (see #7) to the "change" event
        */
        for (const input of this.io) {
          input.addEventListener("change", this.onChange.bind(this));
        }
        // #12 Register all [name = "buttons"] (see #9) to the "click" event
        for (const button of this.btn) {
          button.addEventListener("click", this.onClick.bind(this));
        }
        // #13 Register <form> (see #6) to the "reset" event
        this.form.addEventListener("reset", this.onReset.bind(this));
        // #14 Register <form> (see #6) to the "submit" event
        this.form.addEventListener("submit", this.onSubmit.bind(this));
      }

      /**
       * This method is an event handler bound to some form
       * controls (see #11) that will be invoked when the
       * "change" event occurs (user leaves the form control).
       * Each value entered will be assigned to a key name
       * identical to the form control's [name] and formatted
       * (except "type") to this.data object (see #10).
       * @param event - Event object type "change"
       */
      onChange(event) {
        const name = event.target.name;
        switch (name) {
          case "type":
            this.data.type = this.io.type.value;
            break;
          case "weight":
            this.data.weight = `${this.io.weight.value} lbs.`;
            break;
          case "dimensions":
            const wlh = this.dim
              .map(d => `${d.value} in.`)
              .join(` x `);
            this.data.dimensions = wlh;
            break;
          case "notes":
            this.data.notes = `\n${this.io.notes.value}`;
            break;
          default:
            break;
        }
      }

      /**
       * This method is an event handler bound to all form
       * controls with [name = "buttons"] (see #12) that will be
       * invoked when the "click" event occurs (user clicks a
       * <button>). Each key name and any values will be
       * displayed. If the submit <button> (see #5) was
       * clicked, a timestamp id will be assigned as well.
       * This feature can be used to check this.data at any 
       * time.
       * @param event - Event object type "click"
       */
      onClick(event) {
        this.io.viewData.textContent = `\nData\n====\n`;
        if (event.target.id != "view") {
          this.data.pkgid = this.pkgID();
        }
        ["pkgid", "type", "weight", "dimensions", "notes"]
        .forEach(n => {
          if (!this.data[n]) {
            this.data[n] = " ";
          }
          this.io.viewData.textContent += `${n}: ${this.data[n]}\n`;
        });
      }

      /**
       * This method is an event handler bound to the <form> 
       * (see #13) that will be invoked when the "reset" event 
       * is triggered (user clicks the <button type = "reset">).
       * <form> is reset, this.data, and the <output>s are
       * cleared.
       * This feature can be used at any time.
       * @param event - Event object type "reset"
       */
      onReset(event) {
        ["pkgid", "type", "weight", "dimensions", "notes"]
        .forEach(n => {
          this.data[n] = " ";
        });
        this.io.views.textContent = "";
      }

      /**
       * This method is an event handler bound to the <form> 
       * (see #14) that will be invoked when the "submit" event 
       * is triggered (user clicks the submit <button> (see #5)
       * or clicks the enter/return key). A FormData object 
       * will be instantiated and all of this.data (see #10)
       * key names/values will be assigned to it. 
       * this.sendPkg() method will be invoked. Normal <form> 
       * behavior will be prevented. 
       * This feature can only be used when the user has
       * entered correct data in the <select> and all of the
       * <input>s (see #2).
       * @param event - Event object type "submit"
       */
      onSubmit(event) {
        const fData = new FormData();
        ["pkgid", "type", "weight", "dimensions", "notes"]
        .forEach(n => {
          fData.set(n, this.data[n]);
        });
        this.sendPkg(fData);
        event.preventDefault();
      }

      /**
       * This method will pass a FormData object and send it to
       * a live test server. It will display the response if it
       * was successful.
       * @param data - FormData object
       */
      async sendPkg(data) {
        const response = await fetch(
          "https://httpbin.org/post", {
            method: "POST",
            body: data
          });
        const json = await response.text();
        this.io.viewResponse.textContent = `\nResponse\n========\n`;
        this.io.viewResponse.textContent += json;
      }

      // This method generates a timestamp
      pkgID() {
        return Date.now();
      }
    }
    // Instantiate 
    const pkgData = new Package("shipments");
  </script>
</body>

</html>

Другие вопросы по теме