-
Notifications
You must be signed in to change notification settings - Fork 1k
feat(components): add Repeater component #5797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Conversation
Unifies emit overloads per @typescript-eslint/unified-signatures. Removes unused toRef and stop; useSortable cleanup is already handled by vueuse via scope disposal.
|
Hey man, |
fix(ui): unify Repeater emits signatures and clean up unused vars - also fixed other CI errors
Fixes accessibility violations ("button-name") by adding aria-labels
to icon-only buttons. Labels are localized using Nuxt UI locale
infrastructure.
This resolves failing a11y snapshot tests in both nuxt and vue projects.
No visual changes.
|
Hey @salehattari, |
fix(ui): add accessible labels to Repeater action buttons
|
Hey @salehattari, |
commit: |
|
Hey, @atinux @TheAlexLichter - I'm not sure if I'm tagging the right people to address it but -
Any guidance? 🙏 |
|
Thanks @salehattari for your work on this PR! A Repeater is really needed, especially for forms. Small naming thought: if the main goal is form usage (state + validation), maybe a name like A few points to consider:
For UI reference, I really like Filament’s Repeater: https://filamentphp.com/docs/3.x/forms/fields/repeater It keeps the header/actions compact and works great in real forms. With ButtonsWith CollapsingWith 'One Line' Mode |
|
nice, I like it |
📝 WalkthroughWalkthroughThis pull request introduces a new Repeater component for dynamic list management. The implementation includes a Vue 3 component with drag-and-drop reordering powered by useSortable, item management controls (add/remove/move), configurable min/max item limits, and comprehensive slot-based customization. The component integrates with the existing theme system via variants and sizes, exports TypeScript type definitions for props/emits/slots, and includes localization support across 70+ locales. Supporting files include theme configuration, documentation, a playground demo page, and a test suite with snapshot and accessibility checks. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes The change spans 80+ files with heterogeneous purposes (component logic, theming, localization, tests, documentation). While locale additions are highly repetitive and require minimal verification, the core component logic demands careful review for correctness of the drag-and-drop integration, state management, slot rendering, and API surface. The well-structured organization and comprehensive test coverage help offset the breadth of the change. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/runtime/components/Repeater.vue`:
- Around line 233-237: The v-for :key currently can be an object or fallback to
index which breaks Vue's key contract and breaks sortable lists; change the
component to require or compute a stable primitive key for each item: add a
helper (e.g., getItemKey(item)) and use it in the template key binding (replace
the current :key expression), and implement logic that (1) prefers a stable
item.id (or a configured trackBy prop), (2) if absent assigns a stable per-item
primitive id (e.g., attach a generated uid property like _repeaterId to the item
in a setup/created hook) or throws/warns to require an id; ensure the template
binds :key="getItemKey(item)" and update any relevant lifecycle code that
assigns/generated ids so keys remain stable across reorders.
In `@src/runtime/locale/en.ts`:
- Around line 115-121: The 'repeater.add' string uses "Add Item" with
inconsistent capitalization compared to other entries like 'repeater.remove';
change the value of the repeater.add key from "Add Item" to "Add item" so UI
text is consistent (locate the repeater object and update the add property).
In `@src/runtime/locale/uz.ts`:
- Around line 115-121: The new Uzbek strings under the repeater object
(repeater.dragToReorder, repeater.moveUp, repeater.moveDown, repeater.remove,
repeater.add) use the U+2019 right single quotation mark; replace those
apostrophes with the escaped straight apostrophe sequence (\' ) to match the
rest of this file's convention for Uzbek strings so all entries use a consistent
apostrophe representation.
🧹 Nitpick comments (2)
test/components/Repeater.spec.ts (1)
101-116: Good test coverage with snapshot and accessibility checks.The parameterized test cases effectively cover variants, sizes, props, and slots. The accessibility test using
axeis a valuable addition.Consider adding interaction tests in a follow-up to verify emit behavior for
add,remove, andmoveactions, and to confirmcanAdd/canRemoveguards work correctly at boundary conditions.src/runtime/components/Repeater.vue (1)
8-16: Type safety could be improved formodelValueandnewItemTemplate.The component uses a generic
Tin the setup script, butRepeaterPropsusesany[]and() => any. This limits type inference for consumers. If Vue's SFC generic limitations prevent stronger typing here, consider documenting the workaround for typed usage.
| <div | ||
| v-for="(item, index) in items" | ||
| :key="(item as any).id ? (item as any).id : item" | ||
| data-slot="item" | ||
| :class="ui.item({ class: props.ui?.item })" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "v-for.*:key=" src/runtime/components/ -A1 | head -80Repository: nuxt/ui
Length of output: 8354
🌐 Web query:
Vue 3 v-for key best practices object as key vs index
💡 Result:
- Always provide a
:keyonv-for, especially when rendering components, to keep component/DOM state predictable. [1] - Best key = stable, unique, primitive identifier from your data (e.g.,
id,slug,uuid). Vue’skeyexpects astring | number | symbol(not an object). [2][3]<li v-for="item in items" :key="item.id">{{ item.name }}</li>
Object property key vs index (when iterating an object)
When doing v-for="(value, key, index) in myObject":
- Prefer using the object’s property name (
key) as:key(it’s a stable string identifier as long as the property names are stable). [3]<li v-for="(value, prop) in myObject" :key="prop"> {{ prop }}: {{ value }} </li>
When (not) to use the index
- Avoid
:key="index"for arrays when items can be inserted/removed/reordered/filtered, because index tracks position, not identity (can cause state to “move” to the wrong row). Vue also notes its default “in-place patch” behavior is only safe for very simple, stateless list output. [3] - Using index is only reasonably safe when the list is static (no reorder/insert/delete) and the rendered content is simple/stateless. [3]
Rule of thumb: if your data has a real identity, key by that identity; if you’re iterating an object, key by the object’s property name; use index only as a last resort for truly static/simple lists. [1][2][3]
Sources:
[1] Vue Style Guide (Essential): keyed v-for https://vuejs.org/style-guide/rules-essential.html
[2] Vue API: key special attribute expects number | string | symbol https://vuejs.org/api/built-in-special-attributes.html
[3] Vue Guide: List Rendering / Maintaining State with key + object iteration aliases https://vuejs.org/guide/essentials/list
🏁 Script executed:
sed -n '1,130p' src/runtime/components/Repeater.vue | cat -nRepository: nuxt/ui
Length of output: 4177
🏁 Script executed:
# Check if items are expected to have an id property
rg -n "id.*as.*key|item.*id" src/runtime/components/Repeater.vue -B2 -A2Repository: nuxt/ui
Length of output: 855
Using object as a Vue key violates the API contract and causes issues in sortable lists.
Vue requires keys to be string | number | symbol, not objects. Additionally, the suggested fallback to index is an anti-pattern for reorderable/sortable lists, where positional keys cause state to move to the wrong item when the array is mutated.
Since the component has drag-and-drop and sorting capabilities, keys must be stable and based on item identity, not position. Either require items to have a unique id property, or use a composite key that preserves identity:
Suggested fix
<div
v-for="(item, index) in items"
- :key="(item as any).id ? (item as any).id : item"
+ :key="(item as any).id ?? `fallback-${index}`"
data-slot="item"
:class="ui.item({ class: props.ui?.item })"
>Alternatively, document that items must have a stable unique identifier (e.g., id property) for proper tracking during reordering.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| v-for="(item, index) in items" | |
| :key="(item as any).id ? (item as any).id : item" | |
| data-slot="item" | |
| :class="ui.item({ class: props.ui?.item })" | |
| <div | |
| v-for="(item, index) in items" | |
| :key="(item as any).id ?? `fallback-${index}`" | |
| data-slot="item" | |
| :class="ui.item({ class: props.ui?.item })" |
🤖 Prompt for AI Agents
In `@src/runtime/components/Repeater.vue` around lines 233 - 237, The v-for :key
currently can be an object or fallback to index which breaks Vue's key contract
and breaks sortable lists; change the component to require or compute a stable
primitive key for each item: add a helper (e.g., getItemKey(item)) and use it in
the template key binding (replace the current :key expression), and implement
logic that (1) prefers a stable item.id (or a configured trackBy prop), (2) if
absent assigns a stable per-item primitive id (e.g., attach a generated uid
property like _repeaterId to the item in a setup/created hook) or throws/warns
to require an id; ensure the template binds :key="getItemKey(item)" and update
any relevant lifecycle code that assigns/generated ids so keys remain stable
across reorders.
| repeater: { | ||
| dragToReorder: 'Drag to reorder item', | ||
| moveUp: 'Move item up', | ||
| moveDown: 'Move item down', | ||
| remove: 'Remove item', | ||
| add: 'Add Item' | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent capitalization in add key.
The add value uses "Add Item" with uppercase "Item", while other keys like remove use lowercase "item". This creates inconsistent UI text.
Suggested fix
repeater: {
dragToReorder: 'Drag to reorder item',
moveUp: 'Move item up',
moveDown: 'Move item down',
remove: 'Remove item',
- add: 'Add Item'
+ add: 'Add item'
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| repeater: { | |
| dragToReorder: 'Drag to reorder item', | |
| moveUp: 'Move item up', | |
| moveDown: 'Move item down', | |
| remove: 'Remove item', | |
| add: 'Add Item' | |
| }, | |
| repeater: { | |
| dragToReorder: 'Drag to reorder item', | |
| moveUp: 'Move item up', | |
| moveDown: 'Move item down', | |
| remove: 'Remove item', | |
| add: 'Add item' | |
| }, |
🤖 Prompt for AI Agents
In `@src/runtime/locale/en.ts` around lines 115 - 121, The 'repeater.add' string
uses "Add Item" with inconsistent capitalization compared to other entries like
'repeater.remove'; change the value of the repeater.add key from "Add Item" to
"Add item" so UI text is consistent (locate the repeater object and update the
add property).
| repeater: { | ||
| dragToReorder: 'Tartibni o‘zgartirish uchun sudrab olib o‘ting', | ||
| moveUp: 'Yuqoriga ko‘chirish', | ||
| moveDown: 'Pastga ko‘chirish', | ||
| remove: 'O‘chirish', | ||
| add: 'Qo‘shish' | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent apostrophe character in Uzbek translations.
The new translations use ' (U+2019 RIGHT SINGLE QUOTATION MARK) for the Uzbek apostrophe sound (e.g., o'zgartirish), but existing translations in this file use different representations:
- Escaped straight apostrophe:
\'(e.g., Line 13:ko\'rsatish) - Modifier letter turned comma:
ʻU+02BB (e.g., Line 5:Oʻzbek)
Consider standardizing on one character for consistency. Using the escaped straight apostrophe \' would match the majority of existing strings in this file.
🔧 Suggested fix for consistency
repeater: {
- dragToReorder: 'Tartibni o'zgartirish uchun sudrab olib o'ting',
- moveUp: 'Yuqoriga ko'chirish',
- moveDown: 'Pastga ko'chirish',
- remove: 'O'chirish',
- add: 'Qo'shish'
+ dragToReorder: 'Tartibni o\'zgartirish uchun sudrab olib o\'ting',
+ moveUp: 'Yuqoriga ko\'chirish',
+ moveDown: 'Pastga ko\'chirish',
+ remove: 'O\'chirish',
+ add: 'Qo\'shish'
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| repeater: { | |
| dragToReorder: 'Tartibni o‘zgartirish uchun sudrab olib o‘ting', | |
| moveUp: 'Yuqoriga ko‘chirish', | |
| moveDown: 'Pastga ko‘chirish', | |
| remove: 'O‘chirish', | |
| add: 'Qo‘shish' | |
| }, | |
| repeater: { | |
| dragToReorder: 'Tartibni o\'zgartirish uchun sudrab olib o\'ting', | |
| moveUp: 'Yuqoriga ko\'chirish', | |
| moveDown: 'Pastga ko\'chirish', | |
| remove: 'O\'chirish', | |
| add: 'Qo\'shish' | |
| }, |
🤖 Prompt for AI Agents
In `@src/runtime/locale/uz.ts` around lines 115 - 121, The new Uzbek strings under
the repeater object (repeater.dragToReorder, repeater.moveUp, repeater.moveDown,
repeater.remove, repeater.add) use the U+2019 right single quotation mark;
replace those apostrophes with the escaped straight apostrophe sequence (\' ) to
match the rest of this file's convention for Uzbek strings so all entries use a
consistent apostrophe representation.






🔗 Linked issue
Resolves #5308
❓ Type of change
📚 Description
This PR introduces a new
Repeatercomponent.It allows users to create dynamic, sortable lists of forms or items. It is designed to handle complex objects, supports custom templates, and is fully integrated with the Nuxt UI theme system.
@vueuse/integrations/useSortable(wrappingsortablejs).Features
useSortable(with touch support).tv(Tailwind Variants) supportingoutline,soft, etc.maxItemsandminItems.T extends Record<string, any>).Checklist
src/runtime/components/Repeater.vue)src/theme/repeater.ts)playgrounds/nuxt/app/pages/components/repeater.vue)test/components/Repeater.spec.ts)docs/content/docs/components/repeater.md)Preview
URepeater.-.salehattari.mp4