Skip to content

Polymer Data Binding — Reactive Data Binding with LitElement

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Polymer Data Binding. We cover key concepts, practical examples, and best practices to help you master this topic.

Data binding connects component properties to the DOM, automatically updating the view when data changes and optionally propagating changes back to the data source.

What You'll Learn

  • LitElement reactive properties
  • Property decorators and options
  • Bindings: text, attribute, property, event
  • One-way and two-way binding patterns
  • Computed properties

Why It Matters

Manual DOM updates are error-prone and verbose. Data binding eliminates boilerplate and ensures the view reflects application state.

Real-World Use

A form with 20+ fields, validation messages, and a preview panel — bindings keep all parts in sync without manual DOM manipulation.

Data Binding Architecture

flowchart TD
    A[Data Binding] --> B[Reactive Properties]
    A --> C[Bindings]
    B --> D[Decorators]
    B --> E[Options]
    C --> F[Text Bindings]
    C --> G[Property Bindings]
    C --> H[Event Bindings]
    C --> I[Attribute Bindings]
    A --> J[Computed Properties]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Reactive Properties

import { LitElement, html, css } from 'lit';

class UserProfile extends LitElement {
  static properties = {
    name: { type: String },
    age: { type: Number },
    isActive: { type: Boolean },
    roles: { type: Array },
    metadata: { type: Object },
    updated: { type: Date }
  };

  constructor() {
    super();
    this.name = '';
    this.age = 0;
    this.isActive = false;
    this.roles = [];
    this.metadata = {};
    this.updated = new Date();
  }

  render() {
    return html`
      <p>Name: ${this.name}</p>
      <p>Age: ${this.age}</p>
      <p>Active: ${this.isActive}</p>
      <p>Roles: ${this.roles.join(', ')}</p>
      <p>Updated: ${this.updated.toLocaleDateString()}</p>
    `;
  }
}
customElements.define('user-profile', UserProfile);
<user-profile name="Alice" age="30" isActive></user-profile>

Expected output: Properties declared in static properties are reactive. type determines attribute deserialization.

Property Options

import { LitElement, html } from 'lit';

class ConfigPanel extends LitElement {
  static properties = {
    label: { type: String },
    color: { type: String, attribute: 'data-color' },
    count: { type: Number, reflect: true },
    items: { state: true },
    _internal: { state: true }
  };

  constructor() {
    super();
    this.label = 'Default';
    this.color = '#4a90d9';
    this.count = 0;
    this.items = [];
    this._internal = 'hidden';
  }

  render() {
    return html`<p>${this.label} (count: ${this.count})</p>`;
  }
}
customElements.define('config-panel', ConfigPanel);

Expected output: reflect: true syncs property to attribute. state: true properties are reactive but not reflected to attributes.

Text and Property Bindings

import { LitElement, html } from 'lit';

class ProductCard extends LitElement {
  static properties = {
    product: { type: Object },
    discount: { type: Number },
    currency: { type: String }
  };

  constructor() {
    super();
    this.product = { name: 'Widget', price: 29.99, image: 'widget.jpg' };
    this.discount = 0.1;
    this.currency = 'USD';
  }

  get discountedPrice() {
    return this.product.price * (1 - this.discount);
  }

  get formattedPrice() {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: this.currency
    }).format(this.discountedPrice);
  }

  render() {
    return html`
      <img src="${this.product.image}" alt="${this.product.name}">
      <h3>${this.product.name}</h3>
      <p class="price">${this.formattedPrice}</p>
      ${this.discount > 0 ? html`<span class="discount">-${Math.round(this.discount * 100)}%</span>` : ''}
    `;
  }
}
customElements.define('product-card', ProductCard);

Expected output: ${expr} renders text content. Properties set via attribute-like syntax. Expressions use JS inside template literals.

Event Bindings

import { LitElement, html } from 'lit';

class CounterApp extends LitElement {
  static properties = {
    count: { type: Number },
    step: { type: Number }
  };

  constructor() {
    super();
    this.count = 0;
    this.step = 1;
  }

  _increment() { this.count += this.step; }
  _decrement() { this.count -= this.step; }
  _reset() { this.count = 0; }
  _setStep(e) { this.step = parseInt(e.target.value) || 1; }

  render() {
    return html`
      <div>
        <button @click=${this._decrement}>-</button>
        <span class="count">${this.count}</span>
        <button @click=${this._increment}>+</button>
      </div>
      <div>
        <label>Step: <input type="number" .value=${this.step} @input=${this._setStep}></label>
      </div>
      <button @click=${this._reset}>Reset</button>
    `;
  }
}
customElements.define('counter-app', CounterApp);

Expected output: @event=${handler} binds events. .property= sets DOM properties directly.

Two-Way Binding Pattern

import { LitElement, html } from 'lit';

class FormInput extends LitElement {
  static properties = {
    value: { type: String },
    label: { type: String },
    type: { type: String }
  };

  constructor() {
    super();
    this.value = '';
    this.label = '';
    this.type = 'text';
  }

  _onInput(e) {
    this.value = e.target.value;
    this.dispatchEvent(new CustomEvent('value-changed', {
      detail: { value: this.value },
      bubbles: true,
      composed: true
    }));
  }

  render() {
    return html`
      <label>${this.label}
        <input type=${this.type} .value=${this.value} @input=${this._onInput}>
      </label>
    `;
  }
}
customElements.define('form-input', FormInput);

class RegistrationForm extends LitElement {
  static properties = { email: { type: String }, password: { type: String } };

  constructor() { super(); this.email = ''; this.password = ''; }

  render() {
    return html`
      <form-input label="Email" type="email"
        .value=${this.email}
        @value-changed=${e => this.email = e.detail.value}>
      </form-input>
      <form-input label="Password" type="password"
        .value=${this.password}
        @value-changed=${e => this.password = e.detail.value}>
      </form-input>
      <p>Email: ${this.email}</p>
    `;
  }
}
customElements.define('registration-form', RegistrationForm);

Expected output: Parent binds to child's value property and listens for value-changed event to propagate changes.

Computed Properties

import { LitElement, html } from 'lit';

class InvoiceCalculator extends LitElement {
  static properties = {
    items: { type: Array },
    taxRate: { type: Number }
  };

  constructor() {
    super();
    this.items = [
      { name: 'Widget', price: 10, qty: 2 },
      { name: 'Gadget', price: 25, qty: 1 }
    ];
    this.taxRate = 0.08;
  }

  get subtotal() {
    return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
  }

  get tax() {
    return this.subtotal * this.taxRate;
  }

  get total() {
    return this.subtotal + this.tax;
  }

  render() {
    return html`
      <table>
        <tr><th>Item</th><th>Price</th><th>Qty</th><th>Total</th></tr>
        ${this.items.map(item => html`
          <tr>
            <td>${item.name}</td>
            <td>$${item.price}</td>
            <td>${item.qty}</td>
            <td>$${item.price * item.qty}</td>
          </tr>
        `)}
      </table>
      <p>Subtotal: $${this.subtotal.toFixed(2)}</p>
      <p>Tax (${(this.taxRate * 100).toFixed(1)}%): $${this.tax.toFixed(2)}</p>
      <p>Total: $${this.total.toFixed(2)}</p>
    `;
  }
}
customElements.define('invoice-calculator', InvoiceCalculator);

Expected output: Computed properties derive from reactive state. When items or taxRate change, all derived values update.

Common Mistakes

  1. Mutating arrays/objects in place - Use immutable updates (new array/object reference) for reactivity.

  2. Using @event on non-existent methods - Bind to existing methods or inline arrow functions.

  3. Forgetting type declarations - Without type, attributes are treated as strings.

  4. Misunderstanding reflect - reflect:true syncs to attribute; only use for CSS selectors.

  5. Creating circular updates - Two-way binding loops without proper guards.

Practice Questions

  1. What types of expressions can you use in LitElement template literals?
  2. How do you bind to DOM properties vs HTML attributes?
  3. How do you implement two-way data binding in LitElement?
  4. What is the difference between properties and state in LitElement?
  5. How do computed properties re-evaluate?

Challenge: Build a shopping cart with quantity input (two-way binding), computed subtotal/tax/total, currency formatting, discount codes, and summary panel.

FAQ

How does LitElement detect property changes?

LitElement's update lifecycle checks property values using shallow comparison (===).

Can I use nested property paths in bindings?

No. LitElement only observes top-level properties. Use immutable patterns for nested data.

What is the difference between @event and .property bindings?

@event binds event listeners. .property sets DOM properties. ?attribute toggles boolean attributes.

How do I prevent re-rendering on certain changes?

Override shouldUpdate(changedProperties) to control when the component re-renders.

Mini Project

Build a product configurator with: product selection (two-way binding), option toggles, quantity input, color picker, computed price summary, real-time preview panel, and order summary.

What's Next

Data binding connects templates to data. Learn how Polymer Properties and Observers handle complex reactivity.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro