How to Use Cascading Select Lists in an Interactive Grid in Oracle APEX

Make a grid column's list depend on another column of the same row, clear it when the parent changes, and keep each pair valid with a composite key.

In an editable grid, one column often limits the choices of another. A user picks a category in a row, and the product list of that same row should show only the products of that category. Each row has its own pair of values, so the filter must work per row, not for the whole page.

Oracle APEX does this declaratively with a Cascading List of Values on the grid column. This guide builds a stock request grid where the Product list depends on the Category of the same row, keeps the pair valid with a composite foreign key, and adds a validation with a clear message.

The older guide How to Create Cascading Select Lists (Parent-Child LOVs) in Oracle APEX links page items on a form with Parent Item(s). This one works inside an Interactive Grid: the parent is a column, the LOV query uses the column name as a bind variable, and every row filters on its own.

Step 1: Create the Tables

The categories and products come from the PRODUCT_CATEGORIES and PRODUCTS tables of the earlier guides. If you start from scratch, create them with a few rows:

Example:

create table product_categories (
    category_id number generated by default as identity
                constraint product_categories_pk primary key,
    name        varchar2(50) not null
                constraint product_categories_name_uk unique
);

create table products (
    product_id number generated by default as identity
               constraint products_pk primary key,
    name       varchar2(60)  not null,
    category   varchar2(30)  not null,
    price      number(10, 2) not null
               constraint products_price_ck check (price >= 0)
);

insert into product_categories (name) values ('Computers');
insert into product_categories (name) values ('Displays');
insert into product_categories (name) values ('Accessories');
insert into product_categories (name) values ('Audio');
insert into product_categories (name) values ('Cameras');

insert into products (name, category, price) values ('Laptop Pro 14', 'Computers', 1450);
insert into products (name, category, price) values ('Laptop Air 13', 'Computers', 990);
insert into products (name, category, price) values ('Monitor 27', 'Displays', 320);
insert into products (name, category, price) values ('Monitor 32', 'Displays', 480);
insert into products (name, category, price) values ('Keyboard', 'Accessories', 45);
insert into products (name, category, price) values ('Mouse', 'Accessories', 25);
insert into products (name, category, price) values ('Docking Station', 'Accessories', 180);
insert into products (name, category, price) values ('Headset', 'Audio', 95);
commit;

Now add a unique key on PRODUCTS (PRODUCT_ID, CATEGORY) and create the table STOCK_REQUESTS, which stores both the category and the product of each request. Three sample requests follow:

Example:

alter table products
  add constraint products_id_category_uk unique (product_id, category);

create table stock_requests (
    request_id   number generated by default as identity
                 constraint stock_requests_pk primary key,
    category     varchar2(30) not null,
    product_id   number       not null,
    quantity     number(6)    not null
                 constraint stock_requests_qty_ck check (quantity > 0),
    requested_by varchar2(255) default on null
                 coalesce(sys_context('APEX$SESSION', 'APP_USER'),
                          sys_context('USERENV', 'SESSION_USER')),
    requested_on date default on null sysdate,
    constraint stock_requests_product_fk
        foreign key (product_id, category)
        references products (product_id, category)
);

create index stock_requests_product_ix on stock_requests (product_id, category);

insert into stock_requests (category, product_id, quantity)
select category, product_id, 5 from products where name = 'Laptop Air 13';
insert into stock_requests (category, product_id, quantity)
select category, product_id, 20 from products where name = 'Mouse';
insert into stock_requests (category, product_id, quantity)
select category, product_id, 3 from products where name = 'Monitor 32';
commit;

select r.request_id, r.category, p.name as product, r.quantity, r.requested_by
from   stock_requests r
join   products p on p.product_id = r.product_id
order by r.request_id;

Output:

Table PRODUCTS altered.

Table STOCK_REQUESTS created.

Index STOCK_REQUESTS_PRODUCT_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

   REQUEST_ID CATEGORY       PRODUCT             QUANTITY REQUESTED_BY
_____________ ______________ ________________ ___________ _______________
            1 Computers      Laptop Air 13              5 APXSOL
            2 Accessories    Mouse                     20 APXSOL
            3 Displays       Monitor 32                 3 APXSOL
  • PRODUCT_ID alone is already unique, so the unique key PRODUCTS_ID_CATEGORY_UK adds no new rule to PRODUCTS. It exists so that a foreign key can point at the pair.
  • The composite foreign key STOCK_REQUESTS_PRODUCT_FK references (PRODUCT_ID, CATEGORY). A request can only store a product together with that product's own category.
  • REQUESTED_BY and REQUESTED_ON are filled by DEFAULT ON NULL, so no trigger or process is needed. The sample rows show APXSOL because they were inserted from SQL, outside an APEX session.
  • The index STOCK_REQUESTS_PRODUCT_IX covers the foreign key columns.

Step 2: Create the Interactive Grid Page

In App Builder, open the application and click Create Page. On the Component tab, click Interactive Grid:

Create a Page wizard with the Interactive Grid tile marked with a red box
The Interactive Grid tile.

Enter Page Number 250 and Name Stock Requests. Keep Data Source Local Database and Source Type Table, enter STOCK_REQUESTS as Table / View Name, and turn on Editing Enabled:

Create Interactive Grid wizard with Page Number 250, Name Stock Requests, Table / View Name STOCK_REQUESTS and Editing Enabled on, marked with red boxes
An editable grid on STOCK_REQUESTS.

Scroll down to Navigation. Turn off Use Breadcrumb, keep Use Navigation on with Create a new entry, and set Icon to fa-cubes. Click Next:

Create Interactive Grid wizard Navigation section with Use Breadcrumb off, Use Navigation on and Icon fa-cubes marked with red boxes
A menu entry for the page.

Primary Key Column 1 is already REQUEST_ID (Number). Click Create Page:

Create Interactive Grid wizard with Primary Key Column 1 REQUEST_ID (Number) marked with a red box
The primary key of the grid.

The wizard creates the grid region with one column per table column, and the process Stock Requests - Save Interactive Grid Data that saves the changes. At this point, CATEGORY is a Text Field and PRODUCT_ID a Number Field.

Step 3: Turn Category into a Select List

In Page Designer, expand Stock Requests > Columns and select CATEGORY. Set Type to Select List. In the List of Values group, set Type to SQL Query and enter this query:

Example:

select name as display_value,
       name as return_value
from   product_categories
order by name

Keep Display Null Value on and set Null Display Value to - Select Category -:

Page Designer showing the CATEGORY column with Type Select List, List of Values Type SQL Query with the query, and Null Display Value - Select Category - marked with red boxes
The parent column lists the categories.

The table stores the category name, so the name is both the display and the return value.

Step 4: Make Product Depend on Category

Select the column PRODUCT_ID. Set Type to Select List, Heading to Product, and the heading Alignment to start (left):

Page Designer showing the PRODUCT_ID column with Type Select List and Heading Product with Alignment start marked with red boxes
The child column becomes a select list.

In the Layout group, set Column Alignment to start too. The wizard aligned the numbers to the end, which looks odd for product names:

Page Designer showing the Layout group of PRODUCT_ID with Column Alignment start marked with a red box
Product names aligned left.

In the List of Values group, set Type to SQL Query and enter this query. :CATEGORY is the CATEGORY column of the same row:

Example:

select name as display_value,
       product_id as return_value
from   products
where  category = :CATEGORY
order by name

Set Null Display Value to - Select Product -. Then, in the Cascading List of Values group, set Parent Column(s) to CATEGORY and keep Parent Required on:

Page Designer showing PRODUCT_ID with List of Values Type SQL Query, the query with where category = :CATEGORY, Null Display Value - Select Product - and Parent Column(s) CATEGORY marked with red boxes
The cascading settings of the child column.

Parent Column(s) does two things. It sends the row's CATEGORY value with the LOV request, so the bind variable has a value. And when the category of a row changes, APEX clears the product of that row and loads the new list.

With Parent Required on, the product list stays empty while the row has no category. It then shows only - Select Product -.

Step 5: Show the Default Columns as Display Only

The wizard made REQUESTED_BY and REQUESTED_ON editable and required, because they are NOT NULL. The database fills them, so users should only see them. Select REQUESTED_BY and set Type to Display Only:

Page Designer showing the REQUESTED_BY column with Type Display Only marked with a red box
The column is shown but not edited.

In the Source group, turn on Query Only. The grid process then leaves the column out of INSERT and UPDATE, and DEFAULT ON NULL fills it:

Page Designer showing the Source group of REQUESTED_BY with Query Only on, marked with a red box
The database sets the value.

Make the same two changes on REQUESTED_ON. Then click Save.

Step 6: Add a Validation for the Pair

The cascading list guides the user, but the browser can still send any product ID. The composite foreign key already stops a wrong pair. In a first test without a validation, a forced product failed with this message in the grid:

Output:

ORA-02291: integrity constraint (APXSOL.STOCK_REQUESTS_PRODUCT_FK) violated - parent key not found

That keeps the data safe, but the message is not for users. In the Processing tab, create a validation named Product belongs to category. Set Editable Region to Stock Requests, Type to Rows returned, and enter this query:

Example:

select null
from   products
where  product_id = :PRODUCT_ID
and    category   = :CATEGORY

Set Error Message to Choose a product of the selected category. and Associated Column to PRODUCT_ID:

Page Designer showing the validation Product belongs to category with Editable Region Stock Requests, Type Rows returned, the query, Error Message and Associated Column PRODUCT_ID marked with red boxes
The validation runs once per changed row.

Because Editable Region is set, the validation runs for each grid row, and :PRODUCT_ID and :CATEGORY hold that row's values. In the Server-side Condition group, Execution Scope is For Created and Modified Rows by default, so deleted rows are not checked:

Page Designer showing the Server-side Condition group with Execution Scope For Created and Modified Rows marked with a red box
The default scope is enough here.

Step 7: Test It

ORDER_EDITOR opened Stock Requests. The grid showed the three sample requests:

Stock Requests interactive grid with three rows: Computers Laptop Air 13, Accessories Mouse and Displays Monitor 32
The Category and Product columns show names.

Add Row inserted an empty row. Before a category was chosen, the Product list held only - Select Product -. After Audio was chosen as the category, the Product list held - Select Product - and Headset, the only Audio product. Headset and quantity 4 were entered:

New grid row with Category Audio, Product Headset and Quantity 4 marked with a red box
The product list follows the category of the row.

Then the category of the same row was changed to Accessories. APEX cleared Headset at once and marked the empty Product cell as required. The Product list now held Docking Station, Keyboard, Mouse and Webcam HD:

Grid row with Category Accessories, Product reset to - Select Product - with an error icon and Quantity 4, marked with a red box
Changing the parent clears the child.

Keyboard was chosen, the quantity set to 10, and Save clicked. APEX showed Changes saved, and the new row showed ORDER_EDITOR as Requested By:

Stock Requests grid with the message Changes saved and the new row Accessories, Keyboard, 10, ORDER_EDITOR marked with a red box
The row is saved and stamped by the database.

Example:

select r.request_id, r.category, p.name as product, r.quantity,
       r.requested_by, to_char(r.requested_on, 'YYYY-MM-DD') as requested_on
from   stock_requests r
join   products p on p.product_id = r.product_id
order by r.request_id;

Output:

   REQUEST_ID CATEGORY       PRODUCT             QUANTITY REQUESTED_BY    REQUESTED_ON
_____________ ______________ ________________ ___________ _______________ _______________
            1 Computers      Laptop Air 13              5 APXSOL          2026-10-14
            2 Accessories    Mouse                     20 APXSOL          2026-10-14
            3 Displays       Monitor 32                 3 APXSOL          2026-10-14
           22 Accessories    Keyboard                  10 ORDER_EDITOR    2026-10-14

A forced value came last. In the browser console, the grid model of the Displays row was given the product Laptop Pro 14, which belongs to Computers, and Save was clicked. The validation stopped the save:

Grid with the error notification Choose a product of the selected category. and the Laptop Pro 14 cell in the Displays row marked with red boxes
A product from another category is refused.

Example:

select r.request_id, r.category, p.name as product
from   stock_requests r
join   products p on p.product_id = r.product_id
where  r.category = 'Displays';

insert into stock_requests (category, product_id, quantity)
select 'Computers', product_id, 1 from products where name = 'Mouse';

Output:

   REQUEST_ID CATEGORY    PRODUCT
_____________ ___________ _____________
            3 Displays    Monitor 32

Error starting at line : 6 File @ b27/02-checks.sql
In command -
insert into stock_requests (category, product_id, quantity)
select 'Computers', product_id, 1 from products where name = 'Mouse'
Error report -
ORA-02291: integrity constraint (APXSOL.STOCK_REQUESTS_PRODUCT_FK) violated -
parent key not found
Help: https://docs.oracle.com/error-help/db/ora-02291/

The Displays row still holds Monitor 32. The INSERT shows that the foreign key refuses a wrong pair from any program, not only from this page.

Things to Know

  • Use the column name as the bind variable in the child LOV query, such as :CATEGORY. In a grid, it holds the value of the current row.
  • Parent Column(s) clears the child value when the parent changes. Users must pick the product again, as the test showed.
  • The composite foreign key has a cost: a product cannot move to another category while requests point to it. Moving Mouse to Computers failed with ORA-02292 in this example:

Example:

update products
set    category = 'Computers'
where  name = 'Mouse';

Output:

Error starting at line : 1 File @ b27/03-move-product.sql
In command -
update products
set    category = 'Computers'
where  name = 'Mouse'
Error report -
ORA-02292: integrity constraint (APXSOL.STOCK_REQUESTS_PRODUCT_FK) violated -
child record found
Help: https://docs.oracle.com/error-help/db/ora-02292/
  • If products must move between categories, drop the composite key and keep only the validation, or do not store the category in the request at all.
  • Keep the validation even with the foreign key: the key protects the data, the validation gives the user a clear message on the right cell.

Related Guides

Conclusion

To build cascading select lists in an Interactive Grid in Oracle APEX, make both columns Select Lists, use the parent column as a bind variable in the child LOV query, and set Parent Column(s) on the child. Each row then filters its own list, and a change of the parent clears the child.

Protect the pair in the database with a composite foreign key, and add a row validation for a clear message. DEFAULT ON NULL with Query Only columns fills who and when without any code.

Get more Vinish.dev tutorials on Google

See my articles more often when you search Google.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted