65.9K
CodeProject 正在变化。 阅读更多。
Home

SharePoint 2013 客户端渲染:列表表单 + KnockoutJs

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2015年3月22日

CPOL

12分钟阅读

viewsIcon

122201

downloadIcon

393

使用 CSR 和 KnockoutJs 自定义 SharePoint 2013 列表窗体的三个示例。

引言

在本文中,我将演示使用 KnockoutJs 和 SharePoint 客户端渲染来自定义列表窗体的三个实际示例。

  1. 只读字段,可切换到编辑模式
  2. 内联为查找字段添加值
  3. 联动字段

背景

有些人认为 SharePoint 和其他企业平台的用户体验不重要。我强烈不同意。

糟糕的用户体验会使日常工作变得枯燥且效率低下。当人们面对糟糕的界面时,更容易出错。更糟糕的是,如果用户界面过于复杂且难以使用,人们往往会完全避免使用它。

但是我们**可以**改变它,我相信 CSR 和 KnockoutJs 是实现这一目标的绝佳工具组合。

客户端渲染 (CSR) 是 SharePoint 2013 中用于显示列表视图、列表窗体和搜索结果的默认 JavaScript 渲染引擎。

如果您想了解更多关于客户端渲染的信息,请参考以下文章:

  1. SharePoint 2013 客户端渲染:列表视图
  2. SharePoint 2013 客户端渲染:列表表单

KnockoutJs 是一个 MVVM 框架,在 JavaScript 和 HTML 领域实现了双向数据绑定概念。您可以在其官方网站上了解更多关于 KnockoutJs 的信息。

本文无意解释 KnockoutJs 或 CSR 的工作原理,而是旨在展示如何有效地将两者结合使用,它们的优势是什么,以及需要注意的地方。因此,如果您对 KnockoutJs 或 CSR 完全不熟悉,请先访问上面的链接并掌握基础知识,否则本文可能会令人困惑。

示例 1:只读字段,可切换到编辑模式

假设有一个很少更改的字段。例如,在下面的列表窗体中,很明显“Title”字段不应经常修改。

为了强调重命名城市是个坏主意,我将默认使该字段只读,并在其旁边放置一个“编辑”按钮,以便当用户单击“编辑”时,会显示一个带有警告消息的确认对话框,如下所示:

在实现方面,这意味着遵循以下 4 个步骤:

  1. 覆盖字段模板
  2. 为字段创建**只读模式**,该模式默认显示,并包含处于显示模式的字段控件 + “编辑按钮”。
  3. 为该字段创建**编辑模式**,该模式包含处于编辑模式的字段控件。
  4. 实现“编辑”**按钮操作**,以便在单击时显示确认对话框,如果单击“确定”,则隐藏只读模式并显示编辑模式。

让我们快速回顾一下每个步骤。

步骤 1:覆盖编辑窗体上的默认字段模板

第一步很简单,可以使用以下代码片段实现:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

  Templates: {

      Fields: {
          "Title": {
              EditForm: function(ctx) {
                return 'some html code here';
              },
          },
      },

  },

});

注意:覆盖时应使用字段的内部名称(在本例中为“Title”)。

结果截图

步骤 2:为字段创建只读模式

最简单的情况下,我们可以直接使用 `ctx.CurrentFieldValue` - 但这仅对简单的文本字段有效。更好的方法是重用字段的默认显示模板,这将适用于任何字段类型。

不幸的是,似乎没有完全支持的方法可以在 CSR 中重用默认字段模板。我自己通常会从 TemplateManager 对象的 `_defaultTemplates` 属性中获取这些模板,但也有一些其他方法可以做到这一点 - 请使用最适合您的方法。

因此,在使用 `_defaultTemplates` 的情况下,要获取默认模板,我使用这段代码:

SPClientTemplates._defaultTemplates.Fields.default.all.all[<Field type>][<Control mode>]

在此,*<Field type>* 是字段的类型,例如“Text”、“Note”等;*<Control mode>* 可以是“EditForm”、“NewForm”和“DisplayForm”之一。

默认模板是一个函数,它显然只接受 `ctx` 作为参数。了解了所有这些之后,我现在可以轻松地为字段创建只读模式:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

  Templates: {

      Fields: {
          "Title": {
              EditForm: function(ctx) {
                var fieldType = ctx.CurrentFieldSchema.FieldType;
                var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
                return defaultTemplates[fieldType]["DisplayForm"](ctx) + '<button>Edit</button>';
              },
          },
      },

  },

});

结果截图

步骤 3:创建编辑模式

了解了如何重用模板后,创建编辑模式是一个简单的练习:

    var fieldType = ctx.CurrentFieldSchema.FieldType;
    var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
    
    return defaultTemplates[fieldType]["DisplayForm"](ctx) + '<button>Edit</button>'
        + defaultTemplates[fieldType]["EditForm"](ctx);

结果截图

步骤 4:实现“编辑”按钮操作

现在,当然可以使用 jQuery 或原生 JavaScript 来实现模式切换,但是 KnockoutJs (KO) 和其他现代双向绑定 JavaScript 框架提供了更直观、更简单的方式来创建动态界面!...

为了将 KnockoutJs 用于此任务,我需要做三件简单的事情:

  1. 将 KnockoutJs 部署到页面
  2. 稍微调整我们的 HTML 并添加 `data-bind` 属性
  3. 创建一个表示页面模型的 JavaScript 对象并调用 `ko.applyBindings`

可以通过任何您想要的方式部署 KnockoutJs - 通过 ScriptLink 自定义操作、母版页、JSLink 等。

在 KnockoutJs 准备就绪并可使用后,让我们更改 HTML 并添加 `data-bind` 属性。这是我得到的结果:

    var fieldType = ctx.CurrentFieldSchema.FieldType;
    var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
    
    return '<div data-bind="visible: !editMode()">'
        + defaultTemplates[fieldType]["DisplayForm"](ctx)
        + '<button data-bind="click: switchToEditMode">Edit</button>'
        + '</div>'
        + '<div data-bind="visible: editMode()">'
        + defaultTemplates[fieldType]["EditForm"](ctx)
        + '</div>';

所以您可以看到,我将只读模式和编辑模式包装在单独的 div 中,它们根据特定的 `editMode` 字段可见或隐藏。这个字段是可观察的,因此它实际上是一个函数,这就是为什么我调用它来获取其值。

此外,“编辑”按钮具有 `click` 绑定,因此无论何时单击它,都会调用特定的 `switchToEditMode` 方法。

现在让我们创建页面模型对象并在此处添加 `editMode` 和 `switchToEditMode`:

var model = {
  editMode: ko.observable(false),
  switchToEditMode: function() {
    if (confirm('Are you sure want to rename a city!?'))
      model.editMode(true);
  } 
};

这个页面模型显然应该使用 `ko.applyBindings` 绑定到 HTML。`applyBindings` 方法作用于 DOM,因此在调用 `applyBindings` 之前,必须先渲染由我们的模板生成的 HTML。

因此,放置 `ko.applyBindings` 的明显位置是 CSR 的 PostRender 处理程序。但重要的是要理解,在列表窗体中,渲染过程会为每个字段控件发生,这意味着 PostRender 将被调用多次。因此,我通常会在那里添加一个额外的条件,检查表单中的最后一个字段,以便 `ko.applyBindings` 只调用一次。

最终代码

所以这是最终代码:

SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

  Templates: {

      Fields: {
          "Title": {
              EditForm: function(ctx) {
                var fieldType = ctx.CurrentFieldSchema.FieldType;
                var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
                return '<div data-bind="visible: !editMode()">'
                    + defaultTemplates[fieldType]["DisplayForm"](ctx)
                    + '<button data-bind="click: switchToEditMode">Edit</button>'
                    + '</div>'
                    + '<div data-bind="visible: editMode()">'
                    + defaultTemplates[fieldType]["EditForm"](ctx)
                    + '</div>';
              },
          },
      },

  },
  
  OnPostRender: {
  
    if (ctx.ListSchema.Field[0].Name == "Liked") // this is the last field on the form
    {
        var model = {
         editMode: ko.observable(false),
         switchToEditMode: function() {
           if (confirm('Are you sure want to rename a city!?'))
             model.editMode(true);
          } 
        };
        ko.applyBindings(model);
    }
  
  }

});

别忘了,在正确地将脚本包含到页面并使其与最小下载策略协同工作方面,总会有一些样板代码。

我通常使用这个骨架代码来实现此目的,它已被证明非常可靠:

SP.SOD.executeFunc("clienttemplates.js", "SPClientTemplates", function() {


  function init() {

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
        
        // overrides go here
        
    });
  }

  RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~siteCollection/Style Library/file.js"), init);
  init();

});

注意:不要忘记更改文件名和路径。

示例 2:内联为查找字段添加值

有时,用户需要频繁地向某个查找项添加值。在这种情况下,内联界面可用于添加查找项,可以节省大量时间和挫败感。所以,假设我输入了一个新城市,但查找中没有相应的国家/地区:

我不想打开新窗口并导航到“国家/地区”列表等。相反,我希望“添加”按钮就在此窗体中:

每当单击此按钮时,我都希望显示一个简单的界面,例如一个文本框,允许我输入国家/地区的名称 + 确定和取消按钮。

好的。让我们来实现这一点。

UI

在 UI 方面,一切都与前一个示例非常相似,只是将 `editMode` 替换为 `addMode`。

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

      Templates: {

          Fields: {
              "Country": {
                  NewForm: function(ctx) {
                    var fieldType = ctx.CurrentFieldSchema.FieldType;
                    var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
                    return '<div data-bind="visible: !addMode()">'
                    	+ '<table><tr><td>'
                        + defaultTemplates[fieldType]["NewForm"](ctx)
                    	+ '</td><td>'
                        + '<button data-bind="click: switchToAddMode">Add</button>'
                    	+ '</td></tr></table>'
                        + '</div>'
                        + '<div data-bind="visible: addMode()">'
                    	+ '<input type="text" />'
                        + '<button>OK</button>'
                        + '<button>Cancel</button>'
                        + '</div>';
                  },
              }
          },

      },

      OnPostRender: function(ctx) {
        if (ctx.ListSchema.Field[0].Name == "Liked")
        {
          var model = {
            addMode: ko.observable(false),
            switchToAddMode: function() {
              model.addMode(true);
            }
            
          };
          ko.applyBindings(model);
        }
      }

    });

正如您所见,这与前一个示例的代码有 90% 相同。

我添加了 `

` 标签,因为查找字段的默认模板会渲染过多的 `
`,而使用表格和两个 `
` 似乎是在同一行上将“添加”按钮与下拉列表保持在一起的最合法方式。

现在我们有了基本 UI,让我们让“确定”和“取消”按钮生效。

向查找项添加条目

单击“确定”按钮时,应发生 3 件主要事情:

  1. 新国家/地区应添加到“国家/地区”列表中。
  2. 新国家/地区也应出现在下拉列表中(因为我们不想刷新整个页面!)。
  3. 字段应返回到初始模式。

向列表添加条目很简单,例如可以使用一小段 JSOM 来完成:

var context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle("Country");
var item = list.addItem(new SP.ListItemCreationInformation());
item.set_item("Title", model.countryName());
item.update();
context.executeQueryAsync(
    function() {
      alert("item added");
    },
    function() {
      alert("error");
    });

这里 `model.countryName` 应该是绑定到文本框值的 KnockoutJs 可观察对象,因此它包含用户在字段中输入的内容。

现在,在添加条目后,我们还应该将新国家/地区添加到下拉列表中,但不幸的是,没有现成的 API 或受支持的方法可以做到这一点。默认模板就像黑盒子一样,因此我们拥有的选项要么是重新实现整个字段(这是正确的方法,但涉及相当多的代码),要么通过 DOM 来 hack 下拉列表。

今天,为了保持示例简单,我将采用第二种方法,但请记住,任何 DOM hack 本质上都是糟糕的做法,在下次更新后(O365 更新非常频繁)它可能突然停止工作,仅此而已...

通过 DOM,向下拉列表添加元素是一项非常简单的任务。首先,让我们看一下下拉列表元素的源代码:

值显然等于条目 ID。现在创建适当的代码很容易:

var context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle("Countries");
var item = list.addItem(new SP.ListItemCreationInformation());
item.set_item("Title", model.countryName());
item.update();
context.load(item,"ID");
context.executeQueryAsync(
    function() {
      
      // add new country to dropdown
      var option = document.createElement("option");
      option.value = item.get_id();
      option.innerHTML = model.countryName();
    
      var select = document.querySelector("#countryTD select");
      select.appendChild(option);
      
      // turn off the edit mode and clear input
      model.editMode(false);
      model.countryName('');
    },
    function() {
      alert("error");
    });

注意几点:

  1. 我添加了 `context.load` 调用以确保返回创建项的 ID。
  2. 成功回调最后的两行代码会将字段切换回初始模式并清除国家/地区名称文本框。显然,这两行代码也可以用于“取消”按钮处理程序。
  3. `document.querySelector` 执行与 jQuery 相同的选择器工作,并且自 IE8 起得到原生支持,所以我一直使用它,但如果您在页面上部署了 jQuery,那么当然可以使用 jQuery 完成相同的目的。
  4. `#countryTD` 指的是我添加到包含字段默认模板的 `
` 元素上的 ID。

就这样!现在,如果您正确添加了 `data-bind` 属性,一切都应该正常工作!

完整的代码

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

      Templates: {

          Fields: {
              "Country": {
                  NewForm: function(ctx) {
                    var fieldType = ctx.CurrentFieldSchema.FieldType;
                    var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
                    return '<div data-bind="visible: !addMode()">'
                    	+ '<table><tr><td id='countryTD'>'
                        + defaultTemplates[fieldType]["NewForm"](ctx)
                    	+ '</td><td>'
                        + '<button data-bind="click: switchToAddMode">Add</button>'
                    	+ '</td></tr></table>'
                        + '</div>'
                        + '<div data-bind="visible: addMode()">'
                    	+ '<input type="text" data-bind="value: countryName" />'
                        + '<button data-bind="click: add">OK</button>'
                        + '<button data-bind="click: cancel">Cancel</button>'
                        + '</div>';
                  },
              }
          },

      },

      OnPostRender: function(ctx) {
        if (ctx.ListSchema.Field[0].Name == "Liked") // last field of the form
        {
          var model = {
            addMode: ko.observable(false),
            switchToAddMode: function() {
              model.addMode(true);
            },
            
            countryName: ko.observable(''),
            
            add: function() {
                var context = SP.ClientContext.get_current();
                var list = context.get_web().get_lists().getByTitle("Countries");
                var item = list.addItem(new SP.ListItemCreationInformation());
                item.set_item("Title", model.countryName());
                item.update();
                context.load(item,"ID");
                context.executeQueryAsync(
                    function() {
                      
                      // add new country to dropdown
                      var option = document.createElement("option");
                      option.value = item.get_id();
                      option.innerHTML = model.countryName();
                    
                      var select = document.querySelector("#countryTD select");
                      select.appendChild(option);
                      
                      // turn off the edit mode and clear input
                      model.editMode(false);
                      model.countryName('');
                    },
                    function() {
                      alert("error");
                    });
            },
            
            cancel: function() {
              model.editMode(false);
              model.countryName('');
            }
            
          };
          ko.applyBindings(model);
        }
      }

    });

这是您获得的结果:

是的,当然它也会将相应的条目添加到“国家/地区”列表中。

示例 3:联动字段

显然,可以使用同一个 KnockoutJs 模型控制多个字段。让我们这样做,并使字段相互关联。

因此,在我的示例窗体中,我有一些“Visited”和“Liked”字段。“Visited”字段标记是否访问过某个城市,而在“Liked”字段中,我可以选择我喜欢什么。逻辑上,我应该在访问过该城市之前看不到“Liked”字段。

为了实现此逻辑,我需要覆盖“Liked”和“Visited”这两个字段。对于“Liked”字段,自定义非常简单:只需将字段控件包装在一个 div 中,并将此字段的可见性绑定到模型中的 `visited` 字段(我稍后将创建该字段)。

return '<div data-bind="visible: visited()">' + defaultTemplates[fieldType]["EditForm"] + '</div>';

这里 `visited` 是一个可观察对象,它应该用“Visited”字段的值初始化。这样做非常简单:

var model = {
  visited: ko.observable(ctx.ListData.Items[0]["Visited"] == 1)
}

现在让我们处理“Visited”字段。

实际上,字段的显示方式没有任何问题,我们应该做的唯一事情就是实时跟踪此字段的变化(例如,用户清除了复选框 =>“Liked”字段立即消失)。不幸的是,同样,默认字段模板就像黑盒子一样,没有 API 可以订阅字段的变化。再次,最正确的方法是重新实现该字段,尽管 DOM hack 甚至字符串替换也可以工作。

这一次,让我们坚持正确的方法,并重新实现这个字段。在这种特定情况下,实际上非常容易做到:

"Visited": {
    EditForm: function(ctx) {
        ctx.FormContext.registerGetValueCallback(ctx.CurrentFieldSchema.Name, function() {
            return model.visited();
        });
        return '<input data-bind="checked: visited" type="checkbox" />';
    }
}

在这种情况下,我只提供 `GetValueCallback`,尽管在理想情况下,建议您也使用 `registerInitCallback`、`registerHasValueChangedCallback`、`registerFocusCallback` 和 `registerValidationErrorCallback`。

但无论如何,如果您曾经创建过字段模板,您肯定会注意到 KnockoutJs 在这里多么契合,以及它如何显著简化字段模板的创建。

为了完成这个示例,我稍微调整了一下代码,使模型变量在模板函数内部可见,但就这样,完成了,字段现在链接在一起了。

此示例的完整代码

    var model = {
      visited: function(){}
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({

      Templates: {

          Fields: {
              "Visited": {
                EditForm: function(ctx) {
                    ctx.FormContext.registerGetValueCallback(ctx.CurrentFieldSchema.Name, function() {
                      return model.visited();
                    });
                    return '<input type="checkbox" data-bind="checked: visited" />';
                }
              },
              "Liked": {
                EditForm: function(ctx) {
                    var fieldType = ctx.CurrentFieldSchema.FieldType;
                    var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
                    return '<div data-bind="visible: visited()">' + defaultTemplates[fieldType]["EditForm"](ctx) + '</div>';
                }
              }
          },

      },

      OnPostRender: function(ctx) {
        if (ctx.ListSchema.Field[0].Name == "Liked")
        {
          model.visited = ko.observable(ctx.ListData.Items[0]["Visited"] == 1);
          ko.applyBindings(model);
        }
      },

    });

源代码

像往常一样,您可以通过左侧面板的“浏览代码”链接浏览示例的源代码,或通过以下链接将其下载为 zip 存档:

一些建议

如何将 HTML 标记与代码分离

上面的示例至少有一个明显的问题:HTML 标记是在 JS 字符串中管理的,而不是在单独的文件中。对于小型示例来说没问题,但对于较大的示例来说就不太好了。

一个简单的解决方案是创建一个KO 模板在 SharePoint 的单独文件中,然后通过内容编辑器 Web 部件或其他方法将其加载到页面上。

或者,如果您正在构建许多通用的字段控件并在不同窗体中重用它们,可以考虑KO 组件及其外部加载器

为什么不使用 AngularJs 或其他框架?

KnockoutJs 只是一个例子,但当然许多现代 JavaScript 框架和库都可以胜任。特别是对于 AngularJs,对我来说,这就像用杀鸡的牛刀... :)

为什么不使用完全自定义的窗体 + JSOM?

好问题。在某些情况下,完全自定义的窗体实际上是个好主意,并且很有意义。

但是,我认为 CSR 在以下情况更好:

  1. 您只需要自定义几个字段,而保留其他所有字段不变。
  2. 您有一些字段不容易在您的窗体中重新实现(例如 Taxonomy、User 等)。
  3. 您无法保证用户不会更改字段设置或向此特定列表添加其他字段。
  4. 您自定义了在许多不同列表中重复使用的字段。

如何更快地开发 CSR 自定义

我曾经构建过一个工具,可以更快地创建 CSR 自定义。我的意思是,**快得多**。最初,这只是我为自己写的一个工具,因为我经常使用 CSR。现在它是开源的,您可以选择使用它。

该工具称为Cisar,本质上是 CSR 的实时编辑器。也就是说,您编写代码,然后立即看到您的窗体或列表视图如何根据您编写的内容进行转换。没有延迟,也没有页面重新加载。工作原理如下:

结论

SharePoint 2013 中的列表窗体是在客户端渲染的,这使我们能够利用 KnockoutJs 等现代框架的强大功能,使窗体自定义变得简单、易读且更有效。

重用默认 CSR 字段模板的可能性相当有限,但有了 KnockoutJs 的强大功能,即使重新实现这些模板也变成了一项相对容易的任务。

通过使用 CSR 和 KnockoutJs,可以创建灵活、易读且可维护的解决方案,这些解决方案将与 SharePoint 完全兼容,这在 SharePoint Online 中尤其重要,那里的更新一直在进行,而使用 DOM 技巧是一种非常危险的方法。

© . All rights reserved.