Data binding in Angular templates connects component state to the DOM. Interpolation ({{ }}) renders text; property binding ([property]) sets DOM properties; attribute, class, and style bindings handle the full range of DOM manipulation needs. Understanding which binding type to use for each scenario — and why property binding is different from attribute binding — prevents a common category of Angular bugs where the right DOM property is not being set.
All Binding Forms
@Component({
selector: 'app-post-card',
standalone: true,
imports: [NgClass, NgStyle], // for [ngClass] and [ngStyle]
template: `
<!-- ── Interpolation — inserts text ────────────────────────────────── -->
<h2>{{ post.title }}</h2>
<p>By {{ post.authorName }} · {{ post.viewCount }} views</p>
<!-- ── Property binding — sets DOM properties ──────────────────────── -->
<img [src]="post.coverImageUrl ?? '/assets/default.jpg'"
[alt]="post.title">
<button [disabled]="!canEdit">Edit</button>
<!-- ── Attribute binding — for HTML attrs without DOM property ──────── -->
<td [attr.colspan]="colSpan">...</td>
<button [attr.aria-label]="'Delete post: ' + post.title">🗑</button>
<div [attr.data-post-id]="post.id"></div>
<!-- ── Class binding — toggle a single CSS class ───────────────────── -->
<article
[class.featured]="post.isFeatured"
[class.published]="post.isPublished"
[class.draft]="!post.isPublished"
>
<!-- ── [ngClass] — multiple classes at once ────────────────────────── -->
<div [ngClass]="{
'card': true,
'card-large': post.viewCount > 1000,
'card-new': isNewPost(post),
'card-mine': post.authorId === currentUserId
}"></div>
<!-- ── Style binding — inline style from component value ───────────── -->
<div [style.backgroundColor]="post.isPublished ? '#e8f5e9' : '#fff3e0'">
<div [style.font-size.px]="fontSize"></div> <!-- px unit shorthand ──── -->
<!-- ── [ngStyle] — multiple styles at once ──────────────────────────── -->
<div [ngStyle]="{
'opacity': post.isPublished ? 1 : 0.6,
'border-left': '4px solid ' + categoryColor
}"></div>
<!-- ── Safe navigation operator — prevents null errors ─────────────── -->
<p>{{ post.author?.displayName }}</p>
<img [src]="post.author?.avatarUrl">
`,
})
export class PostCardComponent {
@Input({ required: true }) post!: PostSummaryDto;
@Input() canEdit = false;
@Input() currentUserId = '';
colSpan = 3;
fontSize = 16;
categoryColor = '#2196f3';
isNewPost(post: PostSummaryDto): boolean {
const daysSincePublished = (Date.now() - new Date(post.publishedAt).getTime())
/ (1000 * 60 * 60 * 24);
return daysSincePublished < 7;
}
}
colspan is a HTML attribute with no matching property called “colspan” — you must use [attr.colspan]. Similarly, aria-* and data-* attributes have no direct DOM property equivalents. When in doubt: if you can set it with element.propertyName = value in JavaScript, use [propertyName]; otherwise use [attr.attributeName].?.) in template expressions to prevent “Cannot read properties of null” errors when displaying nested optional values. {{ post.author?.displayName }} renders nothing (blank) if post.author is null or undefined, rather than throwing an error. Combine with the nullish coalescing operator for a fallback value: {{ post.author?.displayName ?? 'Unknown Author' }}. This is especially useful when API responses have nullable nested objects.[innerHTML]="'<b>' + title + '</b>'" risks XSS if title contains user-provided HTML. Angular sanitises bound HTML values but the safest approach is to never bind user content as HTML. Use interpolation ({{ title }}) for text — Angular escapes HTML entities automatically. If you genuinely need to render HTML from a trusted source (server-generated HTML), use Angular’s DomSanitizer.bypassSecurityTrustHtml() explicitly, acknowledging the security responsibility.Common Mistakes
Mistake 1 — Using property binding for attributes without DOM properties
❌ Wrong — [colspan]="3"; no DOM property named “colspan”; binding has no effect.
✅ Correct — [attr.colspan]="3"; sets the HTML attribute correctly.
Mistake 2 — Forgetting safe navigation on nullable nested properties
❌ Wrong — {{ post.author.displayName }}; throws if author is null before data loads.
✅ Correct — {{ post.author?.displayName }}; renders blank while data is loading.