AngularJS 1.6.x 与 Bootstrap 模态对话框交互
在本教程中,我将讨论一些可以在不与 ui-bootstrap 集成的情况下使用模态对话框与 AngularJS 配合使用的技术。
引言
两年前,我遇到了这个问题。我需要在 AngularJS 应用程序中打开一个 Bootstrap 模态对话框。不知何故,我无法让 ui-bootstrap 工作。ui-bootstrap 是一个第三方组件,可以与 AngularJS 集成以支持任何与 Bootstrap 相关的行为。有时使用它非常痛苦。经过一些研究,我意识到,无需使用 ui-bootstrap,就可以很容易地为 Bootstrap 模态对话框添加动态行为。
简单总结一下,为了演示我将要涵盖的功能,演示源代码包中捆绑了三个不同的前端应用程序示例。第一个将展示打开模态对话框的方法。第二个在前一个的基础上增加了一个功能,它将展示如何在模态对话框关闭时清除输入字段。最后一个示例将展示如何将输入值从调用控制器传递到模态对话框。
基本架构
示例应用程序使用 Bootstrap 3.3.7 进行 UI 标记。主页面有一个 navbar
,它没有任何功能。在 navbar
下面,有一个可以按下的按钮,该按钮会显示模态对话框。该应用程序有两个控制器,一个是主应用程序的。另一个是模态对话框的控制器。模态对话框本身是通过 HTML 文件分隔的。它通过 ngInclude
导入到主页面。第一个难点是如何通过 AngularJS 触发模态对话框显示。
在继续探讨解决第一个问题的方案之前,让我们先看一下主页面。主页面的 HTML 代码在一个名为 _app1.html_ 的文件中,如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Angular BootStrap Modal Example 1</title>
<link href="./assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="./assets/app/css/app.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<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="#">Angular Bootstrap Modal</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="#">Home</a></li>
</ul>
</div>
</div>
</nav>
<div class="container app-topmargin"
ng-app="sampleAngularApp" ng-controller="AppController">
<div class="row">
<div class="col-xs-12 col-sm-offset-2 col-sm-8 col-md-offset-3 col-md-3">
<button class="btn btn-primary" ng-click="openDlg()">Open Modal</button>
</div>
</div>
<ng-include src="'./assets/app/templates/sampleModal.html'"></ng-include>
</div>
<script src="./assets/jquery/js/jquery-2.2.4.min.js"></script>
<script src="./assets/bootstrap/js/bootstrap.min.js"></script>
<!-- angular js related libraries -->
<script src="./assets/angular-1.6.7/angular.min.js"></script>
<script src="./assets/app/js/sampleModalModule.js"></script>
<script src="./assets/app/js/app.js"></script>
</body>
</html>
此文件的最关键部分是以下部分:
<div class="container app-topmargin" ng-app="sampleAngularApp" ng-controller="AppController">
<div class="row">
<div class="col-xs-12 col-sm-offset-2 col-sm-8 col-md-offset-3 col-md-3">
<button class="btn btn-primary" ng-click="openDlg()">Open Modal</button>
</div>
</div>
<ng-include src="'./assets/app/templates/sampleModal.html'"></ng-include>
</div>
外部 div
元素引用了一个名为 "sampleAngularApp
" 的 AngularJS 应用 (ng-app
)。它还引用了一个名为 "AppController
" 的 AngularJS 控制器。这基本上表明任何 HTML 元素(包括外部 div
)都处于名为 "sampleAngularApp
" 的 AngularJS 应用程序及其控制器 "AppController
" 的控制之下。还有一个按钮元素,它具有 ng-click
事件处理程序。当用户单击此按钮时,此元素会执行所有操作,在 AngularJS 控制器内部,会调用一个名为 openDlg()
的函数来处理此事件。正如您所猜测的那样,这就是触发模态对话框显示的函数。
这是主页面的 JavaScript 代码,如下所示:
var app = angular.module("sampleAngularApp", [ "sampleModalModule" ]);
app.controller("AppController", ["$scope", function ($scope) {
$scope.openDlg = function () {
console.log("clicked here...");
// This is where the magic happens.
...
};
}]);
这只是一个典型的 AngularJS 主应用程序。第一行定义了一个名为 "sampleAngularApp
" 的 AngularJS 模块。这基本上就是 ng-app
引用的内容。从第二行开始,它定义了应用程序的控制器 "AppController
"。控制器中只有 openDlg()
函数的定义。我在其中放了一个注释,以指示我放置显示模态对话框的代码的位置。
在深入研究显示模态对话框的代码之前。我想向您展示如何导入模态对话框。模态对话框通过 ng-include
元素导入。这是 HTML 代码:
<ng-include src="'./assets/app/templates/sampleModal.html'"></ng-include>
我想指出的是 src
属性。双引号中的内容是单引号括起来的 string
。这是正确的语法。如果删除单引号,加载主页面时会出错。
我想说的另一点是 src
属性应该指向模板文件,该文件可以从当前 URL 位置找到。请记住这一点。如果找不到模板,您可能会在开发者控制台中看到错误。
以下是定义模态对话框模板的 HTML 代码:
<div id="modalDlg" class="modal fade" tabindex="-1"
role="dialog" ng-controller="ModalDlgController">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close"
data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span></button>
<h4 class="modal-title">Sample Modal Dialog</h4>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="someValueField">Some Value</label>
<input type="text" class="form-control" ng-model="someValueField"
placeholder="Enter Some Text Value">
</div>
<button class="btn btn-default"
ng-click="modalButtonClick()">Take Action</button>
</form>
</div>
</div>
</div>
</div>
此 HTML 布局略有不同。它不是完整的 HTML 页面。您只能看到模态对话框的布局。这没关系,因为它将通过 ng-include
包含到主页面中。其中,还有一个名为 "ModalDlgController
" 的 ngController
。这是这个新控制器对应的代码:
var module = angular.module("sampleModalModule", []);
module.controller("ModalDlgController", ["$scope", function ($scope) {
$scope.someValueField = "";
$scope.modalButtonClick = function () {
console.log("do action on Modal");
console.log("Current 'someValueField' value is [[" + $scope.someValueField + "]]");
};
}]);
在此控制器 "ModalDlgController
" 中,有一个名为 "someValueField
" 的作用域变量。它通过 ng-model
绑定到模板上的输入字段。对这个作用域变量的任何更改都会在显示中反映出来。模板上还有一个可以按下的按钮。此按钮不执行太多操作。控制器中有一个名为 "modelButtonClick()
" 的作用域函数。这个函数所做的就是将作用域变量 "someValueField
" 的当前值输出到开发者控制台。
现在我们已经具备了示例应用程序的所有组件,我可以向您展示为了显示模态对话框而需要进行的更改。
第一个示例应用程序
下载示例源代码并解压缩后,您将看到三个不同的文件夹。它们各自包含一个可运行的 AngularJS 应用程序。每个应用程序都展示了对前一个工作示例的改进。第一个在 "app1" 文件夹中。在此示例中,我想解决的问题是弹出模态对话框。
根据 Bootstrap 文档。要弹出模态对话框,只需查询模态对话框的元素,然后调用附加的 JavaScript 函数 modal()
,并传入参数 "show
"。这大致是实现此功能的代码:
$("#modalDlgId").modal("show");
然而,应用程序架构不是 jQuery。它是 AngularJS,它在一定程度上支持 jQuery。AngularJS 拥有自己的 jQuery 实现,称为 jqlite。可以使用 AngularJS 查询模态对话框作为元素,然后在此元素上调用附加的 JavaScript 函数 modal()
。
还记得我在主控制器 "AppController
" 中添加的注释吗?这是我查询模态对话框并将其弹出以供显示的实际代码:
var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
dlgElem.modal("show");
}
这是完整的 _app.js_:
var app = angular.module("sampleAngularApp", [ "sampleModalModule" ]);
app.controller("AppController", ["$scope", function ($scope) {
$scope.openDlg = function () {
console.log("clicked here...");
var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
dlgElem.modal("show");
}
};
}]);
AngularJS 提供了一些辅助方法。其中一个是 angular.element()
,它类似于 jQuery 选择器。在这种情况下,我使用它来查找 ID 为 "modalDlg
" 的元素。 "if
" 块检查返回的元素是否不为 null
。然后我使用该元素调用 modal()
方法。请注意,modal()
函数由 jQuery 附加到该元素。调用它将显示模态对话框。
这是单击按钮并显示模态对话框时的屏幕截图:
您可以做一个简单的测试,在弹出模态对话框的文本字段中键入一些内容。然后关闭它。再次打开同一个模态对话框,您会看到您键入的内容仍然在那里。在很多情况下,这是不可接受的行为。正确的行为是,当模态对话框关闭时,它将重置其所有子输入字段。
第二个示例应用程序
下一个目标是找到一种方法,在模态对话框打开或关闭时清理其输入字段。现在我们知道 AngularJS 有一个名为 element()
的辅助方法可以查询页面上的元素,我们可以将相同的原理应用于这个问题。Bootstrap 模态对话框具有可以用于清理的事件回调。
根据 Bootstrap 文档,模态对话框关闭时的回调事件可以按如下方式处理:
$('#exampleModal').on('hide.bs.modal', function (event) {
...
});
好了,解决方案如下:
- 获取模态对话框的元素。
- 将上述事件回调附加到此元素。在该回调中,清理模态对话框中的输入字段。
第二个示例在 "app2" 文件夹中。在 "app1" 中进行的更改之上,我为模态对话框添加了关闭时的事件回调。它在 ModalDlgController
中。这是代码:
var module = angular.module("sampleModalModule", []);
module.controller("ModalDlgController", ["$scope", function ($scope) {
$scope.someValueField = "";
var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
dlgElem.on("hide.bs.modal", function() {
$scope.someValueField = "";
console.log("reset data model..");
});
}
$scope.modalButtonClick = function () {
console.log("do action on Modal");
console.log("Current 'someValueField' value is [[" + $scope.someValueField + "]]");
};
}]);
设置事件回调的代码段是这个:
var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
dlgElem.on("hide.bs.modal", function() {
$scope.someValueField = "";
console.log("reset data model..");
});
}
思路与上一节相同,我首先使用 angular.element()
查找对话框的元素。然后我使用它调用 on()
方法(也来自 jQuery)来设置事件回调。这很棒的一点是,在事件回调中,我实际上可以将作用域变量设置为空 string
。这就是重置对话框中的输入字段的方法。
要进行测试,您可以打开对话框,在对话框的输入字段中输入一些值,然后关闭它。再次打开模态对话框,原始输入值应该已消失。
我想解决的最后一个技术问题是,我必须设法将值(或值)从主应用程序传递到模态对话框。基本上,我想将值从我的主控制器传递给子控制器或非子控制器。结果证明,这也很容易做到。
第三个示例应用程序
我创建了第三个应用程序,以演示将数据从一个 AngularJS 控制器传递到另一个 AngularJS 控制器的方法。在继续之前,我想指出这可能不是最佳解决方案,因为它可能会导致紧耦合,而您又想避免这种情况。
示例代码可以在 "app3" 文件夹中找到。
如何将值从一个控制器传递到另一个控制器?结果表明,使用 AngularJS,这很容易实现。我的做法是先查询元素。一旦找到元素,我就会在其上调用 .scope()
。它将返回附加到该元素的 AngularJS 作用域。然后,我可以将任何值传递给此作用域中的作用域变量。这是 _app.js_ 的完整文件:
var app = angular.module("sampleAngularApp", [ "sampleModalModule" ]);
app.controller("AppController", ["$scope", function ($scope) {
$scope.openDlg = function () {
console.log("clicked here...");
var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
dlgElem.scope().someValueField = "This text is from the caller.";
dlgElem.modal("show");
}
};
}]);
感兴趣的片段是这个:
var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
dlgElem.scope().someValueField = "This text is from the caller.";
dlgElem.modal("show");
}
这里所做的是:
- 查询模态对话框的元素。
- 检查并确保元素存在。
- 调用元素的
.scope()
,它返回元素附加到的作用域。然后引用作用域变量并为其分配新值。 - 弹出模态对话框。
如果一切正常,当您进行测试时,您应该会看到从这个主应用程序控制器传递到模态对话框的值。如果您更改了该值,然后关闭模态对话框。再次打开它,您应该会看到值已重置为从主应用程序传递过来的值。
运行演示应用程序
在本教程中,我介绍了解决三个简单技术问题的三种不同解决方案。第一个是在 AngularJS 应用程序中弹出 Bootstrap 模态对话框。第二个展示了如何在模态对话框关闭时清理输入字段,可以轻松修改为在模态对话框打开之前进行清理。最后一个示例展示了如何将数据从一个控制器传递到另一个控制器,以便主应用程序控制器可以将一些数据传递给模态对话框。
下载示例源代码并解压缩后,您将找到三个应用程序文件夹,从 _app1_ 到 _app3_。在其中,您应该会找到一些 _*.sj_ 文件。这些文件应重命名为 _*.js_ 文件。
此外,演示源代码不包含 AngularJS 和 Bootstrap 源代码。请下载它们并自行添加。每个 _app_ 文件夹中的 _app.html_ 文件将向您展示如何操作。如果您遇到任何错误,请检查开发者的控制台以获取详细信息。
我使用 Jetty Web Server 进行测试。我为 CodeProject 撰写了另一篇文章,讨论了 使用 Jetty Web Server 提供静态 Web 内容的方法。为了测试此演示应用程序,我使用了该文章中描述的第二种方法。
操作步骤如下:
- 下载 Jetty Web Server 并解压缩。
- 在 Jetty Web Server 的根目录下,找到 _webapps_ 文件夹。
- 解压缩示例应用程序存档。您会找到一个名为 _angular-bootstrap-modal_ 的文件夹。在此文件夹内,有三个子文件夹,_app1_ 到 _app3_,最后一个是 _WEB-INF_。
- 将 angular-bootstrap-modal 文件夹复制到 Jetty 的 webapp 文件夹中。
- 假设您已将 _*.sj_ 文件重命名为 _*.js_。并已添加 AngularJS 和 Bootstrap 代码文件。如果还没有,那么现在就是时候进行这些操作了。
- 使用命令
java -jar start.jar
运行 Jetty Web Server。
Web 服务器启动后,您可以导航到:
- https://:8080/angular-bootstrap-modal/app1/app.html
- https://:8080/angular-bootstrap-modal/app2/app.html
- https://:8080/angular-bootstrap-modal/app3/app.html
回顾
这三种方法是解决技术问题的最佳方法吗?我认为不是。我劝您在考虑将这些方法应用于解决您当前的具体问题时要三思。弹出模态对话框的第一种方法破坏了封装。调用者组件不应该了解被使用组件的实现细节。但我所做的打破了这条规则。第三种方法在调用者组件和被使用组件之间创建了紧耦合。第二种方法用于清理输入字段稍微好一些,但 UI 和后端 JavaScript 代码之间存在耦合。
这些不是使用 Bootstrap 组件的最佳方式。这就是为什么存在 ui-bootstrap 的原因。我宁愿让 ui-bootstrap 与我的项目配合使用,也不愿使用这些作为替代解决方案。但是,了解它们很有趣。
希望您喜欢本教程。祝您的项目顺利。
历史
- 2018年2月2日 - 初稿