构建一个体面的联系人列表网页“组件”






4.97/5 (19投票s)
JavaScript、jQuery、jQuery-UI、jQuery-tablesorter 以及 CSS 和 HTML 的冒险经历
更新
CPian “Kornfeld Eliyahu Peter” 欣然提供了 ASP.NET 实现,我已将源代码添加到仓库,并更新了文章,指出了 Ruby ERB 格式和 ASPX 格式之间的一些细微差别。关于 ASP.NET 实现,以下是 Peter 的评论/建议:
在查看你的项目时,我立刻想到了 ASP.NET MVC,Ruby on Rails 与其几乎相同。因此,我决定将其移植到 ASP.NET Web Forms,以使其更有趣。然而,你可能会惊讶我需要更改的地方很少。事实上,我从未触碰过 JavaScript 和 HTML(我认为这是有道理的,但 nevertheless 很有趣)。
事实上,我有两次更改了 HTML,但这两次都不是很相关。
- 你从未关闭过 <input> 元素,而我的环境对此类事情非常挑剔——所以我关闭了它们以摆脱这些消息!
- 在开关按钮中,你在 <label> 中使用了 <div>。同样,HTML 和我的 IDE 都不喜欢这样。所以我将其更改为 span,并为相关的 CSS 添加了 display: block;。
正如你所见,差异不超过两种框架/语言之间的差异——你的逻辑是永恒的!老实说,我惊讶于移植时更改的内容如此之少。总之——我不知道你的“控件”的最终目的是什么,但我仍然看到你的列表中联系人数量庞大的问题。我尝试了 700 个联系人,页面变得有点慢……我还想说,这只是对你的代码的移植,仅此而已。
源代码
源代码在 GitHub 上: https://github.com/cliftonm/ContactListDemo,包括 ASP.NET 版本。
引言
我决定承担起构建一个看起来不错的联系人列表的任务,该列表最终可以成为我正在构建的一个网站的组件。我心中有一些基本要求:
- 联系人表格
- 可按名字和姓氏排序
- 一个滑块按钮,允许你切换名字/姓氏列(影响排序)
- 可搜索,带有 A-Z 索引(类似于实体通讯录中的“A”、“B”、“C”等标签)
- 以及包含电子邮件地址、家庭/工作/手机号码的列,可以通过按钮组选择
接下来是我的冒险经历,我将各种技术整合在一起,并对其进行调整,以获得我想要的行为和外观。遇到的各种挑战包括:
- 优化屏幕空间的使用——单选按钮和复选框,除了有些过时外,还会占用屏幕空间,jQuery-UI 元素的默认行为也是如此。
- 一致的字体——这些控件使用多少不同的字体、大小和粗细真是令人惊讶,导致外观非常混乱。
- jQuery / Javascript 来实现过滤表格数据等功能
- API——每个组件都有不同的 API,而每个 API 都有不同的“心智模型”来使用该组件。
跟随
我决定为我将要介绍的每个步骤提供单独的页面。这样做很重要,因为你可以看到随着我添加组件的行为和样式,它们之间的差异。我会指出截图的 HTML 文件,以便你可以查看过程每一步的完整 HTML。
入门
虽然后端是 Ruby on Rails,但你在这里看不到太多 Ruby 代码,因为本文几乎完全是关于客户端的。
设置网页草稿板
Ruby
在 RubyMine IDE 中创建了一个初始的 Rails 项目后,我创建了一个包含一个表的数据库模式
create_table "contacts", :force => true do |t| t.string "first_name" t.string "last_name" t.string "email" t.string "home_phone" t.string "work_phone" t.string "cell_phone" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end
我用一些随机数据填充了它(抱歉,女士们,我的名字都是男性的)
Contact.delete_all # http://en.wikipedia.org/wiki/List_of_most_popular_given_names first_names = ['James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Charles', 'Joseph', 'Thomas'] # http://names.mongabay.com/most_common_surnames.htm last_names = ['SMITH', 'JOHNSON', 'WILLIAMS','JONES', 'BROWN', 'DAVIS', 'MILLER', 'WILSON', 'MOORE', 'TAYLOR', 'ANDERSON', 'THOMAS', 'JACKSON', 'WHITE', 'HARRIS', 'MARTIN', 'THOMPSON', 'GARCIA', 'MARTINEZ', 'ROBINSON'] 20.times do fname = first_names[rand(first_names.length)].capitalize lname = last_names[rand(last_names.length)].capitalize home = rand.to_s[2..4]+'-'+rand.to_s[2..4]+'-'+rand.to_s[2..5] work = rand.to_s[2..4]+'-'+rand.to_s[2..4]+'-'+rand.to_s[2..5] cell = rand.to_s[2..4]+'-'+rand.to_s[2..4]+'-'+rand.to_s[2..5] Contact.create(:first_name => fname, :last_name => lname, :email => "#{fname}.#{lname}@mail.com", :home_phone => home, :work_phone => work, :cell_phone => cell) end
ASP.NET
using System; using System.Data; namespace ContactListDemo { public class Schema : DataTable { public Schema() { TableName = "contacts"; Columns.Add( "first_name", typeof( string ) ); Columns.Add( "last_name", typeof( string ) ); Columns.Add( "email", typeof( string ) ); Columns.Add( "created_at", typeof( DateTime ) ).AllowDBNull = false; Columns.Add( "updated_at", typeof( DateTime ) ).AllowDBNull = false; Columns.Add( "home_phone", typeof( string ) ); Columns.Add( "work_phone", typeof( string ) ); Columns.Add( "cell_phone", typeof( string ) ); } } public class Contacts : Schema { // http://en.wikipedia.org/wiki/List_of_most_popular_given_names private string[ ] _FirstNames = { "James", "John", "Robert", "Michael", "William", "David", "Richard", "Charles", "Joseph", "Thomas" }; // http://names.mongabay.com/most_common_surnames.htm private string[ ] _LastNames = { "SMITH", "JOHNSON", "WILLIAMS", "JONES", "BROWN", "DAVIS", "MILLER", "WILSON", "MOORE", "TAYLOR", "ANDERSON", "THOMAS", "JACKSON", "WHITE", "HARRIS", "MARTIN", "THOMPSON", "GARCIA", "MARTINEZ", "ROBINSON" }; public Contacts ( ) { Random oRnd = new Random( ); for ( int i = 0; i < 200; i++ ) { string szFName = _FirstNames[ oRnd.Next( _FirstNames.Length ) ].ToUpper( ); string szLName = _LastNames[ oRnd.Next( _LastNames.Length ) ].ToUpper( ); string szEmail = string.Format( "{0}.{1}@mail.com", szFName, szLName ); string szHome = string.Format( "{0}-{1}-{2}", oRnd.Next( 10, 9999 ), oRnd.Next( 10, 9999 ), oRnd.Next( 10, 99999 ) ); string szWork = string.Format( "{0}-{1}-{2}", oRnd.Next( 10, 9999 ), oRnd.Next( 10, 9999 ), oRnd.Next( 10, 99999 ) ); string szCell = string.Format( "{0}-{1}-{2}", oRnd.Next( 10, 9999 ), oRnd.Next( 10, 9999 ), oRnd.Next( 10, 99999 ) ); Rows.Add( szFName, szLName, szEmail, DateTime.Today, DateTime.Today, szHome, szWork, szCell ); } } } }
这结束了我们的后端之旅。
JavaScript 文件
但是,我们需要配置一些组件。无论你使用什么服务器平台,你都需要
- jQuery
- jQuery-UI
- jQuery-tablesorter
使用 jQuery-tablesorter
HTML:app\views\contacts\build1.html.erb
在很多方面,这是最容易的部分。
要创建一个具有可排序列的表格,如这个
……编写一些基本的 HTML
Ruby
<div> <table id='contactsTable'> <thead> <tr> <th>Last Name</th> <th>First Name</th> <th>Email</th> <th>Home</th> <th>Work</th> <th>Cell</th> </tr> </thead> <% @contacts.each do |contact|%> <tr> <td><%= contact.last_name%></td> <td><%= contact.first_name%></td> <td><%= contact.email%></td> <td><%= contact.home_phone%></td> <td><%= contact.work_phone%></td> <td><%= contact.cell_phone%></td> </tr> <% end %> </table> </div>
ASP.NET
<body> <form id="form1" runat="server"> <div> <table id='contactsTable' class='tablesorter-default'> <thead> <tr> <th>Last Name</th> <th>First Name</th> <th>Email</th> <th>Home</th> <th>Work</th> <th>Cell</th> </tr> </thead> <% foreach ( System.Data.DataRow oContact in Contacts.Rows ) { %> <tr> <td><%= oContact["last_name"]%></td> <td><%= oContact["first_name"]%></td> <td><%= oContact["email"]%></td> <td><%= oContact["home_phone"]%></td> <td><%= oContact["work_phone"]%></td> <td><%= oContact["cell_phone"]%></td> </tr> <% } %> </table> </div> </form> </body>
和一个单行 Javascript 调用
<script type="text/javascript"> $(document).ready(function() { $("#contactsTable").tablesorter(); }); </script>
可用性和呈现问题
虽然乍一看这看起来相当不错,但它存在一些问题。主要问题是:
- 并非所有字段都应可排序。按电子邮件地址和家庭、工作、手机号码排序没有多大意义。
- 信息过载。当我查找联系人时,我通常只需要一件事,而不是全部。如果我包含邮寄地址和实物地址,这将在我们已经拥有的四个字段之外再增加两个字段。
中等问题
- 有时我知道这个人的名字,想通过名字查找他,有时我想通过姓氏查找某人。与其不得不转移我的视线到第二列通过名字查找人,然后向左移动以查看姓氏是否匹配,然后再向右移动查看我想要的相关信息,我希望能够交换名字和姓氏列,这样当我视觉搜索列表时,我的眼睛基本上是沿着从左到右的路径移动的。我将在单独的部分解决这个问题。
小问题是:
- 它占用了整个屏幕。
- 我希望有“斑马”条纹,以创建一个不那么单调(和乏味)的列表。
优化表格
HTML:app\views\contacts\build2.html.erb
移除特定列的排序功能
首先要做的是移除对电子邮件和电话字段的排序能力。jQuery-tablesorter 是一个*非常好的*组件,让它不排序特定列非常容易。
<script type="text/javascript">
$(document).ready(function() {
$("#contactsTable").tablesorter({
headers : {
0: { sorter: "text" },
1: { sorter: "text" },
2: { sorter: false },
3: { sorter: false },
4: { sorter: false },
5: { sorter: false },
6: { sorter: false }
}
})
});
</script>
固定宽度
好吧,这很容易,除非我们应该始终遵循这样一个规则:如果你要固定某物的宽度,就为其编写一个外部 div 来定义所需的宽度,并将内部 div 的宽度设置为 100%。这样,内部组件就可以被重用,而无需处理物理尺寸等问题,这些是特定于应用程序的实现。你还会注意到这以后会派上用场!所以
<div style="width:500px"> <div style="width:100%"> ... etc ...
是的,这实际上是坏习惯,但总有一天(在本篇文章中不会)我将使用一个类似 Foundation 的网格系统来使用此组件,并移除固定宽度——这主要是为了让我看看在较窄的显示下它看起来如何,因为其他内容打算放在最终页面上。
斑马条纹
这可以在 tablesorter 中轻松指定(你还可以在文档中阅读许多其他有用的功能)。
$("#contactsTable").tablesorter({ widgets : [ 'zebra' ], ... etc ...
我们现在有了以下内容
越来越好!
移除噪音
HTML:app\views\contacts\build3.html.erb
接下来,我们需要一种方法来选择我们想要查看的特定信息,而不是在网格中填充大量不需要的内容,当我们查找联系人时。为此,我将使用 jQuery-UI 按钮集。这需要一些 HTML
<div id="MyButtonList"> <input type="checkbox" id="toggle_email"><label for="toggle_email">email</label> <input type="checkbox" id="toggle_home"><label for="toggle_home">home</label> <input type="checkbox" id="toggle_work"><label for="toggle_work">work</label> <input type="checkbox" id="toggle_cell"><label for="toggle_cell">cell</label> </div>
并在文档就绪事件中再添加一个单行代码
$('#MyButtonList').buttonset();
当然,它看起来很糟糕,因为它很大,而且我们刚刚在布局中添加了第二个字体。但我们稍后会处理这个问题。
现在我们需要编写点击按钮集的实现。我们将编写四个非侵入式事件点击处理程序
$('#MyButtonList').buttonset(); $("#toggle_email").click(function() { showOrHide('#toggle_email', 3) }); $("#toggle_home").click(function() { showOrHide('#toggle_home', 4) }); $("#toggle_work").click(function() { showOrHide('#toggle_work', 5) }); $("#toggle_cell").click(function() { showOrHide('#toggle_cell', 6) }); function showOrHide(button, colNum) { if ($(button).is(":checked")) { $('#contactsTable tr *:nth-child('+colNum+')').removeClass('hidden'); } else { $('#contactsTable tr *:nth-child('+colNum+')').addClass('hidden'); } }
可用性和呈现问题
此时,我们有了一个基本可用的实现(还差一个功能),但仍然存在可用性和呈现问题:
主要问题
- 当我们刷新页面时:网格不反映选定的数据列
- 字体不匹配,可视化过于庞大
- 对于按钮集,我完全不清楚灰色按钮表示“选中”还是白色按钮表示“选中”。事实上,当我实现底层 JavaScript 时,我惊讶地发现灰色表示“未选中”!
- 由于我们不再选择所有列,因此列之间存在大量空白。
刷新时设置选定的列
这通过在文档就绪事件中强制设置列状态来处理。
showOrHide('#toggle_email', 3) showOrHide('#toggle_home', 4) showOrHide('#toggle_work', 5) showOrHide('#toggle_cell', 6)
字体不匹配和尺寸
查看 tablesorter 的 CSS,我发现正在使用的字体是:
font: 12px/18px Arial, Sans-serif;
……我将在 MyButtonList 的 CSS 中指定它。下一个问题是尺寸。经过大量的调整,我决定行高样式为 0.8。这是通过将其与我接下来要实现的按钮滑块高度相匹配来确定的。
使按钮“选中”时更清晰
为了让按钮更明显地表示它被选中,我还选择将选定的文本加粗。按钮集的最终 CSS 如下所示:
<style> #MyButtonList .ui-button.ui-state-active .ui-button-text { font: 12px/18px Arial, Sans-serif; line-height: 0.8; color: black; background-color: white; font-weight:bold; } #MyButtonList .ui-button .ui-button-text { font: 12px/18px Arial, Sans-serif; line-height: 0.8; color: black; background-color: #eeeeee; } </style>
现在的样子是:
移除空白区域
现在我们已经移除了不希望看到的列,让我们也移除空白区域,同时保留网格背景的完整宽度。此外,作为可用性问题,我发现将姓名放在左侧,联系人数据(电子邮件和电话)放在右侧很有帮助。我认为这是一种将联系人的姓名与联系人的其他信息分开的视觉上令人愉悦的方式。我们通过列样式来实现这一点:
<th style="white-space:nowrap">Last Name</th> <th style="white-space:nowrap">First Name</th> <th style="white-space:nowrap; width:99%"></th> <th style="white-space:nowrap;">Email</th> <th style="white-space:nowrap;">Home</th> <th style="white-space:nowrap;">Work</th> <th style="white-space:nowrap;">Cell</th>
我们对表格行也做同样的事情(未显示)。
请注意,第三列是一个空列,宽度为 99%。姓名列现在会根据内容进行调整,而其余的“数据”列会移到右侧,例如:
选择名字/姓氏列顺序
HTML:app\views\contacts\build4.html.erb
一个非常漂亮的纯 CSS 的开关(或者我称之为按钮滑块)可以在这里找到。完整的 CSS 很冗长,没有必要展示,但这是我最初完成的样子:
按钮滑块的实现很简单。我们再次观察点击事件,并切换名字和第二列的顺序。
$("#myonoffswitch").click(function() { var tbl = $('#contactsTable'); moveColumn(tbl, 1, 0); });
另外,在文档就绪事件中,我们希望在页面刷新时设置列顺序的状态。
if (!$("#myonoffswitch").is(":checked")) { var tbl = $('#contactsTable'); moveColumn(tbl, 1, 0); }
moveColumn
函数完成了所有工作。
function moveColumn(table, from, to) { var rows = $('tr', table); var cols; rows.each(function() { cols = $(this).children('th, td'); cols.eq(from).detach().insertBefore(cols.eq(to)); }); }
可用性和呈现问题
- 字体又不一样了。
- 我不喜欢“关闭”模式有不同的背景。这其实不是一个开关,而是一个“状态”控件。
- 它与按钮集的对齐效果不佳。理想情况下,按钮集应右对齐到网格边缘,滑块应与按钮集相同高度并垂直对齐。
字体、大小、颜色问题
解决这些问题相当直接。字体与表格使用的字体相同。在代码中,你会注意到我为处理控件宽度时需要修改的三个地方添加了注释。我还缩小了边框。
对齐问题
为了左对齐按钮滑块和右对齐按钮集,需要在宽度为 500px 的 div 中进行一些 div 操作。
对于按钮滑块:
<div style="width:100%">
对于按钮集:
<div id="MyButtonList" style="float:right">
我们还需要清除表格的 div,以便它强制换到新行。
<div style="clear:left; width:100%">
我们现在有了类似这样的东西:
添加索引过滤器
HTML:app\views\contacts\build5.html.erb
最后,我想在左侧添加一个 A 到 Z 的索引。当用户点击一个字母时,它会根据姓名首字母过滤联系人列表。如果姓氏是第一列,则过滤姓氏。如果名字是第一列,则过滤名字。
要实现这一点,需要将所有内容向右移动 20 像素,所以我们从滑块按钮 div 开始:
<div style="margin-left:20px; width:100%">
我们还将表格向右移动 20 像素(顺便说一句,稍微调整了顶部填充,以便与上方的控件保持一定距离)。
<div style="margin-left:20px; width:100%; padding-top: 3px">
然后,在表格 div 之前,我们添加一个 div,其中包含一些 Ruby 代码来生成索引,设置 ID,并在用户点击时调用一个 Javascript 函数,传入字母索引。
Ruby
<div class="index-filter"> <table style="font: 12px/18px Arial, Sans-serif;"> <tr> <td> <a href='#' id='filter-none' onclick='showAll()'>*</a> </td> </tr> <% ('A'..'Z').each do |s| %> <tr> <td> <% filter_by = %Q|filterBy("#{s}")| %> <a href='#' id='<%="filter-#{s}" %>' onclick='<%="#{filter_by}"%>'><%= "#{s}" %></a> </td> </tr> <% end %> </table> </div>
ASP.NET
<div class="index-filter"> <table style="font: 12px/18px Arial, Sans-serif;"> <tr> <td> <a href='#' id='filter-none' onclick='showAll()'>*</a> </td> </tr> <% foreach ( char s in Enumerable.Range( 'A', 'Z' - 'A' + 1 ).Select( x => x ) ) { %> <tr> <td> <a href='#' id='filter-<%= s %>' onclick='filterBy("<%= s %>")'><%= s %></a> </td> </tr> <% } %> </table> </div>
(是的,表格样式应该写在 CSS 中,而不是嵌入在 HTML 中。)
我们需要一些 CSS 来让列表出现在正确的位置,即在左侧,与表格主体(而非表头)的顶部对齐。
.index-filter { clear:left; float:left; width:2px; margin-top: 25px; margin-left: 2px; }
请注意,我在索引列表前加了一个星号以取消过滤。showAll
和 filterBy
方法如下所示:
function showAll() { $("#filterable").find("tr").each(function(idx, row){ row.hidden=false; }); } // Filter by the first letter of the first column, which will be either last name or first name. function filterBy(letter) { var rows = $("#filterable").find("tr"); rows.each(function(idx, row) { if (row.children[0].innerHTML.indexOf(letter)==0) { row.hidden=false; } else { row.hidden=true; } }); }
我们还需要将行包装在带有“filterable”ID 的 tbody 标签中。
Ruby
<tbody id='filterable'> <% @contacts.each do |contact|%> <tr> <td style="white-space:nowrap"><%= contact.last_name%></td> <td style="white-space:nowrap"><%= contact.first_name%></td> <td style="white-space:nowrap; width:99%"></td> <td style="white-space:nowrap;"><%= contact.email%></td> <td style="white-space:nowrap;"><%= contact.home_phone%></td> <td style="white-space:nowrap;"><%= contact.work_phone%></td> <td style="white-space:nowrap;"><%= contact.cell_phone%></td> </tr> <% end %> </tbody>
ASP.NET
<td style="white-space: nowrap"><%= oContact["last_name"]%></td> <td style="white-space: nowrap"><%= oContact["first_name"]%></td> <td style="white-space: nowrap; width: 99%"></td> <td style="white-space: nowrap;"><%= oContact["email"]%></td> <td style="white-space: nowrap;"><%= oContact["home_phone"]%></td> <td style="white-space: nowrap;"><%= oContact["work_phone"]%></td> <td style="white-space: nowrap;"><%= oContact["cell_phone"]%></td>
我们现在有了可用的东西,但看起来像这样:
一个重要的教训
顺便说一句,我最初使用 jQuery 函数的方式如下:
$("#filterable").find("tr").hide(); ... show only selected rows ...
但这给我带来了很多麻烦,因为事实证明,jQuery 使用 hide()
函数设置 CSS,而我却试图使用 hidden
属性使行可见!这是一个非常重要的教训——确保你对属性和 CSS 的使用与你想要的行为一致!
可用性和呈现问题
- 索引显示为链接,垂直间距太宽。
修正索引的外观
CSS 来救援:
.index-filter tr td { line-height:9px; } .index-filter tr td a { text-decoration: none; color: black; } .index-filter tr td a:hover { color: white; background-color: black; font-weight:bold; }
现在我们已经移除了下划线,并且对鼠标悬停在哪个索引字母上有了更清晰的指示。
关于滚动条、分页和颜色
我决定不实现滚动条或分页。我觉得有内部滚动条的页面很烦人——我宁愿滚动整个浏览器窗口。此外,对于联系人列表之类的东西,我认为分页不合适。即使是很长的列表,当它已排序时,滚动速度也极快。分页只会碍事。最后,我想将按钮集用绿色表示选中,用红色表示未选中,但同样,这对红绿色盲的人来说是糟糕的用户界面设计。
结论
希望你喜欢这次旅行,并觉得我创建的联系人列表展示至少在某种程度上具有美学吸引力!特别感谢 Peter 制作了 ASP.NET 移植版本!