十一、与其他库的整合

UI 框架永远不会独立存在,尤其是 web 框架。web 是如此丰富的平台,由如此动态的社区驱动,有成千上万的库、小部件和组件可以在无数场景中使用,这为开发人员节省了大量时间。

在本章中,我们将了解如何将各种库集成到联系人管理应用中。我们将从 Bootstrap 和 jQueryUI 中添加 UI 小部件,使用sortable.js添加一些拖放支持,使用 D3 添加图形。我们还将看到如何利用 SASS 而不是 CSS。最后,我们还将看到如何集成聚合物组件。

使用引导窗口小部件

从本书开始,我们就依赖于引导程序来设计应用的样式和布局。但是,我们没有使用该库的 JS 小部件。让我们看看如何将这些小部件集成到应用中。

加载库

由于 jQuery 由 Bootstrap 的 JS 小部件使用,我们首先需要安装它:

> npm install jquery --save

接下来,我们需要将 jQuery 和引导 JS 资源添加到供应商包中:

aurelia_project/aurelia.json

{ 
  //Omitted snippet... 
  { 
    "name": "vendor-bundle.js", 
    "prepend": [ 
      "node_modules/bluebird/js/browser/bluebird.core.js", 
      "scripts/require.js" 
    ], 
    "dependencies": [ 
      //Omitted snippet... 
      "jquery", 
      { 
        "name": "bootstrap", 
        "path": "../node_modules/bootstrap/dist", 
        "main": "js/bootstrap.min", 
        "deps": ["jquery"], 
        "exports": "$", 
        "resources": [ 
          "css/bootstrap.min.css" 
        ] 
      }, 
      //Omitted snippet... 
    ] 
    //Omitted snippet... 
  } 
  //Omitted snippet... 
} 

在这里,我们将 jQuery 添加到 bundle 的依赖项中,然后更新 Bootstrap 的条目,以便在 jQuery 完成后加载 JS 小部件。

应用中的bootstrap模块也配置为导出全局jQuery对象。这意味着我们将能够在 JS 代码中从bootstrap导入jQuery对象,并且我们将确保引导窗口小部件已经在 jQuery 上注册。

创建 bs 工具提示属性

让我们看一个简单的例子,使用 Aurelia 的 Bootstrap JS 小部件。我们将创建一个自定义属性,该属性将封装引导tooltip小部件:

src/resources/attributes/bs-tooltip.js

import {inject, DOM, dynamicOptions} from 'aurelia-framework'; 
import $ from 'bootstrap'; 

const properties = [ 
  'animation', 'container', 'delay', 'html',  
  'placement', 'title', 'trigger', 'viewport' 
]; 

@dynamicOptions 
@inject(DOM.Element) 
export class BsTooltipCustomAttribute { 

  isAttached = false; 

  constructor(element) { 
    this.element = element; 
  } 

  attached() { 
    const init = {}; 
    for (let property of properties) { 
      init[property] = this[property]; 
    } 
    $(this.element).tooltip(init); 
    this.isAttached = true; 
  } 

  detached() { 
    this.isAttached = false; 
    $(this.element).tooltip('destroy'); 
  } 
} 

这里,我们首先从引导导入 jQuery 全局对象开始。这将确保引导 JS 库已正确加载并注册到 jQuery 命名空间。我们还声明了tooltip小部件支持的属性列表,因此该属性可以使用动态选项,而忽略不支持的选项。

我们将使用动态选项而不是显式选项,只是为了编写更少的代码。接下来我们将编写一些更改处理程序方法,如果我们使用一个显式的属性列表,所有属性都在BsTooltipCustomAttribute类上声明为可绑定的,那么我们必须为每个属性编写一个不同的更改处理程序。所有这些更改处理程序都将做几乎相同的事情:更新引导程序小部件上的相应选项。相反,因为我们使用动态选项,所以我们可以编写一个更改处理程序来调用所有选项。

我们现在可以创建一个名为bs-tooltip的自定义属性。它作为构造函数参数接收放置它的 DOM 元素。当连接到 DOM 时,它将绑定到每个受支持属性的属性的值分配给一个init对象。然后将该对象传递给tooltip初始化方法,该方法在承载该属性的元素上调用。最后一行将创建tooltip小部件。

最后,当与 DOM 分离时,它只调用tooltip小部件上的destroy方法。

bs-tooltip属性的第一个版本不支持更新属性。这可以通过使用propertyChanged回调方法来更新tooltip小部件来添加:

src/resources/attributes/bs-tooltip.js

//Omitted snippet... 
export class BsTooltipCustomAttribute { 
  //Omitted snippet... 

  propertyChanged(name) { 
    if (this.isAttached && properties.indexOf(name) >= 0) { 
      $(this.element).data('bs.tooltip').options[name] = this[name]; 
    } 
  } 
} 

在这里,当一个属性的值发生变化并且该属性当前已附加到 DOM 时,我们首先确保该属性受到小部件的支持,然后简单地更新小部件的属性。

使用属性

我们现在可以向任何元素添加引导tooltip。让我们用list-editor组件中的引导tooltip替换删除按钮的title属性:

src/resources/elements/list-editor.html

<!-- Omitted snippet... --> 
<button type="button" class="btn btn-danger le-remove-btn"  
        click.delegate="removeItem($index)"  
        bs-tooltip="title.bind: 'resources.actions.remove' & t;  
                    placement: right"> 
    <i class="fa fa-times"></i> 
  </button> 
  <!-- Omitted snippet... --> 

在这里,我们只需从删除按钮中删除t="[title]..."属性,并将其替换为bs-tooltip属性。在这个属性中,我们定义了一个title选项,我们将与前面相同的翻译结果绑定到该选项。我们使用.bind命令和t绑定行为的事实将导致工具提示的title在当前区域设置更改时更新。我们还指定使用placement选项将tooltip放置到托管元素的right上。

不要忘记加载bs-tooltip属性,无论是作为resources功能的configure函数中的全局资源,还是使用require语句加载到list-editor模板中。

如果此时运行应用,并将鼠标悬停在list-editor实例之一的移除按钮上,则会显示一个引导tooltip小部件。

创建 bs 日期选择器元素

我们的联系人管理应用可以从中受益匪浅的一个小部件是日期选择器。这将使大多数用户进入生日更加舒适。

Bootstrap 本身不包括日期选择器,但有些可以作为插件使用。在本节中,我们将安装bootstrap-datepicker插件,加载它,并创建一个新的自定义元素,该元素将封装托管日期选择器的input元素。

安装引导日期选择器插件

我们将首先安装引导插件:

> npm install bootstrap-datepicker --save

接下来,我们需要将其添加到供应商捆绑包中:

aurelia_project/aurelia.json

{ 
  //Omitted snippet... 
  { 
    "name": "vendor-bundle.js", 
    "prepend": [ 
      "node_modules/bluebird/js/browser/bluebird.core.js", 
      "scripts/require.js" 
    ], 
    "dependencies": [ 
      //Omitted snippet... 
      { 
        "name": "bootstrap-datepicker", 
        "path": "../node_modules/bootstrap-datepicker/dist", 
        "main": "js/bootstrap-datepicker.min", 
        "deps": ["jquery"], 
        "resources": [ 
          "css/bootstrap-datepicker3.standalone.css" 
        ] 
      }, 
      //Omitted snippet... 
    ] 
  } 
  //Omitted snippet... 
} 

在这里,我们将bootstrap-datepicker库添加到供应商包中。就像标准的引导窗口小部件一样,这个插件在 jQuery 对象上添加了新的函数,因此它需要依赖 jQuery 才能注册自己。它还将自己的样式表作为附加资源加载。

创建自定义元素

现在插件已经准备好使用,我们可以开始构建自定义元素了。我们的bs-datepicker元素将公开一个双向可绑定date属性,该属性将被指定所选日期作为Date对象。它还将公开一个可绑定的options属性,该属性将用于提供传递给底层bootstrap-datepicker小部件实例的选项。

首先,让我们编写它的模板:

src/resources/elements/bs-datepicker.html

<template> 
  <require from="bootstrap-datepicker/css/ 
                 bootstrap-datepicker3.standalone.css"></require> 
  <input ref="input" class="form-control" /> 
</template> 

这个模板只需要样式表 bootstrap-datepicker,然后声明一个input元素。对该input的引用将分配给绑定上下文的input属性,因此视图模型可以使用它来承载日期选择器。

接下来,让我们编写视图模型类:

src/resources/elements/bs-datepicker.js

import {bindable, bindingMode} from 'aurelia-framework'; 
import $ from 'bootstrap'; 
import 'bootstrap-datepicker'; 

export class BsDatepickerCustomElement { 

  static defaultOptions = { autoclose: true, zIndexOffset: 1050 }; 

  @bindable({ defaultBindingMode: bindingMode.twoWay }) date; 
  @bindable options; 

  isAttached = false; 
  isUpdating = false; 

  createDatepicker() { 
    const options = Object.assign({},  
      BsDatepickerCustomElement.defaultOptions,  
      this.options); 
    $(this.input).datepicker(options) 
      .on('clearDate', this.updateDate) 
      .on('changeDate', this.updateDate); 
    if (this.date) { 
      this.updateDatepickerDate(); 
    } 
  } 

  destroyDatepicker() { 
    $(this.input) 
      .datepicker() 
      .off('clearDate', this.updateDate) 
      .off('changeDate', this.updateDate) 
      .datepicker('destroy'); 
  } 

  updateDate = function() { 
    if (!this.isUpdating) { 
      this.date = $(this.input).datepicker('getUTCDate'); 
    } 
  }.bind(this); 

  updateDatepickerDate() { 
    $(this.input).datepicker('setUTCDate', this.date); 
  } 

  optionsChanged() { 
    if (this.isAttached) { 
      this.destroyDatepicker(); 
      this.createDatepicker(); 
    } 
  } 

  dateChanged() { 
    if (this.isAttached) { 
      this.isUpdating = true; 
      this.updateDatepickerDate(); 
      this.isUpdating = false; 
    } 
  } 

  attached() { 
    this.createDatepicker(); 
    this.isAttached = true; 
  } 

  detached() { 
    this.isAttached = false; 
    this.destroyDatepicker(); 
  } 
} 

我们首先从引导导入全局 jQuery 对象;记住,我们配置了引导库,这样当我们将 jQuery 对象添加到供应商包以写入bs-tooltip属性时,它就会导出 jQuery 对象。

接下来,我们加载bootstrap-datepicker插件,使其正确注册到 jQuery,然后创建自定义元素的类。

它首先声明一个静态defaultOptions属性,用于设置创建小部件时传递给小部件的选项的默认值。

当元素附加到 DOM 时,它会在input上创建一个datepicker小部件实例。它还订阅小部件的clearDatechangeDate事件,因此当小部件选择的日期更改时,它可以更新自己的date属性;然后初始化小部件的选定日期。

您可能想知道为什么我们要添加这些事件侦听器,为什么我们不只是绑定到input的值。这是因为小部件已经处理了input值的验证及其作为Date对象的解析,因此我们的自定义元素依赖于datepicker的选定日期要简单得多。基本上,我们的定制元素只是将其date可绑定属性与所选日期datepicker连接起来。当小部件的选定日期更改时,会触发其中一个事件侦听器,并将小部件的新值分配给元素的date属性。类似地,由于元素的date属性默认使用双向绑定,当date属性更改时,主要是在模板中使用时初始化元素时,dateChanged方法由绑定系统调用,小部件的选定日期将更新。我们还使用了一个isUpdating属性来防止元素和小部件之间的无限更新循环。

当元素与 DOM 分离时,它首先取消订阅小部件的clearDatechangeDate事件,然后调用其destroy方法。

最后,当元素的options属性更改时,小部件将被销毁,然后重新创建。这是因为,在撰写本文时,bootstrap-datepicker插件没有提供任何 API 来在创建小部件后更新其选项。

如您所见,此元素手动处理 Aurelia 和引导小部件之间的数据绑定。在 Aurelia 中集成外部 UI 库时,您在这里看到的模式(在小部件上注册事件处理程序以及来回同步数据)非常常见。

奥雷利亚社区的一个小组正在这方面做一些非常有趣的工作。他们开发了他们称之为桥的东西,允许我们在 Aurelia 应用中使用各种 UI 框架。他们已经为剑道 UI 发布了这样一个桥接器,并且正在为引导和物化等桥接器进行工作。如果你对这个主题感兴趣,我建议你看看他们的作品:https://github.com/aurelia-ui-toolkits

使用该元件

现在,我们可以轻松地将form组件中绑定到触点生日的input替换为新的bs-datepicker元素:

src/contacts/components/form.html

<!-- Omitted snippet... --> 
<div class="form-group"> 
  <label class="col-sm-3 control-label"  
         t="contacts.birthday"></label> 
  <div class="col-sm-9"> 
    <bs-datepicker date.bind="contact.birthday & validate"> 
    </bs-datepicker> 
  </div> 
</div> 
<!-- Omitted snippet... --> 

这里,我们简单地用一个bs-datepicker元素替换前面的input元素。我们将元素的date属性绑定到contactbirthday属性,用validate绑定行为装饰绑定,因此属性仍然有效。

由于我们新元素的date属性需要一个Date对象而不是字符串值,因此我们需要更改Contact模型类,因此当从 JS 对象创建时,它会将其birthday属性解析为Date实例。此外,我们需要将birthday的默认值从空字符串更改为null

src/contacts/models/contact.js

//Omitted snippet... 
export class Contact { 

  static fromObject(src) { 
    const contact = Object.assign(new Contact(), src); 
    if (contact.birthday) { 
      contact.birthday = new Date(contact.birthday); 
    } 
    //Omitted snippet... 
  } 

  //Omitted snippet... 
  birthday = null; 
  //Omitted snippet... 
} 

现在,Contact实例的birthday属性将是null值或Date对象。

此时,如果运行应用,导航到 creation 或 edition 组件,并将焦点放在生日input,则日期选择器应该会出现。您应该能够浏览日历并选择日期。

不要忘记加载bs-datepicker元素,可以将其作为resources功能的configure函数中的全局资源,也可以使用require语句加载到form模板中。

bs 日期选择器元素国际化

此时,我们的bs-datepicker元素不支持国际化。在典型的现实应用中,输入中显示的日期格式以及日历的文本和属性(如一周的第一天)应本地化。

谢天谢地,bootstrap-datepicker包含本地化数据作为附加的 JS 模块。我们只需要在 bundle 中包含我们需要的区域设置的模块。

重新配置 jQuery 和 Bootstrap 的捆绑

但是,在编写本文时,本地化模块不支持模块加载机制,而是完全依赖于全局范围内的 jQuery 对象。因此,我们需要改变使用 jQuery 和引导窗口小部件的方式,方法是使用供应商包的prepend属性,将它们加载为全局库,而不是作为 AMD 模块:

aurelia_project/aurelia.json

//Omitted snippet... 
{ 
  "name": "vendor-bundle.js", 
  "prepend": [ 
    "node_modules/bluebird/js/browser/bluebird.core.js", 
    "node_modules/jquery/dist/jquery.min.js", 
    "node_modules/bootstrap/dist/js/bootstrap.min.js", 
    "node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js", 
    "node_modules/bootstrap-datepicker/dist/locales/ 
       bootstrap-datepicker.fr.min.js", 
    "scripts/require.js" 
  ], 
  "dependencies": [ 
    //Omitted snippet... 
  ] 
} 
//Omitted snippet... 

在这里,我们将 jQuery、引导窗口小部件、bootstrap-datepicker插件和它的法语本地化模块添加到包的预置库中(英语本地化数据内置在插件本身中,因此我们不需要包含它)。这意味着这些库只需在包的开头合并,而不作为 AMD 模块加载,而是使用全局window范围。当然,这意味着 jQuery、Bootstrap 和日期选择器插件的条目必须从dependencies数组中删除。

由于前置库只能是 JS 文件,这也意味着我们必须更改加载引导样式表的方式:

index.html

<!-- Omitted snippet... --> 
<head> 
    <title>Learning Aurelia</title> 
    <link href="node_modules/bootstrap/dist/css/bootstrap.min.css"  
          rel="stylesheet"> 
    <link href="node_modules/bootstrap-datepicker/dist/css/ 
                bootstrap-datepicker3.standalone.css"  
          rel="stylesheet"> 
  <!-- Omitted snippet... --> 
<head> 
<!-- Omitted snippet... --> 

当然,bootstrap.cssbootstrap-datepicker3.standalone.cssrequire语句必须分别从src/app.htmlsrc/resources/elements/bs-datepicker.html模板中删除。

最后,必须从bs-tooltip.jsbs-datepicker.js文件中删除bootstrapbootstrap-datepickerimport语句,因为 jQuery、Bootstrap 和日期选择器插件将从全局范围访问。

更新元素

要本地化日期选择器小部件,我们只需设置language选项:

src/contacts/components/form.html

<!-- Omitted snippet... --> 
<bs-datepicker date.bind="contact.birthday & validate" 
               options.bind="{ language: locale }"> 
</bs-datepicker> 
<!-- Omitted snippet... --> 

这意味着我们需要将这个locale属性添加到form的视图模型中。我们还需要订阅适当的事件,以便在当前区域设置更改时更新属性:

src/contacts/components/form.js

//Omitted snippet... 
import {I18N} from 'aurelia-i18n'; 
import {EventAggregator} from 'aurelia-event-aggregator'; 

@inject(DOM.Element, Animator, I18N, EventAggregator) 
export class ContactForm { 

@bindable contact; 

constructor(element, animator, i18n, eventAggregator) { 
    this.element = element; 
    this.animator = animator; 
    this.i18n = i18n; 
    this.eventAggregator = eventAggregator; 
  } 

  bind() { 
    this.locale = this.i18n.getLocale(); 
    this._localeChangedSubscription = this.eventAggregator 
      .subscribe('i18n:locale:changed', () => { 
        this.locale = this.i18n.getLocale(); 
      }); 
  } 

  unbind() { 
    this._localeChangedSubscription.dispose(); 
    this._localeChangedSubscription = null; 
  } 

  //Omitted snippet... 
} 

在这里,我们首先从aurelia-i18n库导入I18N类,从aurelia-event-aggregator库导入EventAggregator类。然后我们向 DIC 提示它们都应该被注入视图模型的构造函数中。

当组件进行数据绑定时,我们使用I18N实例的getLocale方法初始化locale属性,并订阅i18n:locale:changed事件,以便保持locale属性的最新状态。

最后,当组件解除绑定时,我们将处理事件订阅。

此时,如果您运行应用并在法语和英语之间来回切换当前区域设置时使用生日日期选择器,则input中显示的日期格式以及日历文本和设置应相应更新。

使用 jQuery UI 小部件

jQueryUI 小部件库仍然相当流行。在 Aurelia 应用中集成这些小部件与我们刚刚使用引导程序小部件所做的非常相似,尽管不像我们在下一节中看到的那样轻松。

让我们使用 jQueryUI 创建一个tooltip属性,这样我们就可以将它与 Bootstrap 进行比较。

以下代码片段摘自本书资产中的chapter-11/samples/using-jqueryui示例。

安装库

我们首先需要通过在项目目录中打开控制台并运行以下命令来安装 jQuery 和 jQuery UI:

> npm install jquery --save
> npm install github:components/jqueryui#1.12.1 --save

接下来,我们需要将这些库添加到供应商包中。最简单的方法是将它们放在prepend部分:

aurelia_project/aurelia.json

//Omitted snippet... 
{ 
  "name": "vendor-bundle.js", 
  "prepend": [ 
    "node_modules/bluebird/js/browser/bluebird.core.js", 
    "node_modules/jquery/dist/jquery.min.js", 
    "node_modules/components-jqueryui/jquery-ui.min.js", 
    "scripts/require.js" 
  ], 
  "dependencies": [ 
    //Omitted snippet... 
  ] 
} 
//Omitted snippet... 

因为 CSS 文件不能全局加载到prepend部分,所以让我们将它们加载到index.html文件中:

index.html

<!-- Omitted snippet... --> 
<head> 
<title>Aurelia</title> 
  <link href="node_modules/bootstrap/dist/css/bootstrap.min.css"  
        rel="stylesheet"> 
  <link href="node_modules/components-jqueryui/themes/base/all.css"  
        rel="stylesheet"> 
  <!-- Omitted snippet... --> 
</head> 
<!-- Omitted snippet... --> 

现在,我们可以创建属性了。

创建 jq 工具提示属性

首先,我们的新属性与使用引导的属性非常相似:

src/resources/attributes/jq-tooltip.js

import {inject, DOM, dynamicOptions} from 'aurelia-framework'; 

const properties = [ 
  'classes', 'content', 'disabled', 'hide', 'position', 
  'show', 'track',  
]; 

@dynamicOptions 
@inject(DOM.Element) 
export class JqTooltipCustomAttribute { 

  isAttached = false; 

  constructor(element) { 
    this.element = element; 
  } 

  attached() { 
    const options = {}; 
    for (let property of properties) { 
      options[property] = this[property]; 
    } 
    $(this.element).tooltip(options); 
    this.isAttached = true; 
  }   

  detached() { 
    this.isAttached = false; 
    $(this.element).tooltip('destroy'); 
  } 
} 

我们首先定义 jQueryUItooltip小部件支持的options,这样该属性就可以使用动态选项,而忽略在此过程中不支持的选项;jq-tooltip属性的行为与我们在上一节中创建的bs-tooltip属性完全相同。接下来,我们向 DI 容器提示,应该将承载该属性的 DOM 元素注入构造函数。

当属性附加到 DOM 时,它检索绑定到每个受支持属性的属性实例的值,以构建一个options对象。然后将该对象传递给tooltip初始化方法,该方法应用于承载该属性的元素。

当属性与 DOM 分离时,小部件的destroy方法被调用到承载该属性的元素上。

此时,该属性不支持属性更改。由于 jQuery 的tooltip小部件提供了一个用于更新选项的 API,因此此实现不必像bs-tooltip属性那样销毁并重新创建小部件来更新属性:

src/resources/attributes/jq-tooltip.js

//Omitted snippet... 
propertyChanged(name) { 
  if (this.isAttached && properties.indexOf(name) >= 0) { 
    $(this.element).tooltip('option', name, this[name]); 
  } 
} 
//Omitted snippet... 

在这里,我们只需添加propertyChanged回调方法,如果属性附加到 DOM 并且小部件支持更新的属性,则该方法将更新小部件实例。

现在我们的属性已经准备好了,让我们将移除按钮的title属性替换为list-editor组件中的jq-tooltip自定义属性:

src/resources/elements/list-editor.html

<!-- Omitted snippet.. --> 
<button type="button" class="btn btn-danger le-remove-btn"  
        click.delegate="removeItem($index)" 
        jq-tooltip="content.bind: 'resources.actions.remove' & t"> 
  <i class="fa fa-times"></i> 
</button> 
<!-- Omitted snippet.. --> 

在这里,我们只需在适当的button元素上添加一个jq-tooltip属性。我们将它的content属性绑定到适当的翻译,这是由t绑定行为修饰的。

不要忘记加载jq-tooltip属性,可以将其作为resources功能的configure函数中的全局资源,也可以使用require语句加载到list-editor模板中。

但是,如果您运行应用并用鼠标悬停在list-editor元素的移除按钮上,您将看到tooltip不会显示。

这是由一个众所周知的限制引起的;社区中的一些人会说这是tooltip小部件中的一个 bug(我也同意),它强制宿主元素具有title属性,即使它没有被使用。

因此,如果宿主元素上不存在空的title属性,那么让我们更新该属性并添加一个方法来创建空的title属性:

src/resources/attributes/jq-tooltip.js

//Omitted snippet... 
attached() { 
  if (!this.element.hasAttribute('title')) { 
    this.element.setAttribute('title', ''); 
  } 
  //Omitted snippet... 
} 
//Omitted snippet... 

现在您可以运行应用,tooltip应该会正确显示。

使用 SASS 代替 CSS

SASS是语法上非常棒的样式表,根据他们的网站,它是世界上最成熟、最稳定、最强大的专业级 CSS 扩展语言。不管这种说法是否属实,它都是最受欢迎的说法之一,至少我可以说我经常使用它。

在 Aurelia 应用中使用 SASS 而不是 CSS 非常简单,至少对于基于 CLI 的项目来说是如此。CLI 已经提供了对许多 CSS 处理器的支持,如 SASS、LESS 和手写笔。

让我们使用 CLI 重新创建联系人管理应用,并在创建过程中启用 SASS 处理器:

Using SASS instead of CSS

您可以为所有其他问题选择默认值。

创建项目并获取依赖项后,我们可以将以下目录和文件从应用的工作副本移动到新创建的项目:

  • aurelia_project/environments
  • locales
  • src
  • index.html

我们还需要从package.json文件中复制dependencies,并运行另一个npm install以获取所有应用依赖项。最后,我们需要从aurelia_project/aurelia.json文件复制供应商包配置。

您可以查看该账簿资产中的chapter-11/samples/using-sass样本作为参考。

将 CSS 替换为 SASS

让我们将应用中的 CSS 文件转换为 SASS 文件,将.css扩展名替换为.scss扩展名:

src/resources/elements/list-editor.scss

list-editor .animated .le-item { 
  &.au-enter-active { 
    animation: blindDown 0.2s; 
    overflow: hidden; 
  } 

  &.au-leave-active { 
    animation: blindUp 0.2s; 
    overflow: hidden; 
  } 
} 

@keyframes blindDown { 
  0% { max-height: 0px; } 
  100% { max-height: 80px; } 
} 

@keyframes blindUp { 
  0% { max-height: 80px; } 
  100% { max-height: 0px; } 
} 

由于 CLI 创建的构建任务现在包含一个 SASS 处理器,src目录中的每个.scss文件都将转换为具有相同路径的.css文件,并将包含在该路径下的app-bundle中。

例如,resources/elements/list-editor.scss文件将被转换为 CSS,结果将被捆绑为app-bundle中的resources/elements/list-editor.css

这意味着require语句必须使用.css扩展继续引用样式表:

src/resources/elements/list-editor.html

<template> 
  <require from="./list-editor.css"></require> 
  <!-- Omitted snippet... --> 
</template> 

如果此时运行应用,则所有内容都应该像以前一样进行样式设置。

可排序拖放

可排序(https://github.com/RubaXa/Sortable 是一个著名的拖放库。其简单而强大的 API 使其集成非常容易。

让我们在我们的联系人管理应用中使用它,允许用户通过拖放为list-editor元素重新排序项目。

安装库

首先,我们需要通过在项目目录中打开控制台并运行以下命令来安装库:

> npm install sortablejs --save

接下来,我们需要将其添加到供应商捆绑包中:

aurelia_project/aurelia.json

//Omitted snippet... 
{ 
  "name": "vendor-bundle.js", 
  "prepend": [ 
    //Omitted snippet... 
  ], 
  "dependencies": [ 
    "sortablejs", 
    //Omitted snippet... 
  ] 
}, 
//Omitted snippet... 

此时,我们可以开始在应用中使用该库。

添加拖放到列表编辑器

让我们首先为列表项添加一个句柄。此句柄是用户可以在列表中上下拖动项目的区域。此外,我们需要添加一个div元素,它将充当可排序项目的容器:

src/resources/elements/list-editor.html

<!-- Omitted snippet... --> 
<div ref="container"> 
  <div class="form-group le-item ${animated ? 'au-animate' : ''}"  
       repeat.for="item of items"> 
    <template with.bind="item"> 
      <div class="col-sm-1"> 
        <i class="fa fa-bars fa-2x sort-handle pull-right"></i> 
      </div> 
      <template replaceable part="item"> 
        <div class="col-sm-2"> 
          <template replaceable part="label"></template> 
        </div> 
        <!-- Omitted snippet... --> 
      </template> 
      <!-- Omitted snippet... --> 
    </template> 
  </div> 
</div> 
<!-- Omitted snippet... --> 

在这里,我们首先将包含列表项的div元素上的引用分配给视图模型的container属性。sortableAPI 将需要此container来启用对其子项的拖放。接下来,我们从 label 列中删除col-sm-offset-1CSS 类,并添加一个大小为 1 的列,使用 Bootstrap 的col-sm-1CSS 类,该类包含一个bars字体超级图标,并充当sort-handle,使用相同名称的 CSS 类。

我们还要添加一个 CSS 规则来更改拖动手柄的鼠标光标:

src/resources/elements/list-editor.css

/* Omitted snippet... */ 
list-editor .sort-handle { 
 cursor: move; 
} 

我们现在可以使用sortable添加拖放支持:

src/resources/elements/list-editor.js

//Omitted snippet... 
import sortable from 'sortablejs'; 

export class ListEditor { 
  //Omitted snippet... 
 moveItem(oldIndex, newIndex) { 
    const item = this.items[oldIndex]; 
    this.items.splice(oldIndex, 1); 
    this.items.splice(newIndex, 0, item); 
  } 

 attached() { 
    this.sortable = sortable.create(this.container, { 
      sort: true, 
      draggable: '.le-item', 
      handle: '.sort-handle',  
      animation: 150, 
      onUpdate: (e) => { 
        if (e.newIndex != e.oldIndex) { 
          this.animated = false; 
          this.moveItem(e.oldIndex, e.newIndex);  
          setTimeout(() => { this.animated = true; }); 
        } 
      } 
    }); 
    setTimeout(() => { this.animated = true; }); 
  } 

 detached() { 
    this.sortable.destroy(); 
    this.sortable = null; 
  } 
  //Omitted snippet... 
} 

这里,我们从导入sortableAPI 开始。然后,当元素附加到 DOM 时,我们在具有le-itemCSS 类的container项上创建一个sortable实例。我们向sortable指定具有sort-handleCSS 类的项的子元素应用作拖动句柄。最后,当一个项目在列表中的不同位置被删除时,会触发onUpdate回调,我们从items数组中先前的位置删除删除的项目,然后将其插入到新位置。

我们需要使用splice删除然后添加移动的项,因为 Aurelia 无法观察数组的索引设置器。它只能通过覆盖Array.prototype的方法,如splice来对数组的更改做出反应。

此外,我们需要在移动项目之前从项目中删除animatedCSS 类,这样触发动画的 CSS 规则就不会匹配。然后,我们使用setTimeout将其添加回来,因此只有在模板引擎完成删除旧视图并添加新视图后,才会添加它。这样,移除或添加项目时播放的动画在拖放项目时不会播放,这看起来很奇怪。

最后,当list-editor与 DOM 分离时,我们对sortable实例调用destroy方法,以防止内存泄漏。

此时,您可以运行应用,对联系人列表属性之一的项目重新排序,并保存表单。在“详细信息”视图中,项目应以新的顺序显示。

用 D3 作图

以图形形式呈现数据是现代应用的另一个常见需求。说到 Web,D3.js是一个众所周知的库,它提供了一个非常强大的 API 来显示 DOM 中的数据。

在下一节中,我们将向联系人管理应用添加一个树状视图,它将显示按地址部分分组的联系人。考虑所有联系人的所有地址,第一级节点将是国家,然后每个国家将其州作为子国,然后每个国家将其城市,依此类推。

我们将在本节中构建的树状图只是一个简单、糟糕的例子,说明了 D3 可以实现什么。转到https://d3js.org/ 浏览数百个样本,亲自体验这个库的强大功能。

安装库

让我们首先通过在项目目录中打开控制台并运行以下命令来安装库:

> npm install d3 --save

与往常一样,我们需要将其添加到供应商捆绑包中:

aurelia_project/aurelia.json

//Omitted snippet... 
{ 
  "name": "vendor-bundle.js", 
  "prepend": [ 
    //Omitted snippet... 
  ], 
  "dependencies": [ 
    { 
      "name": "d3", 
      "path": "../node_modules/d3/build", 
      "main": "d3.min" 
    }, 
    //Omitted snippet... 
  ] 
} 
//Omitted snippet... 

此时,D3 已准备好使用。

准备申请

在创建树本身之前,让我们先围绕它准备应用。我们将添加一个route组件,该组件将使用网关加载联系人,我们将在其中显示树。我们还将在联系人main中为该组件添加一个route,然后添加允许在列表和树之间来回导航的链接。

让我们从route开始:

src/contacts/main.js

//Omitted snippet... 
config.map([ 
  { route: '', name: 'contacts', moduleId: './components/list',  
    title: 'contacts.contacts' }, 
  { route: 'by-address', name: 'contacts-by-address',  
    moduleId: './components/by-address',  
    title: 'contacts.byAddress' }, 
  { route: 'new', name: 'contact-creation',  
    moduleId: './components/creation',  
    title: 'contacts.newContact' }, 
  { route: ':id', name: 'contact-details',  
    moduleId: './components/details' }, 
  { route: ':id/edit', name: 'contact-edition',  
    moduleId: './components/edition' }, 
  { route: ':id/photo', name: 'contact-photo',  
    moduleId: './components/photo' }, 
]); 
//Omitted snippet... 

在这里,我们只需添加一个名为contacts-by-addressrouteby-address路径匹配,并指向by-address组件,我们将在一分钟内创建该组件。

接下来,让我们向列表组件添加一个链接,该链接指向尚不存在的树组件:

src/contacts/components/list.html

<template> 
  <section class="container au-animate"> 
    <h1 t="contacts.contacts"></h1> 
    <p> 
      <a route-href="route: contacts-by-address"  
         t="contacts.viewByAddress"></a> 
    </p> 
    <!-- Omitted snippet... --> 
  </section> 
</template> 

您可能注意到新的routetitle属性和新链接的文本都使用了新的翻译,我将其添加作为练习留给读者。和往常一样,本章的示例应用可以作为参考。

最后,我们将创建by-address组件。为了使事情尽可能地解耦,我们将在一个名为contact-address-tree的自定义元素中隔离与 D3 相关的代码。by-address组件的唯一责任是将此自定义元素与应用的其余部分连接起来。

让我们从视图模型开始:

src/contacts/components/by-address.js

import {inject} from 'aurelia-framework'; 
import {Router} from 'aurelia-router'; 
import {ContactGateway} from '../services/gateway'; 

@inject(ContactGateway, Router) 
export class ContactsByAddress { 

  contacts = []; 

  constructor(contactGateway, router) { 
    this.contactGateway = contactGateway; 
    this.router = router; 
  } 

  activate() { 
    return this.contactGateway.getAll().then(contacts => { 
      this.contacts.splice(0); 
      this.contacts.push.apply(this.contacts, contacts);  
    }); 
  } 

  navigateToDetails(contact) { 
    this.router 
      .navigateToRoute('contact-details', { id: contact.id }); 
  } 
} 

这个视图模型非常简单。激活时,它使用注入网关检索联系人的完整列表。它还公开了一个方法,该方法触发对给定联系人详细信息组件的导航。单击树中的联系人节点时将调用此方法。

正如您可以想象的那样,该模板非常简单:

src/contacts/components/by-address.html

<template>  
  <require from="./by-address.css"></require> 
  <require from="../elements/address-tree"></require> 

  <section class="container au-animate"> 
    <h1 t="contacts.byAddress"></h1> 

    <p> 
      <a route-href="route: contacts" t="contacts.viewByName"></a> 
    </p> 

    <contact-address-tree contacts.bind="contacts"  
                          click.call="navigateToDetails(contact)"> 
    </contact-address-tree> 
  </section> 
</template> 

该模板只声明一个contact-address-tree元素,绑定加载的contacts,并在点击联系人节点时调用navigateToDetails

CSS 文件只是设置contact-address-tree元素的大小:

src/contacts/components/by-address.css

contact-address-tree { 
  display: block; 
  width: 100%; 
  min-height: 400px; 
} 

创建联系人地址树自定义元素

现在一切都准备好使用新元素了,让我们创建它。

由于我们正在添加更多联系人专用自定义元素,我建议我们在contacts功能中创建一个新的elements目录,将联系人form移动到其中,并在其中创建这些新元素。本章完整的应用示例可作为参考。

我们将首先列出一些 CSS 规则,这些规则将设置各种树部分的样式,例如分支节点、叶节点和链接:

src/contacts/elements/address-tree.css

contact-address-tree .node circle { 
  fill: #d9edf7; 
  stroke: #337ab7; 
  stroke-width: 1.5px; 
} 

contact-address-tree .node text { 
  font: 15px; 
} 

contact-address-tree .node text { 
  text-shadow: 0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff; 
} 

contact-address-tree .leaf { 
  cursor: pointer; 
} 

contact-address-tree .leaf circle { 
  fill: #337ab7; 
} 

contact-address-tree .leaf text { 
  font-weight: bold; 
} 

contact-address-tree .link { 
  fill: none; 
  stroke: #777; 
  stroke-width: 1.5px; 
} 

由于树视图的呈现将由 D3API 处理,因此定制元素不需要模板。因此,它将使用noView装饰器声明,CSS 文件的路径将传递到该装饰器,因此它将作为资源加载:

src/contacts/elements/address-tree.js

import {inject, DOM, noView, bindable} from 'aurelia-framework'; 
import * as d3 from 'd3'; 

@inject(DOM.Element) 
@noView(['./address-tree.css']) 
export class ContactAddressTreeCustomElement {      

  @bindable contacts; 
  @bindable click; 

  constructor(element) { 
    this.element = element; 
  } 
} 

此外,视图模型的构造函数将被注入 DOM 元素本身,因此 D3API 可以将其用作视口来渲染树。它还公开了一个contactsclick可绑定属性。

这是奥雷利亚的部分。现在,让我们添加一个attached方法,该方法将渲染元素内部的树。此方法中的代码将完全忽略 Aurelia,只需使用d3API 和 DOMelement本身即可:

src/contacts/elements/address-tree.js

//Omitted snippet... 
export class ContactAddressTreeCustomElement { 
  //Omitted snippet... 

 attached() { 
    // Calculate the size of the viewport 
    const margin = { top: 20, right: 200, bottom: 20, left: 12 }; 
    const height = this.element.clientHeight  
      - margin.top - margin.bottom; 
    const width = this.element.clientWidth  
      - margin.right - margin.left; 

    // Create the host elements and the tree factory 
    const tree = d3.tree().size([height, width]); 
    const svg = d3.select(this.element).append('svg') 
        .attr('width', width + margin.right + margin.left) 
        .attr('height', height + margin.top + margin.bottom); 
    const g = svg.append('g') 
        .attr('transform',  
              `translate(${margin.left}, ${margin.top})`); 

    // Create the hierarchy, then initialize the tree from it 
    const rootNode = this.createAddressTree(this.contacts); 
    const hierarchy = d3.hierarchy(rootNode); 
    tree(hierarchy); 

    // Render the nodes and links 
    const link = g.selectAll('.link') 
      .data(hierarchy.descendants().slice(1)) 
      .enter().append('path') 
      .attr('class', 'link') 
      .attr('d', d => `M${d.y},${d.x}C${(d.y + d.parent.y) / 2}, 
                       ${d.x} ${(d.y + d.parent.y) / 2}, 
                       ${d.parent.x} ${d.parent.y}, 
                       ${d.parent.x}`); 

    const node = g.selectAll('.node') 
      .data(hierarchy.descendants()) 
      .enter().append('g') 
      .attr('class', d => 'node ' + (d.children ? 'branch' : 'leaf')) 
      .attr('transform', d => `translate(${d.y}, ${d.x})`) 
      .on('click', e => { this.onNodeClicked(e); }); 

    node.append('title') 
      .text(d => d.data.name); 

    node.append('circle') 
      .attr('r', 10); 

    node.append('text') 
      .attr('dy', 5) 
      .attr('x', d => d.children ? -15 : 15) 
      .style('text-anchor', d => d.children ? 'end' : 'start') 
      .text(d => d.data.name); 
  } 
} 

这段代码是迈克·博斯托克(Mike Bostock)在中的样本的简化改编 https://bl.ocks.org/mbostock/4339083

详细解释d3API 的工作原理远远超出了本书的范围。但是,前面代码段中的内联注释可以让您很好地了解它的工作原理。

你可能注意到了一些缺失的部分:createAddressTreeonNodeClicked方法还不存在。

后者相当简单:

src/contacts/elements/address-tree.js

//Omitted snippet... 
export class ContactAddressTreeCustomElement { 
  //Omitted snippet... 

 onNodeClicked(node) { 
    if (node.data.contact && this.click) { 
      this.click({ contact: node.data.contact }); 
    } 
  } 
} 

此方法只是确保单击的节点是联系人节点,并且在使用单击的contact对象调用它之前,click属性已正确绑定。这将使用.call命令执行绑定到click属性的表达式,并将节点的联系人作为contact参数传递给它。

前者稍微复杂一点。其工作是将联系人列表转换为树型数据结构,作为d3API 的数据源:

src/contacts/elements/address-tree.js

//Omitted snippet... 
export class ContactAddressTreeCustomElement { 
  //Omitted snippet... 

 createAddressTree(contacts) { 
    const rootNode = { name: '', children: [] }; 
    for (let contact of contacts) { 
      for (let address of contact.addresses) { 
        const path = this.getOrCreateAddressPath( 
          rootNode, address); 
        const pathTail = path[path.length - 1]; 
        pathTail.children.push({ 
          name: contact.fullName,  
          contact 
        }); 
      } 
    } 
    return rootNode; 
  } 

  getOrCreateAddressPath(rootNode, address) { 
    const countryNode = this.getOrCreateNode( 
      rootNode, address.country); 
    const stateNode = this.getOrCreateNode( 
      countryNode, address.state); 
    const cityNode = this.getOrCreateNode( 
      stateNode, address.city); 
    const streetNode = this.getOrCreateNode( 
      cityNode, address.street); 
    const numberNode = this.getOrCreateNode( 
      streetNode, address.number); 
    return [countryNode, stateNode, cityNode,  
      streetNode, numberNode]; 
  } 

  getOrCreateNode(parentNode, name) { 
    name = name || '?'; 

    const normalizedName = this.normalizeNodeName(name); 
    let node = parentNode.children 
      .find(n => n.normalizedName === normalizedName); 
    if (!node) { 
      node = { name, normalizedName, children: [] }; 
      parentNode.children.push(node); 
    } 
    return node; 
  } 

  normalizeNodeName(name) { 
    return name.toLowerCase().trim().replace(/\s+/, ' '); 
  } 
} 

这里,createAddressTree方法首先创建一个根节点,其空列表为children。然后,它在每个联系人的addresses上循环,并为每个联系人创建地址的节点路径,从国家开始,向下搜索到街道号码。不会再次创建整个路径或已存在的部分路径的节点,而只是简单地检索。最后,联系人自身的叶节点将附加到路径中的最后一个节点,即街道编号的节点。

此时,如果您运行应用并转到地址树视图,您应该会看到联系人显示在树中。

使用聚合物组件

Polymer是一个流行的库,它严重偏向 web 组件。它的社区提供了广泛的组件,其中一个google-map元素封装了谷歌地图 API,以便以 HTML 声明方式显示地图。

Aurelia 提供了一个名为aurelia-polymer的集成库,允许在 Aurelia 应用中使用聚合物组件。在下一节中,我们将把它集成到联系人管理应用中。在 details 组件中,我们将显示一个显示联系人地址的小地图。

安装库

聚合物及其库通常使用Bower进行安装。Bower 和 NPM 可以并排使用,没有任何问题,因此,如果您的开发环境中还没有 Bower 和 NPM,那么我们首先通过打开控制台并运行以下命令来安装它:

> npm install -g bower

Bower 是 web 库的另一个包管理器,可以在上找到 https://bower.io/

完成后,让我们创建 Bower 的项目文件:

bower.json

{ 
  "name": "learning-aurelia", 
  "private": true, 
  "dependencies": { 
    "polymer": "Polymer/polymer#^1.2.0", 
    "google-map": "GoogleWebComponents/google-map#^1.1.13", 
    "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.20" 
  } 
} 

此文件与package.json非常相似。它描述了由 Bower 管理的项目的依赖关系。这里,我们包括聚合物和谷歌地图组件。

我们还包括了webcomponentjs,它是各种 web 组件 API 的 polyfill,例如定制元素 API 和 HTML 导入 API。由于这两个 API 是 Polymer 所必需的,因此如果您所针对的浏览器本机不支持这两个 API,则需要使用此 polyfill。

您可以在此处检查您喜爱的浏览器是否支持所需的 API:http://caniuse.com/#feat=custom-元素 SV1http://caniuse.com/#feat=imports

与 NPM 一样,必须安装项目文件中列出的包。因此,在项目目录中打开控制台并运行以下命令:

> bower install

完成此操作后,我们需要安装的最后一件事是 Polymer 和 Aurelia 之间的桥梁,通过在项目目录中打开控制台并运行以下命令来完成:

> npm install aurelia-polymer --save

配置应用

现在所有的东西都安装好了,我们需要配置我们的应用,以便它可以加载聚合物组件。

首先,我们将aurelia-polymer库添加到供应商包中:

aurelia_project/aurelia.json

//Omitted snippet... 
{ 
  "name": "vendor-bundle.js", 
  "prepend": [ 
    //Omitted snippet... 
  ], 
  "dependencies": [ 
    { 
      "name": "aurelia-polymer", 
      "path": "../node_modules/aurelia-polymer/dist/amd", 
      "main": "index" 
    }, 
    //Omitted snippet... 
  ] 
} 
//Omitted snippet... 

当然,由于这个库是一个 Aurelia 插件,我们需要将它加载到应用的主configure函数中:

src/main.js

//Omitted snippet... 
export function configure(aurelia) { 
  aurelia.use 
    .standardConfiguration() 
    .plugin('aurelia-polymer')  
    .plugin('aurelia-animator-css') 
  //Omitted snippet... 
} 

如前所述,Polymer 依赖于 HTML 导入。在撰写本文时,基于 CLI 的 Aurelia 应用不支持使用 HTML 导入加载视图。因此,我们将无法在模板中加载需要它们的组件。我们别无选择,只能将它们加载到index.html文件中:

index.html

<!-- Omitted snippet... --> 
<head> 
  <!-- Omitted snippet... --> 
  <script src="bower_components/webcomponentsjs/ 
               webcomponents-lite.js"></script> 
  <link rel="import" href="bower_components/polymer/polymer.html"> 
  <link rel="import"  
        href="bower_components/google-map/google-map.html"> 
</head> 
<!-- Omitted snippet... --> 

在这里,我们首先加载 Web 组件 API polyfill。如果您不需要 polyfill,可以删除此线。接下来,我们进口聚合物和google-map组件。

在准备生产的应用中,单独进口聚合物和每个组分是次优的。强烈建议将组件硫化成一束,可加载到index.html文件中:https://github.com/Polymer/vulcanize

此时,与聚合物的集成已启动并运行。google-map元件已准备好使用。

显示谷歌地图

首先,我们创建一个自定义元素,用于显示固定有单个地址的地图,以确保一切正常:

src/contacts/elements/address-map.html

<template> 
  <button class="btn btn-default"  
          click.delegate="isMapVisible = !isMapVisible"> 
    ${isMapVisible ? 'contacts.hideMap' : 'contacts.showMap' & t} 
  </button> 
  <google-map if.bind="isMapVisible"  
              style="display: block; height: 400px;"  
              api-key="your_key"> 
  </google-map> 
</template> 

google-map聚合物组件在幕后加载谷歌地图 API。为了正确加载,您需要一个 Google Maps API 密钥。您可以按照中的说明创建一个 https://developers.google.com/maps/documentation/javascript/get-api-key#key

在这里,我们首先添加一个按钮来切换isMapVisible属性的值。接下来,我们添加一个google-map聚合物元素。它的api-key属性应该设置为您自己的 Google Maps API 密钥。

至于视图模型,目前几乎为空:

src/contacts/elements/address-map.js

export class AddressMapCustomElement {  
  isMapVisible = false; 
} 

最后,我们需要将此address-map元素添加到触点的details组件中:

src/contacts/components/details.html

<!-- Omitted snippet... --> 
<div class="form-group" repeat.for="address of contact.addresses"> 
  <label class="col-sm-2 control-label"> 
    ${'contacts.types.' + address.type & t} 
  </label> 
  <div class="col-sm-10"> 
    <p class="form-control-static"> 
      ${address.number} ${address.street}</p> 
    <p class="form-control-static"> 
      ${address.postalCode} ${address.city}</p> 
    <p class="form-control-static"> 
      ${address.state} ${address.country}</p> 
    <address-map address.bind="address"></address-map> 
  </div> 
</div> 
<!-- Omitted snippet... --> 

此时,如果您运行应用并导航到联系人的详细信息,您应该会在每个地址下看到一个按钮。如果你点击它,地图就会出现。

地理编码地址

为了在地图上显示地址作为标记,我们需要获得地址的地理坐标。因此,我们将创建一个名为Geocoder的新服务,它将使用提名,这是一种基于OpenStreetMap数据(的搜索服务 http://www.openstreetmap.org/ ),查找给定地址的纬度和经度:

src/contacts/services/geocoder.js

import {HttpClient} from 'aurelia-fetch-client'; 

export class Geocoder { 

  http = new HttpClient().configure(config => { 
    config 
      .useStandardConfiguration() 
      .withBaseUrl('http://nominatim.openstreetmap.org/'); 
  }); 

  search(address) { 
    const query = { 
      format: 'json', 
      street: `${address.number} ${address.street}`, 
      city: address.city, 
      state: address.state, 
      country: address.country, 
      postalcode: address.postalCode, 
      limit: 1, 
    }; 
    return this.http.fetch(`search?${toQueryString(query)}`) 
      .then(response => response.json()) 
      .then(dto => dto.length === 0 ? null : dtoToResult(dto[0])); 
  } 
} 

function toQueryString(query) { 
  return Object.getOwnPropertyNames(query) 
    .map(name => { 
      const key = encodeURIComponent(name); 
      const value = encodeURIComponent(query[name]); 
      return `${key}=${value}`; 
    }) 
    .join('&'); 
} 

function dtoToResult(dto) { 
  return { 
    latitude: parseFloat(dto.lat), 
    longitude: parseFloat(dto.lon) 
  }; 
} 

这个类首先使用 Namingm 的 URL 和标准配置创建一个HttpClient实例。然后,它公开了一个search方法,该方法期望一个Address对象作为参数向 Namingm 端点发送请求并返回结果Promise。如果找不到地址,则使用null解析此Promise,或者使用包含匹配位置的latitudelongitude的对象解析此Promise

显示标记

现在我们可以对地址进行地理编码,让我们更新address-map元素以显示其标记:

src/contacts/elements/address-map.js

import {inject, bindable} from 'aurelia-framework'; 
import {Geocoder} from '../services/geocoder'; 

@inject(Geocoder) 
export class AddressMapCustomElement { 

  @bindable address; 

  isAttached = false; 
  isMapVisible = false; 
  isGeocoded = false; 
  latitude = null; 
  longitude = null; 

  constructor(geocoder) { 
    this.geocoder = geocoder; 
  } 

  addressChanged() { 
    if (this.isAttached) { 
      this.geocode(); 
    } 
  } 

  attached() { 
    this.isAttached = true; 
    this.geocode(); 
  } 

  detached() { 
    this.isAttached = false; 
  } 

  geocode() { 
    if (this.address) { 
      this.geocoder.search(this.address).then(position => { 
        if (position) { 
          this.latitude = position.latitude; 
          this.longitude = position.longitude; 
          this.isGeocoded = true; 
        } else { 
          this.isMapVisible = false; 
          this.isGeocoded = false;  
          this.latitude = null; 
          this.longitude = null; 
        } 
      }); 
    } 
  } 
} 

在这里,我们首先向视图模型中注入一个Geocoder实例。我们还添加了一个可绑定的address属性。当元素附加到 DOM 时,我们对地址进行地理编码,如果找到了它的坐标,我们将设置latitudelongitude属性的值。我们还将isGeocoded设置为true。该标志最初设置为false,如果地址无法本地化,将用于禁用切换按钮。如果找不到地址,我们隐藏地图,禁用切换按钮,并将latitudelongitude重置为null

在元素连接到 DOM 后,每当address发生变化时,我们都会对其进行地理编码,以使latitudelongitude属性保持最新。

至于模板,我们不需要做太多更改:

src/contacts/elements/address-map.html

<template> 
  <button class="btn btn-default"  
          click.delegate="isMapVisible = !isMapVisible"  
          disabled.bind="!isGeocoded"> 
    ${isMapVisible ? 'contacts.hideMap' : 'contacts.showMap' & t} 
  </button> 
  <google-map if.bind="isMapVisible"  
              latitude.bind="latitude"  
              longitude.bind="longitude"  
              zoom="15"  
              style="display: block; height: 400px;" 
             api-key="your_key"> 
    <google-map-marker latitude.bind="latitude"  
                       longitude.bind="longitude"  
                       open="true"> 
      ${address.number} ${address.street}  
      ${address.postalCode} ${address.city}  
      ${address.state} ${address.country} 
    </google-map-marker> 
  </google-map> 
</template> 

在这里,我们首先在isGeocodedfalse时禁用切换按钮。接下来,我们绑定google-map元素的latitudelongitude,并将其zoom设置为15,使其以地址的位置为中心。

最后,我们在google-map元素中添加一个google-map-marker元素。我们还将该标记的latitudelongitude绑定,并将其open属性设置为true,以便在渲染时打开其信息窗口。在标记中,我们将完整地址显示为文本,文本将在信息窗口中呈现。

你可能想知道这个google-map-marker元素来自哪里。事实上,HTML 导入机制允许从单个文件加载多个组件。当我们在index.html中导入bower_components/google-map/google-map.html文件时,许多组分注册到了 Polymer,其中包括 map 和标记。

如果此时运行应用,导航到联系人的详细信息,并单击地址的查看地图按钮,地图应在适当位置显示标记,并显示完整地址的信息窗口。

总结

将 UI 库集成到 Aurelia 应用中几乎总是遵循相同的过程:在其周围创建自定义元素或属性。通过利用 Aurelia 的双向数据绑定,大多数情况下并不太复杂。

对于遵循良好实践和社区标准的库尤其如此,例如支持公共模块加载器、公开数据更改事件以及在其公共 API 中使用析构函数。较旧的库或不遵循这些标准的库,集成起来可能会更痛苦。奥雷莉亚则尽其所能让事情变得简单。