Passing Dynamic Attributes to Components Without Database Query

This is the simplest, most efficient, and most flexible way to pass data into a web component. Unlike attributes, properties support any data type, eliminating JSON serialization and enabling type validation in the setter. Assigning data is straightforward: set the property on the element: el.data = myObj;. Using getters and setters is recommended, as they allow your component to process incoming values, enforce type safety, keep data within valid bounds, and allow updating the UI when the data changes. Follow standard HTML behavior: instead of throwing errors for invalid input, convert or sanitize values gracefully.


Passing Data In

1) Use Properties.

This is the simplest, most efficient, and most flexible way to pass data into a web component. Unlike attributes, properties support any data type, eliminating JSON serialization and enabling type validation in the setter. Assigning data is straightforward: set the property on the element: el.data = myObj;. Using getters and setters is recommended, as they allow your component to process incoming values, enforce type safety, keep data within valid bounds, and allow updating the UI when the data changes. Follow standard HTML behavior: instead of throwing errors for invalid input, convert or sanitize values gracefully.

class MyEl extends HTMLElement {
  #shadow;
  #data;

  constructor() {
    super();
    this.#shadow = this.attachShadow({ mode: 'open' });
    this.#data = [1,2,3,4];
    this.render();
  }

  get data() {
    return this.#data;
  }
  set data(value) {
    if (!Array.isArray(value)) {
      console.warn("Invalid data format. Expected an array.");
      return;
    }
    this.#data = structuredClone(value);
    this.render();
  }

  render() {
    this.#shadow.innerHTML = `Current value for data<br><pre>${JSON.stringify(this.#data)}</pre>`;
  }
}
customElements.define("my-el", MyEl);
setTimeout(() => {
  const el = document.getElementById('el');
  if (el) {
    el.data = [132, 212, 4630, 569];
  }
}, 1500);
<my-el id="el"></my-el>

2) Method Calls

Another way to pass data into a web component is by calling one of its public methods. This approach provides more control than properties or attributes, because you can:

  • Validate and transform the data before storing it.
  • Support multiple arguments (not just a single property value).
  • Perform additional logic like merging new data with old, debouncing updates, or firing custom events.

This is especially useful when the parent code already manages complex objects or workflows and simply wants to push updates into the component when needed.

You can also use a setter method, which makes the method feel like a property assignment (el.data = ...), but internally it is still a method that runs logic whenever data changes.

Example: Regular Method + Setter

class MyEl extends HTMLElement {
  #shadow;
  #data;

  constructor() {
    super();
    this.#shadow = this.attachShadow({ mode: "open" });
    this.#data = [];
    this.render();
  }

  setData(newData, append = false) {
    if (!Array.isArray(newData)) {
      console.warn("Invalid format, expected an array.");
      return;
    }

    this.#data = append ? this.#data.concat(newData) : structuredClone(newData);
    this.render();
  }

  set data(value) {
    this.setData(value, false);
  }
  get data() {
    return this.#data;
  }

  render() {
    this.#shadow.innerHTML = `
      <strong>Current Data:</strong>
      <pre>${JSON.stringify(this.#data, null, 2)}</pre>
    `;
  }
}
customElements.define("my-el", MyEl);
setTimeout(() => {
  const el = document.getElementById("el");
  el.setData([{ id: 1, name: "Alice" }]);
  setTimeout(() => {
    el.setData([{ id: 2, name: "Bob" }], true);
  }, 1500);
  setTimeout(() => {
    el.data = [{ id: 3, name: "Charlie" }];
  }, 3000);
}, 1000);
<my-el id="el"></my-el>

Calling methods is often the most flexible since it supports multiple parameters, validation, and transformation. Methods are good when updates are event-driven (e.g., from API calls or user input). Setter methods provide a natural property-like syntax.

But methods require JavaScript calls (not declarative like attributes). So they are harder to use directly in static HTML.

3) Use an attribute.

Personally, I sparingly use this way of passing data in, but some frameworks require data to be passed in through attributes. This is similar to your example in your question. <my-el data="[{a:1},{a:2}....]"></my-el>. Be careful to follow the rules related to escaping attribute values. If you use this method you will likely need to use JSON.parse on your attribute’s value and that may fail. It can also make the HTML very ugly and large to have the massive amount of data showing in a attribute.

Potential Issues with JSON in Attributes:

  • Attributes are always strings only, so data must be stringified and then parsed inside the component.
  • Special characters must be escaped (“, >, etc.).
  • Large JSON objects can bloat the DOM and become unreadable.

Example: Passing JSON via an Attribute

class MyEl extends HTMLElement {
  #shadow;
  #data;

  constructor() {
    super();
    this.#shadow = this.attachShadow({ mode: "open" });
    this.#data = [];
  }

  static get observedAttributes() {
    return ["data"];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      try {
        this.#data = JSON.parse(newValue);
      }
      catch (err) {
        console.error("Invalid JSON in data attribute:", err);
        this.#data = [];
      }
      this.render();
    }
  }

  render() {
    this.#shadow.innerHTML = `
      <strong>Data from attribute:</strong>
      <pre>${JSON.stringify(this.#data, null, 2)}</pre>
    `;
  }
}
customElements.define("my-el", MyEl);
setTimeout(() => {
  const el = document.getElementById("el");
  el.setAttribute("data", JSON.stringify([{ id: 3, name: "Charlie" }]));
}, 2000);
<!-- Passing JSON directly in the attribute (escaped quotes required) -->
<my-el id="el" data="[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]"></my-el>

4) Pass it in through child elements.

This approach is useful when your data naturally maps to repeated child elements, similar to how the <select> tag uses <option> tags. Each child element can store its own data in attributes or text content, and your web component can process them inside connectedCallback.

A key point: the child elements don’t need to be real HTML elements with semantic meaning. They can be custom elements, generic <div>s, or even made-up tags. If your only goal is to pass data, the choice of tag name is irrelevant — the parent component will treat them purely as data carriers. Using descriptive tag names (like <data-row> or <item>) just makes the markup easier for humans to understand.

That said, this works best when your component uses a Shadow DOM, because the “data-only” child elements are hidden from the page’s rendered output. If you skip Shadow DOM and render directly into the light DOM, these children will still be visible in the document, which can lead to unwanted extra markup in your layout. If your component can not use Shadow DOM then you’ll want to either:

  • Use semantically neutral tags (like <script type="application/json"> or <template>) that don’t display visually, or
  • Remove the child nodes after parsing their data.
<my-el>
  <data-row p1="v1" p2="v2"></data-row>
  <data-row p1="v3" p2="v4"></data-row>
</my-el>

<!-- Or just as valid: -->
<my-el>
  <item a="1" b="2"></item>
  <item a="3" b="4"></item>
</my-el>

Warning: if you expect the children may change at runtime, you will need to include a MutationObserver to watch for updates.

MutationObserver example (supports any child tag)

class MyEl extends HTMLElement {
  #shadow;
  #data;
  #observer;

  constructor() {
    super();
    this.#shadow = this.attachShadow({ mode: "open" });
    this.#data = [];
    this.#observer = new MutationObserver(() => this.updateData());
  }

  connectedCallback() {
    this.#observer.observe(this, { childList: true });
    this.updateData();
  }

  disconnectedCallback() {
    this.#observer.disconnect();
  }

  updateData() {
    this.#data = Array.from(this.children).map((child) => {
      const obj = { tag: child.tagName.toLowerCase() };

      for (const attr of child.attributes) {
        obj[attr.name] = attr.value;
      }

      if (child.textContent.trim()) {
        obj.text = child.textContent.trim();
      }

      return obj;
    });

    this.render();
  }

  render() {
    this.#shadow.innerHTML = `
      <div>Current Data:</div>
      <pre>${JSON.stringify(this.#data, null, 2)}</pre>
    `;
  }
}
customElements.define("my-el", MyEl);
setTimeout(() => {
  const el = document.getElementById("el");
  let newChild = document.createElement("foo-bar");
  newChild.setAttribute("x", "123");
  newChild.setAttribute("y", "456");
  newChild.textContent = "Hello from foo-bar!";
  el.appendChild(newChild);
  newChild = document.createElement("item");
  newChild.setAttribute("x", "999");
  newChild.setAttribute("y", "0");
  newChild.textContent = "Hello from item!";
  el.appendChild(newChild);
}, 3000);
<my-el id="el">
  <data-row p1="hello" p2="world"></data-row>
  <item a="99">Some text</item>
</my-el>

This way, any child element, regardless of tag name, can be treated as structured input. The component just reads attributes and text, leaving you free to pick tags that best express meaning for your project.

Leave a Reply

Your email address will not be published. Required fields are marked *