My approach in organizing my Vue application involves using classes to encapsulate data, manage their own state (edited, deleted, etc), and synchronize with the back-end system. However, this method seems to conflict with Vue in some respects.
To illustrate the issue with minimal code, let's consider a simplified version of my current setup.
Initial Strategy
Let's assume we have an object that defines the data as follows:
ModelState.ts
export public enum ModelState { saved, modified, deleted, new }
NameModel.ts
export public class NameModel {
public name: string = 'New name'
public state: ModelState = ModelState.modified
public save(): Promise<boolean> {
this.state = ModelState.saved
return Promise.resolve(true);
}
}
Now, I want to render this in a Vue component like this:
<script setup lang="ts">
import { NameModel } from 'NameModel'
import { ModelState } from 'ModelState'
export interface MyComponentProps{
nameModel: NameModel
}
const props = defineProps<MyComponentProps>();
function saveClicked() {
props.nameModel.save()
}
</script>
<template>
<div>
<input v-model="nameModel.name">
<button
v-if="nameModel.state == ModelState.modified"
@click="saveClicked()">Save</button>
</div>
</template>
The problem here is that although clicking the Save button updates the state property of NameModel
, the UI doesn't reflect this change due to the proxy wrapping caused by passing NameModel
as a prop.
Alternative Approaches
In an attempt to solve this, I made the state property reactive in the NameModel
class:
NameModel.ts
import { ref, type Ref } from 'vue'
export public class NameModel {
public name: string = 'New name'
public state: Ref<ModelState> = ref(ModelState.modified)
public save(): Promise<boolean> {
this.state.value = ModelState.saved
return Promise.resolve(true);
}
}
Despite passing all unit tests, this modification resulted in a runtime error due to Vue unwrapping the reactive properties and changing the type of the object's state property from Ref<ModelState>
to ModelState
.
Revised Approach
To resolve the issue, I shifted the state management logic from the data model to the component. While this solution works, it compromises separation of concerns and requires duplicating the logic across components that mutate the state.
The updated code now looks like this:
export public class NameModel {
public name: string = 'New name'
public state: ModelState = ModelState.modified
public save(): Promise<boolean> {
return Promise.resolve(true);
}
}
<script setup lang="ts">
import { NameModel } from 'NameModel'
import { ModelState } from 'ModelState'
export interface MyComponentProps{
nameModel: NameModel
}
const props = defineProps<MyComponentProps>();
function saveClicked() {
props.nameModel.save().then((success) => {
if (success) props.nameModel.state = ModelState.saved
})
}
</script>
<template>
<div>
<input v-model="nameModel.name">
<button
v-if="nameModel.state == ModelState.modified"
@click="saveClicked()">Save</button>
</div>
</template>
Although functional, this workaround feels inadequate. What might be a better approach for handling this?
Additional Details
While this example provides a basic demonstration, my actual implementation is more complex.
I have a Vue component with slots for various user experiences - view, edit, delete, add new. Despite being usable with any data model inheriting from a base class like PersistedEditable
, this component now embeds the state management logic, leading to issues with distinct data models requiring different behaviors or permissions.
Furthermore, plans to implement WebSocket notifications for backend changes pose a challenge as updating the data model's state should ideally be handled within the model itself, not the Vue component.
Motivation (EDIT)
Adding more context to my query, I aim to leverage Vue's reactive UI state management capabilities, motivated by past positive experiences with Vue and its reactivity features.
However, my goal is to separate data state management from UI state management. For instance, when receiving a WebSocket message about data deletion, I want all displaying components to adjust based on this state change without duplicating logic across components.
Playgrounds (EDIT)
Second attempt - runtime error due to unwrapping
Third attempt - works but logic feels misplaced
Composable - suggested by @TheHiggsBroson
Renderless component - I think this is what @TheHiggsBroson suggested