commit 573aeaa79996e59dcad82dcfc32b31dc5a38b6e5
parent 6cb21137ffee76d1285045c4ac8b43727fb3ce62
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 21 Jun 2025 14:24:39 -0700
Migrate other components to useObjectModel
All components with object values now use useObjectModel instead of
defineModel, and their corresponding tests now check for emitted events
instead of internal model values.
Diffstat:
9 files changed, 262 insertions(+), 122 deletions(-)
diff --git a/src/components/PaceInput.vue b/src/components/PaceInput.vue
@@ -18,24 +18,13 @@
</template>
<script setup>
-import DecimalInput from '@/components/DecimalInput.vue';
-import TimeInput from '@/components/TimeInput.vue';
-
import { DISTANCE_UNITS } from '@/utils/units';
-/**
- * The component value
- */
-const model = defineModel({
- type: Object,
- default: {
- distanceValue: 5,
- distanceUnit: 'kilometers',
- time: 1200,
- },
-});
+import DecimalInput from '@/components/DecimalInput.vue';
+import TimeInput from '@/components/TimeInput.vue';
+import useObjectModel from '@/composables/useObjectModel';
-defineProps({
+const props = defineProps({
/**
* The prefix for each field's aria-label
*/
@@ -43,8 +32,23 @@ defineProps({
type: String,
default: 'Input',
},
+
+ /**
+ * The component value
+ */
+ modelValue: {
+ type: Object,
+ default: () => ({
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ }),
+ },
});
+// Generate internal ref tied to modelValue prop
+const emit = defineEmits(['update:modelValue']);
+const model = useObjectModel(props, emit, 'modelValue');
</script>
<style scoped>
diff --git a/src/components/RaceOptions.vue b/src/components/RaceOptions.vue
@@ -19,12 +19,22 @@
<script setup>
import DecimalInput from '@/components/DecimalInput.vue';
+import useObjectModel from '@/composables/useObjectModel';
-const model = defineModel({
- type: Object,
- default: {
- model: 'AverageModel',
- riegelExponent: 1.06,
+const props = defineProps({
+ /**
+ * The component value
+ */
+ modelValue: {
+ type: Object,
+ default: () => ({
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ }),
},
});
+
+// Generate internal ref tied to modelValue prop
+const emit = defineEmits(['update:modelValue']);
+const model = useObjectModel(props, emit, 'modelValue');
</script>
diff --git a/src/components/SplitOutputTable.vue b/src/components/SplitOutputTable.vue
@@ -27,7 +27,7 @@
</td>
<td>
- <time-input v-model="targets[index].splitTime" label="Split duration" :showHours="false"/>
+ <time-input v-model="model[index].splitTime" label="Split duration" :showHours="false"/>
</td>
<td>
@@ -53,14 +53,7 @@ import { formatDuration, formatNumber } from '@/utils/format';
import { DISTANCE_UNITS, convertDistance, getDefaultDistanceUnit } from '@/utils/units';
import TimeInput from '@/components/TimeInput.vue';
-
-/**
- * The split targets
- */
-const targets = defineModel({
- type: Array,
- default: () => [],
-})
+import useObjectModel from '@/composables/useObjectModel';
const props = defineProps({
/**
@@ -70,8 +63,20 @@ const props = defineProps({
type: String,
default: 'metric',
},
+
+ /**
+ * The split targets
+ */
+ modelValue: {
+ type: Array,
+ default: () => [],
+ },
});
+// Generate internal ref tied to modelValue prop
+const emit = defineEmits(['update:modelValue']);
+const model = useObjectModel(props, emit, 'modelValue');
+
/**
* The target table results
*/
@@ -79,15 +84,15 @@ const results = computed(() => {
// Initialize results array
const results = [];
- for (let i = 0; i < targets.value.length; i += 1) {
+ for (let i = 0; i < model.value.length; i += 1) {
// Calculate split and total times
- const splitTime = targets.value[i].splitTime || 0;
+ const splitTime = model.value[i].splitTime || 0;
const totalTime = i === 0 ? splitTime : results[i - 1].time + splitTime;
// Calculate split and total distances
const totalDistance = convertDistance(
- targets.value[i].distanceValue,
- targets.value[i].distanceUnit, 'meters',
+ model.value[i].distanceValue,
+ model.value[i].distanceUnit, 'meters',
);
const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance;
@@ -98,8 +103,8 @@ const results = computed(() => {
// Add row to results array
results.push({
distance: totalDistance,
- distanceValue: targets.value[i].distanceValue,
- distanceUnit: targets.value[i].distanceUnit,
+ distanceValue: model.value[i].distanceValue,
+ distanceUnit: model.value[i].distanceUnit,
time: totalTime,
splitTime,
pace,
diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue
@@ -28,6 +28,7 @@ import VueFeather from 'vue-feather';
import { sort, defaultTargetSets } from '@/utils/targets';
import TargetEditor from '@/components/TargetEditor.vue';
+import useObjectModel from '@/composables/useObjectModel';
/**
* The selected target set
@@ -37,15 +38,7 @@ const model = defineModel('selectedTargetSet', {
default: '_new',
});
-/**
- * The target sets
- */
-const targetSets = defineModel('targetSets', {
- type: Object,
- default: {},
-});
-
-defineProps({
+const props = defineProps({
/**
* Whether to allow custom names for workout targets
*/
@@ -69,8 +62,20 @@ defineProps({
type: String,
default: 'standard'
},
+
+ /**
+ * The target sets
+ */
+ targetSets: {
+ type: Object,
+ default: () => ({}),
+ },
});
+// Generate internal ref tied to modelValue prop
+const emit = defineEmits(['update:targetSets']);
+const targetSets = useObjectModel(props, emit, 'targetSets');
+
/**
* The dialog element
*/
diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue
@@ -176,27 +176,49 @@ const selectedTargetSet = computed({
/**
* The target sets for the current calculator
*/
-const targetSets = computed(() => {
- if (options.value.calculator === 'pace') {
- return paceTargetSets.value;
- } else if (options.value.calculator === 'race') {
- return raceTargetSets.value;
- } else {
- return workoutTargetSets.value;
- }
+const targetSets = computed({
+ get: () => {
+ if (options.value.calculator === 'pace') {
+ return paceTargetSets.value;
+ } else if (options.value.calculator === 'race') {
+ return raceTargetSets.value;
+ } else {
+ return workoutTargetSets.value;
+ }
+ },
+ set: (newValue) => {
+ if (options.value.calculator === 'pace') {
+ paceTargetSets.value = newValue;
+ } else if (options.value.calculator === 'race') {
+ raceTargetSets.value = newValue;
+ } else {
+ workoutTargetSets.value = newValue;
+ }
+ },
});
/**
* The advanced options for the current calculator
*/
-const advancedOptions = computed(() => {
- if (options.value.calculator === 'pace') {
- return {};
- } else if (options.value.calculator === 'race') {
- return raceOptions.value;
- } else {
- return workoutOptions.value;
- }
+const advancedOptions = computed({
+ get: () => {
+ if (options.value.calculator === 'pace') {
+ return {};
+ } else if (options.value.calculator === 'race') {
+ return raceOptions.value;
+ } else {
+ return workoutOptions.value;
+ }
+ },
+ set: (newValue) => {
+ if (options.value.calculator === 'pace') {
+ // do nothing
+ } else if (options.value.calculator === 'race') {
+ raceOptions.value = newValue;
+ } else {
+ workoutOptions.value = newValue;
+ }
+ },
});
/**
diff --git a/tests/unit/components/PaceInput.spec.js b/tests/unit/components/PaceInput.spec.js
@@ -20,31 +20,52 @@ test('should be initialized to modelValue', () => {
expect(wrapper.findComponent({ name: 'time-input' }).vm.modelValue).to.equal(1000);
});
-test('should update modelValue when inputs are modified', async () => {
+test('should emit event when inputs are modified', async () => {
// Initialize component
const wrapper = shallowMount(PaceInput);
// Update distance value
await wrapper.findComponent({ name: 'decimal-input' }).setValue(3);
- expect(wrapper.vm.modelValue).to.deep.equal({
- distanceValue: 3,
- distanceUnit: 'kilometers',
- time: 1200,
- });
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ distanceValue: 3,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ }],
+ ]);
// Update distance unit
await wrapper.find('select').setValue('miles');
- expect(wrapper.vm.modelValue).to.deep.equal({
- distanceValue: 3,
- distanceUnit: 'miles',
- time: 1200,
- });
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ distanceValue: 3,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ }],
+ [{
+ distanceValue: 3,
+ distanceUnit: 'miles',
+ time: 1200,
+ }],
+ ]);
// Update time
await wrapper.findComponent({ name: 'time-input' }).setValue(1000);
- expect(wrapper.vm.modelValue).to.deep.equal({
- distanceValue: 3,
- distanceUnit: 'miles',
- time: 1000,
- });
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ distanceValue: 3,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ }],
+ [{
+ distanceValue: 3,
+ distanceUnit: 'miles',
+ time: 1200,
+ }],
+ [{
+ distanceValue: 3,
+ distanceUnit: 'miles',
+ time: 1000,
+ }],
+ ]);
});
diff --git a/tests/unit/components/RaceOptions.spec.js b/tests/unit/components/RaceOptions.spec.js
@@ -18,21 +18,29 @@ test('should be initialized to modelValue', () => {
expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.2);
});
-test('should update modelValue when inputs are modified', async () => {
+test('should emit event when inputs are modified', async () => {
// Initialize component
const wrapper = shallowMount(RaceOptions);
// Update model
await wrapper.find('select').setValue('CameronModel');
- expect(wrapper.vm.modelValue).to.deep.equal({
- model: 'CameronModel',
- riegelExponent: 1.06,
- });
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ }],
+ ]);
// Update Riegel exponent
await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3);
- expect(wrapper.vm.modelValue).to.deep.equal({
- model: 'CameronModel',
- riegelExponent: 1.3,
- });
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ }],
+ [{
+ model: 'CameronModel',
+ riegelExponent: 1.3,
+ }],
+ ]);
});
diff --git a/tests/unit/components/SplitOutputTable.spec.js b/tests/unit/components/SplitOutputTable.spec.js
@@ -103,7 +103,7 @@ test('should correctly calculate paces and cumulative times from entered split t
expect(rows.length).to.equal(3);
});
-test('should correctly update modelValue with split times', async () => {
+test('should emit update event when split times are changed', async () => {
// Initialize component
const wrapper = shallowMount(SplitOutputTable, {
propsData: {
@@ -119,11 +119,18 @@ test('should correctly update modelValue with split times', async () => {
await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190);
await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(200);
- // Assert modelValue correctly updated
- expect(wrapper.vm.modelValue).to.deep.equal([
+ // Assert update events correctly emitted
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [[
+ { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', splitTime: 180 },
+ { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', splitTime: 190 },
+ { result: 'time', distanceValue: 3000, distanceUnit: 'meters', splitTime: 180 },
+ ]],
+ [[
{ result: 'time', distanceValue: 1, distanceUnit: 'kilometers', splitTime: 180 },
{ result: 'time', distanceValue: 2, distanceUnit: 'kilometers', splitTime: 190 },
{ result: 'time', distanceValue: 3000, distanceUnit: 'meters', splitTime: 200 },
+ ]],
]);
});
diff --git a/tests/unit/components/TargetSetSelector.spec.js b/tests/unit/components/TargetSetSelector.spec.js
@@ -86,12 +86,16 @@ test('Create New Target Set option should correctly add target set', async () =>
expect(options[3].element.value).to.equal('_new');
expect(options.length).to.equal(4);
- // Assert target sets were correctly updated
- targetSets[key1] = {
- name: 'New target set',
- targets: [],
- };
- expect(wrapper.vm.targetSets).to.deep.equal(targetSets);
+ // Assert update event emitted with correct target sets
+ expect(wrapper.emitted()['update:targetSets']).to.deep.equal([
+ [{
+ [key1]: {
+ name: 'New target set',
+ targets: [],
+ },
+ ...targetSets,
+ }],
+ ]);
// Add another target set
await wrapper.find('select').setValue('_new');
@@ -115,12 +119,27 @@ test('Create New Target Set option should correctly add target set', async () =>
expect(options[4].element.value).to.equal('_new');
expect(options.length).to.equal(5);
- // Assert target sets were correctly updated
- targetSets[key2] = {
- name: 'New target set',
- targets: [],
- };
- expect(wrapper.vm.targetSets).to.deep.equal(targetSets);
+ // Assert update event emitted with correct target sets
+ expect(wrapper.emitted()['update:targetSets']).to.deep.equal([
+ [{
+ [key1]: {
+ name: 'New target set',
+ targets: [],
+ },
+ ...targetSets,
+ }],
+ [{
+ [key1]: {
+ name: 'New target set',
+ targets: [],
+ },
+ ...targetSets,
+ [key2]: {
+ name: 'New target set',
+ targets: [],
+ },
+ }],
+ ]);
});
test('Revert event should correctly reset a default target set', async () => {
@@ -163,14 +182,27 @@ test('Revert event should correctly reset a default target set', async () => {
expect(options[2].element.value).to.equal('_new');
expect(options.length).to.equal(3);
- // Assert target sets were correctly updated
- targetSets._split_targets.name = '5K Mile Splits';
- targetSets._split_targets.targets[2] = {
- type: 'distance',
- distanceValue: 5,
- distanceUnit: 'kilometers',
- };
- expect(wrapper.vm.targetSets).to.deep.equal(targetSets);
+ // Assert update event emitted with correct target sets
+ expect(wrapper.emitted()['update:targetSets']).to.deep.equal([
+ [{
+ '_split_targets': {
+ name: '5K Mile Splits',
+ targets: [
+ { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
+ ],
+ },
+ '1234567890123': {
+ name: '2nd target set',
+ targets: [
+ { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers' },
+ { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
+ { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' },
+ ],
+ },
+ }],
+ ]);
});
test('Revert event should correctly delete a custom target set', async () => {
@@ -211,9 +243,19 @@ test('Revert event should correctly delete a custom target set', async () => {
expect(options[1].element.value).to.equal('_new');
expect(options.length).to.equal(2);
- // Assert target sets were correctly updated
- delete targetSets['1234567890123'];
- expect(wrapper.vm.targetSets).to.deep.equal(targetSets);
+ // Assert update event emitted with correct target sets
+ expect(wrapper.emitted()['update:targetSets']).to.deep.equal([
+ [{
+ '_split_targets': {
+ name: '1st target set',
+ targets: [
+ { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
+ ],
+ },
+ }],
+ ]);
});
test('edit button should open target editor with the correct props for default set', async () => {
@@ -313,17 +355,33 @@ test('should sort target set after target editor is closed', async () => {
});
await wrapper.findComponent({ name: 'target-editor' }).vm.$emit('close');
- // Assert target set was sorted
- expect(wrapper.findComponent({ name: 'target-editor' }).vm.modelValue).to.deep.equal({
- name: '5K Mile Splits',
- targets: [
- { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
- { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
- { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
- { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
- { type: 'time', timeValue: 60 },
- ],
- });
+ // Assert update events were emitted correctly
+ expect(wrapper.emitted()['update:targetSets']).to.deep.equal([
+ [{
+ _split_targets: {
+ name: '5K Mile Splits',
+ targets: [
+ { type: 'time', timeValue: 60 },
+ { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
+ { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
+ ],
+ },
+ }],
+ [{
+ _split_targets: {
+ name: '5K Mile Splits',
+ targets: [
+ { type: 'distance', distanceValue: 1, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 2, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 3, distanceUnit: 'miles' },
+ { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' },
+ { type: 'time', timeValue: 60 },
+ ],
+ },
+ }],
+ ]);
});
test('should correctly pass setType prop to TargetEditor', async () => {