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

OurBG's avatar
Level 1

Handling multiple filters with Inertia and Vue

I'm following Jeffrey's tutorial on the filtering and search. However, there's only search and I have multiple filters on my page. I'll show my code and then tell you what problems I'm encountering;

<template v-slot:top>
            <Input placeholder="Search ..." v-model="search" />
        </template>
        
        <div class='sm:flex sm:space-x-5'>
            <Dropdown v-model="form.contact_type" />
            <Dropdown v-model="form.membership_type" />
            <Dropdown v-model="status" />
        </div>
 data() {
        return {
            search: this.filters.search ?? null,
        },

    watch: {
        search: _.throttle(function(value) {
            Inertia.get('/contacts', { search: value }, {
                preserveState : true,
                replace       : true,
                preserveScroll: true,
            })
        }, 400),
    },

Thanks to people here, this works okay. However when I add more filters (the other dropdowns) it starts getting messy. The watcher (that needs to be deep) always fires on initial page load and loads the page with empty query strings in the URL (which is just ugly, not a huge problem).

I tried making all filters single object, tried switching to post instead of get so. What would be the best way to handle all the filters at the same time and have the watchers work (plus they need to work with pagination).

0 likes
14 replies
undeportedmexican's avatar

I'm no expert on te matter, but I think I would do things a bit different:

  • Rather than defining a 'search' object with all the filters, define a filter object with all the filters, that way the code will be more readable.
  • You don't need to pass through the value to the search object, on your search watcher, since that value is accessible to you via reactivity, just pass along the search object.
  • Your models need to be a nested property of filter. ( filter.serach, filter.contact_type, filter.membership_type, filter.status)
  • Make sure you return the filters from your controller so you can have them available in inertia with the updated values.
  • Keep the get method so that the URL will update with the current filters, and you can keep state properly
 data() {
        return {
            filter: this.filters,
        },

    watch: {
        filter: _.throttle(function() {
            Inertia.get('/contacts',  this.filter, {
                preserveState : true,
                replace       : true,
                preserveScroll: true,
            })
        }, 400),
    },
OurBG's avatar
Level 1

@undeportedmexican hey, thank you, it was actually kind of my mistake that my javascript included only the search code, I was actually passing a filter.

The problems I encounter now are two:

  1. The dropdown values are objects, which are a mess when passed through get, so I guess I should create a params variable inside the watcher's function to just pass the IDs.

  2. Every time on page reload even if you haven't filtered anything the watcher fires the inertia.get and puts empty query string in the URL.

I'll implement the bullet points you've mentioned now.

undeportedmexican's avatar

@OurBG I believe the default setting of the watcher is to be lazy, which means unless you change the watcher to include a handle property and an immediate property set to 'true', the watcher shouldn't be called upon page load.

Can you share how you're feeding the filter values to your template? I believe they are probably not being set correctly. Also, please share the logic in your controller.

OurBG's avatar
Level 1

I'm fixing once in a while the page reload, but everything else is constantly breaking. Something that'd take me 20 minutes with a Vue APP is now taking me more than 2 days with the magic of Inertia ...

On top of that the headless UI that Tailwind comes with doesn't work for some reason with the inertia reloads.

Thank you for your help, got me a little further :)

undeportedmexican's avatar

@OurBG Once I wrapped my head around it, it really made everything tick real quick. If you share more of your code I can probably help more.

OurBG's avatar
Level 1

@undeportedmexican okay, I'll share more then, thanks for your time.

So the first issue is that my Dropdown vue components are taking an object as their model. Meaning that the form.contact_type is an object. This results in a weird query string in the URL, so I am changing the params to be passed to Inertia like this:

let params = {
    search: this.filter.search,
    contact_type: this.filter.contact_type.id,
}

Inertia.get('/contacts', this.filter, {
    preserveState : true,
    replace       : true,
    preserveScroll: true,
})

I was also removing the empty properties from the params object, since otherwise it always loads with the full query string with empty properties: page?search=contact_type=

The second issue is that

 watch: {
        handler: {
            'this.filter': _.throttle(function() {                
					[...]
            }, 400)
        },
		deep: true,
}

Fires on each load. At times with some attempts, I manage to stop that, not really sure how since I'm basically always setting the filters to whatever comes from Inertia's props, so it triggers the watcher (which doesn't happen for the search, but for the dropdowns, it happens.

When this happens it also breaks the pagination since on pagination it sets the filters again triggering "filtering" and going back to page 1.

I'm setting them like this:

props: {
    filters: Object,
},
   data() {
        return {
            filter: {
                search: this.filters.search,
				contact_type: this.filters.contact_type;
            }
        }
    },

This is how I return the values from the controller:

return Inertia::render('Contacts/List', [
           // irrelevant data
            'search'  => $request->search ?? '',
            'filters' => [
                 'contact_type'    => $request->contact_type ? ContactType::find($request->contact_type)->only(['id', 'name']) : '',
                 'membership_type' => $request->membership_type ? MembershipType::find($request->membership_type)->only(['id', 'name']) : '',
                 'status'          => $request->status ? Contact::getStatusesByName($request->status) : '',
            ]
        ]);

That's quite messy as well. There are a shitton of small problems that kind of seem that they require a lot of backend and frontend acrobatics and messy code to match up.

Either this or I'm doing something fundamentally wrong (like I was when I was first setting the properties in a created hook rather than the data property) which would be the ideal case.

Additionally, when I got it to kind of work with some minor bugs, my Dropdowns are using HeadlessUI from TailwindUI, the Listbox option there didn't catch the active property once Inertia is brought in, even though I can see that the model and the selected property are absolutely the same.

undeportedmexican's avatar

@OurBG Ok, we can attack each item individually.

The first thing I would change, is the way your controller is returning the filters, which will probably impact how you're building the View.

What I mean is, my controller would typically look like this:

return Inertia::render('Contacts/List', [
           // irrelevant data
            'search'  => $request->search ?? '',
            'filters' => [
                 'contact_type'    => $request->input('contact_type') ,
                 'membership_type' => $request->input('membership_type'),
                 'status'          => $request->input('status'),
            ]
        ]);

This way you're simplifying what the filter object looks like, of course you would need to build the actual dropdown, so for membership_type and contact_type, you would need to also pass all the possible values, which you probably are already using:

return Inertia::render('Contacts/List', [
           // irrelevant data
            'search'  => $request->search ?? '',
            'filters' => [
                 'contact_type'    => $request->input('contact_type') ,
                 'membership_type' => $request->input('membership_type'),
                 'status'          => $request->input('status'),
            ],
			'contact_types' => ContacType::select('id', 'name')->get(),
			'membership_types' => ContacType::select('id', 'name')->get(),
        ]);

And for your dropdown, you should only need to pass the id to update the value. That simplifies some stuff. Not sure if your design permits this.

For the watcher, I believe something off there. You're watching something called 'handler'. It should actually look something like this:

 watch: {
            filter: {
						handler: _.throttle(function() {                
											[...]
            						}, 400),
						deep: true
			}
}

I believe that will take care of the firing on every reload.

Those are the things I would change. What do you think?

OurBG's avatar
Level 1

@undeportedmexican hey! I implemented what you said, had to rework my app-wide dropdown components and it's a bit cleaner now.

However, the dropdowns needed to be a little messy, since I can't really tap into the Listbox and Listboxoptions a lot, here's their JS code:

export default {
    props: [
        'modelValue', 'label', 'options'
    ],
    data() {
        return {
            content: this.modelValue,
            selected: '',
        }
    },
    methods: {        
        handleInput (e) {
            this.content = e;
            this.$emit('update:modelValue', this.content)
        },
        isNumeric(n) {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }
    },
    components: {
        Listbox,
        ListboxButton,
        ListboxLabel,
        ListboxOption,
        ListboxOptions,
        CheckIcon,
        SelectorIcon,
    },
    created() {
        if(this.modelValue) {
            this.selected = this.isNumeric(this.modelValue) ? parseInt(this.modelValue) : this.modelValue;
        } else {
            let firstItem = this.options[Object.keys(this.options)[0]];
            this.selected = firstItem.id;
        }
    },
    computed: {
        selectedObject() {
            let firstItem = this.options[Object.keys(this.options)[0]];

            if(this.selected) {
                return this.options.find(object => {
                    return object.id == this.selected
                });
            } else {
                 this.selected = firstItem.id;
                 return firstItem;
            }
        }
    },
    watch: {
        selected(value) {
            this.handleInput(value)
        },
        modelValue(value) {
            this.selected = value;
            this.handleInput(value);
        }
    }

I'd really like to clean this up a lot if possible. Also, when I change the page, the dropdown fires both watchers, which makes the parent component think that it's a new search, resetting the page to 1.

undeportedmexican's avatar

@OurBG I'm not familiar with Listbox, but I believe the reason it's triggering the watcher and causing havoc is because they're setting the selected property on the created() hook, which basically happens after the watchers are defined.

I'm not 100% sure, but I think you can do that either in the beforeMount hook, or directly in the data() object., however since there's some logic there the beforeMount() sound more promising.

OurBG's avatar
Level 1

@undeportedmexican Yes, this is definitely the issue, the fact that I need to set the selected properties. Turns out the only earlier hook I can use is BeforeCreate.

But it's not setting the properties fine (and it "fixes" the problem with the firing of watchers).

I have to circumvent the watchers somehow or redo them since I definitely need to set initial values of the same properties that are watched.

OurBG's avatar
Level 1

@undeportedmexican I mean it's not setting them at all, probably it's too early in the cycle to set them? It only works properly from created onward.

My guess is that when the data property is created, the values there override whatever is set in beforeCreate()

OurBG's avatar
Level 1

@undeportedmexican I made it with a variable in data doneFetching: false which I set to true after the created hook and then check for it being true in the watcher. A little hacky, but works.

undeportedmexican's avatar

@OurBG One of the reasons I try not to use pre-built components are because of stuff like this 😅, they're normally not made to my needs.

I found something on the vue documentation that may help (), apparently, you can create the watcher on the fly, after you're done feeding setting the data on the created() hook:

created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }

So you could try something like:

created() {
        if(this.modelValue) {
            this.selected = this.isNumeric(this.modelValue) ? parseInt(this.modelValue) : this.modelValue;
        } else {
            let firstItem = this.options[Object.keys(this.options)[0]];
            this.selected = firstItem.id;
        }

		this.$watch('selected', (value) => {
 				this.handleInput(value);
 		})

		this.$watch('modelValue', (value) => {
 				this.selected = value;
            	this.handleInput(value);
 		})
    },

I mean, it still feels a little hacky, but less so than using the doneFetching flag IMO

Please or to participate in this conversation.