起因
在表格展示的时候, 有些状态需要切换的, 之前都是直接使用的 checkbox. 有天老大说要优化界面, 把大部分的 checkbox 换成仿 iOS 的 SwitchButton, 开始想得很简单嘛, 新建了一个指令, 考虑了下使用场景和情况, 减少使用成本, 使用方法和原始 input 差不多, 很快就完工了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| var switchBtn = function () { return { restrict: 'A', templateUrl: 'views/common/switchBtn.html', css: { href: 'styles/common/switchBtn.css', persist: true }, replace: true, scope: { ngModel: '=', firstStatus: '@', onChange: '=', text: '=', noText: '=', item: '=', }, controller: function ($scope) {}, link: function ($scope, $element, $attrs) { $element.on('selectstart', function () { return false; }); var firstStatus = false; if ($scope.firstStatus && $scope.firstStatus.toLowerCase() == 'false') { firstStatus = false; } else if ($scope.firstStatus && $scope.firstStatus.toLowerCase() == 'true') { firstStatus = true; } if (typeof $scope.ngModel != 'undefined' && $scope.ngModel != null) { $scope.ngModel = firstStatus || $scope.ngModel; } else { $scope.ngModel = firstStatus; } if ($scope.noText) { $scope.text = ['', '']; } else if (!$scope.noText && !$scope.text) { $scope.text = ['on', 'off']; } $scope.changeSwitch = function () { $scope.ngModel = !$scope.ngModel; if ($scope.onChange) { if ($scope.onChange($scope.ngModel, $scope.item) == false) { $scope.ngModel = !$scope.ngModel; } } }; $scope.getText = function () { return $scope.text[$scope.ngModel ? 0 : 1]; }; }, }; };
|
1 2 3 4
| <div class="switch-select" ng-click="changeSwitch()"> <div class="switch-bg" ng-class="{true:'on',false:'off'}[ngModel]"><span ng-bind="getText()"></span></div> <div class="switch-btn" ng-class="{true:'on',false:'off'}[ngModel]"></div> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
.switch-select { position: relative; display: inline-block; vertical-align: top; width: 45px; height: 16px; cursor: pointer; }
.switch-bg { height: 100%; width: 100%; color: #fff; border-radius: 18px; font-size: 12px; text-transform: uppercase; transition: All 0.3s ease; -webkit-transition: All 0.3s ease; -moz-transition: All 0.3s ease; -o-transition: All 0.3s ease; }
.switch-bg span { position: absolute; top: 1px; line-height: 1; }
.switch-bg.on { background-color: #00af2c; border: 1px solid #068506; }
.switch-bg.on span { left: 6px; }
.switch-bg.off { background-color: #ed5b49; border: 1px solid #d2402e; }
.switch-bg.off span { right: 6px; }
.switch-btn { transition: All 0.3s ease; -webkit-transition: All 0.3s ease; -moz-transition: All 0.3s ease; -o-transition: All 0.3s ease; background: #eee; background: linear-gradient(#eee, #fafafa); border-radius: 100%; height: 14px; position: absolute; top: 1px; width: 14px; }
.switch-btn.on { left: 30px; }
.switch-btn.off { left: 1px; }
.switch-btn .switch-btn-content { background: #dedede; background: linear-gradient(#dedede, #cacaca); border-radius: 50%; height: 9px; width: 9px; margin-top: 5px; margin-left: 5px; padding: 0; }
|
加入文件, 替换 input, 简单好用, 给老大看效果也还不错, 写好文档和使用方法,
<div switch-btn ng-model="item.isTagEnum" no-text="true"></div>
交差完工.
问题
过了两天, 突然发现在表单里面刚刚加载完表单以后点击按钮没有反应, 需要等个 2-3s 才有事件. 开始以为是 ng-repate 的性能问题, 一个 repeat 里面指令太多, 试试 trackby - 无效, 试试减少其他元素(因为之前对填入的数据都加了一个 autoInputTips 的)- 无效, 最后发现只是循环这一个指令都比较慢, 而且数据量越少越快, 感觉很奇怪, 断点到指令的 link, 也发现 link 加载顺序不对, 大概有 2-3s 延迟才执行这个指令的 link, 换成其他指令, 即使很复杂的, 也没有出现加载缓慢的情况.
排查
仔细排查这个指令和其他指令的不同…一点点删改, 发现删除了
css: { href: 'styles/common/switchBtn.css', persist: true },
之后, 指令加载就正常了. 确定是 angularcss 的问题. 给指令加 compile. 去看 network, 发现 css 请求是在 controller 和 prelink 执行完了以后执行的, 于是感觉是因为 css 延迟加载导致指令加载顺序异常.
原理
去看 angularcss 源码, 在 650 行左右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var directive = $delegate[0]; var compile = directive.compile; if (!directive.css) { directive.css = $directive.css; } directive.compile = function () { var link = compile ? compile.apply(this, arguments) : false; return function (scope) { var linkArgs = arguments; $timeout(function () { if (link) { link.apply(this, linkArgs); } }); $rootScope.$broadcast('$directiveAdd', directive, scope); }; }; return $delegate;
|
果然如同猜想, 在指令有 css 的时候, angularcss 改写指令的 compile 方法, 返回的新的 postlink 在$timeout 函数里面执行原始的 link, 所以才导致了 link 函数加载顺序异常.
解决
确定原因以后解决起来就简单了, 去掉组件的独立引用 css, 合并到 common.scss 里面, 在 index 里面引入. 顺便提醒各项目组尽量少使用独立的 angularcss, 而且只在各自的顶级指令里面使用 angularcss, 会改变加载顺序而且由于使用 timeout, 会影响性能.
后记
改完以后基本使用没问题, 性能也还 ok, 因为也使用的 ng-model 传入值, 用起来感觉和原始 input 差不多, 但是有个问题, 传入的值如果不是对象而是值的时候, 会因为改变的是值而不是对象绑定不到原始值上. 比如
<div switch-btn ng-model="isDisplay" no-text="true"></div>
在 switchBtn 指令里面改变$scope.isDel 的值则不会改变原始 scope 里的值, 因为这样传递 scope 是值传递.
那怎么解决呢? 很简单, 用$scope.$parent 来取到原始节点的 scope, 然后改变值就可以了.
1 2 3
| if (!/\./.test(attr['ngModel'])) { $scope.$parent[attr['ngModel']] = value; }
|
看到这里可能有人有疑惑… 如果在 ngIf 下面使用, 还是会绑定不到值啊?
是的… 可是原本的 input NgModel 就存在这个问题, 为了统一, 也不准备修复
原理是 ngIf 创建了自己的$scope:true 的原型继承作用域, 虽然可以通过原型拿到值, 但是绑定的时候还是绑定不到原型链上的.
后记的后记
后来我用的时候嫌麻烦, 改成了最简单的 css 实现.
效果:
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| .mui-switch { width: 52px; height: 31px; position: relative; border: 1px solid #dfdfdf; background-color: #fdfdfd; box-shadow: #dfdfdf 0 0 0 0 inset; border-radius: 20px; border-top-left-radius: 20px; border-top-right-radius: 20px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; background-clip: content-box; display: inline-block; -webkit-appearance: none; -moz-appearance: none; user-select: none; outline: none; }
input[type='checkbox'].mui-switch:focus { outline: #fff none 0; }
.mui-switch:before { content: ''; width: 29px; height: 29px; position: absolute; top: 0px; left: 0; border-radius: 20px; border-top-left-radius: 20px; border-top-right-radius: 20px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; background-color: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); }
.mui-switch:checked { box-shadow: #64bd63 0 0 0 16px inset;
border-color: #52a4ff; background-color: #52a4ff; }
.mui-switch:checked:before { left: 21px; }
.mui-switch.mui-switch-animbg { transition: background-color ease 0.4s; }
.mui-switch.mui-switch-animbg:before { transition: left 0.3s; }
.mui-switch.mui-switch-animbg:checked { box-shadow: #dfdfdf 0 0 0 0 inset; background-color: #52a4ff; transition: border-color 0.4s, background-color ease 0.4s; }
.mui-switch.mui-switch-animbg:checked:before { transition: left 0.3s; }
.mui-switch.mui-switch-anim { transition: border cubic-bezier(0, 0, 0, 1) 0.4s, box-shadow cubic-bezier(0, 0, 0, 1) 0.4s; }
.mui-switch.mui-switch-anim:before { transition: left 0.3s; }
.mui-switch.mui-switch-anim:checked {
box-shadow: #52a4ff 0 0 0 16px inset; background-color: #52a4ff; transition: border ease 0.4s, box-shadow ease 0.4s, background-color ease 1.2s; }
.mui-switch.mui-switch-anim:checked:before { transition: left 0.3s; }
.mui-switch { &.mui-switch-middle { height: 26px; width: 44px;
&:before { width: 24px; height: 24px; }
&:checked { &:before { left: 18px; } } }
&.mui-switch-small { height: 22px; width: 36px;
&:before { width: 20px; height: 20px; }
&:checked { &:before { left: 14px; } } } }
|
1
| <input class="mui-switch mui-switch-animbg mui-switch-middle" type="checkbox" ng-model="item.isDisplay" />
|