LWC Zero to Hero Module 3 — Templates & Directives (lwc:if, for:each, Slots)
🔀 LWC Zero to Hero — Module 3 of 15
Templates & Directives
Conditional rendering, loops, and slots are how your template decides WHAT to show and HOW MANY times. Master these and you control everything the user sees.
Module 3 of 15 (counting Module 0 as a bonus prerequisite) · Phase 2: LWC Fundamentals
🎯 What You'll Master in This Module
Module 2 taught you that templates use
{expressions} to display values. Now you'll learn directives — special template attributes that control whether something renders at all, how many times it repeats, and where child content gets inserted. These three skills (conditionals, loops, slots) are used in almost every component you'll ever build.✓ Modern conditional rendering with lwc:if / lwc:elseif / lwc:else
✓ The legacy if:true / if:else syntax and why you should avoid it now
✓ Looping over lists with for:each and the required key attribute
✓ The iterator directive for first/last/even/odd row styling
✓ Combining conditionals and loops together correctly
✓ Default slots — letting parent components inject content
✓ Named slots — multiple content insertion points in one component
📋 In This Module
Concept 1 of 7
lwc:if / lwc:elseif / lwc:else
These directives control whether a block of markup renders AT ALL, based on a true/false condition.
lwc:if is the modern, recommended syntax (introduced to replace the older if:true/if:else you may see in older code or Trailhead content).⚡ Why This Matters
Almost every component has SOME content that should only appear under certain conditions — an error message, a loading spinner, an admin-only button.
lwc:if is how you express "only show this when X is true" directly in the template.BASIC lwc:if / lwc:else
<template>
<template lwc:if={isLoading}>
<p>Loading orders...</p>
</template>
<template lwc:else>
<p>{orderCount} orders found</p>
</template>
</template>
THREE-WAY BRANCHING WITH lwc:elseif
<template lwc:if={isLoading}>
<p>Loading...</p>
</template>
<template lwc:elseif={hasError}>
<p class="error">Something went wrong: {errorMessage}</p>
</template>
<template lwc:else>
<p>{orderCount} orders found</p>
</template>
// In JS — these are usually getters:
get isLoading() { return this.status === 'loading'; }
get hasError() { return this.status === 'error'; }
🛠️ Practical Mini-Implementation
The classic "three states" pattern (loading / error / success) shown above is something you'll build in almost every data-driven component. Notice each condition is a GETTER in the JS class, never inline logic in the template (recall Module 2's rule: only simple property/getter names inside
{ }).⚠️ Common Gotcha
lwc:if, lwc:elseif, and lwc:else must be placed directly on a <template> tag, not on a regular HTML element like <div>, when you need to wrap MULTIPLE elements without an extra wrapper div in the DOM. You CAN put lwc:if directly on a <div> if you only need to conditionally show that one element — but wrapping in <template> is required for multiple sibling elements.Concept 2 of 7
The Legacy if:true / if:else Syntax
Before
lwc:if existed, LWC used if:true and if:false directives. You'll still see this syntax in older codebases, Trailhead modules, and some Stack Exchange answers — it still WORKS, but Salesforce now recommends lwc:if for new code.⚡ Why This Matters
Interviewers specifically test whether you know BOTH syntaxes and can explain why the newer one is preferred — this is a very common "what's new in LWC" interview question, and it signals whether you're current with the platform or only know outdated Trailhead content.
LEGACY SYNTAX (still works, but not recommended for new code)
<template if:true={isLoading}>
<p>Loading...</p>
</template>
<template if:false={isLoading}>
<p>{orderCount} orders found</p>
</template>
// Notice: if:false repeats the SAME condition (isLoading) — no true "else" linkage
// And there is NO if:elseif equivalent at all — this is the biggest limitation
WHY lwc:if IS BETTER — side by side
// Legacy: no elseif means nested templates for 3+ branches (messy!)
<template if:true={isLoading}>...</template>
<template if:false={isLoading}>
<template if:true={hasError}>...</template>
<template if:false={hasError}>...</template>
</template> // nested — harder to read!
// Modern: lwc:elseif flattens this into clean sequential branches
<template lwc:if={isLoading}>...</template>
<template lwc:elseif={hasError}>...</template>
<template lwc:else>...</template>
🛠️ Practical Mini-Implementation
If you're maintaining an OLDER component that uses
if:true/if:false, you don't need to rush and rewrite it — both syntaxes coexist fine in the same org. But for any NEW component you write today, default to lwc:if/lwc:elseif/lwc:else.⚠️ Common Gotcha
You cannot mix
if:true with lwc:else, or lwc:if with if:false — they are two separate, non-interoperable directive families. Pick ONE family per conditional block and stay consistent within it.Concept 3 of 7
for:each & the Required key Attribute
for:each repeats a block of markup once for every item in an array — exactly like the loops you learned conceptually in Module 0, but expressed declaratively in the template instead of with a JavaScript for loop. Every repeated element MUST have a unique key attribute.⚡ Why This Matters
Any time you display a list — accounts, orders, search results — you're using
for:each. It's one of the most-used directives in all of LWC, and the key requirement is one of the most commonly asked "why do I need this" interview questions.BASIC for:each — looping over an array
<template for:each={orders} for:item="order">
<div key={order.Id} class="order-row">
<p>{order.Name} — ${order.Amount}</p>
</div>
</template>
// orders = [{Id:'1', Name:'Order A', Amount:500}, {Id:'2', Name:'Order B', Amount:750}]
// for:item="order" names the loop variable — use whatever name makes sense
WHY THE key ATTRIBUTE IS REQUIRED
// ❌ Missing key — LWC throws a console warning/error
<template for:each={orders} for:item="order">
<div>{order.Name}</div> <!-- no key! -->
</template>
// ✅ Correct — key helps LWC track which DOM element maps to which data item
<template for:each={orders} for:item="order">
<div key={order.Id}>{order.Name}</div>
</template>
// When orders changes (item added/removed/reordered), the key lets LWC
// efficiently update only what changed instead of re-rendering everything
🛠️ Practical Mini-Implementation
Always use a genuinely UNIQUE, STABLE identifier as the key — a Salesforce record's
Id field is perfect. Never use the array index (key={index}) if the list can be reordered, filtered, or have items added/removed — index-based keys cause LWC to misattribute DOM elements to the wrong data after such changes, leading to visual bugs (wrong row highlighted, input values appearing on the wrong row, etc.).⚠️ Common Gotcha
The
key attribute goes on the element INSIDE the for:each template (the repeated element), never on the <template> tag itself. Also, if your repeated block has multiple top-level elements, only ONE of them needs the key — but it's cleanest to always wrap repeated content in a single element with the key.Concept 4 of 7
The Iterator Directive
iterator:iteratorName is an alternative to for:each that gives you extra metadata about each item's position in the list — whether it's the first item, the last item, its index, and odd/even status. Use it when your styling or logic depends on POSITION, not just the data itself.⚡ Why This Matters
Striped table rows (alternating colors), "Load More" buttons that should only appear after the last item, or removing a border from the first/last row — all of these need position awareness that plain
for:each doesn't give you directly.ITERATOR SYNTAX — accessing first, last, index, even, odd
<template iterator:it={orders}>
<div key={it.value.Id}
class={it.value.rowClass}>
<template lwc:if={it.first}>
<span class="badge">Newest</span>
</template>
{it.value.Name} (row {it.index})
<template lwc:if={it.last}>
<button onclick={handleLoadMore}>Load More</button>
</template>
</div>
</template>
// it.value = the current item (same as for:item)
// it.index = numeric position (0-based)
// it.first / it.last = boolean — true only for first/last item
// it.odd / it.even = boolean — useful for striped rows
🛠️ Practical Mini-Implementation
Striped table rows using
This is the cleanest way to alternate row background colors without writing any extra JavaScript logic — it's all available directly from the iterator.
it.even in a getter back in the JS class:// In a getter pattern combined with the data itself, or computed inline via the iterator's odd/even directly in a class binding:<div key={it.value.Id} class={it.even ? 'row-even' : 'row-odd'}>This is the cleanest way to alternate row background colors without writing any extra JavaScript logic — it's all available directly from the iterator.
⚠️ Common Gotcha
Many developers reach for
iterator by default even when they don't need position metadata — but for:each is simpler and should be your DEFAULT choice. Only switch to iterator when you specifically need first, last, index, odd, or even. Using iterator unnecessarily adds complexity without benefit.Concept 5 of 7
Combining Conditionals & Loops
Real components almost always need BOTH a loop AND a conditional — show a loading state, OR an empty state if the list has zero items, OR the actual list if it has items. Knowing how to nest these directives correctly (and where NOT to nest them) is essential.
⚡ Why This Matters
This "loading / empty / populated" three-state pattern is the single most common template structure across all LWC list-displaying components — mastering it once means you'll reuse this exact pattern in dozens of components.
THE COMPLETE "LOADING / EMPTY / POPULATED" PATTERN
<template>
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading"></lightning-spinner>
</template>
<template lwc:elseif={isEmpty}>
<p class="empty-state">No orders found.</p>
</template>
<template lwc:else>
<template for:each={orders} for:item="order">
<div key={order.Id}>{order.Name}</div>
</template>
</template>
</template>
// JS getters powering this:
get isEmpty() { return !this.isLoading && this.orders.length === 0; }
🛠️ Practical Mini-Implementation
Notice the
for:each lives INSIDE the lwc:else block — the conditional directive wraps the loop directive, not the other way around. This nesting order (condition decides WHICH state to show, loop only runs once you're already in the "populated" branch) is the correct mental model for combining them.⚠️ Common Gotcha
You CANNOT put both
lwc:if and for:each on the SAME <template> tag — each <template> element can only carry one directive type. If you need both a condition AND a loop on the same content, you need two nested <template> tags, one for each directive.Concept 6 of 7
Default Slots — Template Composition
A slot is a placeholder in a CHILD component's template that lets the PARENT component inject its own markup into a specific spot. This is fundamentally different from
@api properties (Module 5) — instead of passing data, you're passing entire chunks of HTML structure.⚡ Why This Matters
Building reusable "container" or "wrapper" components — a custom card, a modal, a panel with a consistent border/shadow but flexible content — requires slots. Without them, every variation of content would need its own separate component.
CHILD COMPONENT — defines a slot
<!-- card.html (the reusable wrapper component) -->
<template>
<div class="card-wrapper">
<slot></slot> <!-- placeholder for whatever the parent puts here -->
</div>
</template>
PARENT COMPONENT — fills the slot
<!-- parentComponent.html -->
<template>
<c-card>
<h2>Order Summary</h2> <!-- this content gets
<p>Total: $1,500</p> "slotted" into <slot> -->
</c-card>
</template>
// Result: card-wrapper div now contains the h2 and p from the parent,
// while still keeping card's own border/shadow/padding styling intact
🛠️ Practical Mini-Implementation
A common real use case: a reusable "section" wrapper component with consistent styling (border, padding, shadow) used across dozens of pages, where every page needs DIFFERENT content inside it. Instead of building 20 different components, you build ONE wrapper component with a slot, and every page supplies its own content through that slot.
⚠️ Common Gotcha
If the parent doesn't put anything between the child component's tags, the slot can show FALLBACK content — anything you put inside the
<slot></slot> tags in the child acts as a default. <slot>No content provided</slot> shows "No content provided" only when the parent supplies nothing.Concept 7 of 7
Named Slots — Multiple Insertion Points
A component can have only ONE default (unnamed) slot, but it can have MULTIPLE named slots — each with a unique name, letting the parent target specific sections (a header, a body, a footer) independently within the same child component.
⚡ Why This Matters
A reusable modal or card component almost always needs distinct header/body/footer regions that the parent fills differently each time — named slots are the only way to achieve this level of structured composition.
CHILD COMPONENT — multiple named slots
<!-- panel.html -->
<template>
<div class="panel">
<header>
<slot name="header"></slot>
</header>
<div class="panel-body">
<slot></slot> <!-- unnamed/default slot for main content -->
</div>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
PARENT COMPONENT — targeting named slots with slot attribute
<c-panel>
<h2 slot="header">Order Details</h2>
<!-- no slot attribute = goes into the default/unnamed slot -->
<p>Order #1001 — Status: Shipped</p>
<button slot="footer">Close</button>
</c-panel>
// h2 → renders inside <slot name="header">
// p → renders inside the unnamed <slot>
// button → renders inside <slot name="footer">
🛠️ Practical Mini-Implementation
Build ONE generic
c-panel (or modal, card, accordion-item) component with header/body/footer named slots, then reuse it everywhere across your "XYZ Company" app — order details, contact info, settings panels — each one supplying completely different header/body/footer content while sharing identical visual structure and styling.⚠️ Common Gotcha
The
slot="header" attribute goes on the ELEMENT IN THE PARENT being slotted in — NOT inside the child's <slot name="header"> definition. Mixing these up (putting name where slot should be, or vice versa) is the most common slots bug. Remember: child defines slots with name, parent targets them with slot.💬 Module 3 Interview Questions (6)
Q1What is the difference between lwc:if and the legacy if:true, and why does Salesforce recommend the newer syntax?
if:true and if:false were the original LWC conditional directives, but they have a major limitation: there is no if:elseif equivalent, forcing nested templates for any conditional with more than two branches, which becomes hard to read. lwc:if, lwc:elseif, and lwc:else were introduced as the modern replacement, allowing clean sequential branching without nesting. Both syntaxes still function in current LWC, but Salesforce recommends lwc:if for all new development going forward, and the two families (if:true/if:false versus lwc:if/lwc:elseif/lwc:else) cannot be mixed within the same conditional block.
"lwc:if/lwc:elseif/lwc:else is the modern syntax that supports clean multi-branch conditionals without nesting, unlike the legacy if:true/if:false which has no elseif equivalent — both still work, but lwc:if is recommended for new code."
Q2Why is the key attribute required in for:each, and what bug can occur if you use the array index as the key?
The key attribute helps LWC's rendering engine track which DOM element corresponds to which specific data item, so that when the underlying array changes (items added, removed, or reordered), LWC can efficiently update only what actually changed rather than re-rendering the entire list. Using the array index as the key is problematic because the index is tied to POSITION, not to the actual item's identity — if an item is removed from the middle of the list, every subsequent item's index shifts, causing LWC to misattribute DOM state (like input field values, focus state, or applied CSS classes) to the wrong items after the change. A stable, unique identifier like a record's Id should always be used instead.
"The key attribute lets LWC track item identity across re-renders for efficient DOM updates; using array index as the key breaks this when the list reorders or items are removed, since the index represents position rather than the item's actual identity, causing UI state to attach to the wrong rows."
Q3When would you use the iterator directive instead of for:each?
for:each should be the default choice for simply rendering a list. The iterator directive (iterator:iteratorName) is used specifically when you need metadata about an item's POSITION within the list — properties like first, last, index, odd, and even. Common use cases include applying a special style or badge only to the first or last item in a list, alternating row background colors (striped tables) using the odd/even properties, or showing a "Load More" button only after the final rendered item. If you don't need any of this positional information, for:each is simpler and should be preferred.
"for:each is the default for rendering lists; the iterator directive is used only when you need positional metadata like first, last, index, or odd/even — for example, styling alternating rows or showing special UI only on the first or last item."
Q4You need a template with both a loading state and a list of items once loaded. Can you put lwc:if and for:each on the same template tag? If not, how do you structure this?
No, you cannot put lwc:if and for:each on the same template tag — each template element can only carry one directive type at a time. The correct structure is to nest them: an outer conditional directive (lwc:if/lwc:elseif/lwc:else) decides which overall state to display (loading, empty, or populated), and within the branch where data should be shown, a separate nested template tag with for:each handles the actual looping. This is the standard "loading / empty / populated" pattern used across most LWC components that display lists.
"lwc:if and for:each cannot coexist on one template tag; instead, nest a for:each template inside the appropriate branch of an outer lwc:if/lwc:elseif/lwc:else structure, with the conditional determining which state to show and the loop only running within the populated branch."
Q5What is the difference between a default slot and a named slot, and how does a parent component target each one?
A component can have only one default (unnamed) slot, written simply as <slot></slot> in the child's template — any content the parent places inside the child's tags without a slot attribute goes here automatically. Named slots, by contrast, allow multiple distinct insertion points within the same child component, each defined with a unique name attribute like <slot name="header"></slot>. The parent targets a named slot by adding a matching slot="header" attribute to the element it wants placed there; any parent content without a slot attribute falls into the default slot instead. This allows one reusable child component to have several independently customizable regions, such as a header, body, and footer.
"A default slot is unnamed and receives any parent content without a slot attribute; named slots use a name attribute in the child and are targeted by matching slot attributes in the parent, allowing multiple independent content regions in one reusable component."
Q6What happens if a parent component using a child with a named slot doesn't provide any content for that slot?
If the child component's slot definition includes fallback content between its opening and closing slot tags — for example, <slot name="footer">Default footer text</slot> — that fallback content will render automatically whenever the parent doesn't supply anything for that specific named slot. This applies to both default and named slots: any markup placed directly inside the <slot></slot> tags in the child acts as a sensible default, ensuring the component doesn't render an empty or broken-looking region when a parent simply omits that slot's content.
"Markup placed inside a child component's slot tags serves as fallback content, automatically rendering whenever the parent component does not provide its own content for that particular default or named slot."
📝 Module 3 Recap — Templates & Directives Mastered
✅ lwc:if / lwc:elseif / lwc:else is the modern conditional rendering syntax — prefer it over legacy if:true/if:false for new code
✅ for:each loops over arrays and REQUIRES a unique, stable key (use record Id, never array index)
✅ The iterator directive adds first/last/index/odd/even metadata — use only when position matters
✅ Conditionals and loops nest (conditional wraps loop), but cannot share the same template tag
✅ Default slots (<slot></slot>) let parents inject content into one unnamed placeholder
✅ Named slots (<slot name="x">) allow multiple independent content regions, targeted via slot="x" in the parent
✅ Fallback content inside <slot> tags renders automatically when the parent provides nothing
🎯 Before Moving to Module 4...
Try this: build a simple component that fetches a hardcoded array of 5 "products" (name, price) in connectedCallback(), shows a loading spinner for 1 second using setTimeout, then displays the list using for:each with proper key attributes, alternating row colors using the iterator directive. This single exercise touches every directive from this module. Module 4 goes deeper into HOW that reactivity actually works under the hood — @track, getters, and what LWC considers a "change" worth re-rendering for.
☕
☕ Enjoyed this article?
SF Interview Pro is 100% free and maintained by a Salesforce professional. No ads, no paywalls, and no signup required. If this guide helped you prepare for an interview, earn a certification, or grow your Salesforce career, consider buying me a coffee! ☕💜
rajnishrk264@ybl
Scan with GPay, PhonePe, Paytm, or BHIM
📚 Keep Preparing
New interview questions every week 🚀
Follow for fresh Salesforce Q&A, free courses, and real interview experiences — straight from the trenches.
👥 Follow on LinkedIn