4

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>
surajs02
  • 451
  • 7
  • 18
  • Why do you use `Object.assign()` to change the background of an element? – Andreas Feb 08 '21 at 15:21
  • 1
    [`HTMLElement.style`](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style): _"While this property is considered **read-only**, it is possible to set an inline style **by assigning a string** directly to the style property. In this case the string is forwarded to `CSSStyleDeclaration.cssText`. Using `style` in this manner will **completely overwrite all inline styles** on the element."_ – Andreas Feb 08 '21 at 15:24
  • `Object.assign()` was used as it can set multiple styles but here only `background-color` was set to focus on the difference between `Object.assign` and object spread – surajs02 Feb 08 '21 at 15:25
  • @Andreas Good point regarding overwrites, it'd be better to find some alternative to set multiple styles. However, why is `Object.assign` able to set the `background-color` whilst object spread fails? – surajs02 Feb 08 '21 at 15:28
  • `Object.assign()` sets the properties directly. _"whilst object spread fails"_ -> my second comment – Andreas Feb 08 '21 at 15:29
  • 1
    @surajs02 `Object.assign` invokes the setters on the existing CSSStyleDeclaration. Using object spread creates a new object (which is not a CSSStyleDeclaration, and thus does not have the setters that are tied to the DOM). – CertainPerformance Feb 08 '21 at 15:29
  • @Andreas I was aware of using string to set `style`, however, that would result in an overwrite. In this case, I wanted to understand the difference between `Object.assign` and object spread. – surajs02 Feb 08 '21 at 15:36
  • @CertainPerformance I see, I didn't realise object spread created a new object hence lost inherited members such as setters (thought it merged similar to `Object.assign`) - that explains the issue and answers the question (best to stick to Object.assign in this case), please update your answer with the information in your comment (to help others understand the issue) and I'll accept the answer :) – surajs02 Feb 08 '21 at 15:37
  • The difference has been mentioned at least four times now... `Object.assign()` sets the properties one by one. The spread approach tries to overwrite the content of `.style` with a plain old object which doesn't work because the `.style` property is "read-only" (see my second comment) – Andreas Feb 08 '21 at 15:38
  • @Andreas That makes sense after clarifying the subtle differences between `Object.assign` and object spread :) – surajs02 Feb 08 '21 at 15:48

2 Answers2

4

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.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • Very concise answer, to clarify: `element.style` is read-only hence can't be mutated by the new object created by object spread (which as a new object is also missing the inherited members of `element.style`), whilst `Object.assign` mutates `element.style` (strange since it's read only) - is this correct? – surajs02 Feb 08 '21 at 15:47
  • Accepting this answer as it summarises points mentioned in comments that explain the issue of why `Object.assign` (mutative) is able to set `style` properties whilst object spread (new copy) cannot, thank you all for the advice :) – surajs02 Feb 08 '21 at 15:54
  • @surajs02 The `element.style` property is read-only, the properties of `element.style` are not. This means you cannot assign `element.style = newStyle` but you can do `element.style.backgroundColor = "green"` which is what `Object.assign()` is doing. – 3limin4t0r Feb 08 '21 at 16:32
  • Properties are unfrozen, that makes sense, thank you for the clear explanation :) – surajs02 Feb 08 '21 at 16:36
0

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' };
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • _"While this property is considered read-only, it is possible to set an inline style by assigning a string directly to the style property. In this case the string is forwarded to `CSSStyleDeclaration.cssText`. Using `style` in this manner will completely overwrite all inline styles on the element."_ – Andreas Feb 08 '21 at 15:27
  • Quoting: "The spread operator defines new properties in the target, Object.assign() uses a normal “set” operation to create them. That has two consequences." that agrees with your answer https://2ality.com/2016/10/rest-spread-properties.html#spread-defines-properties%2C-object.assign()-sets-them – Louay Al-osh Feb 08 '21 at 15:29