Insert Rows in a Dynamic Table

You can configure a dynamic table to insert a child row beneath a selected parent row.

Building on the employee table setup in Add a Dynamic Table to a Page, this example uses Public Workers to display each employee’s phone contacts as child rows. You’ll arrange the service data into a hierarchy, with employees as parent rows, and add fields to the layout to display their contact details. Users select an employee, click Add Phone Contact, and enter the phone type and number.

Use a Buffering Tree Data Provider to hold pending insertions and an underlying tree data provider to supply the hierarchical data. The table displays buffered changes alongside existing rows. This example retrieves employees and their phones from the same Public Workers resource used by the publicWorkersListSDP example in Create Variables to Temporarily Store Data in a Buffer, then uses a Mutable Array Tree Data Provider to present them as a tree. An Array Tree Data Provider can also be used with a Buffering Tree Data Provider; preserve its key and data-update conventions when adapting the example.

Note: Adding a row to the buffer does not save it to the HR service. This example demonstrates local child-row insertion only. Public Workers is read-only. Persisting a new phone contact requires a separate writable service and the necessary permissions; it is outside the scope of this example.

Before you begin:

Retrieve and Arrange the HR Data

  1. Create a page variable named employees, of type object[], with an empty array as its default value.

  2. Create a JavaScript action chain for the page’s vbEnter event. Use a Call REST action to retrieve worker identities and their phone contacts. Map the response into a common row shape, with each employee’s contacts in children:

    const { $variables } = context;
    const response = await Actions.callRest(context, {
      endpoint: 'oracle_hcm_centralcomponentspublicUI:publicWorkers/getall_publicWorkers',
      uriParams: {
        limit: 2,
        expand: 'phones',
        onlyData: true,
        fields: 'PersonId,PersonNumber,DisplayName;phones:PhoneId,PhoneType,PhoneTypeMeaning,PhoneNumber',
      },
    });
    if (!response.ok) {
      return; // Handle the failed request in your application's normal error flow.
    }
    $variables.employees = response.body.items.map(worker => ({
      id: worker.PersonId,
      DisplayName: worker.DisplayName,
      PersonNumber: worker.PersonNumber,
      children: (worker.phones?.items || []).map(phone => ({
        id: phone.PhoneId,
        DisplayName: 'Phone',
        contactType: phone.PhoneTypeMeaning || phone.PhoneType,
        contactValue: phone.PhoneNumber,
      })),
    }));

    Use the endpoint identifier generated for your Public Workers connection; the identifier above is the one used by this example. The request is limited to two workers for the demonstration. Adapt the query and paging to your application’s requirements. Use translation resources for display text such as Phone.

    This extends the flat employee list with phone child rows; the REST response is mapped into an array for the tree provider rather than bound directly through publicWorkersListSDP. Preserve the service identifiers as returned, including string PersonId values.

    Keep an empty children array for employees with no contacts. If the table’s Quick Start created a Service Data Provider, the table will be rebound to the buffering provider below; avoid fetching the same data through both paths.

Configure the Tree Data Providers

For an overview of the available tree types and their constructor parameters, see Create Variables for Tree Data Providers. The steps below configure the mutable provider and buffering wrapper for this employee/contact example.

  1. Create a variable named employeeTree and select Mutable Array Tree Data Provider as its type. In the Properties pane, click Assign under Constructor Parameters and map the parameters as follows:

    Parameter Value
    data $variables.employees
    keyAttribute The string id
    options The object { "useKeyPaths": "on" }

    With key paths enabled, each key identifies a row in the hierarchy. For an employee with ID 1001, the parent key is [1001]; a contact with ID 101 beneath that employee has key [1001, 101]. Keep keys as arrays rather than converting them to strings. See MutableArrayTreeDataProvider.

  2. Create a variable named employeeBuffer and select Buffering Tree Data Provider as its type. Map its dataProvider Constructor Parameter to $variables.employeeTree.instance. Leave the optional options parameter unmapped.

  3. In the Page Designer, select the dynamic table. In its Data tab, bind data to $variables.employeeBuffer.instance.

    Keep the table’s Public Workers metadata and rule-set bindings. The table now displays the hierarchical data and pending insertions through the buffering provider.

Identify the Selected Employee

  1. Create an object type named employeeSelection with the fields rowKey (any[]) and rowData (object). Create the following page variables:

    Variable Type Initial value
    selectedEmployee employeeSelection { "rowKey": [], "rowData": {} }
    contactDraft object { "contactType": "Work phone", "contactValue": "" }
    newContactCounter number 0
  2. Configure the table for single row selection (selection-mode.row="single"). Add a listener for firstSelectedRowChanged and create an action chain with the parameters rowKey (an array of any type) and rowData (an object).

    Map the event values to the parameters:

    Chain parameter Expression
    rowKey $event.detail.value ? $event.detail.value.key : null
    rowData $event.detail.value ? $event.detail.value.data : null
  3. Store only a parent selection. In a JavaScript chain, declare both parameters explicitly, including their JSDoc entries:

    /**
     * Store a selected employee, or clear the selection for a child row.
     * @param {Object} context Page context.
     * @param {Object} params Selected row.
     * @param {any[]} params.rowKey Selected tree key path.
     * @param {Object} params.rowData Selected row data.
     */
    async run(context, { rowKey, rowData }) {
      context.$variables.selectedEmployee =
        Array.isArray(rowKey) && rowKey.length === 1
          ? { rowKey, rowData }
          : { rowKey: [], rowData: {} };
    }

    This example has two levels: employees have one-part keys and contacts have two-part keys. Use that key structure to identify the parent. The selection event’s row data contains the displayed fields, so do not depend on a discriminator that is absent from the layout. Clearing the selection or selecting a child clears rowKey. Do not bind the table’s current-row property to selectedEmployee.

Add the Phone Contact

  1. Add a button labeled Add Phone Contact. Bind its disabled property to [[ !$variables.selectedEmployee.rowKey.length ]].

  2. Add a dialog with ID add-contact-dialog, titled Add Phone Contact, and set cancel-behavior to escape. Place two required Input Text components in an oj-validation-group with ID contact-validation:

    Input Two-way value binding
    Phone Type ``
    Phone Number ``

    Add Cancel and Add buttons to the footer. Use translation resources for labels and other display text.

    Add Phone Contact dialog with Phone Type set to Work phone and Phone Number set to 202-555-0187

  3. Create an action chain for Add Phone Contact. Return if selectedEmployee.rowKey.length is not 1. Otherwise, reset contactDraft to { contactType: 'Work phone', contactValue: '' }, then use a Call Component action to call open on #add-contact-dialog.

  4. For Cancel, use a Call Component action to call close on #add-contact-dialog, without adding a row. Cancel and Escape leave the buffer unchanged. Resetting the draft whenever the dialog opens prevents canceled values from carrying over.

  5. For Add, create a JavaScript action chain. Use the following code in its run(context) method. The chain uses the standard Actions import (vb/action/actions).

    const { $variables } = context;
    const selected = $variables.selectedEmployee;
    if (selected.rowKey.length !== 1) {
      return;
    }
    const selector = '#contact-validation';
    const valid = await Actions.callComponentMethod(context, {
      selector, method: 'getProperty', params: ['valid'],
    });
    if (valid !== 'valid') {
      await Actions.callComponentMethod(context, { selector, method: 'showMessages' });
      await Actions.callComponentMethod(context, {
        selector, method: 'focusOn', params: ['@firstInvalidShown'],
      });
      return;
    }
    const id = -(++$variables.newContactCounter);
    const draft = $variables.contactDraft;
    $variables.employeeBuffer.instance.addItem({
      metadata: { key: [...selected.rowKey, id] },
      data: {
        id,
        DisplayName: draft.contactType,
        contactType: draft.contactType,
        contactValue: draft.contactValue,
      },
    });
    await Actions.callComponentMethod(context, {
      selector: '#add-contact-dialog', method: 'close',
    });

    The key extends the selected employee’s key path; for example, [1001, -1]. Negative IDs are temporary identifiers for this local example. Use a key scheme that cannot collide with existing rows.

    Add the row through the buffer. Do not also append it to employees, which would duplicate the insertion. See BufferingTreeDataProvider.

Verify the Result and Implement Saving

Preview the page, select an employee, and click Add Phone Contact. Enter Work phone and a demonstration value such as 202-555-0187, then click Add. Expand the employee row if necessary. The new phone contact should appear beneath the selected employee, alongside any existing phone contacts, while the other employee’s contacts remain unchanged.

Employee phone contacts table showing two pending Work phone contacts beneath the selected employee

Also check the following:

Public Workers and its phone child collection are read-only; do not send create requests to this resource. If your application must save new contacts, use a separate writable service for which the user has the required permissions, and implement a save action that handles success and failure. getSubmittableItems and setItemStatus support that workflow; see BufferingTreeDataProvider. The tree’s common display shape is not an HR create-request payload: map each item to the correct child endpoint, parent identity, and service attributes. Do not report a contact as saved merely because it appears in the table; verify saving by reloading data from the backend.