Tips for maintaining data reactivity while transmitting it to a function

Consider the following setup for my component:

<script setup lang="ts">
interface FileMetadata {
  owner: string,
  location: string,
  size: number,
  // And around 50 mores...
}

interface File {
  fileName: string,
  metadata: FileMetadata
}

const file = defineModel<File>({ required: true });

const buildControls = (metadata: FileMetadata) => {
  return [
    {
      label: 'Owner',
      model: metadata.owner,  // Is this correct?
      type: 'text'
    },
    {
      label: 'Location',
      model: metadata.location,  // Is this correct?
      type: 'text'
    },
    {
      label: 'Size',
      model: metadata.size,  // Is this correct?
      type: 'number'
    },
    // And around 50 mores...
  ];
};

const controls = buildControls(file.value.metadata);  // The value is already unpacked here
</script>

<template>
  <div v-for=(item, idx) in controls>
    <v-text-field v-model="item[idx].model" :label="item[idx].label" :type="item[idx].type" />
  </div>
</template>

This component receives a File object through its v-model and displays a view for users to edit its metadata. Whenever the model changes (a different file is passed), the values of the text fields should update accordingly. Similarly, any changes made by users in the text fields should also reflect in the model. This behavior represents a two-way binding.

The view renders correctly, but I'm encountering an issue where I cannot alter the values in the text fields (they revert to their initial values when losing focus). I suspect this happens because the file is already unpacked when the buildControls() function is invoked, causing a loss of reactivity. However, if I refrain from unpacking it (by not accessing its .value), I lose access to its metadata attribute.

If I try to make controls reactive by using

const controls = reactive(buildControls(file.value.metadata));

then I am able to modify the values in the text fields, but the file model does not update accordingly.

In my understanding, the value of the model key should be a Ref object. However, I seem to be unable to achieve this.

One potential solution could involve storing only the key name of the FileMetadata instead of the entire object in the model key. This way, it can be accessed later in the template as file.metadata[item.model]. Nevertheless, I find this approach less than ideal. Why shouldn't I be able to simply store an object instead?

What error have I made in this setup? How can I ensure the data maintains its reactivity as intended?

Answer №1

controls does not exhibit reactivity and must be modified:

const controls = ref();
...
controls.value = buildControls(...);

Alternatively:

const controls = reactive(buildControls(...));

The issue lies in the fact that controls is designed for two-way synchronization with file, making implementation challenging due to its structure. The challenge of storing mixed mutable and static data is a common one, with insufficient reasons to keep them unified.

If the component is expected to emit changes to file to a parent based on modifications to v-model="item[idx].model", tracking specific changes can be problematic.

A list of metadata keys that both static and mutable data can utilize should be established. It is recommended to maintain static field data as a constant, allowing types to be derived for enhanced type safety (implementation of ValidateKeys can be observed here):

const fileMetaFields = {
  owner: {
    label: 'Owner',
    type: 'text'
  },
  ...
}

type FileMetaKeys = keyof typeof fileMetaFields;

type FileMeta = ValidateKeys<FileMetaKeys, {
  owner: string,
  ...
}

Additional utility types can be created to automatically translate type: 'text' to string.

If the component is meant to update the parent's model, it may not require its own state. The use of v-model in v-text-field restricts the ways in which the model can be updated, necessitating desugaring:

  function updateModelField({ key, value }) {
    emit('update:modelValue', {
      ...file.value,
      metadata: {
        ...file.value.metadata
        [key]: value
      },
    });
  }

  ...

  <div v-for=(field, key) in fileMetaFields>
    <v-text-field
      :label="field.label"
      :modelValue="file.value.metadata[key]" />
      @update:modelValue="updateModelField({ key, value: $event })"
      ... 

This highlights the challenge with file. To prevent the model prop from being mutated in a child and achieve Vue's two-way binding in a parent, the entire object must be cloned to modify one field. This inefficiency can be mitigated by providing the child with only the necessary data (metadata instead of file) and updating file in the parent using a custom event instead of v-model.

Therefore, in the child:

  const props = defineProps<{ data: FileMeta }>();

  function updateModelField({ key, value }) {
    emit('update:field', { key, value });
  }

In the parent:

<FileMetaForm
  :data="file.metadata"
  @update:field="file.metadata[$event.key] = $event.value"
>

Answer №2

To maintain data reactivity, utilize the computed property in your code like so:

<script setup lang="ts">
interface FileMetadata {
  owner: string,
  location: string,
  size: number,
  // Plus approximately 50 more properties...
}

interface File {
  fileName: string,
  metadata: FileMetadata
}

const file = defineModel<File>({ required: true });

const buildControls = (metadata: FileMetadata) => {
  return [
    {
      label: 'Owner',
      model: metadata,  // Is this the right approach?
      type: 'text',
      field: "owner"
    },
    {
      label: 'Location',
      model: metadata,  // Is this the right approach?
      type: 'text',
      field: "location"
    },
    {
      label: 'Size',
      model: metadata,  // Is this the right approach?
      type: 'number',
      field: "size"
    },
    // Plus approximately 50 more properties...
  ];
};

const controls = buildControls(file.value.metadata);  // Data is already unpacked here
</script>

<template>
  <div v-for="item in controls">
    <v-text-field v-model="item.model[item.field]" :label="item.label" :type="item.type" />
  </div>
</template>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

TypeScript requires that when explicitly specifying the return type of a generator, the `Generator.prototype.return` function must accept

I am utilizing a generator function to serve as a consumer for accumulating strings: function *concatStrings(): Generator<void, string, string> { let result = ''; try { while (true) { const data = yield; result += data; ...

What is the best way to obtain a distinct collection from two arrays that eliminates the second appearance of an element based on a key's value, rather than the first as in the Lodash uniqueBy function?

Let's say I have two arrays... const arr1 = [ { id: 1: newBid: true } ]; const arr2 = [ { id: 1, newBid: false }, { id: 2, newBid: false } ]; My goal is to end up with an array that looks like this [ { id: 1, newBid: false }, { id: 2, newBid: fals ...

Encountering issues with properly updating the Vuex store

Looking at my Vuex 2 store: const datastore = new Vuex.Store({ state: { socketcluster: { connection: false, channels: [] }, selected_offers: [] }, mutations: { addOffer: function(offer) { datastore.state.s ...

Error message in Angular: Unable to locate a differ that supports the object '[object Object]' of type 'object.' NgFor is only able to bind to iterables like Arrays

When making an API call in my Angular project, I receive the following JSON response: { "data": { "success": true, "historical": true, "date": "2022-01-01", "base": "MXN&quo ...

Is there a feature in VS Code that can automatically update import paths for JavaScript and TypeScript files when they are renamed or

Are there any extensions available for vscode that can automatically update file paths? For example, if I have the following import statement: import './someDir/somelib' and I rename or move the file somelib, will it update the file path in all ...

Using a Jasmine spy to monitor an exported function in NodeJS

I've encountered difficulties when trying to spy on an exported function in a NodeJS (v9.6.1) application using Jasmine. The app is developed in TypeScript, transpiled with tsc into a dist folder for execution as JavaScript. Application Within my p ...

Utilizing Arrays in Typescript within the Angular Framework

I have developed a Rest API that provides data to populate two drop-down lists in a form. The information retrieved from the API is grabbed by the Angular backend and assigned to the respective drop-downs. Rather than making separate Get requests for each ...

Uploading raw data to Firebase bucket

I am currently developing a nodejs/typescript application that leverages Firebase Functions, and I am facing a challenge with uploading a JSON object to a bucket. The issue arises from the fact that the JSON data is stored in memory and not as an actual fi ...

Change a nullable string property within an interface to a non-nullable string property

Looking at two interfaces, one with a nullable vin and the other without: interface IVehicle { vin: string | null; model: string; } interface IVehicleNonNullVin { vin: string; model: string; } The goal is to convert a model from IVehicle ...

The utilization of rxjs' isStopped function is now considered

We currently have this method implemented in our codebase: private createChart(dataset: any): any { if (!this.unsubscribeAll.isStopped) { this.chart = this.miStockChartService.createChart(dataset, this.chartId, this.options, this.extend ...

Issues arise with Typescript compiler on Windows systems due to soft symlinks causing compilation failures

In my TypeScript project, symlinks function properly on both macOS and Linux. However, when executing tsc in git-bash on Windows (not within WSL), the files cannot be resolved by tsc. ...

Apologies, but the module '@vue/babel-preset-app' could not be located

After creating a new Vue application, I encounter an error when running the server and the compilation fails. Can anyone help me identify the source of this issue? Below are screenshots of my Terminal and browser. The main.js file import Vue from ' ...

"Building your own utilities in Nuxtjs: A step-by-step guide

Currently, I am using Nuxtjs version 2.15.4 and I am looking to update my Utils functionality. Currently, I am using a mixin for my utils but I would like to implement something similar to the following code snippet: import {func1 , func3} from '~/plu ...

Steps for retrieving a route parameter

Here is a sample configuration of my Vue router: export default new VueRouter({ mode: 'history', routes: [ /** * Authentication */ { name: 'login', path: '/', ...

Developing a union type to guarantee every value in the `enum` is accounted for

My code includes an enum that lists operations (which cannot be altered): enum OpType { OpA = 0, OpB = 1, } In addition, there are object types defined to hold data required for each operation: type A = { readonly opType: OpType.OpA; readonly foo: ...

Dealing with Typescript (at-loader) compilation issues within a WebPack environment

Currently, I am using Visual Studio 2017 for writing an Angular SPA, but I rely on WebPack to run it. The current setup involves VS building the Typescript into JS, which is then utilized by WebPack to create the build artifact. However, I am looking to t ...

What is the method for retrieving the index of an enum member in Typescript, rather than the member name?

Here is an example of how to work with enums in TypeScript: export enum Category { Action = 1, Option = 2, RealEstateFund = 3, FuturesContract = 4, ETFs = 5, BDRs = 6 } The following function can be used to retrieve the enum indexe ...

Using Vue CLI to dynamically import modules from project directories

Is it possible to import components from a bundled webpack file using dynamic imports? const url = '${window.location.origin}/assets/default/js/bundle/vue_data.bundle.js'; this.getAxiosInstance().get(url).then(response => { ...

What is the best way to modify the disabled attribute?

After disabling a button using a boolean variable, updating the variable does not remove the disabled attribute. How can I update my code to enable the button when the variable changes? Here is my current code snippet: var isDisabled = true; return ( ...

How to retrieve a value from an Angular form control in an HTML file

I have a button that toggles between map view and list view <ion-content> <ion-segment #viewController (ionChange)="changeViewState($event)"> <ion-segment-button value="map"> <ion-label>Map</ion-label> & ...