How to handle complex data with Settings API

When you call register_setting, the third parameter is the sanitize callback function. You can do whatever data manipulation you need in that function, in order to arrange the user input into the format you need for your code. So after the data validation (integers are integers, sanitize text fields, etc.), you can retrieve the current option value and add or delete from there according to the user input.

You can also do some manipulation in javascript before the data is submitted, but be aware that javascript can be turned off, so you would have to deal with two different types of input. Therefore, this usually isn’t a good idea.

The following code uses javascript in the page to create and delete items in a list, and puts the entire list into one input when the Save button is clicked. The plugin is dependent on the javascript for the content of that one input, and saves it in JSON format (no manipulation in PHP, only in javascript). The sanitize callback creates an array of arrays so that the user can save multiple named lists. It’s not quite the same as your question, but similar and tested.

register_setting( 'example_group', 'example_option_name', 'example_settings_validation' );

function example_settings_defaults( $theme ) {   // provide default option values
    if ( 'weaver-ii' == $theme )
        $page="themes.php?page=WeaverII";
    else if ( 'aspen' == $theme )
        $page="themes.php?page=Aspen";
    else $page="";
    $label="new";
    $used = sanitize_key( $label );
    $entry = array( 'color_class'=>'color', 'user_label'=>$label, 'last_modified'=>'', 'tie_list'=>json_encode( array() ) );
    $new = array( 'theme_page'=>$page, 'last_used'=>$used, 'entries'=>array() );
    $new['entries'][$used] = $entry;
    return $new;
}

function example_settings_validation( $input ) {    // called by Settings API on a Save
    $clean = get_option( 'example_option_name', example_settings_defaults() );
    $label = sanitize_text_field( $input['user_label'] );
    if ( isset( $input['action_delete'] ) ) {  // user clicked Delete list
        if ( isset( $input['user_label'] ) ) {
            $key = sanitize_key( $label );
            unset( $clean['entries'][$key] );
            if ( 0 == count( $clean['entries'] ) ) {
                $new = example_settings_defaults( 'xx');
                $clean['entries'] = $new['entries'];
                $clean['last_used'] = $new['last_used'];
            }
            else if ( $key == $clean['last_used'] ) {  // check if they deleted last used
                $keys = array_keys( $clean['entries'] );
                $clean['last_used'] = $keys[0];
            }
            add_settings_error('deleteID', 'action_delete', $label.__(' entry deleted.', 'xmpl'), 'updated');
        }
    }
    if ( isset( $input['action_save'] ) ) {  // user clicked Save
        $clean['theme_page'] = sanitize_text_field( $input['theme_page'] );
        $entry = array();
        $entry['color_class'] = sanitize_html_class( $input['color_class'] );
        $entry['tie_list'] = sanitize_text_field( $input['tie_list'] );
        $entry['last_modified'] = current_time( 'mysql' );
        if ( empty( $label ) ) $label="new";
        $entry['user_label'] = $label;
        $clean['last_used'] = sanitize_key( $label );
        $clean['entries'][$clean['last_used']] = $entry;
        add_settings_error('saveID', 'action_save', $label.
            sprintf( _x(' entry saved at %s','MySQL timestamp', 'xmpl'), $entry['last_modified'] ), 'updated');
    }
    return $clean;
}