2019 年 1 月 11 日星期五

在子行中编辑父元素/子元素

祝大家新年快乐。去年,在专注于 DataTables 的其他方面(扩展和支持)时,我们有点儿忽略了博客,但在 2019 年,我们将更加定期地发布博客文章。为了振奋精神,我们将再次访问 Editor 父元素/子元素文章。当您有一个一对多的数据库结构时,父元素/子元素编辑是一个非常热门的话题,它可以让最终用户在单个页面上编辑两个表中的数据。

关于这篇文章的最常见问题是“当子行显示时,我们如何执行这项操作,而不是始终显示子表?” 这就是我们将在本文中探讨的内容。作为一个快速开始,这是我们想要实现的结果

名称 用户

父元素表

不是尝试修改前一个父元素/子元素编辑文章,而是我们将从第一原理开始,创建编辑表,这样更容易遵循。在这个过程中的第一步是创建父元素表,这就像在 Editor 示例 中找到的一样,是一个非常简单的 Editor 和 DataTable 组合,我们还将其与 行详细信息 DataTables 示例 相结合。

Editor Javascript

Editor Javascript 尽可能简单 - 一个将数据提交到服务器端脚本的字段(网站名称)

var siteEditor = new $.fn.dataTable.Editor( {
    ajax: '../php/sites.php',
    table: '#sites',
    fields: [ {
        label: 'Site name:',
        name: 'name'
    } ]
} );

DataTables Javascript

对于 DataTables 初始化,我们需要定义三个列

  • 子行显示/隐藏控件
  • 网站名称
  • 分配给该网站的用户数。对于此项,会使用一个 columns.render 函数,它只从数据数组中返回用户数。
var siteTable = $('#sites').DataTable( {
    order: [ 1, 'asc' ],
    ajax: '../php/sites.php',
    columns: [
        {
            className: 'details-control',
            orderable: false,
            data: null,
            defaultContent: '',
            width: '10%'
        },
        { data: 'name' },
        { data: 'users', render: function ( data ) {
            return data.length;
        } }
    ],
    select: {
        style:    'os',
        selector: 'td:not(:first-child)'
    },
    layout: {
        topStart: {
            buttons: [
                { extend: 'create', editor: siteEditor },
                { extend: 'edit',   editor: siteEditor },
                { extend: 'remove', editor: siteEditor }
            ]
        }
    }
} );

还要注意,select.selector 选项用于禁止对子行显示/隐藏控件列进行行选择 - 您不希望用户每次显示或隐藏子行时都更改行选择!

服务器端 (PHP)

对于父表,PHP 脚本从 `Site` 表中读取 `id` 和 `name` 列。`id` 列是必需的,以便在选择行时向子表服务器端脚本提交信息 - `id` 其实并没有在表中显示,同样也没有在编辑器表单中显示,因此使用 `set(false)` 作为一种安全措施。

这里需要注意的一点是使用 `Mjoin` 实例来获取关于每个站点使用的项目数的信息(“`Mjoin`”的意思是“多连接”)。在 编辑器手册 中可获得有关如何使用 `Mjoin` 的详细说明。如果你不需要或不希望在你的父表中显示计数列,则无需 `Mjoin`。

Editor::inst( $db, 'sites' )
    ->fields(
        Field::inst( 'id' )->set( false ),
        Field::inst( 'name' )->validator( 'Validate::notEmpty' )
    )
    ->join(
        Mjoin::inst( 'users' )
            ->link( 'sites.id', 'users.site' )
            ->fields(
                Field::inst( 'id' )
            )
    )
    ->process( $_POST )
    ->json();

子表

现在我们编写事件处理程序,它将显示和隐藏子行,因为它将定义显示和隐藏每个子行的数据表所需的功能。这是对 行详情示例 的一个小的修改。我们不会只将行数据传递给创建子表的方法,而是传递整个行实例,从而访问父表的完整数据表 API。另外,在本例中我们将使用一个销毁函数来整理子表,以确保在关闭子表时没有内存泄漏。

$('#sites tbody').on('click', 'td.details-control', function () {
    var tr = $(this).closest('tr');
    var row = siteTable.row( tr );

    if ( row.child.isShown() ) {
        // This row is already open - close it
        destroyChild(row);
        tr.removeClass('shown');
    }
    else {
        // Open this row
        createChild(row);
        tr.addClass('shown');
    }
} );

基于此,我们需要创建两个函数

  • 针对子表的数据表和编辑器功能的 `createChild`
  • 针对清理的 `destroyChild`

创建数据表

行详情示例 中,一个字符串被给定到 `child()`方法中以在子行中显示该字符串。但是,也可以传递 DOM 元素,因此我们可以通过简单地创建一个表元素、将其插入文档(使用 child().show())并将其初始化为常规数据表,以此创建数据表。

function createChild ( row ) {
    // This is the table we'll convert into a DataTable
    var table = $('<table class="display" width="100%"/>');

    // Display it the child row
    row.child( table ).show();

    // Initialise as a DataTable
    var usersTable = table.DataTable( {
        // ...
    } );
}

你可以从此处看到,当用户请求显示一个子行时,我们可以动态构建任何数据表。每次调用 `createChild()` 函数时都会创建一个新的唯一表,因此也无需为每个表创建 id。

销毁数据表

当关闭子行时,我们不想简单关闭它并保留其中的数据表占据内存 - 这将成为内存泄漏,如果最终用户打开和关闭足够多的行,最终会导致浏览器内存耗尽。相反,我们需要使用 `destroy()`方法来销毁表及其所有事件处理程序。我们还使用一小段 jQuery 从 DOM 中将其删除。

function destroyChild(row) {
    var table = $("table", row.child());
    table.detach();
    table.DataTable().destroy();

    // And then hide the row
    row.child.hide();
}

编辑器的配置

编辑器子行的配置与任何其他 基本编辑器 的配置几乎完全相同,因为它定义了数据、要编辑的表和可编辑字段的 Ajax URL。但是,在本例中,我们需要使用 `ajax.data`选项来向服务器发送父元素的 id(在本例中为站点 id),以便在 `WHERE` 条件中使用。我们还可以通过使用 `field.def`选项(已设置为父行的 id(在本例中为 `rowData.id`))来预先选择包含子表在内的站点,从而简化用户体验。

var rowData = row.data();
var usersEditor = new $.fn.dataTable.Editor( {
    ajax: {
        url: '/media/blog/2016-03-25/users.php',
        data: function ( d ) {
            d.site = rowData.id;
        }
    },
    table: table,
    fields: [ {
            label: "First name:",
            name: "users.first_name"
        }, {
            label: "Last name:",
            name: "users.last_name"
        }, {
            label: "Phone #:",
            name: "users.phone"
        }, {
            label: "Site:",
            name: "users.site",
            type: "select",
            placeholder: "Select a location",
            def: rowData.id
        }
    ]
} );

数据表的配置

DataTable 配置几乎与其他基本 DataTable 相同,同样修改为使用 ajax.data 发送父行的 ID,以确保仅加载属于该父行的行

var usersTable = table.DataTable( {
    pageLength: 5,
    ajax: {
        url: '/media/blog/2016-03-25/users.php',
        type: 'post',
        data: function ( d ) {
            d.site = rowData.id;
        }
    },
    columns: [
        { title: 'First name', data: 'users.first_name' },
        { title: 'Last name', data: 'users.last_name' },
        { title: 'Phone #', data: 'users.phone' },
        { title: 'Location', data: 'sites.name' }
    ],
    select: true,
    layout: {
        topStart: {
            buttons: [
                { extend: 'create', editor: usersEditor },
                { extend: 'edit',   editor: usersEditor },
                { extend: 'remove', editor: usersEditor }
            ]
        }
    }
} );

我们还使用 pageLength 使子行中的页面大小保持较小,但你可以根据需要进行设置。事实上,这突出了子行中的 Editor 和 DataTable 仅仅是常规组件,并且可以使用针对每种组件(如任何其他 Editor 和 DataTable)的任何选项、事件和 API 进行修改。

更新父表

修改子表后,可能需要在一个或多个行中更新父表的Users 计数。为此,我们使用 ajax.reload() 方法(通过传入函数的 row 变量进行访问 - 回想一下 DataTables 的链式 API 允许在所有级别访问顶级方法)

usersEditor.on( 'submitSuccess', function (e, json, data, action) {
    row.ajax.reload(function () {
        $(row.cell( row.id(true), 0 ).node()).click();
    });
} );

上面的第 3 行使用一个合成的 click 事件来触发子行的“显示”操作,因为 Ajax 重新加载会导致其自动关闭(它实际上是一个新添加的行)。这可以通过从服务器获取新的计数并使用 row().data() 为每个行更新数据来提高效率,但这超出了本文的范围。

服务器端 (PHP)

尽管在客户端我们可以随时初始化并显示多个子 Editor,但在服务器端我们只需要一个脚本,它将通过父 ID(即网站)区分 Editor。这是使用已提交的 site 参数和 WHERE 条件 来实现的

if ( ! isset($_POST['site']) || ! is_numeric($_POST['site']) ) {
    echo json_encode( [ "data" => [] ] );
}
else {
    Editor::inst( $db, 'users' )
        ->field( 
            Field::inst( 'users.first_name' ),
            Field::inst( 'users.last_name' ),
            Field::inst( 'users.phone' ),
            Field::inst( 'users.site' )
                ->options( 'sites', 'id', 'name' )
                ->validator( 'Validate::dbValues' ),
            Field::inst( 'sites.name' )
        )
        ->leftJoin( 'sites', 'sites.id', '=', 'users.site' )
        ->where( 'site', $_POST['site'] )
        ->process($_POST)
        ->json();
}

将它们连接在一起

只剩余一个步骤 - 当在父表中更新站点标签时,我们需要在子表中反映这一点。我们可以使用子表的 ajax.reload() 函数来实现这一点

function updateChild ( row ) {
    $('table', row.child()).DataTable().ajax.reload();
}

可以使用以下方式调用该函数

siteEditor.on('submitSuccess', function () {
    siteTable.rows().every(function () {
        if (this.child.isShown()) {
            updateChild(this);
        }
    });
} );

如果您想要查看本文中使用的完整 Javascript,请 点击此处。还可以使用 CSS,尽管它仅适用于行详细信息按钮和突出显示子行。

后续步骤

我希望你从这篇文章中了解到,子行显示不必局限于 基本示例 中的静态数据。你可以将任何所需的交互性添加到子行中,包括一个全面的可编辑 DataTable!