Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

pharmonie's avatar

Update a from element from a child using useForm

In my app I have several FilterMultiSelector components (in this example I simplified it) and I try to find I way to update my form when the child component emits changes to the parent.

This is my Parent Component:

<script setup>
import {useForm, usePage} from '@inertiajs/vue3'
import FilterMultiSelector from '@/Composables/FilterMultiSelector.vue'

const page = usePage()

defineProps({
    assets: {
        type: Array,
    },
})

const form = useForm({
    name: '',
    assets: [],
})
const submit = () => {
    form.post(route('store'), {
        onFinish: () => {
            form.name = ''
            form.assets = []
        },
    })
}

const handleSelectedAssetsUpdate = (objects) => {
    console.log(objects)
    form.assets = [...objects]
}
</script>

<template>
    
  <div class="my-2">
      <FilterMultiSelector
          :objects="assets"
          name="Assets"
          :filterAttributes="['label', 'property_name']"
          @update:selectedObjects="handleSelectedAssetsUpdate"
          :selected-objects="form.assets">
          <template #displayFilter="{object}">
              <div class="flex justify-between text-xs">
                  <span class="font-bold">{{ object.label }}</span>
                  <span class="">{{ object.property_name }}</span>
              </div>
          </template>
          <template #displaySelection="{object}">
              <div class="flex justify-between gap-2">
                  <span class="font-bold">{{ object.label }}</span>
                  <span>{{ object.property_name }}</span>
              </div>
          </template>
      </FilterMultiSelector>
  </div>

</template>

and this is the FilterMultiSelctor component, where basically the remove method needs attention:

<script setup>
import {computed, ref} from 'vue'
import useDetectOutsideClick from '@/Composables/useDetectOutsideClick.js'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import {XMarkIcon} from '@heroicons/vue/24/outline'

let {name, objects, filterAttributes, selectedObjects} = defineProps({
    name: {
        type: String,
    },
    objects: {
        type: Array,
    },
    filterAttributes: {
        type: Array,
        default: () => [],
    },
    selectedObjects: {
        type: Array,
        default: () => [],
    },
})
const emit = defineEmits(['update:selectedObjects'])

const componentRef = ref()
const inputRef = ref()
const searchString = ref('')

const filteredObjects = computed(() => {
    return objects.filter((object) => {
        const searchLower = searchString.value.toLowerCase()

        return filterAttributes.length > 0
            ? filterAttributes.some((key) => {
                  const value = object[key]
                  return typeof value === 'string' && value.toLowerCase().includes(searchLower)
              }) && !selectedObjects.includes(object)
            : Object.keys(object).some((key) => {
                  const value = object[key]
                  return typeof value === 'string' && value.toLowerCase().includes(searchLower)
              }) && !selectedObjects.includes(object)
    })
})

const add = (object) => {
    if (!selectedObjects.includes(object)) {
        selectedObjects.push(object)
        emit('update:selectedObjects', selectedObjects)
    }
    searchString.value = ''
    inputRef.value.focus()
}

const remove = (object) => {
    const newSelectedObjects = selectedObjects.filter((t) => t.id !== object.id)
    emit('update:selectedObjects', [...newSelectedObjects])
}

useDetectOutsideClick(componentRef, () => {
    searchString.value = ''
})
</script>

<template>
    <div class="relative">
        <InputLabel
            for="object"
            :value="name" />
        <TextInput
            ref="inputRef"
            id="object"
            type="text"
            class="mt-1 block w-full"
            v-model="searchString"
            placeholder="Search object ..."
            auotofocus />

        <div
            v-if="filteredObjects.length && searchString"
            class="absolute left-[.5%] top-[60px] z-10 w-[99%] rounded border bg-white p-4 shadow"
            ref="componentRef">
            <div
                v-for="object in filteredObjects.slice(0, 10)"
                :key="object.id"
                @click="add(object)"
                class="rounded px-2 py-1 text-sm hover:cursor-pointer hover:bg-gray-100">
                <slot
                    name="displayFilter"
                    :object="object" />
            </div>
            <div
                v-if="filteredObjects.length > 10"
                class="pl-2 text-xs tracking-widest">
                ...
            </div>
        </div>

        <div
            v-if="selectedObjects.length"
            class="mt-2 flex flex-wrap gap-2">
            <div
                v-for="object in selectedObjects"
                :key="object.id"
                class="inline flex max-w-full items-center whitespace-nowrap rounded bg-primary-500 px-2 py-1 text-xs text-white hover:cursor-pointer hover:bg-primary-400"
                @click="remove(object)">
                <slot
                    name="displaySelection"
                    :object="object" />
                <XMarkIcon class="ml-1 h-4 w-4" />
            </div>
        </div>
    </div>
</template>

The data structure of assets is:

assets:Array
   0: Reactive
      id: 1
      label:"Foo"
      property_name:"Bar"
   1 ...

When I now add three assets: Foo, Bar, Lorem and remove Lorem then I get Foo and Bar. But now, if I remove Bar, I get Foo and Lorem. And if I would now remove Foo, I'd get Bar and Lorem. So there is an issue in reactivity somewhere that I don't see.

0 likes
0 replies

Please or to participate in this conversation.