如何为 ui-bootstrap 日期选择器创建 angularJs 包装器指令?

Posted

技术标签:

【中文标题】如何为 ui-bootstrap 日期选择器创建 angularJs 包装器指令?【英文标题】:How to create an angularJs wrapper directive for a ui-bootstrap datepicker? 【发布时间】:2015-06-16 05:14:54 【问题描述】:

我正在使用ui.bootstrap.datepicker 指令来显示一些日期字段。但是大多数时候我需要相同的设置:我希望它带有一个弹出窗口和一个弹出按钮,并且我想要文本的德语名称。这确实会一遍又一遍地为按钮、文本和格式创建相同的代码,所以我编写了自己的指令以防止自己重复自己。

Here is a plunkr 与我的指令。但是我似乎做错了。如果您使用不使用我的指令的“日期 1”日期选择器选择日期选择器,则一切正常。 我希望日期 2 也是如此,但不是根据我在输入字段中提供的模板(或我期望的任何其他值)显示日期,而是显示日期对象的 .toString() 表示形式(例如 Fri Apr 03 2015 00:00:00 GMT+0200 (CEST) )。

这是我的指令:

angular.module('ui.bootstrap.demo').directive('myDatepicker', function($compile) 
  var controllerName = 'dateEditCtrl';
  return 
      restrict: 'A',
      require: '?ngModel',
      scope: true,
      link: function(scope, element) 
          var wrapper = angular.element(
              '<div class="input-group">' +
                '<span class="input-group-btn">' +
                  '<button type="button" class="btn btn-default" ng-click="' + controllerName + '.openPopup($event)"><i class="glyphicon glyphicon-calendar"></i></button>' +
                '</span>' +
              '</div>');

          function setAttributeIfNotExists(name, value) 
              var oldValue = element.attr(name);
              if (!angular.isDefined(oldValue) || oldValue === false) 
                  element.attr(name, value);
              
          
          setAttributeIfNotExists('type', 'text');
          setAttributeIfNotExists('is-open', controllerName + '.popupOpen');
          setAttributeIfNotExists('datepicker-popup', 'dd.MM.yyyy');
          setAttributeIfNotExists('close-text', 'Schließen');
          setAttributeIfNotExists('clear-text', 'Löschen');
          setAttributeIfNotExists('current-text', 'Heute');
          element.addClass('form-control');
          element.removeAttr('my-datepicker');

          element.after(wrapper);
          wrapper.prepend(element);
          $compile(wrapper)(scope);

          scope.$on('$destroy', function () 
              wrapper.after(element);
              wrapper.remove();
          );
      ,
      controller: function() 
          this.popupOpen = false;
          this.openPopup = function($event) 
              $event.preventDefault();
              $event.stopPropagation();
              this.popupOpen = true;
          ;
      ,
      controllerAs: controllerName
  ;
);

我就是这样使用它的:

<input my-datepicker="" type="text" ng-model="container.two" id="myDP" />

(概念灵感来自this answer)

我正在使用 angular 1.3(plunker 在 1.2 上,因为我刚刚从 angular-ui-bootstrap datepicker 文档中分叉了 plunker)。我希望这不会产生任何影响。

为什么我输入的文本输出错误?如何正确完成?

更新

与此同时,我取得了一些进展。在阅读了有关编译和链接的详细信息后,在this plunkr 中,我使用编译函数而不是链接函数来进行 DOM 操作。我仍然对文档中的这段摘录感到有些困惑:

注意:如果模板已被克隆,则模板实例和链接实例可能是不同的对象。出于这个原因,除了应用于 compile 函数中所有克隆的 DOM 节点的 DOM 转换之外,执行任何其他操作都是不安全的。具体来说,DOM 监听器注册应该在链接函数中完成,而不是在编译函数中。

我特别想知道“适用于所有克隆的 DOM 节点”是什么意思。我最初认为这意味着“适用于 DOM 模板的所有克隆”,但事实并非如此。

无论如何:我的新编译版本在 chromium 中运行良好。在 Firefox 中,我需要首先使用日期选择器选择一个日期,然后一切正常(如果我在日期选择器的日期解析器中将 undefined 更改为 null (plunkr),Firefox 的问题就会自行解决)。所以这也不是最新的事情。此外,我使用ng-model2 而不是在编译期间重命名的ng-model。如果我不这样做,一切都会被打破。仍然不知道为什么。

【问题讨论】:

这让我非常难过!如果您打开 plunker 并在 ui-bootstrap-tpls-0.12.1.js 的第 1541 行放置一个断点,然后从自定义指令 datepicker 中选择一个日期,那么一瞬间,文本框中的日期是正确的,并且那么当你停止调试时它会被 toString 版本覆盖。 只是想提一下,如果我使用你最新的 Plunkr ng-model 在我所有的浏览器中都可以正常工作:Safari、Chrome、Firefox。我唯一改变的是用 ng-model 替换 ng-model2 并注释掉 ng-model2 的 set-if-not-set 位。你可能想再测试一次。 @jme11:我又测试了一遍。但至少我的 Firefox (37.0.1) 拒绝在此字段中输入任何会使其暂时无效的输入。 我在 UI 选择器中遇到了相同的日期 ISO 字符串问题,能够通过使用模型 getter setter 将日期字符串转换为日期对象来解决此问题。 【参考方案1】:

老实说,我不太清楚它是什么原因造成的,以及是什么导致您的日期在输入中显示之前被“toString-ed”。

但是,我确实找到了重构指令的地方,并删除了许多不必要的代码,例如 $compile 服务、属性更改、范围继承、指令中的 require 等。我使用了隔离范围,因为我不要认为每个指令的使用都应该知道父作用域,因为这可能会导致恶性错误。这是我更改后的指令:

angular.module('ui.bootstrap.demo').directive('myDatepicker', function() 
  return 
      restrict: 'A',
      scope: 
          model: "=",
          format: "@",
          options: "=datepickerOptions",
          myid: "@"
      ,
      templateUrl: 'datepicker-template.html',
      link: function(scope, element) 
          scope.popupOpen = false;
          scope.openPopup = function($event) 
              $event.preventDefault();
              $event.stopPropagation();
              scope.popupOpen = true;
          ;

          scope.open = function($event) 
            $event.preventDefault();
            $event.stopPropagation();
            scope.opened = true;
          ;

      
  ;
);

你的 HTML 用法变成:

<div my-datepicker model="container.two" 
                   datepicker-options="dateOptions" 
                   format="format"  
                   myid="myDP">
</div>

编辑:将id 作为参数添加到指令中。 Plunker 已更新。

Plunker

【讨论】:

该解决方案的问题是我失去了覆盖我在指令中设置的默认值的能力。此外,id 字段未在&lt;input&gt; 元素上设置,但这很好,以便我可以从&lt;label&gt; 引用它。如果我例如想要使用 getter/setter 作为模型(使用 ng-model-options=" getterSetter: true ")但不想在全局范围内这样做会很复杂...... @yankee 这些都是很容易解决的问题。您需要做的只是添加另一个属性并在隔离范围内获取它。我将在几秒钟内更新以展示如何为 id 执行此操作。 @yankee 你可以看到更新。问题是您的指令可以随心所欲地灵活。您可以无休止地对其进行参数化。 Omri 所做的是将原始的 DatePicker 指令包装在他自己的指令/模板中,但工作的马仍然是 bootstrap-UI-DatePicker。我不确定优势是什么,但在 Omri 的辩护中,@yankee,不清楚你想要从你的指令中得到什么,他原来没有提供...... @OmriAharon 感谢您指出 ID。我故意将其更改为 id 而不是 myId 以解决前面提到的其他一些问题,但忘记从链接函数中添加 element.removeAttr('id') 。我现在更新了。我确实认为您的方法是最清晰的(并且最终对于可维护性来说是最好的),即使它似乎不是最适合 OP。不过,鉴于该网站的性质,它肯定会对其他人产生启发(因为它显然已经基于您获得的选票数量)。【参考方案2】:

当您将这两行添加到指令定义中时,您的指令将起作用:

return 
    priority: 1,
    terminal: true,
    ...
 

这与指令的执行顺序有关。

所以在你的代码中

<input my-datepicker="" type="text" ng-model="container.two" id="myDP" />

有两个指令:ngModelmyDatepicker。通过优先级,您可以让自己的指令在 ngModel 之前执行。

【讨论】:

太棒了! terminal 正是我所缺少的:-)。 (好吧,我确实需要做更多的测试,但调整了我的plunkr here,它似乎工作正常)。 终端属性告诉 Angular 跳过该元素上的所有指令 ***.com/questions/15266840/…【参考方案3】:

我认为@omri-aharon 的回答是最好的,但我想指出一些这里没有提到的改进:

Updated Plunkr

您可以使用config统一设置格式和文本选项等选项如下:

angular.module('ui.bootstrap.demo', ['ui.bootstrap'])
.config(function (datepickerConfig, datepickerPopupConfig) 
  datepickerConfig.formatYear='yy';
  datepickerConfig.startingDay = 1;
  datepickerConfig.showWeeks = false;
  datepickerPopupConfig.datepickerPopup = "shortDate";
  datepickerPopupConfig.currentText = "Heute";
  datepickerPopupConfig.clearText = "Löschen";
  datepickerPopupConfig.closeText = "Schließen";
);

我发现这更清晰,更容易更新。这还允许您极大地简化指令、模板和标记。

自定义指令

angular.module('ui.bootstrap.demo').directive('myDatepicker', function() 
  return 
      restrict: 'E',
      scope: 
          model: "=",
          myid: "@"
      ,
      templateUrl: 'datepicker-template.html',
      link: function(scope, element) 
          scope.popupOpen = false;
          scope.openPopup = function($event) 
              $event.preventDefault();
              $event.stopPropagation();
              scope.popupOpen = true;
          ;

          scope.open = function($event) 
            $event.preventDefault();
            $event.stopPropagation();
            scope.opened = true;
          ;

      
  ;
);

模板

<div class="row">
    <div class="col-md-6">
        <p class="input-group">
          <input type="text" class="form-control" id="myid" datepicker-popup ng-model="model" is-open="opened" ng-required="true"  />
          <span class="input-group-btn">
            <button type="button" class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
          </span>
        </p>
    </div>
</div> 

如何使用它

<my-datepicker model="some.model" myid="someid"></my-datepicker>

此外,如果您想强制使用德语区域设置格式,您可以添加 angular-locale_de.js。这样可以确保统一使用日期常量,例如 'shortDate',并强制使用德语月份和日期名称。

【讨论】:

【参考方案4】:

这是我的你的 plunker 的猴子补丁,

http://plnkr.co/edit/9Up2QeHTpPvey6jd4ntJ?p=preview

基本上我所做的是更改您的模型,即日期,以使用指令返回格式化字符串

.directive('dateFormat', function (dateFilter) 
  return 
    require:'^ngModel',
    restrict:'A',
    link:function (scope, elm, attrs, ctrl) 
      ctrl.$parsers.unshift(function (viewValue) 
        viewValue.toString = function() 
          return dateFilter(this, attrs.dateFormat);
        ;
        return viewValue;
      );
    
  ;
);

您需要为您的input 标签传递date-format 属性。

如果我是你,我不会走那么远来制定一个复杂的指令。我只需将&lt;datepicker&gt; 添加到具有相同ng-model 的input 标记,并使用按钮控制显示/隐藏。你可以从my plunker开始试验你的选择

【讨论】:

您的 plunkr 不起作用。我可以使用日期选择器选择日期,但我不能使用文本输入。 Firefox 只会拒绝接受 date2 字段中的任何输入,并且一旦我开始输入日期,chrome 就会立即转换并显示 toString() 日期表示。【参考方案5】:

如果创建指令是为了方便添加属性,您可以在原始输入上使用 2 个指令:

<input my-datepicker="" datepicker-popup=" format " type="text" ng-model="container.two" id="myDP" />

然后通过在myDatepicker 指令中将scope: true 更改为scope: false 来避免多个隔离范围。

这很有效,我认为最好创建一个进一步的指令来将日期输入更改为所需的格式:

http://plnkr.co/edit/23QJ0tjPy4zN16Sa7svB?p=preview

为什么你在指令中添加属性会导致这个问题,我不知道,这几乎就像你在同一个输入上有 2 个日期选择器,一个使用你的格式,一个使用默认的 get 之后应用。

【讨论】:

【参考方案6】:

使用 moment.js 和 ui-bootstrap datepicker 组件来创建指令,为日期时间格式提供一套全面的模式。您可以接受隔离范围内的任何时间格式。

【讨论】:

【参考方案7】:

如果有人对 Typescript 实现感兴趣(大致基于 @jme11 的代码):

指令:

'use strict';

export class DatePickerDirective implements angular.IDirective 
    restrict = 'E';
    scope=
        model: "=",
        myid: "@"
    ;
    template = require('../../templates/datepicker.tpl.html');

    link = function (scope, element) 
        scope.altInputFormats = ['M!/d!/yyyy', 'yyyy-M!-d!'];
        scope.popupOpen = false;
        scope.openPopup = function ($event) 
            $event.preventDefault();
            $event.stopPropagation();
            scope.popupOpen = true;
        ;

        scope.open = function ($event) 
            $event.preventDefault();
            $event.stopPropagation();
            scope.opened = true;
        ;
    ;

    public static Factory() : angular.IDirectiveFactory 
        return () => new DatePickerDirective();
    


angular.module('...').directive('datepicker', DatePickerDirective.Factory())

模板:

<p class="input-group">
    <input type="text" class="form-control" id="myid"
           uib-datepicker-popup="MM/dd/yyyy" model-view-value="true"
           ng-model="model" ng-model-options=" getterSetter: true, updateOn: 'blur' "
           close-text="Close" alt-input-formats="altInputFormats"
           is-open="opened" ng-required="true"/><span class="input-group-btn"><button type="button" class="btn btn-default" ng-click="open($event)"><i
        class="glyphicon glyphicon-calendar"></i></button>
          </span>
</p>

用法:

<datepicker model="vm.FinishDate" myid="txtFinishDate"></datepicker>

【讨论】:

【参考方案8】:

我已经尝试完成这项工作(有点 hack),这可能不是您想要的,只是一些粗略的想法。所以你仍然需要稍微调整一下。笨蛋是:

`http://plnkr.co/edit/aNiL2wFz4S0WPti3w1VG?p=preview'

基本上,我更改了指令范围,还添加了 watch for scope var container.two。

【讨论】:

是的,我可以观察我的变量并不断地将它们转换为字符串。但这里实际发生了什么?为什么会这样?

以上是关于如何为 ui-bootstrap 日期选择器创建 angularJs 包装器指令?的主要内容,如果未能解决你的问题,请参考以下文章

如何更改角度 ui-bootstrap 日期选择器的格式

如何为 Android 中的日期和时间选择器对话框设置的特定时间和日期设置通知

如何为带有 $ 符号的 Class 的元素编写选择器

如何为数组中的每个创建一个具有唯一图像和选择器的自定义 UIButton?

angularjs之ui-bootstrap的Datepicker Popup不使用JS实现双日期选择控件

如何为 UITapGestureRecognizer 从不同的 UIView 对象设置选择器