Project

Edit Mode For Phone Numbers

Final UI is shown below:

Phone book edit mode

When we click the phone icon for a person, its row is expanded and all phone numbers are shown. Then we can delete any phone by clicking the delete icon. We can add a new phone from the inputs at the bottom of the expanded area.

PhoneBook Component

The main changes are in the index.tsx file. We added expandable rows to the Ant Design Table component to show phone numbers for each person.

State Management

const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]);
const [newPhoneNumber, setNewPhoneNumber] = useState<string>("");
const [newPhoneType, setNewPhoneType] = useState<PhoneType>(PhoneType.Mobile);

Actions Column

The Actions column includes the phone button that toggles row expansion:

{
  title: L("Actions"),
  key: "actions",
  width: 150,
  render: (_text: string, record: PersonListDto) => (
    <>
      {isGranted("Pages.Administration.PhoneBook.DeletePerson") && (
        <button
          onClick={() => deletePerson(record)}
          title={L("Delete")!}
          className="btn btn-sm btn-icon btn-bg-light btn-active-color-danger me-2"
        >
          <i className="fa fa-trash"></i>
        </button>
      )}
      <button
        onClick={() => {
          const keys = expandedRowKeys.includes(record.id!)
            ? expandedRowKeys.filter((k) => k !== record.id)
            : [...expandedRowKeys, record.id!];
          setExpandedRowKeys(keys);
          setNewPhoneNumber("");
          setNewPhoneType(PhoneType.Mobile);
        }}
        title={L("AddPhone")!}
        className="btn btn-sm btn-icon btn-bg-light btn-active-color-primary"
      >
        <i className="fa fa-phone"></i>
      </button>
    </>
  ),
}

Expanded Row Render

The expanded row displays the phone list and add phone form:

const expandedRowRender = (record: PersonListDto) => {
  return (
    <div className="p-4">
      <div className="mb-3">
        <strong>{L("PhoneNumbers")}:</strong>
        <ul className="list-unstyled mt-2 mb-3">
          {record.phones && record.phones.length > 0 ? (
            record.phones.map((phone: PhoneInPersonListDto) => (
              <li key={phone.id} className="d-flex align-items-center gap-3 mt-2">
                <span className="badge bg-light text-dark">
                  {getPhoneTypeLabel(phone.type!)}
                </span>
                <span>{phone.number}</span>
                <button
                  className="btn btn-sm btn-icon btn-bg-light btn-active-color-danger"
                  title={L("Delete")!}
                  onClick={() => deletePhone(phone.id!, record.id!)}
                >
                  <i className="fa fa-trash"></i>
                </button>
              </li>
            ))
          ) : (
            <li>{L("NoPhoneNumbersFound")}</li>
          )}
        </ul>
      </div>

      <div className="row">
        <div className="col-md-4">
          <input
            value={newPhoneNumber}
            onChange={(e) => setNewPhoneNumber(e.target.value)}
            placeholder={L("PhoneNumber")!}
            className="form-control"
          />
        </div>
        <div className="col-md-4">
          <select
            value={newPhoneType}
            onChange={(e) => setNewPhoneType(Number(e.target.value))}
            className="form-select"
          >
            <option value={PhoneType.Mobile}>{L("PhoneType_Mobile")}</option>
            <option value={PhoneType.Home}>{L("PhoneType_Home")}</option>
            <option value={PhoneType.Business}>{L("PhoneType_Business")}</option>
          </select>
        </div>
        <div className="col-md-4">
          <button
            className="btn btn-success"
            onClick={() => addPhone(record.id!)}
          >
            <i className="fa fa-check"></i> {L("Save")}
          </button>
        </div>
      </div>
    </div>
  );
};

Table Configuration

The Table component is configured with the expandable prop:

<Table
  rowKey={(record) => record.id!}
  size="small"
  bordered
  columns={columns}
  loading={loading}
  dataSource={people}
  pagination={false}
  scroll={{ x: true }}
  expandable={{
    expandedRowKeys: expandedRowKeys,
    expandedRowRender: expandedRowRender,
    showExpandColumn: false,
    onExpand: (expanded, record) => {
      const keys = expanded
        ? [...expandedRowKeys, record.id!]
        : expandedRowKeys.filter((k) => k !== record.id);
      setExpandedRowKeys(keys);
    },
  }}
/>

Phone Management Functions

Functions to add and delete phone numbers:

const addPhone = async (personId: number) => {
  if (!newPhoneNumber.trim()) {
    abp.notify.warn(L("PhoneNumberRequired"));
    return;
  }

  const input = new AddPhoneInput({
      personId: personId,
      number: newPhoneNumber,
      type: newPhoneType,
    });
    await personService.addPhone(input);
    abp.notify.success(L("PhoneSuccessfullyAdded"));
    setNewPhoneNumber("");
    setNewPhoneType(PhoneType.Mobile);
    setExpandedRowKeys([]);
    getPeople();
};

const deletePhone = (phoneId: number, personId: number) => {
  modal.confirm({
    title: L("AreYouSure"),
    content: L("PhoneDeleteWarningMessage"),
    okText: L("Yes"),
    cancelText: L("Cancel"),
    onOk: async () => {
        await personService.deletePhone(phoneId);
            abp.notify.success(L("SuccessfullyDeleted"));
            setPeople((prevPeople) =>
            prevPeople.map((p) => {
                if (p.id === personId && p.phones) {
                p.phones = p.phones.filter((phone) => phone.id !== phoneId);
                }
                return p;
            })
        );
    },
  });
};

const getPhoneTypeLabel = (type: PhoneType): string => {
  switch (type) {
    case PhoneType.Mobile:
      return L("PhoneType_Mobile");
    case PhoneType.Home:
      return L("PhoneType_Home");
    case PhoneType.Business:
      return L("PhoneType_Business");
    default:
      return "";
  }
};

Localization

Add these localization strings to your PhoneBookDemo.xml file:

<text name="AddPhone">Add Phone</text>
<text name="PhoneNumbers">Phone Numbers</text>
<text name="NoPhoneNumbersFound">This person has no phone numbers.</text>
<text name="PhoneDeleteWarningMessage">Are you sure you want to delete this phone number?</text>
<text name="PhoneSuccessfullyAdded">Phone number successfully added</text>
<text name="PhoneNumberRequired">Phone number is required</text>
<text name="PhoneType_Mobile">Mobile</text>
<text name="PhoneType_Home">Home</text>
<text name="PhoneType_Business">Business</text>

Next

Contributors


Last updated: December 17, 2025 Edit this page on GitHub
In this document