Polymer Data Binding — Reactive Data Binding with LitElement
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
Mutating arrays/objects in place - Use immutable updates (new array/object reference) for reactivity.
Using @event on non-existent methods - Bind to existing methods or inline arrow functions.
Forgetting type declarations - Without type, attributes are treated as strings.
Misunderstanding reflect - reflect:true syncs to attribute; only use for CSS selectors.
Creating circular updates - Two-way binding loops without proper guards.
Practice Questions
- What types of expressions can you use in LitElement template literals?
- How do you bind to DOM properties vs HTML attributes?
- How do you implement two-way data binding in LitElement?
- What is the difference between properties and state in LitElement?
- 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
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