交互设计:隐藏或显示大段文本的UI组件有哪些?
应用场景:
在手机上要给列表中的每一项添加一个大段的介绍,应该用什么UI组件
A:
这里可以用,模态对话框,弹出提示,工具提示这类组件。模态对话框的好处,就是用关闭的按钮,用户操作方便;而弹出提示和工具提示只能通过点击来切换
模态对话框:
http://v2.bootcss.com/javascript.html#modals
http://www.runoob.com/bootstrap/bootstrap-modal-plugin.html
Bootstrap 模态框(Modal)插件
模态框(Modal)是覆盖在父窗体上的子窗体。通常,目的是显示来自一个单独的源的内容,可以在不离开父窗体的情况下有一些互动。子窗体可提供信息、交互等。
如果您想要单独引用该插件的功能,那么您需要引用 modal.js。或者,正如 Bootstrap 插件概览 一章中所提到,您可以引用bootstrap.js 或压缩版的 bootstrap.min.js。
用法
您可以切换模态框(Modal)插件的隐藏内容:
通过 data 属性:在控制器元素(比如按钮或者链接)上设置属性 data-toggle="modal",同时设置 data-target="#identifier" 或href="#identifier" 来指定要切换的特定的模态框(带有 id="identifier")。
通过 JavaScript:使用这种技术,您可以通过简单的一行 JavaScript 来调用带有 id="identifier" 的模态框:
$('#identifier').modal(options)
实例
一个静态的模态窗口实例,如下面的实例所示:
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 模态框(Modal)插件</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h2>创建模态框(Modal)</h2>
<!-- 按钮触发模态框 -->
<button class="btn btn-primary btn-lg" data-toggle="modal"
data-target="#myModal">
开始演示模态框
</button>
<!-- 模态框(Modal) -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close"
data-dismiss="modal" aria-hidden="true">
&times;
</button>
<h4 class="modal-title" id="myModalLabel">
模态框(Modal)标题
</h4>
</div>
<div class="modal-body">
在这里添加一些文本
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"
data-dismiss="modal">关闭
</button>
<button type="button" class="btn btn-primary">
提交更改
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal -->
</div>
</body>
</html>
尝试一下 »
结果如下所示:
代码讲解:
使用模态窗口,您需要有某种触发器。您可以使用按钮或链接。这里我们使用的是按钮。
如果您仔细查看上面的代码,您会发现在 <button> 标签中,data-target="#myModal" 是您想要在页面上加载的模态框的目标。您可以在页面上创建多个模态框,然后为每个模态框创建不同的触发器。现在,很明显,您不能在同一时间加载多个模块,但您可以在页面上创建多个在不同时间进行加载。
在模态框中需要注意两点:
第一是 .modal,用来把 <div> 的内容识别为模态框。
第二是 .fade class。当模态框被切换时,它会引起内容淡入淡出。
aria-labelledby="myModalLabel",该属性引用模态框的标题。
属性 aria-hidden="true" 用于保持模态窗口不可见,直到触发器被触发为止(比如点击在相关的按钮上)。
<div class="modal-header">,modal-header 是为模态窗口的头部定义样式的类。
class="close",close 是一个 CSS class,用于为模态窗口的关闭按钮设置样式。
data-dismiss="modal",是一个自定义的 HTML5 data 属性。在这里它被用于关闭模态窗口。
class="modal-body",是 Bootstrap CSS 的一个 CSS class,用于为模态窗口的主体设置样式。
class="modal-footer",是 Bootstrap CSS 的一个 CSS class,用于为模态窗口的底部设置样式。
data-toggle="modal",HTML5 自定义的 data 属性 data-toggle 用于打开模态窗口。
选项
有一些选项可以用来定制模态窗口(Modal Window)的外观和感观,它们是通过 data 属性或 JavaScript 来传递的。下表列出了这些选项:
选项名称
类型/默认值
Data 属性名称
描述
backdrop
boolean 或 string 'static'默认值:true
data-backdrop
指定一个静态的背景,当用户点击模态框外部时不会关闭模态框。
keyboard
boolean默认值:true
data-keyboard
当按下 escape 键时关闭模态框,设置为 false 时则按键无效。
show
boolean默认值:true
data-show
当初始化时显示模态框。
remote
path默认值:false
data-remote
使用 jQuery .load 方法,为模态框的主体注入内容。如果添加了一个带有有效 URL 的 href,则会加载其中的内容。如下面的实例所示:
<a data-toggle="modal" href="remote.html" data-target="#modal">请点击我</a>
方法
下面是一些可与 modal() 一起使用的有用的方法。
方法
描述
实例
Options: .modal(options)
把内容作为模态框激活。接受一个可选的选项对象。
$('#identifier').modal({
keyboard: false
})
Toggle: .modal('toggle')
手动切换模态框。
$('#identifier').modal('toggle')
Show: .modal('show')
手动打开模态框。
$('#identifier').modal('show')
Hide: .modal('hide')
手动隐藏模态框。
$('#identifier').modal('hide')
实例
下面的实例演示了方法的用法:
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 模态框(Modal)插件方法</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h2>模态框(Modal)插件方法</h2>
<!-- 按钮触发模态框 -->
<button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">
开始演示模态框
</button>
<!-- 模态框(Modal) -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">×
</button>
<h4 class="modal-title" id="myModalLabel">
模态框(Modal)标题
</h4>
</div>
<div class="modal-body">
按下 ESC 按钮退出。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"
data-dismiss="modal">关闭
</button>
<button type="button" class="btn btn-primary">
提交更改
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<script>
$(function () { $('#myModal').modal({
keyboard: true
})});
</script>
</body>
</html>
尝试一下 »
结果如下所示:
只需要点击 ESC 键,模态窗口即会退出。
事件
下表列出了模态框中要用到事件。这些事件可在函数中当钩子使用。
事件
描述
实例
show.bs.modal
在调用 show 方法后触发。
$('#identifier').on('show.bs.modal', function () {
// 执行一些动作...
})
shown.bs.modal
当模态框对用户可见时触发(将等待 CSS 过渡效果完成)。
$('#identifier').on('shown.bs.modal', function () {
// 执行一些动作...
})
hide.bs.modal
当调用 hide 实例方法时触发。
$('#identifier').on('hide.bs.modal', function () {
// 执行一些动作...
})
hidden.bs.modal
当模态框完全对用户隐藏时触发。
$('#identifier').on('hidden.bs.modal', function () {
// 执行一些动作...
})
实例
下面的实例演示了事件的用法:
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 模态框(Modal)插件事件</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h2>模态框(Modal)插件事件</h2>
<!-- 按钮触发模态框 -->
<button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">
开始演示模态框
</button>
<!-- 模态框(Modal) -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">×
</button>
<h4 class="modal-title" id="myModalLabel">
模态框(Modal)标题
</h4>
</div>
<div class="modal-body">
点击关闭按钮检查事件功能。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default"
data-dismiss="modal">
关闭
</button>
<button type="button" class="btn btn-primary">
提交更改
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<script>
$(function () { $('#myModal').modal('hide')})});
</script>
<script>
$(function () { $('#myModal').on('hide.bs.modal', function () {
alert('嘿,我听说您喜欢模态框...');})
});
</script>
</body>
</html>
尝试一下 »
结果如下所示:
正如上面实例所示,如果您点击了 关闭 按钮,即 hide 事件,则会显示一个警告消息。
Bootstrap 弹出框(Popover)插件
弹出框(Popover)与工具提示(Tooltip)类似,提供了一个扩展的视图。如需激活弹出框,用户只需把鼠标悬停在元素上即可。弹出框的内容完全可使用 Bootstrap 数据 API(Bootstrap Data API)来填充。该方法依赖于工具提示(tooltip)。
如果您想要单独引用该插件的功能,那么您需要引用 popover.js,它依赖于 工具提示(Tooltip)插件。或者,正如 Bootstrap 插件概览一章中所提到,您可以引用 bootstrap.js 或压缩版的 bootstrap.min.js。
用法
弹出框(Popover)插件根据需求生成内容和标记,默认情况下是把弹出框(popover)放在它们的触发元素后面。您可以有以下两种方式添加弹出框(popover):
通过 data 属性:如需添加一个弹出框(popover),只需向一个锚/按钮标签添加 data-toggle="popover" 即可。锚的 title 即为弹出框(popover)的文本。默认情况下,插件把弹出框(popover)设置在顶部。
<a href="#" data-toggle="popover" title="Example popover">
请悬停在我的上面
</a>
通过 JavaScript:通过 JavaScript 启用弹出框(popover):
$('#identifier').popover(options)
弹出框(Popover)插件不像之前所讨论的下拉菜单及其他插件那样,它不是纯 CSS 插件。如需使用该插件,您必须使用 jquery 激活它(读取 javascript)。使用下面的脚本来启用页面中的所有的弹出框(popover):
$(function () { $("[data-toggle='popover']").popover(); });
实例
下面的实例演示了通过 data 属性使用弹出框(Popover)插件的用法。
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 弹出框(Popover)插件</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="padding: 100px 50px 10px;" >
<button type="button" class="btn btn-default" title="Popover title"
data-container="body" data-toggle="popover" data-placement="left"
data-content="左侧的 Popover 中的一些内容">
左侧的 Popover
</button>
<button type="button" class="btn btn-primary" title="Popover title"
data-container="body" data-toggle="popover" data-placement="top"
data-content="顶部的 Popover 中的一些内容">
顶部的 Popover
</button>
<button type="button" class="btn btn-success" title="Popover title"
data-container="body" data-toggle="popover" data-placement="bottom"
data-content="底部的 Popover 中的一些内容">
底部的 Popover
</button>
<button type="button" class="btn btn-warning" title="Popover title"
data-container="body" data-toggle="popover" data-placement="right"
data-content="右侧的 Popover 中的一些内容">
右侧的 Popover
</button>
</div>
<script>$(function ()
{ $("[data-toggle='popover']").popover();
});
</script>
</div>
</body>
</html>
尝试一下 »
结果如下所示:
选项
有一些选项是通过 Bootstrap 数据 API(Bootstrap Data API)添加或通过 JavaScript 调用的。下表列出了这些选项:
选项名称
类型/默认值
Data 属性名称
描述
animation
boolean默认值:true
data-animation
向弹出框应用 CSS 褪色过渡效果。
html
boolean默认值:false
data-html
向弹出框插入 HTML。如果为 false,jQuery 的 text 方法将被用于向 dom 插入内容。如果您担心 XSS 攻击,请使用 text。
placement
string|function默认值:top
data-placement
规定如何定位弹出框(即 top|bottom|left|right|auto)。当指定为 auto 时,会动态调整弹出框。例如,如果 placement 是 "auto left",弹出框将会尽可能显示在左边,在情况不允许的情况下它才会显示在右边。
selector
string默认值:false
data-selector
如果提供了一个选择器,弹出框对象将被委派到指定的目标。
title
string | function默认值:''
data-title
如果未指定 title 属性,则 title 选项是默认的 title 值。
trigger
string默认值:'hover focus'
data-trigger
定义如何触发弹出框: click| hover | focus | manual。您可以传递多个触发器,每个触发器之间用空格分隔。
delay
number | object默认值:0
data-delay
延迟显示和隐藏弹出框的毫秒数 - 对 manual 手动触发类型不适用。如果提供的是一个数字,那么延迟将会应用于显示和隐藏。如果提供的是对象,结构如下所示:
delay:
{ show: 500, hide: 100 }
container
string | false默认值:false
data-container
向指定元素追加弹出框。实例: container: 'body'
方法
下面是一些弹出框(Popover)插件中有用的方法:
方法
描述
实例
Options: .popover(options)
向元素集合附加弹出框句柄。
$().popover(options)
Toggle: .popover('toggle')
切换显示/隐藏元素的弹出框。
$('#element').popover('toggle')
Show: .popover('show')
显示元素的弹出框。
$('#element').popover('show')
Hide: .popover('hide')
隐藏元素的弹出框。
$('#element').popover('hide')
Destroy: .popover('destroy')
隐藏并销毁元素的弹出框。
$('#element').popover('destroy')
实例
下面的实例演示了弹出框(Popover)插件的方法:
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 弹出框(Popover)插件方法</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="padding: 100px 50px 10px;" >
<button type="button" class="btn btn-default popover-show"
title="Popover title" data-container="body"
data-toggle="popover" data-placement="left"
data-content="左侧的 Popover 中的一些内容 —— show 方法">
左侧的 Popover
</button>
<button type="button" class="btn btn-primary popover-hide"
title="Popover title" data-container="body"
data-toggle="popover" data-placement="top"
data-content="顶部的 Popover 中的一些内容 —— hide 方法">
顶部的 Popover
</button>
<button type="button" class="btn btn-success popover-destroy"
title="Popover title" data-container="body"
data-toggle="popover" data-placement="bottom"
data-content="底部的 Popover 中的一些内容 —— destroy 方法">
底部的 Popover
</button>
<button type="button" class="btn btn-warning popover-toggle"
title="Popover title" data-container="body"
data-toggle="popover" data-placement="right"
data-content="右侧的 Popover 中的一些内容 —— toggle 方法">
右侧的 Popover
</button><br><br><br><br><br><br>
<p class="popover-options">
<a href="#" type="button" class="btn btn-warning" title="<h2>Title</h2>"
data-container="body" data-toggle="popover" data-content="
<h4>Popover 中的一些内容 —— options 方法</h4>">
Popover
</a>
</p>
<script>
$(function () { $('.popover-show').popover('show');});
$(function () { $('.popover-hide').popover('hide');});
$(function () { $('.popover-destroy').popover('destroy');});
$(function () { $('.popover-toggle').popover('toggle');});
$(function () { $(".popover-options a").popover({html : true });});
</script>
</div>
</body>
</html>
尝试一下 »
结果如下所示:
事件
下表列出了弹出框(Popover)插件中要用到的事件。这些事件可在函数中当钩子使用。
事件
描述
实例
show.bs.popover
当调用 show 实例方法时立即触发该事件。
$('#mypopover').on('show.bs.popover', function () {
// 执行一些动作...
})
shown.bs.popover
当弹出框对用户可见时触发该事件(将等待 CSS 过渡效果完成)。
$('#mypopover').on('shown.bs.popover', function () {
// 执行一些动作...
})
hide.bs.popover
当调用 hide 实例方法时立即触发该事件。
$('#mypopover').on('hide.bs.popover', function () {
// 执行一些动作...
})
hidden.bs.popover
当工具提示对用户隐藏时触发该事件(将等待 CSS 过渡效果完成)。
$('#mypopover').on('hidden.bs.popover', function () {
// 执行一些动作...
})
实例
下面的实例演示了弹出框(Popover)插件的事件:
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 弹出框(Popover)插件事件</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<div clas="container" style="padding: 100px 50px 10px;" >
<button type="button" class="btn btn-primary popover-show"
title="Popover title" data-container="body"
data-toggle="popover"
data-content="左侧的 Popover 中的一些内容 —— show 方法">
左侧的 Popover
</button>
</div>
<script>
$(function () { $('.popover-show').popover('show');});
$(function () { $('.popover-show').on('shown.bs.popover', function () {
alert("当显示时警告消息");
})});
</script>
</div>
</body>
</html>
尝试一下 »
结果如下所示:
Bootstrap 提示工具(Tooltip)插件
当您想要描述一个链接的时候,提示工具(Tooltip)就显得非常有用。提示工具(Tooltip)插件是受 Jason Frame 写的 jQuery.tipsy 的启发。提示工具(Tooltip)插件做了很多改进,例如不需要依赖图像,而是改用 CSS 实现动画效果,用 data 属性存储标题信息。
如果您想要单独引用该插件的功能,那么您需要引用 tooltip.js。或者,正如 Bootstrap 插件概览 一章中所提到,您可以引用bootstrap.js 或压缩版的 bootstrap.min.js。
用法
提示工具(Tooltip)插件根据需求生成内容和标记,默认情况下是把提示工具(tooltip)放在它们的触发元素后面。您可以有以下两种方式添加提示工具(tooltip):
通过 data 属性:如需添加一个提示工具(tooltip),只需向一个锚标签添加 data-toggle="tooltip" 即可。锚的 title 即为提示工具(tooltip)的文本。默认情况下,插件把提示工具(tooltip)设置在顶部。
<a href="#" data-toggle="tooltip" title="Example tooltip">请悬停在我的上面</a>
通过 JavaScript:通过 JavaScript 触发提示工具(tooltip):
$('#identifier').tooltip(options)
提示工具(Tooltip)插件不像之前所讨论的下拉菜单及其他插件那样,它不是纯 CSS 插件。如需使用该插件,您必须使用 jquery 激活它(读取 javascript)。使用下面的脚本来启用页面中的所有的提示工具(tooltip):
$(function () { $("[data-toggle='tooltip']").tooltip(); });
实例
下面的实例演示了通过 data 属性使用提示工具(Tooltip)插件的用法。
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 提示工具(Tooltip)插件</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h4>提示工具(Tooltip)插件 - 锚</h4>
这是一个 <a href="#" class="tooltip-test" data-toggle="tooltip"
title="默认的 Tooltip">
默认的 Tooltip
</a>.
这是一个 <a href="#" class="tooltip-test" data-toggle="tooltip"
data-placement="left" title="左侧的 Tooltip">
左侧的 Tooltip
</a>.
这是一个 <a href="#" data-toggle="tooltip" data-placement="top"
title="顶部的 Tooltip">
顶部的 Tooltip
</a>.
这是一个 <a href="#" data-toggle="tooltip" data-placement="bottom"
title="底部的 Tooltip">
底部的 Tooltip
</a>.
这是一个 <a href="#" data-toggle="tooltip" data-placement="right"
title="右侧的 Tooltip">
右侧的 Tooltip
</a>
<br>
<h4>提示工具(Tooltip)插件 - 按钮</h4>
<button type="button" class="btn btn-default" data-toggle="tooltip"
title="默认的 Tooltip">
默认的 Tooltip
</button>
<button type="button" class="btn btn-default" data-toggle="tooltip"
data-placement="left" title="左侧的 Tooltip">
左侧的 Tooltip
</button>
<button type="button" class="btn btn-default" data-toggle="tooltip"
data-placement="top" title="顶部的 Tooltip">
顶部的 Tooltip
</button>
<button type="button" class="btn btn-default" data-toggle="tooltip"
data-placement="bottom" title="底部的 Tooltip">
底部的 Tooltip
</button>
<button type="button" class="btn btn-default" data-toggle="tooltip"
data-placement="right" title="右侧的 Tooltip">
右侧的 Tooltip
</button>
<script>
$(function () { $("[data-toggle='tooltip']").tooltip(); });
</script>
</body>
</html>
尝试一下 »
结果如下所示:
选项
有一些选项是通过 Bootstrap 数据 API(Bootstrap Data API)添加或通过 JavaScript 调用的。下表列出了这些选项:
选项名称
类型/默认值
Data 属性名称
描述
animation
boolean默认值:true
data-animation
提示工具使用 CSS 渐变滤镜效果。
html
boolean默认值:false
data-html
向提示工具插入 HTML。如果为 false,jQuery 的 text 方法将被用于向 dom 插入内容。如果您担心 XSS 攻击,请使用 text。
placement
string|function默认值:top
data-placement
规定如何定位提示工具(即 top|bottom|left|right|auto)。当指定为 auto 时,会动态调整提示工具。例如,如果 placement 是 "auto left",提示工具将会尽可能显示在左边,在情况不允许的情况下它才会显示在右边。
selector
string默认值:false
data-selector
如果提供了一个选择器,提示工具对象将被委派到指定的目标。
title
string | function默认值:''
data-title
如果未指定 title 属性,则 title 选项是默认的 title 值。
trigger
string默认值:'hover focus'
data-trigger
定义如何触发提示工具: click| hover | focus | manual。您可以传递多个触发器,每个触发器之间用空格分隔。
content
string | function默认值:''
data-content
如果未指定 data-content 属性,则使用默认的 content 值。
delay
number | object默认值:0
data-delay
延迟显示和隐藏提示工具的毫秒数 - 对 manual 手动触发类型不适用。如果提供的是一个数字,那么延迟将会应用于显示和隐藏。如果提供的是对象,结构如下所示:
delay:
{ show: 500, hide: 100 }
container
string | false默认值:false
data-container
向指定元素追加提示工具。实例: container: 'body'
方法
下面是一些提示工具(Tooltip)插件中有用的方法:
方法
描述
实例
Options: .tooltip(options)
向元素集合附加提示工具句柄。
$().tooltip(options)
Toggle: .tooltip('toggle')
切换显示/隐藏元素的提示工具。
$('#element').tooltip('toggle')
Show: .tooltip('show')
显示元素的提示工具。
$('#element').tooltip('show')
Hide: .tooltip('hide')
隐藏元素的提示工具。
$('#element').tooltip('hide')
Destroy: .tooltip('destroy')
隐藏并销毁元素的提示工具。
$('#element').tooltip('destroy')
实例
下面的实例演示了提示工具(Tooltip)插件方法的用法。
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 提示工具(Tooltip)插件方法</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<div style="padding: 100px 100px 10px;">
这是一个 <a href="#" class="tooltip-show" data-toggle="tooltip"
title="show">Tooltip 方法 show
</a>.
这是一个 <a href="#" class="tooltip-hide" data-toggle="tooltip"
data-placement="left" title="hide">Tooltip 方法 hide
</a>.
这是一个 <a href="#" class="tooltip-destroy" data-toggle="tooltip"
data-placement="top" title="destroy">Tooltip 方法 destroy
</a>.
这是一个 <a href="#" class="tooltip-toggle" data-toggle="tooltip"
data-placement="bottom" title="toggle">Tooltip 方法 toggle
</a>.
<br><br><br><br><br><br>
<p class="tooltip-options" >
这是一个 <a href="#" data-toggle="tooltip" title="<h2>'am Header2
</h2>">Tooltip 方法 options
</a>.
</p>
<script>
$(function () { $('.tooltip-show').tooltip('show');});
$(function () { $('.tooltip-hide').tooltip('hide');});
$(function () { $('.tooltip-destroy').tooltip('destroy');});
$(function () { $('.tooltip-toggle').tooltip('toggle');});
$(function () { $(".tooltip-options a").tooltip({html : true });
});
</script>
<div>
</body>
</html>
尝试一下 »
结果如下所示:
事件
下表列出了提示工具(Tooltip)插件中要用到的事件。这些事件可在函数中当钩子使用。
事件
描述
实例
show.bs.tooltip
当调用 show 实例方法时立即触发该事件。
$('#myTooltip').on('show.bs.tooltip', function () {
// 执行一些动作...
})
shown.bs.tooltip
当提示工具对用户可见时触发该事件(将等待 CSS 过渡效果完成)。
$('#myTooltip').on('shown.bs.tooltip', function () {
// 执行一些动作...
})
hide.bs.tooltip
当调用 hide 实例方法时立即触发该事件。
$('#myTooltip').on('hide.bs.tooltip', function () {
// 执行一些动作...
})
hidden.bs.tooltip
当提示工具对用户隐藏时触发该事件(将等待 CSS 过渡效果完成)。
$('#myTooltip').on('hidden.bs.tooltip', function () {
// 执行一些动作...
})
实例
下面的实例演示了提示工具(Tooltip)插件事件的用法。
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap 实例 - 提示工具(Tooltip)插件事件</title>
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/scripts/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h4>提示工具(Tooltip)插件 - 锚</h4>
这是一个 <a href="#" class="tooltip-show" data-toggle="tooltip"
title="默认的 Tooltip">默认的 Tooltip
</a>.
<script>
$(function () { $('.tooltip-show').tooltip('show');});
$(function () { $('.tooltip-show').on('show.bs.tooltip', function () {
alert("Alert message on show");
})});
</script>
</body>
</html>
尝试一下 »
结果如下所示:
如何联系我:【万里虎】www.bravetiger.cn
【QQ】3396726884 (咨询问题100元起,帮助解决问题500元起)
【博客】http://www.cnblogs.com/kenshinobiy/
如何用 CSS 创作一个立体滑动 toggle 交互控件
效果预览
在线演示
按下右侧的“点击预览”按钮在当前页面预览,点击链接全屏预览。
https://codepen.io/zhang-ou/pen/zjoOgX
可交互视频教程
此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。
请用 chrome, safari, edge 打开观看。
https://scrimba.com/c/cPvMzTg
源代码下载
本地下载
请从 github 下载。
https://github.com/comehope/front-end-daily-challenges/tree/master/005-sleek-sliding-toggle-checkbox
代码解读
定义 dom,是嵌套了3层的容器:
&lt;div class="checkbox"&gt;
&lt;div class="inner"&gt;
&lt;div class="toggle"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
居中显示:
html,
body,
.checkbox,
.checkbox .inner,
.checkbox .inner .toggle {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
画出外侧椭圆:
.checkbox {
width: 10em;
height: 5em;
background: linear-gradient(silver, whitesmoke);
border-radius: 2.5em;
font-size: 40px;
}
画出内侧椭圆:
.checkbox .inner {
width: 8em;
height: 3.5em;
background: linear-gradient(dimgray, silver);
border-radius: 2em;
box-shadow: inset 0 0 1.5em rgba(0, 0, 0, 0.5);
}
画出圆形按钮:
.checkbox .inner .toggle {
width: 3.5em;
height: 3.5em;
background: linear-gradient(to top, silver, whitesmoke);
border-radius: 50%;
box-shadow: 0 0.4em 0.6em rgba(0, 0, 0, 0.2);
position: relative;
left: -30%;
}
为圆形按钮增加立体效果:
.checkbox .inner .toggle::before {
content: '';
position: absolute;
height: 80%;
width: 80%;
background: linear-gradient(whitesmoke, silver);
border-radius: 50%;
}
在按钮上写上 OFF,行高是根据父元素的高度计算出的:
.checkbox .inner .toggle::before {
content: 'OFF';
text-align: center;
line-height: calc(3.5em * 0.8);
font-family: sans-serif;
color: gray;
}
引入jquery:
&lt;script src="http://code.jquery.com/jquery-3.3.1.min.js"&gt;&lt;/script&gt;
编写脚本,在点击按钮时切换样式类:
$(document).ready(function() {
$('.toggle').click(function() {
$('.inner').toggleClass('active');
});
});
设置 active 时控件的样式:
.checkbox .inner.active {
background: linear-gradient(green, limegreen);
}
.checkbox .inner.active .toggle {
left: 30%;
}
.checkbox .inner.active .toggle::before {
content: 'ON';
color: limegreen;
}
最后,为按钮设置缓动时间,实现动画效果
.checkbox .inner .toggle {
transition: 0.5s;
}
大功告成!
知识点
linear-gradient() https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient
box-shadow https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow
calc() https://developer.mozilla.org/en-US/docs/Web/CSS/calc
::before https://developer.mozilla.org/en-US/docs/Web/CSS/::before
jquery toggleClass http://api.jquery.com/toggleclass/
原文地址:https://segmentfault.com/a/1190000014638655
用Visual Studio Code写Node.j
介绍
vsc的宣传语是:
一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。
按它说的,vsc特别适合来作为前端开发编辑器。
内置html开发神器emmet(zencoding),对css及其相关编译型语言Less和Sass都有很好的支持。
当然,最nice的还是写js代码了,这也是我接下来要着重介绍的功能。
智能提示
因为之前微软推出了typescript语言,结合tsd文件,用visual studio写typescript代码是相当爽的,智能提示的功能非常nb。
这个功能理所应当也被vsc继承了。
目前主流的前端类库/框架,包括node.js及其模块/框架都有相应的tsd文件,可以去DefinitelyTyped上找一下。
在项目中引入对应文件,就可以有智能提示了。
这里以angular为例,使用步骤如下:
全局安装tsd,通过tsd安装.d.ts文件。这样会在项目下面生成.typings目录,目录下面就是下载的.d.ts文件,再写代码的时候就会有智能提示了。具体用法参考tsd用法。
npm install -g tsd
tsd query angular --action install
如果不想自己手工引入,也可以在angular变量后面按ctrl+k,会有个灯泡图片,点击灯泡图片就会有对应提示,选择下载xx.d.ts文件就可以了,编辑器会下载对应文件放在.typings目录。
过程如下图:
再来个node.js的:
说完智能提示,再说代码调试。
调试Node
之前写过文章介绍过node.js的调试方案(Node.js调试)。从vsc发布后,我就一直用它写代码,也是用它来调试node.js代码。
使用方法也很简单,步骤如下:
打开要调试的文件,按f5,编辑器会生成一个launch.json
修改launch.json相关内容,主要是name和program字段,改成和你项目对应的
点击编辑器左侧长得像蜘蛛的那个按钮
点击左上角DEBUG后面的按钮,启动调试
打断点,尽情调试
过程如下图:
最后,再赠送彩蛋一个。
Node API 查看
在写node.js代码的时候,有时会忘记某个模块中有哪些方法及其用法,经常要去官网翻一下api文档。
这里介绍下怎么使用vsc来搞定这一问题。
打开vsc控制台(Help > Toggle Developer Tools > Console)
在控制台写代码,查询模块方法。
过程如下图:
vsc是用atom-shell(现在叫electron)写的,这玩意和node-webkit(现在叫nw.js)一样,都是把node.js和chrome结合起来的工具,所以可以这么使用。
不过vsc使用到的node.js模块并不多,比如引用util和vm等会报错,用node-webkit就不会这样。
结语
vsc和其他编辑器(sublime text,atom,webstorm等)相比,某些方面还存在很多问题。对于一个前端工程师来说,它已经足够好了。
当然了,它在不断改进。等着它支持插件系统,支持vim模式。
微信公众平台开发 JS-SDK开发(图像接口实例)
本文并非是对微信JS-SDK说明文档的复制,而是通过一个简单的例子来更深入的了解使用微信JS-SDK,具体文档请参考官方说明文档《微信JS-SDK说明文档》。微信公众平台面向开发者开放微信内网页开发工具包(微信JS-SDK),通过微信JS-SDK提供的11类接口集,开发者不仅能够在网页上使用微信本身的拍照、选图、语音、位置等基本能力,还可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。
微信JS-SDK提供的11类接口(分享接口、图像接口、音频接口、智能接口、设备信息、界面操作、地理位置、微信扫一扫、微信小店、微信卡券、微信支付)在使用方式上完全相同,唯一需要注意的是,这11类接口并不是都开放的,有些接口非认证用户是没有权限的,比如分享接口,小店接口、卡券接口、支付接口则必须通过微信认证后才能使用。具体权限问题可参考你的接口权限表。
开发前提
开发者ID (应用ID和应用密匙)
在公众号(服务号)设置——功能设置(JS接口安全域名)中填写已备案的域名
服务器环境
简单的后台知识,如PHP
linux服务器,请确保目录有可写权限
本文以php为例
首先在微信开发者平台下载它的示例代码,链接中包含php、java、nodejs以及python的示例代码供第三方参考,这里使用php包。我把php文件夹重命名为weixin放在网站根目录下,如图:
微信公众平台开发 JS-SDK开发保存图片到本地服务器
因为涉及到上传到本地服务器,所以还需要一个api接口(api.php),简单展示下上传保存代码:
后端服务
//保存为同级目录下api.php
require_once './config/app.php';
require_once './config/wexin.php';
//curl
function getcurl($url, $data=array()){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch) ;
return $response;
}
/**
* 获取微信access_token
*/
function get_access_token () {
global $wxConfig;
$url = "https://api.weixin.qq.com/cgi-bin/token";
$data = array(
'grant_type'=>'client_credential',
'appid'=> $wxConfig['appId'],
'secret'=> $wxConfig['appSecret']
);
$file = getcurl($url,$data);
$josn = json_decode($file,true);
$token = $josn['access_token'];
if($token){
return $token;
}else{
return null;
}
}
/**
* 上传图片
* @param media_id
*/
function upload($media_id) {
$access_token = get_access_token();
if (!$access_token) return false;
$url = "http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=".$access_token."&media_id=".$media_id;
if (!file_exists(UPLOAD_PATH)) {
mkdir('./upload/', 0775, true); //将图片保存到upload目录
}
$fileName = date('YmdHis').rand(1000,9999).'.jpg';
$targetName = './upload/'. $fileName;
$ch = curl_init($url);
$fp = fopen($targetName, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp); // 设置输出文件的位置,值是一个资源类型
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
fclose($fp);
return '/upload/'.$fileName; //输出文件名
}
/**
* 输出json
*/
function toJson ($code = 200, $data = array(), $message = 'success') {
return json_encode(array('code' => $code, 'data' => $data, 'message' => $message));
}
if (isset($_GET['api'])) {
$api = $_GET['api'];
//上传
if ($api == 'upload') {
$mediaId = $_POST['media_id'];
$file = upload($mediaId);
if ($file) {
exit (toJson(200, array('url' => $file)));
} else {
exit (toJson(400, null, 'error'));
}
}
}
前端页面
再简单修改下sample.php
<?php
require_once './config/app.php';
require_once './config/wexin.php';
require_once "./libs/wexin/jssdk.php";
$jssdk = new JSSDK($wxConfig['appId'], $wxConfig['appSecret']);
$signPackage = $jssdk->GetSignPackage();
?>
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="blank">
<meta name="format-detection" content="telephone=no, email=no">
<link rel="stylesheet" href="./public/css/weui.css" media="screen">
<title>微信上传DEMO</title>
</head>
<body>
<article>
<a class="weui-btn weui-btn_plain-primary upload-toggle" id="js-upload">上传图片</a>
<figure><img src="" id="js-preview"></figure>
</article>
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script src="./public/js/zepto.min.js"></script>
<script>
wx.config({
debug: true,
appId: '<?php echo $signPackage["appId"];?>',
timestamp: <?php echo $signPackage["timestamp"];?>,
nonceStr: '<?php echo $signPackage["nonceStr"];?>',
signature: '<?php echo $signPackage["signature"];?>',
jsApiList: [
// 所有要调用的 API 都要加到这个列表中
"chooseImage",
"previewImage",
"uploadImage",
"downloadImage"
]
});
wx.ready(function() {
/**
* [weixinUpload 调用微信接口实现上传]
* @param {[function]} choose [选择图片后的回调]
* @param {[function]} upload [上传后的回调]
*/
function weixinUpload(choose, upload) {
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: function(res) {
var localIds = res.localIds;
choose && choose(localIds); //选择图片后回调
// 上传照片
wx.uploadImage({
localId: '' + localIds,
isShowProgressTips: 1, //开启上传进度
success: function(res) {
serverId = res.serverId;
upload && upload(serverId); //上传图片后回调
}
});
}
});
}
/**
* [uploadImage 上传图片到本地服务器]
* @param {[type]} mediaId [图片serverID]
* @param {Function} callback [回调]
*/
function uploadImage(mediaId, callback) {
$.ajax({
type: "POST",
url: "api.php?api=upload",
data: {
media_id: serverId
},
dataType: "json",
success: function(result) {
if (result.code == 200) {
callback(result.data);
} else {
alert("上传失败!");
}
},
error: function() {
alert("系统异常,请稍后重试");
}
});
}
var $uploadButton = $('#js-upload')
var $uploadPreview = $('#js-preview')
//点击上传按钮
$uploadButton.on('click', function() {
weixinUpload(
function(localIds) {
$uploadPreview.attr('src', localIds); //微信本地图片地址,可以用来做上传前预览
},
function(serverId) {
uploadImage(serverId, function(data) {
$uploadPreview.attr('src', data.url); //返回真实的图片地址
});
}
);
})
});
</script>
</body>
</html>
好了,将修改后的代码连同新建的api.php同时上传到服务器。该服务器域名必须和你在微信设置中的域名一致。我上传到了根目录,通过firefox提供的页面二维码功能,在微信上扫一扫即可测试。截图如下:
微信上传图片测试图片说明
[图一] 表示我们的微信配置正确
[图二] 点击 上传图片 按钮后弹出 拍照,从手机相册选择 等功能按钮
[图三] 进入了手机相册
[图四] 选择一张图片后,返回该图片的localId
[图五] 利用localId作为图片预览,并且生成了serverID
[图六] api 上传并保存了图片到本地服务器,返回图片在本地服务器的地址,并且回调在了页面上
小结
通过以上两个简单的例子,基本可以掌握微信JS-SDK的使用方法。11个接口使用方式基本相同,你可以一一尝试。官方的说明是:通过ready接口处理成功验证通过error接口处理失败验证
所有接口通过wx对象(也可使用jWeixin对象)来调用,参数是一个对象,除了每个接口本身需要传的参数之外,还有以下通用参数:
success:接口调用成功时执行的回调函数。
fail:接口调用失败时执行的回调函数。
complete:接口调用完成时执行的回调函数,无论成功或失败都会执行。
cancel:用户点击取消时的回调函数,仅部分有用户取消操作的api才会用到。
trigger: 监听Menu中的按钮点击时触发的方法,该方法仅支持Menu中的相关接口。
具体可参考官方API。
你可能不知道的5种 CSS 和 JS 的交互方式
翻译人员: 铁锚
翻译日期: 2014年01月22日
原文日期: 2014年01月20日
原文链接: 5 Ways that CSS and
JavaScript Interact That You May Not Know About
CSS和JavaScript: 在各个浏览器版本中的分界线似乎变得越来越模糊.
两者所完成的功能差异非常明显,但说到底他们都是前端技术,所以确实需要紧密地配合。
虽然我们将代码拆分到独立的 .js 文件和 .css 文件中,但他们之间还能进行一些更密切的交互,这些通常是普通JS框架所不支持的.
下面是你可能不知道的CSS和JS的交互的5种方式: 使用JavaScript获取CSS伪元素属性
我们可以通过DOM元素的 style 属性获取基本的CSS样式值, 但怎么获取CSS伪元素属性呢? 确实,JavaScript提供了相应的API:// 获取 .element:before 的 color 值
var color = window.getComputedStyle(
document.querySelector('.element'), ':before'
).getPropertyValue('color');
// 获取 .element:before 的 content 值
var content = window.getComputedStyle(
document.querySelector('.element'), ':before'
).getPropertyValue('content');
你可以查看作者的博客文章:
Device State Detection, 如果你想创建动态,独特的网站那会非常有用.classList API
在最受欢迎的JS框架中,我们可以使用 addClass , removeClass , 以及 toggleClass 方法来处理 class 列表. 为了兼容旧版本浏览器,每个框架都会先获取元素的 className属性(类型为String) 并添加/移除 相应的 class, 然后再更新 className 字符串. 现在,有一个新的API可以用来高效地 添加,删除,切换 class,名为 classList:myDiv.classList.add('myCssClass'); // 添加 class
myDiv.classList.remove('myCssClass'); // 移除 class
myDiv.classList.toggle('myCssClass'); // 切换 class大多数浏览器支持 classList 已经好几年了,但是 IE 很杯具的在 10.0 版本才开始支持.直接操作样式表
我们对于使用 element.style.propertyName 这种形式的方法修改样式都很熟悉, 并可以通过 JavaScript 工具来处理,但你知道 怎样在新的和已存在的 stylesheets 中 读取和写入样式规则 吗? 这个API也是很简单的:function addCSSRule(sheet, selector, rules, index) {
// 注意 sheet 的新 API
if(sheet.insertRule) {
sheet.insertRule(selector + "{" + rules + "}", index);
}
else {
sheet.addRule(selector, rules, index);
}
}
// 调用!
addCSSRule(document.styleSheets[0], "header", "float: left");通常是使用此种方式来创建一个新的样式表,但如果你想修改一个现有的样式表也可以,快试试吧!使用 Loader 加载 CSS 文件
对图片, JSON,以及JS脚本的懒加载是减少页面显示时间的有效方法. 我们可以通过 curl.js 或者 jQeury 库加载这些外部资源, 但你知道如何 懒加载样式表 并在加载完成后触发回调函数么?curl(
[
"namespace/MyWidget",
"css!namespace/resources/MyWidget.css"
],
function(MyWidget) {
// 使用 MyWidget 进行
// The CSS reference isn't in the signature because we don't care about it;
// we just care that it is now in the page
}
});
本博客的 原文 就使用懒加载 导入了 PrismJS,基于 pre 元素实现文字的高亮显示. 在所有资源(包括css)加载完成后,会调用回调函数,这应该是很有用的.CSS 的 pointer-events 属性
CSS' pointer-events 属性扮演了一种类似 JavaScript 的方式, 如果 pointer-events 值为 none 则禁用某个元素,不为 none 则允许元素的常用功能. 可能你会问: "这有什么用?" 好处是 pointer-events 能高效地阻止触发 JavaScript 事件..disabled { pointer-events: none; }在元素上的 click 以及通过 addEventListener 添加的回调函数都不会被触发. 这真是一个完美的属性,真的 —— 你在回调函数中不需要先检测 className 以决定某些分支.
上面列举了CSS与Javascript交互的5种方式,还有更多的交互方式吗? 欢迎您进行分享!
ASP.NET MVC使用Bootstrap系列(4)——使用JavaScript插件
阅读目录
序言
Data属性 VS 编程API
下拉菜单(dropdown.js)
模态框(modal.js)
标签页(tab.js)
工具提示(tooltip.js)
弹出框(popover.js)
手风琴组件(collapse.js)
旋转木马组件(carousel.js)
小结
回到顶部
序言
Bootstrap的JavaScript插件是以JQuery为基础,提供了全新的功能并且还可以扩展现有的Bootstrap组件。通过添加data attribute(data 属性)可以轻松的使用这些插件,当然你也可以使用编程方式的API来使用。
为了使用Bootstrap插件,我们需要添加Bootstrap.js或者Bootstrap.min.js文件到项目中。这两个文件包含了所有的Bootstrap插件,推荐引用Bootstrap.min.js。当然你也可以包含指定的插件来定制化Bootstrap.js,已达到更好的加载速度。
回到顶部
Data属性 VS 编程API
Bootstrap提供了完全通过HTML标记的方式来使用插件,这意味着,你可以不写任何JavaScript代码,事实上这也是Bootstrap推荐的使用方式。
举个栗子,若要使Alert组件支持关闭功能,你可以通过添加data-dismiss="alert"属性到按钮(Botton)或者链接(A)元素上,如下代码所示:
<div class="alert alert-danger">
<button data-dismiss="alert" class="close" type="button">x</button>
<strong>警告</strong>10秒敌人到达
</div>
当然,你也可以通过编程方式的API来实现同样的功能:
<div class="alert alert-danger" id="myalert">
<button data-dismiss="alert" class="close" type="button" onclick="$('#myalert').alert('close')">x</button>
<strong>警告</strong>10秒敌人到达
</div>
回到顶部
下拉菜单(dropdown.js)
使用dropdown插件(增强交互性),你可以将下拉菜单添加到绝大多数的Bootstrap组件上,比如navbar、tabs等。注:将下拉菜单触发器和下拉菜单都包裹在 .dropdown 里,或者另一个声明了 position: relative; 的元素中。
如下是一个地域的菜单,通过Razor引擎动态绑定菜单元素:
<div class="form-group">
@Html.LabelFor(model => model.TerritoryId, new { @class = "control-label col-md-2" })
@Html.HiddenFor(model => model.TerritoryId)
<div class="col-md-10">
<div id="territorydropdown" class="dropdown">
<button id="territorybutton" class="btn btn-sm btn-info" data-toggle="dropdown">@Model.Territory.TerritoryDescription</button>
<ul id="territories" class="dropdown-menu">
@foreach (var item in ViewBag.TerritoryId)
{
<li><a href="#" tabindex="-1" data-value="@item.Value">@item.Text</a></li>
}
</ul>
</div>
</div>
</div>
注意:通过添加data属性:data-toggle="dropdown" 到Button或者Anchor上,可以切换dropdown下拉菜单,增加了交互性。其中菜单的<a>元素设置tabindex=-1,这样做是为了防止元素成为tab次序的一部分。
回到顶部
模态框(modal.js)
模态框以弹出框的形式可以为用户提供信息亦或者在此之中完成表单提交功能。Bootstrap的模态框本质上有3部分组成:模态框的头、体和一组放置于底部的按钮。你可以在模态框的Body中添加任何标准的HTML标记,包括Text或者嵌入的Youtube视频。
一般来说,务必将模态框的 HTML 代码放在文档的最高层级内(也就是说,尽量作为 body 标签的直接子元素),以避免其他组件影响模态框的展现和/或功能。
如下即为一个标准的模态框,包含Header、Body、Footer:
将下面这段HTML标记放在视图的Top或者Bottom:
<div class="modal fade" id="deleteConfirmationModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">删除</h4>
</div>
<div class="modal-body">
<p>即将删除 '@Model.CompanyName'. </p>
<p>
<strong>你确定要继续吗</strong>
</p>
@using (Html.BeginForm("Delete", "Customers", FormMethod.Post, new { @id = "delete-form", role = "form" }))
{
@Html.HiddenFor(m => m.CustomerId)
@Html.AntiForgeryToken()
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" onclick="$('#delete-form').submit();">是.删除</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
注意:通过在Button上添加data属性:data-dismiss="modal"可以实现对模态框的关闭,当然你也可以使用编程方式API来完成:
<button type="button" class="btn btn-primary" onclick="$('#deleteConfirmationModal').modal('hide')">取消</button>
为了展示模态框,我们可以不写任何JavaScript代码,通过添加data-toggle="modal"属性到Button或者Anchor元素上即可,同时,为了表示展示哪一个模态框,需要通过data-target来指定模态框的Id。
<a href="#" data-toggle="modal" data-target="#deleteConfirmationModal">Delete</a>
同样,也可以使用编程方式API来打开一个模态框:
$(document).ready(function () {
$('#deleteConfirmationModal').modal('show');
});
回到顶部
标签页(tab.js)
Tabs可以使用在大的表单上,通过Tabs分割成若干部分显示局部信息,比如在Northwind数据库中存在Customer顾客信息,它包含了基本信息和地址,可以通过Tabs进行Split,如下所示:
要使用Tabs也是非常简单的:首先创建标准的无序列表<ul>元素,需要为它的class设置为nav nav-tabs 或者nav nav-pills。其中<li>包含的<a>元素即为Tab元素,需要设置其data-toggle为tab或者pill属性以及点击它指向的<div>内容:
<ul id="customertab" class="nav nav-tabs">
<li class="active"><a href="#info" data-toggle="tab">Customer Info</a></li>
<li><a href="#address" data-toggle="tab">Address</a></li>
</ul>
为了设置Tabs的内容,需要创建一个父<div>元素并设置class为tab-content,在父的div容器中为每一个Tab内容创建div元素,并设置class为tab-pane和标识的Id,通过添加active来激活哪一个Tab内容的显示。
<div class="tab-content well">
<div class="tab-pane active" id="info">
Customer Info
</div>
<div class="tab-pane" id="address">
Customer Address
</div>
</div>
当然也可以通过编程方式的API来激活:
$(document).ready(function () {
$('.nav-tabs a[href="#address"]').tab('show');
});
回到顶部
工具提示(tooltip.js)
Tooltip能为用户提供额外的信息,Boostrap Tooltip能被用在各种元素上,比如Anchor:
<a data-toggle="tooltip" data-placement="top" data-original-title="这是提示内容" href="#" >工具提示?</a>
你可以添加data-toggle="tooltip"来使用tooltip,当然你也可以设置内容的显示位置,通过添加data-placement属性来实现,Bootstrap为我们提供了4种位置:
top
bottom
left
right
最后,通过设置data-original-title设置了需要显示的Title。
注意:为了性能的考虑,Tooltip的data-api是可选的,这意味着你必须手动初始化tooltip插件:
<script type="text/javascript">
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
回到顶部
弹出框(popover.js)
弹出框和Tooltip类似,都可以为用户提供额外的信息,但弹出框可以展示更多的信息,比如允许我们展示一个Header和Content,如下所示:
<a data-content="关于基础建设问题需要有具体的研讨和商量...." data-placement="bottom" title="" data-toggle="popover" href="#" data-original-title="转至百度">城市规划</a>
和Tooltip一样,为了性能的考虑,data-api是可选的,这意味着你必须手动初始化popover插件:
<script type="text/javascript">
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
});
</script>
显示的结果如下所示:
回到顶部
手风琴组件(collapse.js)
手风琴组件有若干panel groups组成,每一个panel group依次包含了若干header和content 元素,最常见的使用场景就是FAQ,如下所示:
为了使用手风琴组件,需要对Panel Heading中的<a>设置data-toggle="collapse"和点击它展开的容器(Div)Id,具体如下所示:
<div class="row">
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion"
href="#questionOne">
问题 1:什么是 Microsoft MVP 奖励?
</a>
</h4>
</div>
<div id="questionOne" class="panel-collapse collapse in">
<div class="panel-body">
解答 1:Microsoft 最有价值专家 (MVP) 奖励是一项针对 Microsoft 技术社区杰出成员的年度奖励,根据 Microsoft 技术社区的成员在过去 12 个月内对 Microsoft 相关离线和在线技术社区所作贡献的大小而确定。
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion"
href="#questionTwo">
问题 2:MVP 奖励存在的意义何在?
</a>
</h4>
</div>
<div id="questionTwo" class="panel-collapse collapse">
<div class="panel-body">
解答 2:我们相信,技术社区可以促进自由且客观的知识交流,因此可以构建出一个可靠、独立、真实且可使每个人获益的专业知识来源。Microsoft MVP 奖励旨在表彰那些能积极与其他技术社区成员分享自身知识的全球杰出技术社区领导者。
</div>
</div>
</div>
</div>
</div>
回到顶部
旋转木马组件(carousel.js)
Carousel它本质上是一个幻灯片,循环展示不同的元素,通常展示的是图片,就像旋转木马一样。你可以在许多网站上看到这种组件,要使用它也是非常方便的:
将Carousel组件被包含在一个class为carousel以及data-ride为"carousel"的<div>元素中。
在上述容器里添加一个有序列表<ol>,它将渲染成小圆点代表当前激活的幻灯片(显示在右下角)。
紧接着,添加一个class为carousel-inner的<div>,这个<div>容器包含了实际的幻灯片
然后,添加左右箭头能让用户自由滑动幻灯片
最后,设置滑动切换的时间间隔,通过设置data- interval来实现。当然也可以通过编程方式API来实现
$('#myCarousel').carousel({
interval: 10000
})
将下面HTML标识放在View中即可:
<div class="container-full">
<div id="myCarousel" class="carousel" data-ride="carousel" data-interval="10000">
<!-- 指示灯 -->
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
</ol>
<div class="carousel-inner">
<div class="item active">
<img src="@Url.Content("~/Images/slide1.jpg")" alt="First slide">
<div class="container">
<div class="carousel-caption">
<h1>Unto all said together great in image.</h1>
<p>Won't saw light to void brought fifth was brought god abundantly for you waters life seasons he after replenish beast. Creepeth beginning.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Read more</a></p>
</div>
</div>
</div>
<div class="item">
<img src="@Url.Content("~/Images/slide2.jpg")" alt="Second slide">
<div class="container">
<div class="carousel-caption">
<h1>Fowl, land him sixth moving.</h1>
<p>Morning wherein which. Fourth man life saying own creeping. Said sixth given.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Learn more</a></p>
</div>
</div>
</div>
<div class="item">
<img src="@Url.Content("~/Images/slide3.jpg")" alt="Third slide">
<div class="container">
<div class="carousel-caption">
<h1>Far far away, behind the word mountains.</h1>
<p>A small river named Duden flows by their place and supplies it with the necessary.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">See all</a></p>
</div>
</div>
</div>
</div>
<a class="left carousel-control" href="#myCarousel" data-slide="prev"><span class="glyphicon glyphicon-chevron-left"></span></a>
<a class="right carousel-control" href="#myCarousel" data-slide="next"><span class="glyphicon glyphicon-chevron-right"></span></a>
</div>
</div>
View Code
展示的效果如下:
回到顶部
小结
在这篇博客中介绍了常见的Bootstrap插件,通过使用数据属性和编程方式的API来使用这些插件,更多插件访问:http://v3.bootcss.com/javascript/ 获取。
本博客为木宛城主原创,基于Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名木宛城主(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。
分类: ASP.NET MVC,Bootstrap
本文转自木宛城主博客园博客,原文链接:http://www.cnblogs.com/OceanEyes/p/using-bootstrap-javascript-plugins.html,如需转载请自行联系原作者
从脚手架开始学前端 【第5期】Vue脚手架搭建
一、安装前言Vue.js(读音 /vjuː/, 类似于 view)是一个构建数据驱动的 web 界面的渐进式框架。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。它不仅易于上手,还便于与第三方库或已有项目整合。二、安装特点Vue 只关注视图层, 采用自底向上增量开发的设计。Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。三、安装小知识点:渐进式的意思是指可以在页面直接引入使用也可以在脚手架中使用1.独立版本我们可以在Vue.js的官网上直接下载vue.js,并在.html中通过 开发环境不要使用最小压缩版,不然会没有错误提示和警告!使用vue多页面开发:引入vue.js创建一个vue根实例 new Vue({选项})2.使用CDN引入BootCDN(国内) : cdn.bootcss.com/vue/2.2.2/v… , (国内不稳定)unpkg:unpkg.com/vue/dist/vu…, 会保持和 npm 发布的最新的版本一致。(推荐使用)cdnjs : cdnjs.cloudflare.com/ajax/libs/v…)3.NPM安装Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,通过 @vue/cli 搭建交互式的项目脚手架。 通过 @vue/cli + @vue/cli-service-global 快速开始零配置原型开发。 一个运行时依赖 (@vue/cli-service),该依赖: 可升级; 基于 webpack 构建,并带有合理的默认配置; 可以通过项目内的配置文件进行配置; 可以通过插件进行扩展。 一个丰富的官方插件集合,集成了前端生态中最好的工具。 一套完全图形化的创建和管理 Vue.js 项目的用户界面。 Vue CLI 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性。 在用Vue.js构建大型应用的时候推荐使用NPM安装方法,NPM有完整的生态,能很好的Webpack或者Browserify 模块打包器配合使用。vue-cli3脚手架安装npm install -g @vue/cli
# OR
yarn global add @vue/cli
# TEST
vue -V创建项目vue create my-project
# OR
vue ui命令讲解根据提示,我们选择搭建项目需要集成的工具,对于新手,一般建议使用默认,如果需要其他集成,用方向键移动到手动选择功能,然后点击回车键Vue CLI v4.2.3
? Please pick a preset: (Use arrow keys) //请选择一个预设:(使用箭头键)
> default (babel, eslint) //默认(babel,eslint)
Manually select features //手动选择功能如果创建过,会提示下面信息Vue CLI v4.2.3
? Target directory my-template already exists. Pick an action: (Use arrow keys) //目标目录my-template已存在
选择一个动作:(使用箭头键)
> Overwrite //覆写上次配置
Merge //合并
Cancel //取消 选择Overwrite覆盖上次配置Vue CLI v4.2.3
? Target directory D:\my-project already exists. Pick an action: Overwrite
Removing D:\my-project...根据自己的习惯,用箭头键选择对应的包管理器,小编用的是Yarn,选中直接点击回车Vue CLI v4.2.3
? Please pick a preset: default (babel, eslint) //当前选中的选项记录
? Pick the package manager to use when installing dependencies: (Use arrow keys) //选择安装依赖项时要使用的程序包管理器:(使用箭头键)
> Use Yarn
Use NPM 使用Yarn拉取项目Vue CLI v4.2.3
✨ Creating project in D:\xunzhaotech\my-project.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
yarn install v1.17.3
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.11: The platform "win32" is incompatible with this module.
info "fsevents@1.2.11" is an optional dependency and failed compatibility check. Excluding it from installation.
success Saved lockfile.
Done in 88.14s.
🚀 Invoking generators...
📦 Installing additional dependencies...
yarn install v1.17.3
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.11: The platform "win32" is incompatible with this module.
info "fsevents@1.2.11" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 17.99s.
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project my-project.
👉 Get started with the following commands:
$ cd my-project
$ yarn serve如果用的是NPM包管理器,选中直接点击回车Vue CLI v4.2.3
? Please pick a preset: default (babel, eslint) //当前选中的选项记录
? Pick the package manager to use when installing dependencies: (Use arrow keys) //选择安装依赖项时要使用的程序包管理器:(使用箭头键)
Use Yarn
> Use NPM 使用NPM拉取项目Vue CLI v4.2.3
✨ Creating project in D:\xunzhaotech\my-project.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
yarn install v1.17.3
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.11: The platform "win32" is incompatible with this module.
info "fsevents@1.2.11" is an optional dependency and failed compatibility check. Excluding it from installation.
success Saved lockfile.
Done in 88.14s.
🚀 Invoking generators...
📦 Installing additional dependencies...
yarn install v1.17.3
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.11: The platform "win32" is incompatible with this module.
info "fsevents@1.2.11" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 17.99s.
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project my-project.
👉 Get started with the following commands:
$ cd my-project
$ npm run serve上述我们使用的默认选项,下面我们进入手动选择选项,点击回车Vue CLI v4.2.3
? Please pick a preset: //请选择一个预设
default (babel, eslint) //默认(babel,eslint)
> Manually select features //手动选择功能根据我们项目的需要,结合方向键和空格键选择我们需要的配置,然后点击回车Vue CLI v4.2.3
? Please pick a preset: Manually select features //请选择一个预设:手动选择功能
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)//选取项目所需的功能:(按<空格>选择,<a>切换全部,<i>反转选择)
>(*) Babel //集成Babel
(*) TypeScript //集成TypeScript
(*) Progressive Web App (PWA) Support //集成渐进式Web应用程序(PWA)支持
(*) Router //集成Router
(*) Vuex //集成Vuex
(*) CSS Pre-processors //集成预处理(Less/SCSS/Sass/Stylus)
(*) Linter / Formatter //集成代码校验和格式化
(*) Unit Testing //集成单元测试(站在程序员的角度测试)
(*) E2E Testing //集成单元测试(站在用户角度的测试 ) 根据我们的需要,选择(Y/n),如果全部使用,直接回车,在选择CSS预处理(CSS Pre-processors) 的时候,根据自己的习惯选择Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes //使用类样式的组件语法
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes //将Babel与TypeScript一起使用(用现代模式,自动检测的polyfill,转译JSX)
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes //使用路由器的历史记录模式?
(需要适当的服务器设置才能在生产中进行索引回退)
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)//选择一个CSS预处理器(默认情况下支持PostCSS,Autoprefixer和CSS模块)
> Sass/SCSS (with dart-sass) //dart-sass保存后生效
Sass/SCSS (with node-sass) //node-sass自动编译实时
Less
Stylus 选择需要校验和格式化的方式(Linter / Formatter),选择ESLint + Standard config点击回车Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: (Use arrow keys) //选择一个lint / formatter配置
ESLint with error prevention only //仅具有错误预防功能的ESLint
ESLint + Airbnb config //ESLint + Airbnb 配置
> ESLint + Standard config //ESLint +标准配置
ESLint + Prettier //ESLint +代码美化
TSLint (deprecated) //TSLint(已弃用) 根据需要选择保存时候校验,或者提交时修复校验,然后点击回车Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection) //选择其他校验功能
>(*) Lint on save //保存时校验(保存时校验)
( ) Lint and fix on commit //校验并修复提交 (提交时校验) Jest是由facebook发布的,最近比较火热的一个测试框架。Jest的优势是:Jest容易安装配置:Jest可以说是零配置的,它会自动识别一些测试文件。只要用npm安装jest之后运行jest,即可完成测试,非常容易。Jest提供snapshot功能: snapshot功能能够确保UI不会意外被改变。Jest会把结果值保存在一个文件当中,每次进行测试的时候会把测试值与文件中的结果值进行比较,如果两个结果值不同,那么开发者可以选择要么改变代码,要么替代结果文件。 3.其他:除了上面所提到的优势,Jest还拥有着非常广阔的API而且更加适合测试React应用。Mocha是JavaScript界中最受欢迎的一款单元测试框架。1.灵活性: Mocha比较灵活,和更多的一些库结合使用。 2.资料较多:Mocha是比较年老的测试框架,在JavaScript界中更加广泛地使用。因此Mocha的community比较大,本文我们选择Mocha + Chai,然后点击回车Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Pick a unit testing solution: (Use arrow keys) //选择一个单元测试解决方案
> Mocha + Chai //Mocha是JavaScript界中最受欢迎的一款单元测试框架
Jest //Jest是由facebook发布的,最近比较火热的一个测试框架 现代比较流行的 e2e 测试框架有Nightwatch、TestCafe、Protractor、WebdriverIO、Cypress,我们选择 NightwatchVue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Pick a unit testing solution: Mocha
? Pick an E2E testing solution: (Use arrow keys)//选择一个E2E测试解决方案:(使用箭头键)
> Cypress (Chrome only) //Cypress(仅Chrome)
Nightwatch (WebDriver-based) //Nightwatch(基于WebDriver) Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Pick a unit testing solution: Mocha
? Pick an E2E testing solution: Nightwatch
? Pick browsers to run end-to-end test on (Press <space> to select, <a> to toggle all, <i> to invert selection)//选择浏览器以运行端到端测试
>(*) Chrome
( ) Firefox 此处我们选择在专用配置文件中,点击回车Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Pick a unit testing solution: Mocha
? Pick an E2E testing solution: Nightwatch
? Pick browsers to run end-to-end test on (Press <space> to select, <a> to toggle all, <i> to invert selection)Chrome
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)//您更喜欢在哪里放置Babel,ESLint等的配置。
> In dedicated config files //在专用配置文件中
In package.json //在package.json中这里我们选择Y,在下次创建项目的时候使用,点击回车Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)Babel
, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Pick a unit testing solution: Mocha
? Pick an E2E testing solution: Nightwatch
? Pick browsers to run end-to-end test on (Press <space> to select, <a> to toggle all, <i> to invert selection)Chrome
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) //将此保存为预设以供将来的项目使用吗? 进入项目并安装依赖PS cd .\my-project\
# AND
yarn install项目目录my-project
│ .browserslistrc
│ .editorconfig
│ .eslintrc.js
│ .gitignore
│ babel.config.js
│ package.json
│ README.md
│ tsconfig.json
│ yarn.lock
│
├─public
│ │ favicon.ico
│ │ index.html
│ │ robots.txt
│ │
│ └─img
│ └─icons
│ android-chrome-192x192.png
│ android-chrome-512x512.png
│ android-chrome-maskable-192x192.png
│ android-chrome-maskable-512x512.png
│ apple-touch-icon-120x120.png
│ apple-touch-icon-152x152.png
│ apple-touch-icon-180x180.png
│ apple-touch-icon-60x60.png
│ apple-touch-icon-76x76.png
│ apple-touch-icon.png
│ favicon-16x16.png
│ favicon-32x32.png
│ msapplication-icon-144x144.png
│ mstile-150x150.png
│ safari-pinned-tab.svg
│
├─src
│ │ App.vue
│ │ main.ts
│ │ registerServiceWorker.ts
│ │ shims-tsx.d.ts
│ │ shims-vue.d.ts
│ │
│ ├─assets
│ │ logo.png
│ │
│ ├─components
│ │ HelloWorld.vue
│ │
│ ├─router
│ │ index.ts
│ │
│ ├─store
│ │ index.ts
│ │
│ └─views
│ About.vue
│ Home.vue
│
└─tests
├─e2e
│ │ .eslintrc.js
│ │ globals.js
│ │
│ ├─custom-assertions
│ │ elementCount.js
│ │
│ ├─custom-commands
│ │ customExecute.js
│ │ openHomepage.js
│ │ openHomepageClass.js
│ │
│ ├─page-objects
│ │ homepage.js
│ │
│ └─specs
│ test-with-pageobjects.js
│ test.js
│
└─unit
example.spec.ts启动项目yarn serve期待Vue会开设[Vue从出师到挂帅]专题进行讲解,敬请期待!!!
AngularJS之WebAPi上传(十)
前言
前面一系列我们纯粹是讲AngularJS,在讲一门知识时我们应该结合之前所学综合起来来做一个小的例子,前面我们讲了在MVC中上传文件的例子,在本节我们讲讲如何利用AngularJS在WebAPi中如何上传,来巩固下WebAPi并结合AngularJS中对应的一些组件学习下。
AngularJS Upload Files for WebAPi
(一)在WebAPi中我们如何获得上传本地文件的物理路径呢?需要实现此类: MultipartFormDataStreamProvider ,从该类中获取上传文件的物理路径并返回。如下:
public class UploadMultipartFormProvider : MultipartFormDataStreamProvider
{
public UploadMultipartFormProvider(string rootPath) : base(rootPath) { }
public override string GetLocalFileName(HttpContentHeaders headers)
{
if (headers != null &&
headers.ContentDisposition != null)
{
return headers
.ContentDisposition
.FileName.TrimEnd('"').TrimStart('"');
}
return base.GetLocalFileName(headers);
}
}
(二)为避免有些浏览器不支持多个文件上传我们通过实现 ActionFilterAttribute 特性来加以判断,如下:
public class MimeMultipart : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(
new HttpResponseMessage(
HttpStatusCode.UnsupportedMediaType)
);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
}
}
以上两个类为我们在WebAPi中上传提供一个保障。接下来我们需要的是如何利用AngularJS来上传。
在AngularJS中上传需要用到 ng-file-upload 组件,而非 angularFileUpload 组件,在此之前一直是利用angularFileUpload组件,但是后来逐步迁移并且命名为ng-file-upload组件。同时为了一些加载效果如在github中顶部有加载线的过程,决定也在这里实现一下看看【说明:上传过程用到angular-loading-bar(加载)和ng-file-upload(上传)组件】。
脚本
(1)为了重用,我们将加载组件进行封装。
//common.load.js
(function () {
'use strict';
angular
.module('common.load', [
'angular-loading-bar',
'ngAnimate'
]);
})();
(2)启动加载线。
//loadBarCtrl.js
(function (app) {
"use strict";
app.controller("loadCtrl", loadCtrl);
function loadCtrl(cfpLoadingBar) {
cfpLoadingBar.start();
}
})(angular.module("common.load"));
(3)上传文件代码
//fileUploadCtrl.js
(function (app) {
'use strict';
app.controller('fileUploadCtrl', fileUploadCtrl);
fileUploadCtrl.$inject = ['$scope', '$http', '$timeout', 'Upload'];
function fileUploadCtrl($scope, $http, $timeout, Upload, cfpLoadingBar) {
$scope.upload = [];
$scope.UploadedFiles = [];
$scope.startUploading = function ($files) {
for (var i = 0; i < $files.length; i++) {
var $file = $files[i];
(function (index) {
$scope.upload[index] = Upload.upload({
url: "/api/upload",
method: "POST",
file: $file,
withCredentials: false
}).progress(function (evt) {
}).success(function (data, status, headers, config) {
$scope.UploadedFiles.push({ FileName: data.FileName, FilePath: data.LocalFilePath, FileLength: data.FileLength });
cfpLoadingBar.complete();
}).error(function (data, status, headers, config) {
});
})(i);
}
}
}
})(angular.module("common.load"));
(4)加载主模块以及依赖模块。
//app.js
(function () {
'use strict';
angular
.module('angularUploadApp', [
'ngRoute',
'ngFileUpload',
'common.load'
])
.config(config)
config.$inject = ['$routeProvider'];
function config($routeProvider) {
$routeProvider
.when('/', {
templateUrl: '../../app/templates/fileUpload.html',
controller: 'fileUploadCtrl'
})
.otherwise({
redirectTo: '/'
});
}
})();
界面
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="~/Content/bootstrap.min.css" rel="stylesheet" />
<link href="~/Content/loading-bar.min.css" rel="stylesheet" media="all" />
</head>
<body ng-app="angularUploadApp" ng-controller="loadCtrl">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="http://www.cnblogs.com/createmyself">Angular File WebAPi Upload by <i style="color:#2da12d;font-weight:bolder;">xpy0928</i></a>
</div>
<div id="navbar" class="navbar-collapse collapse">
</div>
</nav>
<div class="container">
<div ng-include src="'../../app/templates/fileUpload.html'"></div>
</div>
<script src="../../Scripts/jquery-1.10.2.min.js"></script>
<script src="../../Scripts/angular.min.js"></script>
<script src="../../Scripts/angular-animate.min.js"></script>
<script src="../../Scripts/angular-route.js"></script>
<script src="../../app/plugins/loading-bar.min.js"></script>
<script src="../../app/modules/common.load.js"></script>
<script src="../../Scripts/ng-file-upload.min.js"></script>
<script src="../../app/app.js"></script>
<script src="../../app/controllers/loadBarCtrl.js"></script>
<script src="../../app/controllers/fileUploadCtrl.js"></script>
</body>
</html>
模板页:
<div class="row" ng-controller="fileUploadCtrl">
<div class="col-xs-3">
<div>
<input type="file" accept="images/*" ngf-pattern="'.png,.jpg,.gif'" ngf-select="startUploading($files)" multiple>
</div>
</div>
<div class="col-xs-9">
<div class="panel-body">
<div class="panel panel-default" ng-repeat="uploadedFile in UploadedFiles track by $index">
<div class="panel-heading">
<strong>{{uploadedFile.FileName}}</strong>
</div>
<div class="panel-body">
<div class=" media">
<a class="pull-left" href="#">
<img class="media-object" width="100" ng-src="../uploadimages/{{uploadedFile.FileName}}" />
</a>
<div class="media-body">
<div class="lead" style="font-size:14px;color: crimson;width:500px;word-wrap:break-word">{{uploadedFile.FilePath}}</div>
</div>
</div>
</div>
<div class="panel-footer">
图片总字节: <span style="color:black">{{uploadedFile.FileLength}}</span>
</div>
</div>
</div>
</div>
</div>
后台api服务:
[RoutePrefix("api/upload")]
public class FileUploadController : ApiController
{
[Route("")]
[MimeMultipart]
public async Task<FileUploadResult> Post()
{
var uploadPath = HttpRuntime.AppDomainAppPath + "UploadImages";
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
var multipartFormDataStreamProvider = new UploadMultipartFormProvider(uploadPath);
await Request.Content.ReadAsMultipartAsync(multipartFormDataStreamProvider);
string _localFileName = multipartFormDataStreamProvider
.FileData.Select(multiPartData => multiPartData.LocalFileName).FirstOrDefault();
return new FileUploadResult
{
LocalFilePath = _localFileName,
FileName = Path.GetFileName(_localFileName),
FileLength = new FileInfo(_localFileName).Length
};
}
}
首先我们来看看例子搭建的结构:
生成界面效果:
下面我们来演示下最终效果:
总结
(1)在WebAPi中用到路由特性时,若在控制器中如 [RoutePrefix("api/upload")] 此时在方法中若未有 [Route("")] 此时在上传的url必须显示添加Post如: url: "/api/upload/post" 若添加则不用显示添加方法名。
(2)在angular中加载模板为
<div ng-include src="''"></div> 【注】:src中要加单引号,否则出错
或者
<div ng-include></div>
(3)对于在WebAPi中上传可以参看此链接,更加详细。
WebAPi:https://github.com/stewartm83/angular-fileupload-sample
对于AngularJS组件中的加载和上传还有更多用法,可以参看如下链接:
ng-file-upload:https://github.com/danialfarid/ng-file-upload
angular-loading-bar:https://github.com/chieffancypants/angular-loading-bar
本文转自Jeffcky博客园博客,原文链接:http://www.cnblogs.com/CreateMyself/p/5565125.html,如需转载请自行联系原作者
AngularJS之WebAPi上传(十)
前言
前面一系列我们纯粹是讲AngularJS,在讲一门知识时我们应该结合之前所学综合起来来做一个小的例子,前面我们讲了在MVC中上传文件的例子,在本节我们讲讲如何利用AngularJS在WebAPi中如何上传,来巩固下WebAPi并结合AngularJS中对应的一些组件学习下。
AngularJS Upload Files for WebAPi
(一)在WebAPi中我们如何获得上传本地文件的物理路径呢?需要实现此类: MultipartFormDataStreamProvider ,从该类中获取上传文件的物理路径并返回。如下:
public class UploadMultipartFormProvider : MultipartFormDataStreamProvider
{
public UploadMultipartFormProvider(string rootPath) : base(rootPath) { }
public override string GetLocalFileName(HttpContentHeaders headers)
{
if (headers != null &&
headers.ContentDisposition != null)
{
return headers
.ContentDisposition
.FileName.TrimEnd('"').TrimStart('"');
}
return base.GetLocalFileName(headers);
}
}
(二)为避免有些浏览器不支持多个文件上传我们通过实现 ActionFilterAttribute 特性来加以判断,如下:
public class MimeMultipart : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(
new HttpResponseMessage(
HttpStatusCode.UnsupportedMediaType)
);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
}
}
以上两个类为我们在WebAPi中上传提供一个保障。接下来我们需要的是如何利用AngularJS来上传。
在AngularJS中上传需要用到 ng-file-upload 组件,而非 angularFileUpload 组件,在此之前一直是利用angularFileUpload组件,但是后来逐步迁移并且命名为ng-file-upload组件。同时为了一些加载效果如在github中顶部有加载线的过程,决定也在这里实现一下看看【说明:上传过程用到angular-loading-bar(加载)和ng-file-upload(上传)组件】。
脚本
(1)为了重用,我们将加载组件进行封装。
//common.load.js(function () {
'use strict';
angular
.module('common.load', [
'angular-loading-bar',
'ngAnimate'
]);
})();
(2)启动加载线。
//loadBarCtrl.js(function (app) {
"use strict";
app.controller("loadCtrl", loadCtrl);
function loadCtrl(cfpLoadingBar) {
cfpLoadingBar.start();
}
})(angular.module("common.load"));
(3)上传文件代码
//fileUploadCtrl.js(function (app) {
'use strict';
app.controller('fileUploadCtrl', fileUploadCtrl);
fileUploadCtrl.$inject = ['$scope', '$http', '$timeout', 'Upload'];
function fileUploadCtrl($scope, $http, $timeout, Upload, cfpLoadingBar) {
$scope.upload = [];
$scope.UploadedFiles = [];
$scope.startUploading = function ($files) {
for (var i = 0; i < $files.length; i++) {
var $file = $files[i];
(function (index) {
$scope.upload[index] = Upload.upload({
url: "/api/upload",
method: "POST",
file: $file,
withCredentials: false
}).progress(function (evt) {
}).success(function (data, status, headers, config) {
$scope.UploadedFiles.push({ FileName: data.FileName, FilePath: data.LocalFilePath, FileLength: data.FileLength });
cfpLoadingBar.complete();
}).error(function (data, status, headers, config) {
});
})(i);
}
}
}
})(angular.module("common.load"));
(4)加载主模块以及依赖模块。
//app.js(function () {
'use strict';
angular
.module('angularUploadApp', [
'ngRoute',
'ngFileUpload',
'common.load'
])
.config(config)
config.$inject = ['$routeProvider'];
function config($routeProvider) {
$routeProvider
.when('/', {
templateUrl: '../../app/templates/fileUpload.html',
controller: 'fileUploadCtrl'
})
.otherwise({
redirectTo: '/'
});
}
})();
界面
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="~/Content/bootstrap.min.css" rel="stylesheet" />
<link href="~/Content/loading-bar.min.css" rel="stylesheet" media="all" />
</head>
<body ng-app="angularUploadApp" ng-controller="loadCtrl">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="http://www.cnblogs.com/createmyself">Angular File WebAPi Upload by <i style="color:#2da12d;font-weight:bolder;">xpy0928</i></a>
</div>
<div id="navbar" class="navbar-collapse collapse">
</div>
</nav>
<div class="container">
<div ng-include src="'../../app/templates/fileUpload.html'"></div>
</div>
<script src="../../Scripts/jquery-1.10.2.min.js"></script>
<script src="../../Scripts/angular.min.js"></script>
<script src="../../Scripts/angular-animate.min.js"></script>
<script src="../../Scripts/angular-route.js"></script>
<script src="../../app/plugins/loading-bar.min.js"></script>
<script src="../../app/modules/common.load.js"></script>
<script src="../../Scripts/ng-file-upload.min.js"></script>
<script src="../../app/app.js"></script>
<script src="../../app/controllers/loadBarCtrl.js"></script>
<script src="../../app/controllers/fileUploadCtrl.js"></script>
</body>
</html>
模板页:
<div class="row" ng-controller="fileUploadCtrl">
<div class="col-xs-3">
<div>
<input type="file" accept="images/*" ngf-pattern="'.png,.jpg,.gif'" ngf-select="startUploading($files)" multiple>
</div>
</div>
<div class="col-xs-9">
<div class="panel-body">
<div class="panel panel-default" ng-repeat="uploadedFile in UploadedFiles track by $index">
<div class="panel-heading">
<strong>{{uploadedFile.FileName}}</strong>
</div>
<div class="panel-body">
<div class=" media">
<a class="pull-left" href="#">
<img class="media-object" width="100" ng-src="../uploadimages/{{uploadedFile.FileName}}" />
</a>
<div class="media-body">
<div class="lead" style="font-size:14px;color: crimson;width:500px;word-wrap:break-word">{{uploadedFile.FilePath}}</div>
</div>
</div>
</div>
<div class="panel-footer">
图片总字节: <span style="color:black">{{uploadedFile.FileLength}}</span>
</div>
</div>
</div>
</div>
</div>
后台api服务:
[RoutePrefix("api/upload")]
public class FileUploadController : ApiController
{
[Route("")]
[MimeMultipart]
public async Task<FileUploadResult> Post()
{
var uploadPath = HttpRuntime.AppDomainAppPath + "UploadImages";
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
var multipartFormDataStreamProvider = new UploadMultipartFormProvider(uploadPath);
await Request.Content.ReadAsMultipartAsync(multipartFormDataStreamProvider);
string _localFileName = multipartFormDataStreamProvider
.FileData.Select(multiPartData => multiPartData.LocalFileName).FirstOrDefault();
return new FileUploadResult
{
LocalFilePath = _localFileName,
FileName = Path.GetFileName(_localFileName),
FileLength = new FileInfo(_localFileName).Length
};
}
}
首先我们来看看例子搭建的结构:
生成界面效果:
下面我们来演示下最终效果:
总结
(1)在WebAPi中用到路由特性时,若在控制器中如 [RoutePrefix("api/upload")] 此时在方法中若未有 [Route("")] 此时在上传的url必须显示添加Post如: url: "/api/upload/post" 若添加则不用显示添加方法名。
(2)在angular中加载模板为
<div ng-include src="''"></div> 【注】:src中要加单引号,否则出错
或者
<div ng-include></div>
(3)对于在WebAPi中上传可以参看此链接,更加详细。
WebAPi:https://github.com/stewartm83/angular-fileupload-sample
对于AngularJS组件中的加载和上传还有更多用法,可以参看如下链接:
ng-file-upload:https://github.com/danialfarid/ng-file-upload
angular-loading-bar:https://github.com/chieffancypants/angular-loading-bar
TypeScript 2.8下的终极React组件模式
有状态组件、无状态组件、默认属性、Render回调、组件注入、泛型组件、高阶组件、受控组件
如果你了解我,你就已经知道我不编写没有类型定义的javascript代码,所以我从0.9版本后,就非常喜欢TypeScript了。除了有类型的JS,我也非常喜欢React库,所以当把React和Typescript 结合在一起后,对我来说就像置身天堂一样:)。整个应用程序和虚拟DOM中的完整的类型安全,是非常奇妙和开心的。
所以这篇文章说是关于什么的呢?在互联网上有各种关于React组件模式的文章,但没有介绍如何将这些模式应用到Typescript中。此外,即将发布的TS 2.8版本带来了另人兴奋的新功能如、如有条件的类型(conditional types)、标准库中新预定义的条件类型、同态映射类型修饰符等等,这些新功能是我们能够以类型安全的方式轻松地创建常见的React组件模式。
这篇文章篇幅会比较长,所以请你坐下放轻松,与此同时你将掌握Typescript下的 终极React组件模式。
所有的模式/例子均使用typescript 2.8版本和strict mode
准备开始
首先,我们需要安装typescript和tslibs帮助程序库,以便我们生出的代码更小
yarn add -D typescript@next
# tslib 将仅用与您的编译目标不支持的功能
yarn add tslib
有了这个,我们可以初始化我们的typescript配置:
# 这条命令将在我们的工程中创建默认配置 tsconfig.json
yarn tsc --init
现在我们来安装 react、react-dom 和它们的类型定义。
yarn add react react-dom
yarn add -D @types/{react,react-dom}
棒极啦!现在我们可以开始进入我们的组件模式吧,不是吗?
无状态组件
你猜到了,这些是没有state的组件(也被称为展示型组件)。在部分时候,它们也是纯函数组件。让我们用TypeScript创建人造的无状态Button组件。
同使用原生JS一样,我们需要引入React以便我们可以使用JSX
import React from 'react'
const Button = ({ onClick: handleClick, children }) => (
<button onClick={handleClick}>{children}</button>
)
虽然 tsc 编译器现在还会跑出错误!我们需要显式的告诉我们的组件/函数我们的props是什么类型的。让我们定义我们的 props:import React, { MouseEvent, ReactNode } from 'react'
type Props = {
onClick(e: MouseEvent<HTMLElement>): void
children?: ReactNode
}
const Button = ({ onClick: handleClick, children }: Props) => (
<button onClick={handleClick}>{children}</button>
)
现在我们已经解决了所有的错误了!非常好!但我们还可以做的更好!
在@types/react中已经预定义一个类型type SFC<P>,它也是类型interface StatelessComponent<P>的一个别名,此外,它已经有预定义的children和其他(defaultProps、displayName等等…),所以我们不用每次都自己编写!
所以最后的无状态组件是这样的:
import React, { MouseEvent, SFC } from 'react';
type Props = { onClick(e: MouseEvent<HTMLElement>): void };
const Button: SFC<Props> = ({ onClick: handleClick, children }) => (
<button onClick={handleClick}>{children}</button>
);
有状态组件
让我们使用我们的Button组件来创建有状态的计数器组件。
首先我们需要定义initialState
const initialState = { clicksCount: 0 }
现在我们将使用TypeScript来从我们的实现中推断出State的类型。
这样我们不需要分开维护我们的类型定义和实现,我们只有唯一的真相源,即我们的实现,太好了!
type State = Readonly<typeof initialState>
另外请注意,该类型被明确映射为使所有的属性均为只读的。我们需要再次使用State类型来显式地在我们的class上定义只读的state属性。
readonly state: State = initialState
这么做的作用是什么?
我们知道我们在React中不能像下面这样直接更新state:
this.state.clicksCount = 2;
this.state = { clicksCount: 2 }
这将导致运行时错误,但在编译时不会报错。通过显式地使用Readonly映射我们的type State,和在我们的类定义中设置只读的state属性,TS将会让我们立刻知道我们做错了。
例子:编译时的State类型安全
整个容器组件/有状态组件的实现:
我们的容器组件还没有任何Props API,所以我们需要将Compoent组件的第一个泛型参数定义为Object(因为在React中props永远是对象{}),并使用State类型作为第二个泛型参数。
import React, { Component } from 'react';
import Button from './Button';
const initialState = { clicksCount: 0 };
type State = Readonly<typeof initialState>;
class ButtonCounter extends Component<object, State> {
readonly state: State = initialState;
render() {
const { clicksCount } = this.state;
return (
<>
<Button onClick={this.handleIncrement}>Increment</Button>
<Button onClick={this.handleDecrement}>Decrement</Button>
You've clicked me {clicksCount} times!
</>
);
}
private handleIncrement = () => this.setState(incrementClicksCount);
private handleDecrement = () => this.setState(decrementClicksCount);
}
const incrementClicksCount = (prevState: State) => ({
clicksCount: prevState.clicksCount + 1,
});
const decrementClicksCount = (prevState: State) => ({
clicksCount: prevState.clicksCount - 1,
});
你可能已经注意到了我们将状态更新函数提取到类的外部作为纯函数。这是一种常见的模式,这样我们不需要了解渲染逻辑就可以简单的测试这些状态更新函数。此外,因为我们使用了TypeScript并将State显式地映射为只读的,它将阻止我们在这些函数中做一些更改状态的操作:
const decrementClicksCount = (prevState: State) => ({
clicksCount: prevState.clicksCount--,
});
// 这样讲抛出编译错误:
//
// [ts] Cannot assign to 'clicksCount' because it is a constant or a read-only property.
非常酷是吧?:)
默认属性
让我们扩展我们的Button组件,新增一个string类型的颜色属性。
type Props = {
onClick(e: MouseEvent<HTMLElement>): void;
color: string;
};
如果我们想定义默认属性,我们可以在我们的组件中通过Button.defaultProps = {…}来定义。
通过这样做,我们需要改变我们的属性类型定义来标记属性是可选有默认值的。
所以定义是这样的(注意?操作符)
type Props = {
onClick(e: MouseEvent<HTMLElement>): void;
color?: string;
};
此时我们的组件实现是这样的:
const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
);
尽管这样在我们简单的例子中可用的,这有一个问题。因为我们在strict mode模式洗啊,可选的属性color的类型是一个联合类型undefined | string。
比如我们想对color属性做一些操作,TS将会抛出一个错误,因为它并不知道它在React创建中通过Component.defaultProps中已经定义了:
为了满足TS编译器,我们可以使用下面3种技术:
使用__!操作符__在render函数显式地告诉编译器这个变量不会是undefined,尽管它是可选的,如:<button onClick={handleClick!}>{children}</button>
使用__条件语句/三目运算符__来让编译器明白一些属性是没有被定义的:<button onClick={handleClick ? handleClick : undefined}>{children}</button>
创建可服用的__withDefaultProps__高阶函数,它将更新我们的props类型定义和设置默认属性。我认为这是最简洁干净的方案。
我们可以很简单的实现我们的高阶函数(感谢TS 2.8种的条件类型映射):
export const withDefaultProps = <
P extends object,
DP extends Partial<P> = Partial<P>
>(
defaultProps: DP,
Cmp: ComponentType<P>,
) => {
// 提取出必须的属性
type RequiredProps = Omit<P, keyof DP>;
// 重新创建我们的属性定义,通过一个相交类型,将所有的原始属性标记成可选的,必选的属性标记成可选的
type Props = Partial<DP> & Required<RequiredProps>;
Cmp.defaultProps = defaultProps;
// 返回重新的定义的属性类型组件,通过将原始组件的类型检查关闭,然后再设置正确的属性类型
return (Cmp as ComponentType<any>) as ComponentType<Props>;
};
现在我们可以使用withDefaultProps高阶函数来定义我们的默认属性,同时也解决了之前的问题:
const defaultProps = {
color: 'red',
};
type DefaultProps = typeof defaultProps;
type Props = { onClick(e: MouseEvent<HTMLElement>): void } & DefaultProps;
const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
);
const ButtonWithDefaultProps = withDefaultProps(defaultProps, Button);
或者直接使用内联(注意我们需要显式的提供原始Button组件的属性定义,TS不能从函数中推断出参数的类型):
const ButtonWithDefaultProps = withDefaultProps<Props>(
defaultProps,
({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
),
);
现在Button组件的属性已经被正确的定义被使用的,默认属性被反应出来并且在类型定义中是可选的,但在实现中是必选的!
{
onClick(e: MouseEvent<HTMLElement>): void
color?: string
}
组件使用方法仍然是一样的:
render() {
return (
<ButtonWithDefaultProps
onClick={this.handleIncrement}
>
Increment
</ButtonWithDefaultProps>
)
}
当然这也使用与通过class定义的组件(得益于TS中的类结构起源,我们不需要显式指定我们的Props泛型类型)。
它看起来像这样:
const ButtonViaClass = withDefaultProps(
defaultProps,
class Button extends Component<Props> {
render() {
const { onClick: handleClick, color, children } = this.props;
return (
<button style={{ color }} onClick={handleClick}>
{Children}
</button>
);
}
},
);
再次,它的使用方式仍然是一样的:
render() {
return (
<ButtonViaClass onClick={this.handleIncrement}>Increment</ButtonViaClass>
);
}
比如说你需要构建一个可展开的菜单组件,它需要在用户点击它时显示子内容。我们就可以使用各种各样的组件模式来实现它。
render回调/render属性模式
实现组件的逻辑可复用的最好方式将组件的children放到函数中去,或者利用render属性API——这也是为什么Render回调也被称为函数子组件。
让我们用render属性方法实现一个Toggleable组件:
import React, { Component, MouseEvent } from 'react';
import { isFunction } from '../utils';
const initialState = {
show: false,
};
type State = Readonly<typeof initialState>;
type Props = Partial<{
children: RenderCallback;
render: RenderCallback;
}>;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
type ToggleableComponentProps = {
show: State['show'];
toggle: Toggleable['toggle'];
};
export class Toggleable extends Component<Props, State> {
readonly state: State = initialState;
render() {
const { render, children } = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
private toggle = (event: MouseEvent<HTMLElement>) =>
this.setState(updateShowState);
}
const updateShowState = (prevState: State) => ({ show: !prevState.show });
这里都发生了什么,让我们来分别看看重要的部分:
const initialState = {
show: false,
};
type State = Readonly<typeof initialState>;
这里我们和前面的例子一样声明了我们的state
现在我们来定义组件的props(注意这里我们使用了Partitial映射类型,因为我们所有的属性都是可选的,不用分别对每个属性手动添加?标识符):
type Props = Partial<{
children: RenderCallback;
render: RenderCallback;
}>;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
type ToggleableComponentProps = {
show: State['show'];
toggle: Toggleable['toggle'];
};
我们需要同时支持child作为函数,和render属性作为函数,它们两者都是可选的。为了避免重复代码,我们定义了RenderCallback作为我们的渲染函数定义:
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
在读者眼中看起来比较奇怪的部分是我们最后的别名类型:type ToggleableComponentProps!
type ToggleableComponentProps = {
show: State['show'];
toggle: Toggleable['toggle'];
};
这里我们使用了TypeScript的__查找类型(lookup types)__,所以我们又不需要重复地去定义类型了:
show: State['show']我们利用已有的state类型定义了show属性
toggle: Toggleable['toggle']我们利用了TS从类实现推断类类型来定义toggle属性。很好用而且非常强大。
剩下的实现部分很简单,标准的render属性/children作为函数的模式:
export class Toggleable extends Component<Props, State> {
// ...
render() {
const { render, children } = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
// ...
}
现在我们可以把函数作为children传给Toggleable组件了:
<Toggleable>
{({ show, toggle }) => (
<>
<div onClick={toggle}>
<h1>some title</h1>
</div>
{show ? <p>some content</p> : null}
</>
)}
</Toggleable>
或者我们可以把函数作为render属性:
<Toggleable
render={({ show, toggle }) => (
<>
<div onClick={toggle}>
<h1>some title</h1>
</div>
{show ? <p>some content</p> : null}
</>
)}
/>
感谢TypeScript,我们在render属性的参数有了智能提示和正确的类型检查:
如果我们想复用它(比如用在多个菜单组件中),我们只需要创建一个使用Toggleable逻辑的心组件:
type Props = { title: string }
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
<Toggleable
render={({ show, toggle }) => (
<>
<div onClick={toggle}>
<h1>title</h1>
</div>
{show ? children : null}
</>
)}
/>
)
现在我们全新的__ToggleableMenu__组件已经可以在Menu组件中使用了:
export class Menu extends Component {
render() {
return (
<>
<ToggleableMenu title="First Menu">Some content</ToggleableMenu>
<ToggleableMenu title="Second Menu">Some content</ToggleableMenu>
<ToggleableMenu title="Third Menu">Some content</ToggleableMenu>
</>
);
}
}
并且它也像我们期望的那样工作了:
这中模式在我们想更改渲染的内容,而不关心状态改变的情况下非常有用:可以看到,我们将渲染逻辑移到ToggleableMenu组件的额children函数中了,但把状态管理逻辑保留在我们的Toggleable组件中!
组件注入
为了让我们的组件更灵活,我们可以引入组件注入模式。
什么是组件注入模式呢?如果你对React-Router比较熟悉,那你已经在下面这样路由定义的时候使用这种模式了:
<Route path="/foo" component={MyView} />
这样我们不是把函数传递给render/children属性,而是通过component属性“注入”组件。为此,我们可以重构,把我们的内置render属性函数改成一个可复用的无状态组件:
type MenuItemProps = { title: string };
const MenuItem: SFC<MenuItemProps & ToggleableComponentProps> = ({
title,
toggle,
show,
children,
}) => (
<>
<div onClick={toggle}>
<h1>{title}</h1>
</div>
{show ? children : null}
</>
);
有了这个,我们可以使用render属性重构ToggleableMenu:
type Props = { title: string };
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
<Toggleable
render={({ show, toggle }) => (
<MenuItem show={show} toggle={toggle} title={title}>
{children}
</MenuItem>
)}
/>
);
这个完成之后,让我们来开始定义我们新的API——compoent属性。
我们需要更新我们的属性API。
children现在可以是函数或者ReactNode(当component属性被使用时)
component是我们新的API,它可以接受实现了ToggleableComponentProps属性的组件,并且它需要是设置为any的泛型,这样各种各样的实现组件可以添加其他属性到ToggleableComponentProps并通过TS的验证
props我们引入可以传入任意属性的定义。它被定义成any类型的可索引类型,这里我们放松了严格的类型安全检查...
// 我们需要使用我们任意的props类型来创建 defaultProps,默认是一个空对象
const defaultProps = { props: {} as { [name: string]: any } };
type Props = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<any>>;
} & DefaultProps
>;
type DefaultProps = typeof defaultProps;
下一步,我们需要添加新的属性API到ToggleableComponentProps上,以便用户可以通过<Toggleable props={...} />来使用我们的props属性:
export type ToggleableComponentProps<P extends object = object> = {
show: State['show'];
toggle: Toggleable['toggle'];
} & P;
然后我们需要更新我们的render函数:
render() {
const {
component: InjectedComponent,
props,
render,
children,
} = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
// 当 component 属性被使用时,children 是 ReactNode 而不是函数
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
);
}
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
完整的Toggleable组件实现如下,支持 render 属性、children作为函数、组件注入功能:
import React, { Component, ReactNode, ComponentType, MouseEvent } from 'react';
import { isFunction, getHocComponentName } from '../utils';
const initialState = { show: false };
const defaultProps = { props: {} as { [name: string]: any } };
type State = Readonly<typeof initialState>;
type Props = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<any>>;
} & DefaultProps
>;
type DefaultProps = typeof defaultProps;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
export type ToggleableComponentProps<P extends object = object> = {
show: State['show'];
toggle: Toggleable['toggle'];
} & P;
export class Toggleable extends Component<Props, State> {
static readonly defaultProps: Props = defaultProps;
readonly state: State = initialState;
render() {
const {
component: InjectedComponent,
props,
render,
children,
} = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
);
}
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
private toggle = (event: MouseEvent<HTMLElement>) =>
this.setState(updateShowState);
}
const updateShowState = (prevState: State) => ({ show: !prevState.show });
我们最终使用component属性的ToggleableMenuViaComponentInjection组件是这样的:
const ToggleableMenuViaComponentInjection: SFC<ToggleableMenuProps> = ({
title,
children,
}) => (
<Toggleable component={MenuItem} props={{ title }}>
{children}
</Toggleable>
);
请注意,这里我们的props属性没有严格的类型安全检查,因为它被定义成索引对象类型{ [name: string]: any }:
我们可以还是像之前一样使用ToggleableMenuViaComponentInjection组件来实现菜单渲染:
export class Menu extends Component {
render() {
return (
<>
<ToggleableMenuViaComponentInjection title="First Menu">
Some content
</ToggleableMenuViaComponentInjection>
<ToggleableMenuViaComponentInjection title="Second Menu">
Another content
</ToggleableMenuViaComponentInjection>
<ToggleableMenuViaComponentInjection title="Third Menu">
More content
</ToggleableMenuViaComponentInjection>
</>
);
}
}
泛型组件
在我们视线“组件注入模式”的时候,我们失去了对props属性严格的类型安全检查。我们怎样修复这个问题呢?对,你猜到了!我们可以把我们的Toggleable组件实现为一个泛型组件!
首先我们需要把我们的属性泛型化。我们使用默认的泛型参数,所以我们不需要在没必要的时候显式地提供类型(针对 render 属性和 children 作为函数)。
type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<P>>;
} & DefaultProps<P>
>;
我们也需要把ToggleableComponnetProps更新成泛型的。不,等等,它已经是泛型啦!所以还不需要做任何更改。
需要更新的是type DefaultProps,因为不支持从声明实现推倒出泛型类型定义,所以需要把它重构成传统的类型定义->实现:
type DefaultProps<P extends object = object> = { props: P };
const defaultProps: DefaultProps = { props: {} };
就快好啦!
现在让我们把组件类也泛型化。再次说明,我们使用了默认的属性,所以在没有使用组件注入的时候不需要去指定泛型参数!
export class Toggleable<T = {}> extends Component<Props<T>, State> {}
这样就完成了吗?嗯…,我们可以在JSX中使用泛型类型吗?
坏消息是,不能...
但我们可以在泛型组件上引入ofType的工场模式:
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>;
}
}
完整的 Toggleable 组件实现,支持 Render 属性、Children 作为函数、带泛型 props 属性支持的组件注入:import React, {
Component,
ReactNode,
ComponentType,
MouseEvent,
SFC,
} from 'react';
import { isFunction, getHocComponentName } from '../utils';
const initialState = { show: false };
// const defaultProps = { props: {} as { [name: string]: any } };
type State = Readonly<typeof initialState>;
type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<P>>;
} & DefaultProps<P>
>;
type DefaultProps<P extends object = object> = { props: P };
const defaultProps: DefaultProps = { props: {} };
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
export type ToggleableComponentProps<P extends object = object> = {
show: State['show'];
toggle: Toggleable['toggle'];
} & P;
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>;
}
static readonly defaultProps: Props = defaultProps;
readonly state: State = initialState;
render() {
const {
component: InjectedComponent,
props,
render,
children,
} = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
);
}
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
private toggle = (event: MouseEvent<HTMLElement>) =>
this.setState(updateShowState);
}
const updateShowState = (prevState: State) => ({ show: !prevState.show });
有了static ofType工厂函数后,我们可以创建正确类型的泛型组件了。
type MenuItemProps = { title: string };
// ofType 是一种标识函数,返回的是相同实现的 Toggleable 组件,但带有制定的 props 类型
const ToggleableWithTitle = Toggleable.ofType<MenuItemProps>();
type ToggleableMenuProps = MenuItemProps;
const ToggleableMenuViaComponentInjection: SFC<ToggleableMenuProps> = ({
title,
children,
}) => (
<ToggleableWithTitle component={MenuItem} props={{ title }}>
{children}
</ToggleableWithTitle>
);
并且所有的东西都还像一起一样工作,但这次我有的 props={} 属性有了正确的类型检查。鼓掌吧!
高阶组件
因为我们已经创建了带render回调功能的Toggleable组件,实现HOC也会很容易。(这也是 render 回调函数模式的一个大优势,因为我们可以使用HOC来实现)
让我们开始实现我们的HOC组件吧:
我们需要创建:
displayName (以便我们在devtools可以很好地调试)
WrappedComponent (以便我们能够获取原始的组件——对测试很有用)
使用hoist-non-react-staticsnpm包中的hoistNonReactStatics
import React, { ComponentType, Component } from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { getHocComponentName } from '../utils';
import {
Toggleable,
Props as ToggleableProps,
ToggleableComponentProps,
} from './RenderProps';
// OwnProps 是内部组件上任意公开的属性
type OwnProps = object;
type InjectedProps = ToggleableComponentProps;
export const withToggleable = <OriginalProps extends object>(
UnwrappedComponent: ComponentType<OriginalProps & InjectedProps>,
) => {
// 我们使用 TS 2.8 中的条件映射类型来得到我们最终的属性类型
type Props = Omit<OriginalProps, keyof InjectedProps> & OwnProps;
class WithToggleable extends Component<Props> {
static readonly displayName = getHocComponentName(
WithToggleable.displayName,
UnwrappedComponent,
);
static readonly UnwrappedComponent = UnwrappedComponent;
render() {
const { ...rest } = this.props;
return (
<Toggleable
render={renderProps => (
<UnwrappedComponent {...rest} {...renderProps} />
)}
/>
);
}
}
return hoistNonReactStatics(WithToggleable, UnwrappedComponent);
};
现在我们可以使用HOC来创建我们的Toggleable菜单组件了,并有正确的类型安全检查!
const ToggleableMenuViaHOC = withToggleable(MenuItem)
一切正常,还有类型安全检查!好极了!
受控组件
这是最后一个组件模式了!假设我们想从父组件中控制我们的Toggleable组件,我们需要Toggleable组件配置化。这是一种很强大的模式。让我们来实现它吧。
当我说受控组件时,我指的是什么?我想从Menu组件内控制所以的ToggleableManu组件的内容是否显示。
我们需要像这样更新我们的ToggleableMenu组件的实现:
// 更新我们的属性类型,以便我们可以通过 show 属性来控制是否显示
type Props = MenuItemProps & { show?: boolean };
// 注意:这里我们使用了结构来创建变量别,为了不和 render 回调函数的 show 参数冲突
// -> { show: showContent }
// Render 属性
export const ToggleMenu: SFC<ToggleableComponentProps> = ({
title,
children,
show: showContent,
}) => (
<Toggleable show={showContent}>
{({ show, toggle }) => (
<MenuItem title={title} toggle={toggle} show={show}>
{children}
</MenuItem>
)}
</Toggleable>
);
// 组件注入
const ToggleableWithTitle = Toggleable.ofType<MenuItemProps>();
export const ToggleableMenuViaComponentInjection: SFC<Props> = ({
title,
children,
show: showContent,
}) => (
<ToggleableWithTitle
component={MenuItem}
props={{ title }}
show={showContent}
>
{children}
</ToggleableWithTitle>
);
// HOC不需要更改
export const ToggleMenuViaHOC = withToggleable(MenuItem);
有了这些更新后,我们可以在Menu中添加状态,并传递给ToggleableMenu
const initialState = { showContents: false };
type State = Readonly<typeof initialState>;
export class Menu extends Component<object, State> {
readonly state: State = initialState;
render() {
const { showContents } = this.state;
return (
<>
<button onClick={this.toggleShowContents}>toggle showContent</button>
<hr />
<ToggleableMenu title="First Menu" show={showContents}>
Some Content
</ToggleableMenu>
<ToggleableMenu title="Second Menu" show={showContents}>
Another Content
</ToggleableMenu>
<ToggleableMenu title="Third Menu" show={showContents}>
More Content
</ToggleableMenu>
</>
);
}
}
让我们为了最终的功能和灵活性最后一次更新Toggleable组件。为了让 Toggleable 变成受控组件我们需要:
添加show属性到PropsAPI上
更新默认的属性(因为show是可选的)
从Props.show更新组件的初始化state,因为现在我们状态中值可能取决于父组件传来的属性
在componentWillReceiveProps生命周期函数中从props更新state
1 & 2:
const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>
3 & 4:
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static readonly defaultProps: Props = defaultProps
// Bang operator used, I know I know ...
state: State = { show: this.props.show! }
componentWillReceiveProps(nextProps: Props<T>) {
const currentProps = this.props
if (nextProps.show !== currentProps.show) {
this.setState({ show: Boolean(nextProps.show) })
}
}
}
最终支持所有所有模式(Render属性/Children作为函数/组件注入/泛型组件/受控组件)的 Toggleable 组件:
import React, { Component, MouseEvent, ComponentType, ReactNode } from 'react'
import { isFunction, getHocComponentName } from '../utils'
const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
export type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode
render: RenderCallback
component: ComponentType<ToggleableComponentProps<P>>
} & DefaultProps<P>
>
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
export type ToggleableComponentProps<P extends object = object> = {
show: State['show']
toggle: Toggleable['toggle']
} & P
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>
export class Toggleable<T extends object = object> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>
}
static readonly defaultProps: Props = defaultProps
readonly state: State = { show: this.props.show! }
componentWillReceiveProps(nextProps: Props<T>, nextContext: any) {
const currentProps = this.props
if (nextProps.show !== currentProps.show) {
this.setState({ show: Boolean(nextProps.show) })
}
}
render() {
const { component: InjectedComponent, children, render, props } = this.props
const renderProps = { show: this.state.show, toggle: this.toggle }
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
)
}
if (render) {
return render(renderProps)
}
return isFunction(children) ? children(renderProps) : new Error('asdsa()')
}
private toggle = (event: MouseEvent<HTMLElement>) => this.setState(updateShowState)
}
const updateShowState = (prevState: State) => ({ show: !prevState.show })
最终的Toggleable HOC 组件 withToggleable
只需要稍作修改 -> 我们需要在HOC组件中传递 show 属性,并更新我们的OwnPropsAPI
import React, { ComponentType, Component } from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import { getHocComponentName } from '../utils'
import {
Toggleable,
Props as ToggleableProps,
ToggleableComponentProps as InjectedProps,
} from './toggleable'
// OwnProps is for any public props that should be available on internal Component.props
// and for WrappedComponent
type OwnProps = Pick<ToggleableProps, 'show'>
export const withToogleable = <OriginalProps extends object>(
UnwrappedComponent: ComponentType<OriginalProps & InjectedProps>
) => {
// we are leveraging TS 2.8 conditional mapped types to get proper final prop types
type Props = Omit<OriginalProps, keyof InjectedProps> & OwnProps
class WithToggleable extends Component<Props> {
static readonly displayName = getHocComponentName(
WithToggleable.displayName,
UnwrappedComponent
)
static readonly WrappedComponent = UnwrappedComponent
render() {
// Generics and spread issue
// https://github.com/Microsoft/TypeScript/issues/10727
const { show, ...rest } = this.props as Pick<Props, 'show'> // we need to explicitly pick props we wanna destructure, rest is gonna be type `{}`
return (
<Toggleable
show={show}
render={renderProps => <UnwrappedComponent {...rest} {...renderProps} />}
/>
)
}
}
return hoistNonReactStatics(WithToggleable, UnwrappedComponent as any) as ComponentType<Props>
}
总结
使用 TypeScript 和 React 时,实现恰当的类型安全组件可能会很棘手。但随着 TypeScript 2.8中新加入的功能,我们几乎可以在所有的 React 组件模式中编写类型安全的组件。
在这遍非常长(对此十分抱歉)文章中,感谢TypeScript,我们已经学会了在各种各样的模式下怎么编写严格类型安全检查的组件。
在这些模式中最强的应该是Render属性模式,它让我们可以在此基础上不需要太多改动就可以实现其他常见的模式,如组件注入、高阶组件等。
文中所有的demo都可以在我的 Github 仓库中找到。
此外,需要明白的是,本文中演示的模版类型安全,只能在使用 VDOM/JSX 的库中实现。
Angular 模版有 Language service 提供类型安全,但像 ngFor 等简单的构造检查好像都不行...
Vue 的模版不像 Angular,它们的模版和数据绑定只是神奇的字符串(但这有可能在未来会改变。尽管你可以在模版中使用VDOM,但因为各种类型的属性定义,它使用起来十分笨重(这怪 snabdom...))
和往常一样,如果你有任何问题,可以在这或者 twitter(@martin_hotell)联系我,另外,快乐的类型检查伙伴们,干杯!
原文发布时间为:2018年05月25日
原文作者:蚂蚁金服数据体验技术本文来源: 掘金 如需转载请联系原作者