Back to Blog

Implementing Interdependent Fields with Admiral

Content
When building admin panels, it's often necessary to dynamically update the value of one field based on the selection in another. These are known as interdependent fields, and they help streamline form interactions and improve overall data management UX.

Common Use Cases for Interdependent Fields

Interdependent fields are widely used in systems where data is structured and context-dependent:
  • Booking platforms – selecting a specialist dynamically filters available services and time slots;
  • Content management systems – choosing a category loads the relevant subcategories and associated input fields;
  • E-commerce platforms – picking a brand filters the available models and their specifications.

Simplifying Development with Admiral

To efficiently configure interdependent fields, we offer using Admiral – our frontend admin panel framework for building CRUD interfaces. Admiral enables you to quickly define field dependencies without redundant server requests.
Key Benefits of Admiral:
  • Prebuilt components – save time by reusing ready-to-go UI elements;
  • Flexible customization – easily tailor behavior to your product’s specific needs;
  • Laravel compatibility – smooth integration with the popular PHP framework.
You can install Admiral via this link.

How to Configure Interdependent Fields in Admiral

Let’s go through a practical example of setting up interdependent fields using Admiral.

Step 1: Configure the Controller

On the backend, each interdependent field should implement an ajaxSelect method. This method accepts parameters from related fields and returns the corresponding filtered data.
Example

public function ajaxSelect(Request $request, $field, RecordValues $values)
{
   return match ($field) {
       'client_id' => $values->clients($request->input('query'), $request->input('location_id')),
       'location_id' => $values->locations($request->input('query')),
       'services' => $values->services($request->input('query'), $request->input('location_id'), $request->input('master_id')),
       'master_id' => $values->masters($request->input('query'), $request->input('location_id')),
       default => collect(),
   };
}

Step 2: Define Data Fetching Methods

For each dependent field, you need to define methods that generate the appropriate dataset based on the input parameters.
Example: Fetching Clients

public function clients(string $query, int $locationId)
{
   return Client::query()
       ->when($query, fn ($q) => $q->where('name', 'ilike', "%{$query}%"))
       ->where('location_id', $locationId)
       ->limit(20)
       ->get(['id', 'first_name', 'last_name'])
       ->map(fn (Client $client) => [
           'value' => $client->id,
           'label' => $client->full_name,
       ]);
}
In this example, the list of clients is filtered based on the location specified in the location_id field.

Step 3: Setting Up Dependencies for Other Fields

Other dependent fields can be configured in a similar fashion. You simply need to pass the relevant values from the dependent fields to the corresponding data-fetching methods.

Step 4: Defining Interdependent Fields on the Client Side in Admiral

Once the backend logic is in place, you can move on to configuring the frontend behavior within the (createCrud) function. Specifically, you'll be working inside the index.form.create.fields and index.form.edit.fields.
Typically, we create a dedicated component to render the fields. This approach helps isolate the logic and prevents the createCrud function from becoming overly complex or difficult to maintain.

export const CRUD = createCRUD({
   path: '/records',
   resource: 'records',
   form: {
       create: {
           fields: <RecordsFields />,
       },
       edit: {
           fields: <RecordsFields />,
       },
   },
})
To implement the component, you'll need to render the necessary fields and disable any fields that depend on missing values using the disabled prop.
You’ll also need the useUpdateEffect hook, which will trigger data fetching once a related value appears in the form. Unlike the standard useUpdateEffect, useEffect only runs on updates, not on the initial render.
Below there is a basic implementation of a component with interdependent fields. Inline comments explain the logic in more detail:

import React from 'react'
import { AjaxSelectInput, FieldValues, useForm, useUpdateEffect } from '@devfamily/admiral'
import api from '@/src/config/api'


const resource = 'records'


const RecordsFields = () => {
   const { values, setValues, setOptions } = useForm()


   useUpdateEffect(() => {
       setValues((prevValues: FieldValues) => ({
           ...prevValues,
	     // Reset the selected master when you change the location.
           client_id: null,
       }))


       const fetchOptions = async () => {
           if (!values.location_id) return


           try {
               const clientOptions = await api.getAjaxSelectOptions(resource, 'client_id', {
                   location_id: values.location_id,
               })


              
               setOptions((prevOptions: FieldValues) => ({
                   ...prevOptions,
                   client_id: clientOptions,
               }))
           } catch (error) {
               console.error('Failed to fetch options:', error)
           }
       }


       fetchOptions()
       // When the location ID appears in the form, this hook is triggered and then the data is fetched to get the list of clients.
   }, [values.location_id])


   return (
       <>
           <AjaxSelectInput
               label="Location"
               name="location_id"
               placeholder="Location"
               required
               allowClear
               fetchOptions={(field, query) =>
                   api.getAjaxSelectOptions(resource, field, {
                       query,
                   })
               }
           />
           <AjaxSelectInput
               label="Client"
               name="client_id"
               placeholder="Client"
               required
               allowClear
               fetchOptions={(field, query) => {
                   return api.getAjaxSelectOptions(resource, field, {
                       query,
                       location_id: values.location_id,
                   })
               }}
               // The field will be locked until we select the locations in the top  box.
               disabled={!values.location_id}
           />
       </>
   )
}


//The wrapper is needed so that useUpdateEffect is not triggered after the form is initialized.
const RecordsFieldsWrapper = () => {
   const { options } = useForm()


   return !!Object.keys(options).length ? <RecordsFields /> : <>
}


export default RecordsFieldsWrapper
You can see the result in action in the demo video.

Conclusion

Using Admiral to implement interdependent fields significantly improves the development experience when building admin panels. Our admin interface provides out-of-the-box tools for rapidly configuring UI elements and simplifying data management workflows.
Give Admiral a try in your project and experience the benefits of a flexible, robust, and developer-friendly solution.
Still have questions about developing interdependent fields in Admiral? We are here to help!

You may also like: