I have an Object like
class Engine {
id = 0;
crankRPM: = 200;
maxRPM = 2400;
numCylinders = 8;
maxOilTemp = 125;
isRunning = false;
start() { ... }
stop() { ... }
}
Now I want a engine component.
Vue does not want me to mutate the state of a property in any child component so I now have to do this:
engine-ui Component definition:
<template>
<card> // Imagine fancy styling here
{{ engine.id }}
<input :value="engine.crankRPM" @input="$emit('changeCrankRPM', $event.target.value)"></input>
<input :value="engine.maxRPM" @input="$emit('changeMaxRPM', $event.target.value)"></input>
<input :value="engine.numCylinders" @input="$emit('changeMumCylinders', $event.target.value)"></input>
<input :value="engine.maxOilTemp" @input="$emit('changeMaxOilTemp', $event.target.value)"></input>
<toggle type="toggle" :value="engine.isRunning" @input="$emit('changeIsRunning', $event.target.value) </toggle>
</card>
</template>
<script lang="ts">
import { Engine } from "src/code/Engine";
import { defineComponent } from "vue";
export default defineComponent({
name: "engine-ui",
props: {
engine: {
type: Engine,
required: true,
},
},
});
</script>
Usage in parent component:
<template>
...
<engine-ui :engine="myEngine"
@changeCrankRPM="myEngine.crankRPM = $event.target.value"
@changeMAxRPM="myEngine.maxRPM = $event.target.value"
@changeNumCylinders="myEngine.numCylinders = $event.target.value"
@changeOilTemp="myEngine.maxOilTemp = $event.target.value"
@changeIsRunning="myEngine.isRunning = $event.target.value"/>
...
</template>
This is extremely verbose and clunky. If I have another 100 fields in the Engine class, It will become unreadable in every parent component that uses engine-ui as it will be a giant wall of text.
Whats the "right way" to design this?
If I now change the Engine class I have to update the engine-ui component and every occurrence in any parent component because the emits are strings.
CodePudding user response:
Here's a simple example where all you need to do is list the field types. Everything passess through the update function. But you need to be careful there and cast the value to the correct type, as native @input or @change events always return the value as a string.
However, as you can see, this eliminates the verboseness of having an update function for each prop.
It can be easily expanded into rendering custom components for custom input types, should you need to.
const { defineComponent, createApp, toRefs, reactive } = Vue;
class Engine {
id = 0;
crankRPM = 200;
description = 'Some description';
maxRPM = 2400;
numCylinders = 8;
maxOilTemp = 125;
isRunning = false;
}
const app = createApp({
setup() {
const state = reactive({
engine: new Engine(),
engineFields: [
{ key: 'crankRPM', type: 'number' },
{ key: 'maxRPM', type: 'number' },
{ key: 'description', type: 'text' },
{ key: 'numCylinders', type: 'number' },
{ key: 'maxOilTemp', type: 'number' },
{ key: 'isRunning', type: 'boolean' }
]
})
const update = ({ value, field }) => {
state.engine[field.key] = field.type === 'number' ? value : value;
};
return {
...toRefs(state),
update
}
}
}).mount('#app')
<script src="https://unpkg.com/vue@next/dist/vue.global.prod.js"></script>
<div id="app">
<div >
{{ engine.id }}
<template v-for="field in engineFields" :key="field.key">
<label v-if="field.type === 'boolean'">
<input type="checkbox"
:checked="engine[field.key]"
@change="update({ value: $event.target.checked, field })">
{{ field.key }}
</label>
<input v-else
:type="field.type"
:value="engine[field.key]"
@input="update({ value: $event.target.value, field })">
<!-- you can have as many input types as you want,
just replace v-else above with v-else-if and add
another case (e.g: textarea, custom components, ...) -->
</template>
</div>
<pre v-text="engine" />
</div>
CodePudding user response:
Template logic can to be reduced to a minimum in order to make the code easy to maintain.
The idiomatic way is to treat engine as a single value and implement two-way binding
With v-model, it would be:
<engine-ui v-model="myEngine"/>
and
...
<div v-for="(_, field) in modelValue">
<input v-model="modelValue[field]"/>
...
props: {
modelValue: Engine
},
It's not considered a good practice to mutate a prop because this makes data flow more complex. This can be overcome by emitting cloned Engine instance from a child on each fieid change, which can be wasteful. Another way is to keep updates in a parent the same they are now:
...
<engine-ui :value="myEngine" @update="onEngineUpdate" />
...
methods: {
onEngineUpdate({ field, fieldValue }) {
this.myEngine[field] = fieldValue;
},
},
...
and
...
<div v-for="(fieldValue, field) in value">
<input :value="fieldValue" @input="$emit('update', { field, fieldValue : $event.target.value })" />
...
props: {
value: Engine
},
...
