2016 年 3 月 25 日星期五

编辑器中的父/子编辑

当使用数据库中的数据时,使用数据中父/子表示是一个相当常见的模式。它使用户能够选择父表中一行,然后显示该记录中关联的子数据。当使用有强引用(即连接表)的表时,此界面特别有用,因为它可向最终用户显示一个非常简单但功能强大、信息丰富的界面。

使用 编辑器设置父/子编辑是相当常见的问题,因此在本文中,我将详细介绍如何执行此操作。您会看到,可以通过使用 DataTables API事件 轻松实现它。

在这篇文章中,我将使用站点表作为主表,使用用户表作为子表,其中每个用户都有分配给自己的站点。当我们在站点表中选择不同的行时,数据将根据需要加载到用户表中。两个表都是完全可编辑的,下面显示结果的演示。

名称 用户
姓氏 电话号码 位置

父表

创建父/子显示的第一步是创建父表。这是一个非常简单的编辑器和 DataTable 的组合,您可以在 编辑器示例中找到它。

编辑器 Javascript

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

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

DataTables Javascript

DataTable 初始化同样简单 - 我们有两列

  • 站点名称
  • 分配给该站点用户的数量。为此,columns.render 函数将从数据数组中返回用户的数量。

请注意,select.style 用于一次只允许在表中选择一个项目。完全有可能允许选择多行,但为了本文的简单,单行就足够了。

var siteTable = $('#sites').DataTable( {
    ajax: '../php/sites.php',
    columns: [
        { data: 'name' },
        { data: 'users', render: function ( data ) {
            return data.length;
        } }
    ],
    select: {
        style: 'single'
    },
    layout: {
        topStart: {
            buttons: [
                { extend: 'create', editor: siteEditor },
                { extend: 'edit',   editor: siteEditor },
                { extend: 'remove', editor: siteEditor }
            ]
        }
    }
} );

服务器端 (PHP)

最后,对于父表,PHP 脚本当中读取了网站表中的 `id` 和 `name` 列。`id` 列是必须的,以便在选择行时能够将信息提交给子表服务器端脚本 - `id` 实际上并未在表中显示,同样也不在 Editor 表单中(因此出于安全原因使用了 `set(false)`)。

这里值得注意的一点是使用了 `Mjoin` 实例来了解使用每个网站的项的数量(`Mjoin` 是“多重联接”的缩写)。可以在 Editor 手册 中获得有关如何使用 `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();

子表

子表(用户)在构建上与父表非常相似 - 字段和名称不同,但其他 DataTables 和 Editor 表相同的基本模式同样适用于此。

真正使其运转的关键因素是能够提交在父表中选定的行中的 `id` 值。这可从使用 `{selected:true}` selector-modifierrow().data() 方法中轻松获得 - 例如

table.row( { selected: true } ).data();

关于 Select 如何与 DataTables API 集成的所有细节都可在 Select 手册 中找到。

编辑器 Javascript

用户 Editor 实例通过指定为函数的 ajax.data 选项来创建。这意味着每当 Editor 向服务器发出 Ajax 请求时,此函数将运行并扩展提交给服务器的数据。在本例中,我们希望提交从父表中选定的行中的网站 `id`(如上所述)。因此,我们执行以下操作

var usersEditor = new DataTable.Editor( {
    ajax: {
        url: '../php/users.php',
        data: function ( d ) {
            var selected = siteTable.row( { selected: true } );

            if ( selected.any() ) {
                d.site = selected.data().id;
            }
        }
    },
    table: '#users',
    fields: [ ... ]
} );

请注意,为简洁起见已将字段省略 - 如果你想阅读完整的字段列表,请参阅 Editor 联接示例

DataTables Javascript

DataTables 还具有一个 ajax.data 选项,它将在 DataTables 对要显示的数据发出请求时执行。它与同名的 Editor 选项的操作方式完全相同。

var usersTable = $('#users').DataTable( {
    ajax: {
        url: '../php/users.php',
        type: 'post',
        data: function ( d ) {
            var selected = siteTable.row( { selected: true } );

            if ( selected.any() ) {
                d.site = selected.data().id;
            }
        }
    },
    columns: [
        { data: 'users.first_name' },
        { data: 'users.last_name' },
        { data: 'users.phone' },
        { data: 'sites.name' }
    ],
    select: true,
    layout: {
        topStart: {
            buttons: [
                { extend: 'create', editor: usersEditor },
                { extend: 'edit',   editor: usersEditor },
                { extend: 'remove', editor: usersEditor }
            ]
        }
    }
} );

服务器端 (PHP)

子表的 PHP 有两个重要的注意事项

  1. 如果所选网站 id(仅称为 `site`)未作为请求的一部分提交,应在客户端显示一个空数据数组。
  2. 提交所选网站信息时,它应被用作 WHERE 条件,以便子表仅显示匹配该网站的数据。这是使用 Editor->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();
}

整合

我们有两个表,每个表都可以通过 Editor 进行编辑,因此你现在要做的是将它们连接在一起,以便在父表中选择的行将加载子表的新数据。另外,任何一个表数据的变化都将反映在另一个表中。

选择行

当选择一行时,选择扩展将触发select事件,当取消选择一行时,触发deselect事件。因此,我们仅需监听这些事件,并在其中任一事件发生时调用ajax.reload()方法,将新数据载入子表(在此处会执行ajax.data函数,获取新选择的行id)。

siteTable.on( 'select', function () {
    usersTable.ajax.reload();

    usersEditor
        .field( 'users.site' )
        .def( siteTable.row( { selected: true } ).data().id );
} );

siteTable.on( 'deselect', function () {
    usersTable.ajax.reload();
} );

在上述代码中,您会注意到还有对field().def()的调用 - 这是用来设置字段默认值。虽然不是必需的,但如果“站点”字段的默认值与父行中选择的值匹配,这会让最终用户更加轻松。

更新后的数据

当子表中的数据更新(更改了站点、添加了新项目等)后,父表需要更新以反映更改。同样,当父表更新(例如修改站点的名称)后,应更新子表。对于两种情况,我们均可以使用 Editor 的submitSuccess事件。与行选择类似,我们仅需调用ajax.reload()方法,以更新相应的表

siteEditor.on( 'submitSuccess', function () {
    usersTable.ajax.reload();
} );

usersEditor.on( 'submitSuccess', function () {
    siteTable.ajax.reload();
} );

后续步骤

本文的关键要点应当是如何使用一个表中选择的数据来影响另一个表中加载的数据。本例中的表设计得非常简单,一个明显的扩展是增加字段和字段类型,来提升复杂度。

除此之外,您可能希望考虑以下内容

  • 在父表中没有选择行时,隐藏子表
  • 可以显示关于从子表中引用的父表行进行删除的警告
  • 父表中的多行选择,以便在子表中显示多个站点(这需要在服务器端使用OR 表达式

有任何其他建议?请随时在此处或论坛中发布。