Mastering Vue's Template Refs for Dynamic DOM Manipulation
In Vue.js, efficiently accessing and manipulating DOM elements directly from your component logic is a common requirement. Template refs provide a powerful and declarative way to establish a connection between your template and your script. This challenge will test your understanding of how to create and utilize template refs to dynamically interact with DOM elements in a Vue component.
Problem Description
You are tasked with building a Vue component that displays a list of items. For each item, you need to display a "highlight" button. When this button is clicked, the corresponding list item should have a specific CSS class applied to it to visually highlight it. You must achieve this using Vue's template refs mechanism.
Key Requirements:
- Dynamic Item Rendering: The component should accept an array of strings as a prop, where each string represents the content of a list item.
- Highlight Button per Item: For each item in the list, render a button with the text "Highlight".
- Template Refs for Each Item: Each list item (
<li>) should have a template ref assigned to it. This ref should be dynamically generated to refer to the specific<li>element. - Click to Highlight: When a "Highlight" button is clicked, the corresponding
<li>element (accessed via its template ref) should have the CSS classhighlightedadded to it. - Prevent Redundant Class Application: If an item is already highlighted, clicking its "Highlight" button should not cause any errors or unintended behavior.
Expected Behavior:
When the component mounts, it displays a list of items. Each item has a "Highlight" button. Clicking a "Highlight" button for an item will apply the highlighted class to that specific list item. Subsequent clicks on the same button will have no visual effect if the item is already highlighted.
Examples
Example 1:
<template>
<div>
<ul>
<li v-for="(item, index) in items" :key="index" :ref="el => itemRefs[index] = el">
{{ item }}
<button @click="highlightItem(index)">Highlight</button>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
props: {
items: {
type: Array as () => string[],
required: true,
},
},
setup(props) {
const itemRefs = ref<HTMLElement[]>([]); // Array to store refs
const highlightItem = (index: number) => {
const element = itemRefs.value[index];
if (element) {
element.classList.add('highlighted');
}
};
return {
itemRefs,
highlightItem,
};
},
});
</script>
<style>
.highlighted {
background-color: yellow;
font-weight: bold;
}
</style>
Input Component Usage:
<MyComponent :items="['Apple', 'Banana', 'Cherry']" />
Expected DOM State after clicking "Highlight" on "Banana":
<div>
<ul>
<li>
Apple
<button>Highlight</button>
</li>
<li class="highlighted">
Banana
<button>Highlight</button>
</li>
<li>
Cherry
<button>Highlight</button>
</li>
</ul>
</div>
Explanation: The v-for directive iterates through the items prop. A dynamic template ref el => itemRefs[index] = el is used to assign each <li> element to an index in the itemRefs array. When the "Highlight" button for "Banana" is clicked, highlightItem(1) is called. This accesses itemRefs.value[1] (which is the <li> for "Banana") and adds the highlighted class.
Example 2: Edge Case - Already Highlighted Item
If the highlighted class is already present on an <li> and its "Highlight" button is clicked again:
Input Component Usage:
<MyComponent :items="['Dog', 'Cat']" />
Initial State (after clicking "Highlight" on "Dog"):
<div>
<ul>
<li class="highlighted">
Dog
<button>Highlight</button>
</li>
<li>
Cat
<button>Highlight</button>
</li>
</ul>
</div>
Action: Click the "Highlight" button for "Dog" again.
Expected DOM State (no change):
<div>
<ul>
<li class="highlighted">
Dog
<button>Highlight</button>
</li>
<li>
Cat
<button>Highlight</button>
</li>
</ul>
</div>
Explanation: The element.classList.add('highlighted') method is idempotent. If the class already exists, calling add again has no effect, thus fulfilling the requirement of not causing unintended behavior.
Constraints
- The
itemsprop will always be an array of strings. - The number of items in the
itemsarray will not exceed 100. - The solution should be implemented using Vue 3 Composition API with TypeScript.
- Avoid direct DOM manipulation outside of using template refs.
Notes
- Consider how to properly initialize and manage the array that will hold your template refs.
- The
reffunction in Vue's Composition API is key here. You'll need to assign a callback function to the template ref attribute (ref="...") within thev-forloop. - The callback function will receive the DOM element as an argument, allowing you to store it.
- Remember that
refs assigned dynamically withinv-forloops will be an array. - You'll need to add a CSS rule for the
.highlightedclass to see the visual effect.