# Vue 3 Composition API Migration
## Overview
Migrating WikiLambda Vue components from Options API to Composition API using the `setup()` function pattern.
**IMPORTANT**: Only Vue components need migration. Pinia stores work with both APIs - no conversion needed.
## The Pattern
### Hybrid single `<script>` Tag with setup() Function that still uses module.exports
```vue
<script>
const { computed, defineComponent, inject, ref } = require( 'vue' );
const useZObject = require( '../../composables/useZObject.js' );
const useMainStore = require( '../../store/index.js' );
module.exports = exports = defineComponent( {
name: 'my-component',
components: {
'child-component': ChildComponent
},
props: {
keyPath: {
type: String,
required: true
},
objectValue: {
type: Object,
required: true
}
},
emits: [ 'update-value' ],
setup( props, { emit } ) {
// 1. Inject i18n (provided by MediaWiki)
const i18n = inject( 'i18n' );
// 2. Use composables (destructure only what's needed)
const { getZObjectType, getZBooleanValue } = useZObject();
// 3. Access store (DO NOT destructure - use store.property)
const store = useMainStore();
// 4. Reactive state
const localState = ref( 'initial' );
// 5. Computed properties
const displayValue = computed( () => getZObjectType( props.objectValue ) );
// 6. Methods
function handleUpdate( value ) {
store.setValueByKeyPath( value ); // Use store.action()
emit( 'update-value', value );
}
// 7. MUST explicitly return everything the template uses
return {
// Only return what template actually uses
getZObjectType,
displayValue,
handleUpdate,
// Expose store getters/actions used in template
getLabelData: store.getLabelData
};
}
} );
</script>
```
## Key Rules
✅ **DO:**
- Use single `<script>` tag
- Use `setup( props, { emit } )` inside `defineComponent()`
- Access props via `props.propName`
- Use `emit()` for events
- Use `inject( 'i18n' )` for internationalization (MediaWiki provides i18n via app.provide)
- **Access store via `store.property`** (e.g., `store.getLabelData`, `store.setValueByKeyPath()`)
- **Use `storeToRefs()` ONLY for getters returned by reference** in the return statement
- Explicitly return all values/methods template needs
- Destructure only needed methods from composables
❌ **DON'T:**
- Use two `<script>` tags
- Use spread operators in returns
- Use `this` (use props/emit instead)
- Use `getCurrentInstance()` for i18n (use `inject( 'i18n' )` instead)
- **Destructure from store** (can cause reactivity issues and test complications)
- Return everything (only what template needs)
## Pinia Store Access
### When to Use `storeToRefs()`
Per [Pinia docs](https://pinia.vuejs.org/core-concepts/#destructuring-from-a-store), use `storeToRefs()` to destructure **state and getters** while maintaining reactivity:
```javascript
setup( props, { emit } ) {
const store = useMainStore();
// ✅ CORRECT: Use storeToRefs when destructuring getters/state
const { isInitialized, getCurrentView } = storeToRefs( store );
// ✅ CORRECT: Actions can be destructured directly (they're just functions)
const { setValueByKeyPath, fetchZids } = store;
// ✅ CORRECT: Access store directly in computed/methods
const displayValue = computed( () => store.getLabelData( someId ) );
function handleClick() {
setValueByKeyPath( value ); // ✅ Destructured action
store.fetchZids( zids ); // ✅ Or call directly
}
return {
// Reactive refs from storeToRefs
isInitialized,
getCurrentView,
// Other data
displayValue,
handleClick
};
}
```
### The Rules
1. **Destructure getters/state with `storeToRefs()`** - Maintains reactivity
2. **Destructure actions directly from store** - They're just functions, no reactivity needed
3. **Or access everything via `store.property`** - Simpler, always works
**Why?** Pinia getters and state are reactive refs. Direct destructuring without `storeToRefs()` breaks reactivity. Actions are plain functions and can be destructured directly.
## Composables Available
| Composable | Replaces | Methods Provided |
|------------|----------|------------------|
| `useZObject()` | zobjectMixin | getZObjectType, getZBooleanValue, etc. |
| `useType()` | typeMixin | typeToString, isValidZidFormat, etc. |
| `useError(options)` | errorMixin | fieldErrors, hasFieldErrors, clearFieldErrors |
| `useEventLog()` | eventLogMixin | logEvent |
| `usePageTitle(options)` | pageTitleMixin | updatePageTitle |
| `useMetadata()` | metadataMixin | metadata utils |
| `useClipboard(options)` | clipboardMixin | copy, getCopiedText |
**Note**: Some composables accept optional parameters:
- `useError({ keyPath })` - optional keyPath for error tracking
- `useZObject({ keyPath })` - optional keyPath for context
## Migration Steps
1. Remove mixin imports
2. Import corresponding composables
3. Add `setup( props, { emit } )` function
4. Convert `data()` to `ref()` or `reactive()`
5. Convert `computed` to `computed()`
6. Convert `methods` to functions
7. Return all values/methods template uses
8. Test the component
---
---
# Migration Status
## 🏆 MIGRATION COMPLETE: 84 / 84 Files (100%) 🏆
**Every Vue component and view has been successfully migrated to Composition API!**
## ✅ Completed: Components with Mixins (40/40)
### zobjectMixin-only (5)
- ✅ `ZTypedListType.vue`
- ✅ `ZImplementation.vue`
- ✅ `ZMultilingualString.vue`
- ✅ `ZObjectKeyValueSet.vue`
- ✅ `ZMultilingualStringDialog.vue`
### Type Components (4)
- ✅ `ZBoolean.vue`
- ✅ `ZString.vue`
- ✅ `ZFunctionCall.vue`
- ✅ `ZTypedList.vue`
### Wikidata Components (6)
- ✅ `wikidata/Item.vue`
- ✅ `wikidata/Lexeme.vue`
- ✅ `wikidata/Property.vue`
- ✅ `wikidata/LexemeForm.vue`
- ✅ `wikidata/LexemeSense.vue`
- ✅ `wikidata/Enum.vue`
### Single Mixin Components (6)
- ✅ `KeyValueBlock.vue` - errorMixin removed (unused)
- ✅ `FunctionEditorName.vue` - pageTitleMixin → usePageTitle
- ✅ `FunctionSelect.vue` - typeMixin → useType
- ✅ `FunctionViewerDetailsTable.vue` - typeMixin removed (unused)
- ✅ `FunctionEditor.vue` - eventLogMixin → useEventLog
- ✅ `FunctionReport.vue` - typeMixin → useType
### 2-Mixin Components (12)
- ✅ `ZMonolingualString.vue`
- ✅ `ZReference.vue`
- ✅ `ZArgumentReference.vue`
- ✅ `ZHTMLFragment.vue`
- ✅ `ZObjectStringRenderer.vue`
- ✅ `FunctionExplorer.vue`
- ✅ `ZTester.vue`
- ✅ `TypeSelector.vue`
- ✅ `FunctionEvaluator.vue`
- ✅ `FunctionViewerDetails.vue`
- ✅ `PublishDialog.vue`
- ✅ `ModeSelector.vue`
### 3-Mixin Components (5)
- ✅ `ZCode.vue`
- ✅ `ZObjectToString.vue`
- ✅ `FunctionInputParser.vue`
- ✅ `FunctionInputPreview.vue`
- ✅ `ZObjectSelector.vue`
- ✅ `ZObjectKeyValue.vue`
## ✅ Completed: Components without Mixins (40/40)
### Base Components (11)
- ✅ `SafeMessage.vue` - Props only
- ✅ `LocalizedLabel.vue`
- ✅ `StatusIcon.vue`
- ✅ `ExpandedToggle.vue`
- ✅ `KeyBlock.vue` - Props only
- ✅ `TypeToString.vue`
- ✅ `Pagination.vue`
- ✅ `CustomDialogHeader.vue`
- ✅ `WidgetBase.vue`
- ✅ `CodeEditor.vue` - 318 lines
### Function Editor Components (9)
- ✅ `FunctionEditorField.vue` - Props only
- ✅ `FunctionEditorDescription.vue`
- ✅ `FunctionEditorLanguageBlock.vue`
- ✅ `FunctionEditorInputsItem.vue` - 289 lines
- ✅ `FunctionEditorOutput.vue`
- ✅ `FunctionEditorAliases.vue`
- ✅ `FunctionEditorLanguage.vue`
- ✅ `FunctionEditorInputs.vue` - 256 lines
### Function Viewer Components (1)
- ✅ `FunctionTesterTable.vue`
### Visual Editor Components (11)
- ✅ `FunctionSelectItem.vue` - Props only
- ✅ `FunctionInputField.vue`
- ✅ `ExpandableDescription.vue`
- ✅ `FunctionCallSetup.vue`
- ✅ `FunctionInputSetup.vue` - 359 lines
- ✅ `fields/FunctionInputString.vue`
- ✅ `fields/FunctionInputEnum.vue`
- ✅ `fields/FunctionInputLanguage.vue`
- ✅ `fields/FunctionInputWikidata.vue`
### Widget Components (13)
- ✅ `about/About.vue` - 604 lines
- ✅ `about/AboutLanguageBlock.vue` - 613 lines
- ✅ `about/AboutLanguagesDialog.vue` - 393 lines
- ✅ `publish/LeaveEditorDialog.vue`
- ✅ `publish/Publish.vue` - 299 lines
- ✅ `function-report/FunctionReportItem.vue`
- ✅ `function-evaluator/FunctionMetadataItem.vue`
- ✅ `function-evaluator/EvaluationResult.vue`
- ✅ `function-evaluator/FunctionMetadataDialog.vue` - 793 lines
### Type Components (3)
- ✅ `ZTypedListItems.vue`
- ✅ `wikidata/ReferenceSelector.vue` - 255 lines
- ✅ `wikidata/EntitySelector.vue` - 255 lines
## ✅ Completed: View Files (4/4)
- ✅ `App.vue` - Root app with router
- ✅ `views/Default.vue` - Main view
- ✅ `views/FunctionViewer.vue` - Function viewer
- ✅ `views/FunctionEditor.vue` - Simple wrapper
- ✅ `views/FunctionEvaluator.vue` - Simple wrapper
## Composables Created (9/9)
All composables to replace mixins:
- ✅ `useZObject.js` (with tests)
- ✅ `useType.js` (with tests)
- ✅ `useError.js` (with tests)
- ✅ `useEventLog.js` (with tests)
- ✅ `usePageTitle.js` (with tests)
- ✅ `useMetadata.js` (with tests)
- ✅ `useClipboard.js` (with tests)
- ✅ `useClipboardManager.js` (with tests)
- ✅ `useScroll.js` (with tests)
## Pinia Stores
**NO MIGRATION NEEDED** ✅
Stores work with both Options and Composition API. Access via `const store = useMainStore();`
## Key Patterns Established
### Store Access
```javascript
// Get store instance
const store = useMainStore();
// Use storeToRefs for getters you're returning by reference
const { isInitialized, getCurrentView } = storeToRefs( store );
// Access store directly in computed/methods
const value = computed( () => store.getLabelData( id ) );
store.setValueByKeyPath( data );
// Return with organized sections
return {
// Reactive store data
isInitialized,
getCurrentView,
// Other data
value,
handleClick
};
```
### i18n Access
```javascript
// Inject once
const i18n = inject( 'i18n' );
// Return for template use
return { i18n };
// Use in template
{{ i18n( 'message-key' ).text() }}
```
## Test Refactoring Status
### 🔄 IN PROGRESS: Test Behavior Migration
**Goal:** Refactor tests from implementation testing to behavior testing after Composition API migration.
**Status:** test files need refactoring
**See:** `TEST_REFACTORING_TODOS.md` for detailed refactoring plans and progress tracking
## Migration Results
- ✅ **84 files converted** (80 components + 4 views)
- ✅ **9 composables created** to replace 8 mixins
- ✅ **0 linting errors**
- ✅ **All tests passing**
- ✅ **Full JSDoc documentation preserved**
- ✅ **Proper reactivity maintained** with storeToRefs pattern
- 🔄 **Test refactoring in progress**
---
# Test Refactoring TODOs - Behavior Testing Migration
This document outlines the test files that need refactoring from implementation testing to behavior testing after the Vue 3 Composition API migration.
## 🎯 Overview
During the migration from Options API to Composition API, many tests were updated but still use implementation testing patterns (accessing `wrapper.vm` properties, internal component state, etc.). This document prioritizes which tests need refactoring to focus on user behavior instead of implementation details.
## 📊 Current Status
- **10 files** still using `wrapper.vm` (implementation testing) - **9 completed** ✅
- **1 file** still using `$nextTick` (need `waitFor` migration) - **5 completed** ✅
- **72 files** with excessive `find` usage (1009+ calls)
- **6 files** need render wrapper functions (DRY improvement) - **16 completed** ✅
- **44 files** already have render wrappers ✅
- **12 files** render wrapper pattern fixed (simplified stubs handling) ✅
## 🚀 Systematic Cleanup Tasks
### 1. 🔧 Behavior Testing Migration
**Replace `wrapper.vm` access with user behavior testing:**
### 2. ⏱️ Async Testing Migration
**Replace `$nextTick` with `waitFor()` for better async handling:**
### 3. 🧹 DRY Improvement
**Create render wrapper functions to eliminate repetitive setup code:**
## 📋 Quick Reference
### Testing Best Practices
- **Focus on user behavior** over implementation details
- **Use `waitFor()`** for asynchronous assertions
- **Prefer `mount()`** over `shallowMount()` when testing user interactions
- **Test component integration**, not just unit functionality
- **Ensure tests are maintainable** and readable
### Rules
- **find vs get**: Use `get()` when element must exist (test fails if not found), use `find()` when element might not exist
- **wrapper.vm**: Avoid accessing internal component state, test user-observable behavior instead
- **$nextTick**: Replace with `waitFor()` for better async handling
- **Render wrappers**: Create DRY helper functions to eliminate repetitive setup code
- **Render wrapper pattern**: Use simplified stubs handling - `{ stubs: { Component: false } }` instead of complex global object spreading