教老狗HTML新技巧是当今现代JavaScript框架的主要重点。 通过遵循诸如WebComponents的潜在标准,创建自定义指令或通过扩展现有的类 ,您选择的框架很有可能提供扩展HTML标记本身的方法。 在上一篇由Brad Barrow撰写的文章中 ,向您介绍了一个新玩家: Aurelia 。 本文将以布拉德的文章和代码为基础,并向您展示如何通过遵循Aurelia的约定来创建自定义元素和自定义属性。
这篇文章的完整代码可以在我们的GitHub仓库中找到,您可以在这里看到我们要构建的演示的示例 (请花一些时间来初始化应用程序)。
为什么需要更多加价?
在直接进行操作之前,让我们首先了解创建新组件的潜在用例。 为此,我们将从概念上看下图所示的示例。 我们有两个页面,分别由ViewModel
(VM)和View
表示,分别显示有趣的图片和gif视频。 每一个都有一个重复的列表,列表本身会渲染包含图像和文本块的帖子。
Aurelia Reddit Client概念图
通过查看View,我们可以看到数据获取和渲染紧密地耦合在一个VM / View对中。
<template>
<ul class="list-group">
<li class="list-group-item" repeat.for="p of posts">
<img src.bind="p.data.thumbnail" />
<a href="http://reddit.com${p.data.permalink}">
${p.data.title}
</a>
</li>
</ul>
</template>
对于一个简单的示例来说,这可能不是问题,但是随着系统的发展和越来越多的需求的收集,这可能会成为一个主要的缺点。
使用自定义属性增强现有元素
想象一下,我们收到一个请求,要求为每个有趣的页面帖子提供一个弹出窗口。 为了做到这一点,我们可以通过将必要的data-
attribute属性随后在我们的FunnyVM
内部进行初始化,轻松地将Bootstrap的功能直接连接到标记中。 但是,如果我们突然也需要在另一页上呢? 通过声明自定义属性来提供功能可以使我们的生活更加轻松。 这些在以下情况下特别有用:
- 包装现有插件
- 常用绑定的快捷方式,例如样式或类
- 无需直接访问代码即可更改现有的HTML元素/自定义元素
现在,让我们动手做,看看如何构建我们的第一个自定义属性。
创建弹出框
让我们开始看看我们想要实现的目标。 新的属性popover
应该接受用于popover
的placement
, title
和content
的参数。 该位置固定在右侧,因此使用简单的字符串作为值就足够了。 对于其他两个属性,我们将使用Aurelia的数据绑定来映射迭代值。 为了加载文件,我们使用了Aurelia的require
功能。 from
属性包含要导入资源的相对路径。
<require from="./popover"></require>
...
<img src.bind="p.data.thumbnail"
popover="placement: 'right';
title.bind: p.data.url;
content.bind: p.data.title" />
为了实现这一点,我们首先在src
文件夹中创建一个名为popover.js
的新JavaScript文件。 与所有其他Aurelia构造一样,自定义属性是一个简单的导出ES6类,而不是传递到预定义API中的功能集合(就像许多旧框架一样)。
import {customAttribute, bindable, inject} from 'aurelia-framework';
import $ from 'bootstrap';
import bootstrap from 'bootstrap';
...
与其他框架相比,Aurelia通过metadata
描述结构来声明结构。 但是Aurelia不会使用静态函数或复杂的API,而是利用最先进的ES7装饰器来实现这一目标。 我们将从aurelia-framework
包中导入必要的装饰器。 至于控件本身,我们将使用Twitter Bootstrap提供的Popover JavaScript控件 。 因此,我们导入jQuery句柄$
和bootstrap
以初始化Bootstraps的JavaScript代码。
下一步是应用前面提到的元数据,以便Aurelia知道加载文件时会得到什么。 通过附加customAttribute
装饰器,我们使用给定值命名组件。 另一方面, bindable
装饰器声明了一个我们的View可以绑定到的属性。 我们只需为每个可用属性重复此装饰器。
@inject(Element)
@customAttribute('popover')
@bindable('title')
@bindable('content')
@bindable('placement')
export class Popover {
...
第一个inject
装饰器负责将实际的DOM元素作为参数提供给我们的构造方法,然后将其存储以备后用。
constructor(element) {
this.element = element;
}
现在,我们拥有所有必需的信息,可以通过声明一个名为bind
的方法来选择“ 行为”生命周期 。 这可以确保我们在适当的时间初始化组件,这与jQuery的ready方法类似。
bind() {
// initialize the popover
$(this.element).popover({
title: this.title,
placement: this.placement,
content: this.content,
trigger: 'hover' });
}
最后但并非最不重要的一点是,我们添加了更改的处理程序。 请注意,由于绑定源不会随时间变化,因此在我们的示例中实际上并未执行这些操作。
titleChanged(newValue){
$(this.element).data('bs.popover').options.title = newValue;
}
contentChanged(newValue){
$(this.element).data('bs.popover').options.content = newValue;
}
placementChanged(newValue){
$(this.element).data('bs.popover').options.placement = newValue;
}
在GitHub上查看完整文件
现在,我们已经看到了如何通过为现有元素提供属性来添加新功能,让我们继续并开始编写自己的自定义元素。
使用自定义元素创建新标签
为了创建全新的元素,Aurelia利用非常相似的方法来定制属性。 作为示例,我们将重建gif页面的帖子,以由名为reddit-gif
的自定义元素表示,并提供打开和关闭实际视频的可能性。 我们的视图的最终标记应为:
<require from="./reddit-gif"></require>
...
<ul class="list-group">
<li class="list-group-item" repeat.for="p of posts">
<reddit-gif data.bind="p.data"></reddit-gif>
</li>
</ul>
如您所见,我们使用新标记,并通过将数据绑定到data
属性来提供必要的信息。
下一步是创建实际元素。 为此,我们在src
文件夹中创建元素的视图reddit-gif.html
及其虚拟机reddit-gif.js
。 接下来看到的视图将利用gifs.html
的上一个标记,并添加一个按钮,该按钮可切换用于嵌入实际视频的iframe。 同样,Aurelia的视图被包装在模板标签中:
<template>
<button click.delegate="toggleGif()">Toggle Gif</button> <br />
<img src.bind="data.thumbnail == undefined ? '' : data.thumbnail" />
<a href="http://reddit.com${data.permalink}">
${data.title}
</a> <br />
<iframe class="reddit-gif" show.bind="gifActive" src.bind="gifSrc"></iframe>
</template>
查看VM部分,我们确实遵循与创建自定义属性时类似的过程。 但是这次我们利用了一个不同的装饰器,该装饰器将告诉Aurelia我们将创建一个只有一个名为data
属性的customElement
。
import {customElement, bindable} from 'aurelia-framework';
@customElement('reddit-gif')
@bindable('data')
export class RedditGif {
...
接下来,我们定义一个gifActive
成员,以跟踪是否应显示iframe。 最初,我们还将gifSrc
成员设置为空,以便在iframe不可见时不预加载任何内容。
constructor() {
this.gifActive = false;
}
bind() {
this.gifSrc = '';
}
最后但并非最不重要的一点是,我们添加了切换按钮使用的toggleGif
函数,该函数可翻转每个呼叫的可见性和来源。
toggleGif() {
if(this.gifActive) {
this.gifSrc = '';
} else {
this.gifSrc = this.data.url + '#embed';
}
this.gifActive = !this.gifActive;
}
通过约定减少代码量
Aurelia旨在使开发人员体验尽可能愉快。 让我们面对现实:我们很多人都不喜欢打字。 因此,为了节省您一些宝贵的击键并随着时间的推移改善维护,Aurelia使用了一组简单的约定。 例如, bindable
装饰器的完整版本实际上可能看起来像这样,我们仅通过提供属性名称来解决此问题。 所有其他选项将被自动推断。
@bindable({
name:'myProperty', //name of the property on the class
attribute:'my-property', //name of the attribute in HTML
changeHandler:'myPropertyChanged', //name of the method to invoke when the property changes
defaultBindingMode: ONE_WAY, //default binding mode used with the .bind command
defaultValue: undefined //default value of the property, if not bound or set in HTML
})
另一件事是如何缩短多个属性的使用。 因此,我们也可以告诉自定义属性期望动态属性,而不是一个一个地定义它们。 为此,我们使用dynamicOptions
装饰器装饰我们的类。 现在,我们仍然可以重用相同的视图标记,但是不必手动定义所有属性声明,顾名思义,这在动态上下文中非常有用。 这意味着我们可以编写一个称为dynamicPropertyChanged
通用更改处理程序,该更改处理程序在任何绑定属性发生更改时都会被调用。
import {customAttribute, dynamicOptions, inject} from 'aurelia-framework';
import $ from 'bootstrap';
import bootstrap from 'bootstrap';
@inject(Element)
@customAttribute('popover')
@dynamicOptions
export class Popover {
constructor(element) {
// store it for later use
this.element = element;
}
bind() {
$(this.element).popover({
title: this.title,
placement: this.placement,
content: this.content,
trigger: 'hover'
});
}
dynamicPropertyChanged(name, newValue, oldValue) {
$(this.element).data('bs.popover').options[name] = newValue;
}
}
但是自定义元素呢? 好吧,我们已经隐含地使用了一些约定,甚至没有意识到它。 系统会自动将具有相同名称的View和VM对组合在一起。 如果您需要使用其他视图,则可以使用装饰器@useView(relativePath)
。 或者也许通过声明@noView
根本不使用视图。 我们甚至可以发疯,通过添加装饰器useShadowDOM
,使我们的视图在ShadowDOM中呈现。 如果您不熟悉该术语,请查看这篇文章
结论
我们,Aurelia小组希望给您快速概述如何利用自定义元素和属性扩展HTML本身。 在整个示例中,我们希望您能够通过为您提供一个灵活但易于使用的框架来看到我们对开发者体验的关注,该框架不会妨碍您的使用或使您使用奇怪的API。 如有任何疑问,我们想邀请您加入我们的Gitter频道 。 当您编写第一个自定义元素和属性时,我们也希望听到您的经验。
From: https://www.sitepoint.com/extending-html-aurelia-io-way/