Why does object spread fail but Object.assign succeeds when updating a DOM element's style attribute?

Viewed 372

Context

When using vanilla js to update a DOM element's style attribute, why does object spread fail to update whilst Object.assign succeeds?

E.g., in the included code snippet, objectAssignDirect and objectAssignIndirect correctly set background-color whilst objectSpread incorrectly resets the result div's background-color.

Questions

  1. Why does this happen? (is this due to cloning issues or properties such as inherited properties not being copied?)
  2. Is there a way to replicate Object.assign's desired behaviour with object spread?

References

There are several discussions comparing Object.assign and object spread but none seem to address this strange behaviour:

// Using `Object.assign` directly.
const objectAssignDirect = () => { 
  Object.assign(document.querySelector('.myClass').style, { backgroundColor: 'red' }); // Works.
  console.log('Result should be red');
}

// Updating using `Object.assign` with variable.
const objectAssignIndirect = () => {
  const myElement = document.querySelector('.myClass')
  Object.assign(myElement.style, { backgroundColor: 'blue' }); // Works.
  console.log('Result should be blue');
}

// Using object spread with variable.
const objectSpread = () => {
  const myElement = document.querySelector('.myClass')
  myElement.style = { ...myElement.style, backgroundColor: 'green' }; // Fails.
  console.log('Result should be green');
}
body {
  font-size: 2em;
}

.myClass {
  width: 100%;
  height: 50px;
  background-color: black;
  color: white;
  border: 4px solid black;
  text-align: center;
  padding: 10px;
}

button {
  padding: 15px;
  margin: 30px;
  color: white;
  border-radius: 20px;
}

.red {
  background-color: red;
}
.blue {
  background-color: blue;
}
.green {
  background-color: green;
}
<div style="display:flex;justify-content:center;">
  <button class="red" onclick="objectAssignDirect();">Use <code>Object.assign</code> directly</button>
  <button class="blue" onclick="objectAssignIndirect();">Use <code>Object.assign</code> indirectly</button>
  <button class="green" onclick="objectSpread();">Use object spread</button>
</div>

<div class="myClass">Result</div>

2 Answers

Why does this happen?

style is a read-only property, and cannot be assigned to a new value.

element.style = { ...element.style, backgroundColor: 'green' };

Creates a shallow copy of element.style, adds/updates the property backgroundColor and assigns the copy back to element.style. For this reason it fails, because you cannot assign element.style a new value.

Object.assign(element.style, { backgroundColor: 'green' });

Assigns each property/value pair in the second argument to element.style. It does not create a copy of element.style, but mutates the element.style object.

Is there a way to replicate Object.assign's desired behaviour with object spread?

No, object spread is used only in object literals, which will always result in a new object being created. You cannot update an existing object using object spread.

Assiging directly to the .style tag of an element will not result in the style changing. Instead, it'll fail silently:

foo.style = { backgroundColor: 'green' };
console.log(foo.style.backgroundColor);
<div id="foo">foo</div>

You have to assign to a property of the existing .style (the existing CSSStyleDeclaration) in order to invoke the setter that results in the DOM changing. You can't try to reassign the style attribute entirely with something else, because then you won't be invoking the special setter on the CSSStyleDeclaration. Doing { ...myElement.style } creates a new plain object, not a CSSStyleDeclaration. Object.assign works in both of your original snippets because it'll invoke setters.

For another example of this sort of behavior:

const obj = Object.create({ style: {
  set someProp(arg) {
    console.log('Setter invoked; changing DOM');
  }
}});

console.log('Invokes setter:');
obj.style.someProp = 'newVal';
console.log("Doesn't invoke setter:");
obj.style = { someProp: 'newVal' };

Related