Easy way to conditionally create fields an object?

Viewed 71

I am working on an app that relies heavily on data input and so I have to construct different kinds of objects with required and non-required data, and the non-required data piece is my issue here, as this leads to a lot of additional code and I am wondering if there is a way to avoid this.

Here I have a form that has 6 fields (its just one example, but I have the same case in many other places in the app), 2 of them are required, the rest are not, and I wrote this function to handle the object creation:

  const handleAddSupplier = (e) => {
    e.preventDefault();
    const formData = e.target.elements;

    const supplierData = {
      name: formData.name.value,
      companyId: formData.companyId.value,
      ...((formData.country.value || formData.city.value || formData.postalCode.value || formData.street.value) && {
        address: {
          ...(formData.country.value && { country: formData.country.value }),
          ...(formData.city.value && { city: formData.city.value }),
          ...(formData.postalCode.value && { postalCode: formData.postalCode.value }),
          ...(formData.street.value && { street: formData.street.value }),
        },
      }),
    };

    console.log(supplierData);
  };

And it works, but its too much code in my opinion, I suspect that there is an easier way to do this.

The "signature" of the object that must be created is:

{
  name: "",
  companyId: "",
  address: {
    country: "",
    city: "",
    postalCode: "",
    street: ""
  }
}

The address object and its fields are entirely optional, so if none of the fields of address is available (i.e. the user inputs only the required fields and nothing else), the address object should not be created at all, not even as an empty object.

My code works, I am just interested to see if there are other simpler ways.

Edit: I destructured the object a bit and saved myself some writing, but still, what if I have 10 non-required fields, do I conditionally "OR" them all 10?

const { name, companyId, country, city, postalCode, street } = e.target.elements;

const supplierData = {
  name: name.value,
  companyId: companyId.value,
  ...((country.value || city.value || postalCode.value || street.value) && {
    address: {
      ...(country.value && { country: country.value }),
      ...(city.value && { city: city.value }),
      ...(postalCode.value && { postalCode: postalCode.value }),
      ...(street.value && { street: street.value }),
    },
  }),
};

Or another way of writing the same thing:

const {
  name: { value: name },
  companyId: { value: companyId },
  country: { value: country },
  city: { value: city },
  postalCode: { value: postalCode },
  street: { value: street },
} = e.target.elements;

const supplierData = {
  name: name,
  companyId: companyId,
  ...((country || city || postalCode || street) && {
    address: {
      ...(country && { country }),
      ...(city && { city }),
      ...(postalCode && { postalCode }),
      ...(street && { street }),
    },
  }),
};

So this begs the question, is this just a "pick your poison" scenario? Last scenario for example, you write the code to destructure and name the properties prior to creating the object, and then within the object, the code is simpler and easier to read?

And this leads me to another question, the optimization that I am looking for, is it something worth investigating at all, or sticking to either of these scenarios is fine?

3 Answers

you can check them in loop

function process(e) {
  let obj = {}
  for(let item of e.target.elements){
    if(item.value) obj[item.name]=item.value //should do proper check if it's can have valid falsy value, I leave it here for simplicity
  }
  console.log(obj)
}
<form onsubmit="process(event);return false;">
  <div>
    <label for=a>A: </label>
    <input type=text name=a>
  </div>
  <div>
    <label for=b>B: </label>
    <input type=text name=b>
  </div>
  <div>
    <button type="submit">OK</button>
  </div>
</form>


I think you have optimized the length of your code as far as possible with destructuring. Another way to save code is using utility functions (which I would recommend as such destructuring wizardry is prone to typos if you're doing them a lot).

The following utility functions should allow you to pick your values in a more declarative way, ensuring that no extra values are present and empty groups are omitted. Only a variant of the upper function (which is rather short and self explanatory) is necessary for every form you create.

const handleAddSupplier = (e) => {
  e.preventDefault();
  const values = formToValues(e.target);
  const supplierData = {
    ...pickFrom(values, ["name", "companyId"]),
    ...optionalGroupFrom(values, "address", [
      "country",
      "city",
      "postalCode",
      "street",
    ]),
  };
  console.log(supplierData);
};


// Utility functions (you can import them from another file)

function formToValues(form) {
  const elements = form.elements;
  const values = {};
  for (const name in elements)
    if (elements[name].value) values[name] = elements[name].value;
  return values;
}

const optionalGroupFrom = (values, name, keys) => {
  const group = pickFrom(values, keys);
  const hasKeys = Object.keys(group).length !== 0;
  if (hasKeys) return {
    [name]: group
  };
  else return null;
};

const pickFrom = (values, keys) =>
  keys
  .map((key) => ({
    key,
    value: values[key]
  }))
  .filter((pair) => pair.value !== undefined)
  .reduce((map, pair) => {
  console.log( map );
    map[pair.key] = pair.value;
    return map;
  }, {});
<form onsubmit="handleAddSupplier(event); return false;">
  <input type="text" name="name" /> name<br />
  <input type="text" name="companyId" /> companyId<br />
  <input type="text" name="country" /> country<br />
  <input type="text" name="city" /> city<br />
  <input type="text" name="postalCode" /> postalCode<br />
  <input type="text" name="street" /> street<br />
  <input type="submit" value="Submit" />
</form>

Here's a solution that's pretty similar in length but I think this is:

  1. more readable
  2. scales painlessly with new optional fields
const handleAddSupplier = (e) => {
    e.preventDefault();
    const { name, companyId, country, city, postalCode, street } = e.target.elements;

    const supplierData = {
      name: formData.name.value,
      companyId: formData.companyId.value
    };

    const address = {
        country: (country || {}).value,
        city: (city || {}).value,
        postalCode: (postalCode || {}).value,
        street: (street || {}).value
    };
    Object.keys(address).forEach(key => {
        if (!address[key]) {  // Will catch null or undefined
            delete address[key];
        }
    });

    if (Object.keys(address).length) {
        supplierData.address = address;
    }
    console.log(supplierData);
  };
Related