bug(mock-doc): `Element.toggleAttribute()` is missing, so components that use it fail during SSR

Open Beginner friendly
#7,015 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript
Domain
web-dev

Research direction

The method belongs in MockElement in packages/mock-doc/src/node.ts, next to hasAttribute, setAttribute and removeAttribute. Read those first to match their conventions, then reproduce the missing method by calling toggleAttribute on a MockDocument element. Done means toggleAttribute covers the four cases in the issue's table (with and without force) and returns the right boolean, and the existing mock-doc tests still pass.

Written by the indexing model from the issue text.

Description

triage
Prerequisites
Stencil Version

4.45.3 and 5.0.0-rc.1

Current Behavior

mock-doc's MockElement doesn't implement Element.toggleAttribute(). Server rendering runs components on mock-doc, so a component that calls it works in the browser but throws during renderToString(). The render still completes, but the attribute is missing from the output and the error appears only in diagnostics, which makes the bug easy to miss.

toggleAttribute() is the standard way to set a boolean attribute from state, and every browser Stencil 5 supports has it, so component authors reach for it naturally.

Expected Behavior
Workaround

Authors replace it with an explicit branch:

active ? this.el.setAttribute('data-active', '') : this.el.removeAttribute('data-active');

It works, but every component that may be server-rendered has to know to avoid a standard DOM method.

Suggested fix

Add toggleAttribute() to MockElement in packages/mock-doc/src/node.ts, next to hasAttribute, setAttribute and removeAttribute, with the DOM's semantics for the optional force argument:

toggleAttribute(attrName: string, force?: boolean): boolean {
  const has = this.hasAttribute(attrName);
  if (force === undefined ? !has : force) {
    if (!has) this.setAttribute(attrName, '');
    return true;
  }
  if (has) this.removeAttribute(attrName);
  return false;
}
┌─────────────────────────────┬──────────────────┬───────────┬─────────┐
│            Call             │ Attribute before │   After   │ Returns │
├─────────────────────────────┼──────────────────┼───────────┼─────────┤
│ toggleAttribute('a')        │ absent           │ a=""      │ true    │
├─────────────────────────────┼──────────────────┼───────────┼─────────┤
│ toggleAttribute('a')        │ present          │ absent    │ false   │
├─────────────────────────────┼──────────────────┼───────────┼─────────┤
│ toggleAttribute('a', true)  │ present          │ unchanged │ true    │
├─────────────────────────────┼──────────────────┼───────────┼─────────┤
│ toggleAttribute('a', false) │ absent           │ unchanged │ false   │
└─────────────────────────────┴──────────────────┴───────────┴─────────┘

Going through setAttribute and removeAttribute keeps mock-doc's existing attame lowercasing, and the attributeChangedCallback behavior.

System Info

Steps to Reproduce

Reproduction

import { Component, Element, Prop, h } from '@stencil/core';

@Component({ tag: 'my-flag', encapsulation: { type: 'shadow' } })
export class MyFlag {
  @Element() el!: HTMLElement;
  @Prop() active = false;

  componentWillLoad() {
    this.el.toggleAttribute('data-active', this.active);
  }

  render() {
    return <slot />;
  }
}

import { renderToString } from './hydrate/index.js';

const r = await renderToString('<my-flag active>Hi</my-flag>', { fullDocument: false });
console.log(r.html);        // <my-flag active="" class="hydrated" s-id="1">…: no data-active
console.log(r.diagnostics); // TypeError: this.el.toggleAttribute is not a function

The same happens with new MockDocument() directly: typeof doc.createElement('div').toggleAttribute is 'undefined', on 4.45.3 and 5.0.0-rc.1.

### Code Reproduction URL

https://github.com

### Additional Information

_No response_
Dominant language
TypeScript
Stars
13.2k
Forks
865
Avg merge
2d 16h
Merged PRs (30d)
29

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from stenciljs/core

All issues in stenciljs/core

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.