Writing Your Own Widget(自定义组件)
英文地址:http://dojotoolkit.org/reference-guide/1.10/quickstart/writingWidgets.html#quickstart-writingwidgets
从头单独开发一个控件十分困难,我们提供有一些组件,你如果想修改他们或者扩展一个自己的组件,这是毫无问题的。
Dijit的组件具有可扩展性,修改组件时你没有必要熟读源代码。很多时候,你只需要通过设定组件的属性达到自己想要的目标,例如:滑块儿长度默认为0-100,你可以设定属性0-200达到自己想要的结果。当然,有些情况你需要进一步扩展,或许在触发点击事件是你希望有不同的处理,或者替换原有的校验步骤,这种类型的修改,你可以用dojo.declare()方法继承原有组件,重写里面的方法。
你也可以重新写一个组件,你可以用djijit.Declaration的data-dojo-type属性或者dojo.declare()实现这样的效果。
从一个简单的例子开始
我们从头开始看如何创建一个组件。从Dojo的设计思想上讲,一个组件就是一个“JavaScript类”,这个类的构造函数有params和srcNodeRef(这个组件所依附的DOM节点)。
constructor: function(params, srcNodeRef){ console.log("creating widget with params " + dojo.toJson(params) + " on node " + srcNodeRef); }
Dojo和Dijit中所有的组件的编译都是在基类(dijit._WidgetBase)完成。
最简单的你可以创建一个带有一定行为的组件,这个组件可以通过srcNodeRef接收DOM树,然后完成一些操作,而不是通过JS创建一个DOM树。
例1:一个简单的组件
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="../../dijit/themes/claro/claro.css"> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../../dojo/dojo.js'></script> <script> require([ "dojo/_base/declare", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", ], function (declare, parser, ready, _WidgetBase) { declare("MyFirstBehavioralWidget", [_WidgetBase], { // put methods, , etc. here constructor: function (params, srcNodeRef) { console.log("creating widget with params " + dojo.toJson(params) + " on node " + srcNodeRef); } }); ready(function () { // Call the parser so it runs after our widget is , and page has finished loading parser.parse(); }); }); </script> </head> <body class="claro"> <span data-dojo-type="MyFirstBehavioralWidget">hi</span> </body> </html>
组件的定义:
require([ "dojo/_base/declare", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", ], function(declare, parser, ready, _WidgetBase){ declare("MyFirstBehavioralWidget", [_WidgetBase], { // put methods, attributes, etc. here }); ready(function(){ // Call the parser manually so it runs after our widget is defined, and page has finished loading parser.parse(); }); });
组件的声明:
<span data-dojo-type="MyFirstBehavioralWidget">hi</span>
这只是通过HTML标签创建一个MyFirstBehavioralWidget对象。你可以写一个postCreate()方法用这个DOM节点做一些事件处理等。
这种创建方式在有些情况下很有用,但是在另外一些情况下有很大的局限性,换而言之,它必须提供DOM树。通常组件可以自己创建DOM树,替换<span>和<button>与一个复杂的DOM树。注意,有些时候用户创建组件时,可能没有一个DOM节点被替换。
下面这个例子就是一个组件创建自己的DOM树。
例2:通过对象生成DOM树
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="../../dijit/themes/claro/claro.css"> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../../dojo/dojo.js'></script> <script> // the parser is only needed, if you want // to instantiate the widget (in markup) require([ "dojo/_base/declare", "dojo/dom-construct", "dojo/ready", "dojo/_base/window", "dijit/_WidgetBase", ], function (declare, domConstruct, ready, win, _WidgetBase) { declare("MyFirstWidget", [_WidgetBase], { buildRendering: function () { // create the DOM for this widget this.domNode = domConstruct.create("button", {innerHTML: "push me"}); } }); ready(function () { // Create the widget programmatically and place in DOM (new MyFirstWidget()).placeAt(win.body()); }); }); </script> </head> <body class="claro"> </body> </html>
通过JS定义并实例化这个组件。注意并未调用parse(),因为它只有通过标记方式写的时候才需要调用。
// the parser is only needed, if you want // to instantiate the widget declaratively (in markup) require([ "dojo/_base/declare", "dojo/dom-construct", "dojo/ready", "dojo/_base/window", "dijit/_WidgetBase", ], function(declare, domConstruct, ready, win, _WidgetBase){ declare("MyFirstWidget", [_WidgetBase], { buildRendering: function(){ // create the DOM for this widget this.domNode = domConstruct.create("button", {innerHTML: "push me"}); } }); ready(function(){ // Create the widget programmatically and place in DOM (new MyFirstWidget()).placeAt(win.body()); }); });
这个组件做的事情并不多,但是它完成了(无行为)组件最基本的功能:创建一个DOM树。
现在我们写一个可以执行JavaScript的组件。当点击组件的时候,我们会计算点击的次数:
例3:计算点击次数
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="../../dijit/themes/claro/claro.css"> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../../dojo/dojo.js'></script> <script> require([ "dojo/_base/declare", "dojo/dom-construct", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", ], function (declare, domConstruct, parser, ready, _WidgetBase) { declare("Counter", [_WidgetBase], { // counter _i: 0, // step:1 buildRendering: function () { // create the DOM for this widget this.domNode = domConstruct.create("button", {innerHTML: this._i}); }, // step:2 postCreate: function () { // every time the user clicks the button, increment the counter this.connect(this.domNode, "onclick", "increment"); }, increment: function () { this.domNode.innerHTML = ++this._i; } }); ready(function () { // Call the parser so it runs after our widget is , and page has finished loading parser.parse(); }); }); </script> </head> <body class="claro"> <span data-dojo-type="Counter"></span> </body> </html>
定义这个组件:
require([ "dojo/_base/declare", "dojo/dom-construct", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", ], function(declare, domConstruct, parser, ready,_WidgetBase){ declare("Counter", [_WidgetBase], { // counter _i: 0, buildRendering: function(){ // create the DOM for this widget this.domNode = domConstruct.create("button", {innerHTML: this._i}); }, postCreate: function(){ // every time the user clicks the button, increment the counter this.connect(this.domNode, "onclick", "increment"); }, increment: function(){ this.domNode.innerHTML = ++this._i; } }); ready(function(){ // Call the parser manually so it runs after our widget is defined, and page has finished loading parser.parse(); }); });
实例化组件
<span data-dojo-type="Counter"></span>
postCreate() 方法在buildRendering() 方法执行完成后被调用。通过在postCreate()里面会执行connect等只有DOM节点创建后方可执行的方法。
模板组件
好了,我们已经知道如何通过继承dijit._WidgetBase类创建一个组件。下面我们就换一种方式来实现同样的功能,模板方式创建组件虽然不经常用,但是它对于哪些复杂结构的DOM树而言是十分好的。有一个叫dijit._TemplatedMixin的类可以让这一切变得十分容易,它提供buildRendering()方法,你需要做的只是为这个组件指定一个HTML片段作为模板。我们用这种方式来实现点击计数功能。首先我们得为这个组件写一个HTML模板,点击次数会显示在按钮上。然后通过HTML方式创建这个计数器。
组件创建方式:
<div data-dojo-type="FancyCounter" data-dojo-props="label:'counter label'">button label</div>
模板:
<div> <button>press me</button> count: <span>0</span> </div>
注意:模板必须只能有一个顶级节点。
接下来我们为_TemplatedMixin添加一些处理:
<div> <button data-dojo-attach-event='onclick: increment'>press me</button> count: <span data-dojo-attach-point='counter'>0</span>" </div>
data-dojo-attach-point 和data-dojo-attach-event在dijit._TemplatedMixin 中又详细的说明。注意:data-dojo-attach-event为这个节点设置监听事件data-dojo-attach-point 设定附着点(说白了,也就是为了方便在代码中访问到这个DOM节点)。
例4:通过模板方式创建计数器
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="../../dijit/themes/claro/claro.css"> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../../dojo/dojo.js'></script> <script> require([ "dojo/_base/declare", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function (declare, parser, ready, _WidgetBase, _TemplatedMixin) { declare("FancyCounter", [_WidgetBase, _TemplatedMixin], { // counter _i: 0, templateString: "<div>" + "<button data-dojo-attach-event='onclick: increment'>press me</button>" + " count: <span data-dojo-attach-point='counter'>0</span>" + "</div>", increment: function () { this.counter.innerHTML = ++this._i; } }); ready(function () { // Call the parser manually so it runs after our widget is defined, and page has finished loading parser.parse(); }); }); </script> </head> <body class="claro"> <span data-dojo-type="FancyCounter">press me</span> </body> </html>
属性
所有的组件都有属性,属性可以在组件创建时设定,或者在组件使用过程中进行使用,就像DOM节点的组件一样。最大的区别在于你只有在组件创建后才能调用组件的set()和get()方法。
但是组件开发者如何为自己的组件添加属性,如何捕获属性值得变动呢?
属性声明
作为一个组件开发者,你需要在组件的原型(prototype)中声明所有属性,并设定值。这个值在需要设定默认值(如果实例化时没有传送此属性值就采用默认值),并告诉解析器数据类型。我们可以通过如下方式定义属性:
// label: String // Button label label: "push me" // duration: Integer // Milliseconds to fade in/out duration: 100 // open: Boolean // Whether pane is visible or hidden open: true
注意:所有属性的注释需要说明属性的定义规则,甚至你需要说明如何通过set()设定该属性的值,例如:
// value: Date // The date picked on the date picker, as a Date Object. // When setting the date on initialization (ex: new DateTextBox({value: "2008-1-1"}) // or changing it (ex: set('value', "2008-1-1")), you can specify either a Date object or // a string in ISO format value: new Date()
将组件属性与DOM节点属性进行映射
DOJO组件的属性通常都会被映射到DOM的属性。例如:DOM的tabIndex属性会被映射到Dojo组件的focusNode属性上。
这个过程不是通过在组件模板中用${…}方式设定的,实际上,这个过程基本上都是自动完成的。标准的DOM节点如果设定有属性属性,例如tabIndex,alt,aria-labelledby等,他就会被复制到Dojo组件对应的属性上(值复制Dojo组件声明的属性,未声明的属性还是存留在DOM节点上)。
你也可以明确指定DOM节点的属性、innerHTML、class与Dojo组件的映射关系,以覆盖Dojo组件的一些行为。这允许更多复杂情况的属性映射,就像当一个TitlePane有一个“title”参数,他就会被映射到TitlePane.titleNode DOM节点的innerHTML(这个titleNode通过dojo-attach-poin方式定义的,如下所示)。
上面的描述可能比较混乱,但是通过这个例子可以得到更好的理解:
这儿有一个简单的组件用来展示一个上午名片,这个组件有三个属性:
- name
- phone number
- CSS class name to apply to name
每一个属性都有一个对应的_setXXXAttr用来声明它与模板之间的关系:
<!DOCTYPE html> <html > <head> <link rel="stylesheet" href="../_static/js/dojo/../dijit/themes/claro/claro.css"> <style type="text/css"> .businessCard { border: 3px inset gray; margin: 1em; } .employeeName { color: blue; } .specialEmployeeName { color: red; } </style> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../_static/js/dojo/dojo.js'></script> <script> require([ "dojo/_base/declare", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function(declare, parser, ready, _WidgetBase, _TemplatedMixin){ declare("BusinessCard", [_WidgetBase, _TemplatedMixin], { templateString: "<div class='businessCard'>" + "<div>Name: <span data-dojo-attach-point='nameNode'></span></div>" + "<div>Phone #: <span data-dojo-attach-point='phoneNode'></span></div>" + "</div>", // Attributes name: "unknown", _setNameAttr: { node: "nameNode", type: "innerHTML" }, nameClass: "employeeName", _setNameClassAttr: { node: "nameNode", type: "class" }, phone: "unknown", _setPhoneAttr: { node: "phoneNode", type: "innerHTML" } }); ready(function(){ // Call the parser manually so it runs after our widget is defined, and page has finished loading parser.parse(); }); }); </script> </head> <body class="claro"> <span data-dojo-type="BusinessCard" data-dojo-props="name:'John Smith', phone:'(800) 555-1212'"></span> <span data-dojo-type="BusinessCard" data-dojo-props="name:'Jack Bauer', nameClass:'specialEmployeeName', phone:'(800) CALL-CTU'"></span> </body> </html>
你可以通过如下方式将组件的属性映射到DOM节点的属性:
_setDisabledAttr: {node: "focusNode", type: "attribute" }
或者一个一个映射:
_setDisabledAttr: "focusNode"
下面是一个更复杂的例子,用来将一个叫“img”的属性映射到this.imageNode.src:
_setImgAttr: {node: "imageNode", type: "attribute", attribute: "src" }
自定义setters/getters
当你有一个属性进行设置时比上面的简单映射复杂时,你需要为这个属性自定义getters/setters方法。正如下面所示:你需要遵守方法的命名规范,例如:一个名字为foo的属性,你要实现_setFooAttr()和_getFooAttr()方法。这两个方法会自动被调用。
下面是一个例子,一个组件有一个“open”属性用来控制组件的显示或者隐藏:
<!DOCTYPE html> <html > <head> <link rel="stylesheet" href="../_static/js/dojo/../dijit/themes/claro/claro.css"> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../_static/js/dojo/dojo.js'></script> <script> require([ "dojo/_base/declare", "dojo/dom-style", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function(declare, domStyle, parser, ready, _WidgetBase, _TemplatedMixin){ declare("HidePane", [_WidgetBase], { // parameters open: true, _setOpenAttr: function(/*Boolean*/ open){ this._set("open", open); domStyle.set(this.domNode, "display", open ? "block" : "none"); } }); ready(function(){ // Call the parser manually so it runs after our widget is defined, and page has finished loading parser.parse(); }); }); </script> </head> <body class="claro"> <span data-dojo-type="HidePane" data-dojo-props="open:false" data-dojo-id="pane">This pane is initially hidden</span> <button onclick="pane.set('open', true);">show</button> <button onclick="pane.set('open', false);">hide</button> </body> </html>
自定义的setters是十分常见的。通常情况,你不需要为一个属性添加getter(例如:访问Widget.foo属性默认就是通过get(‘foo’)获取),但是有些情况,例如,处理一个编辑器的值,为了保持编辑器的值最新,你最好还是需要自定义一个_getValueAttr() 。
注意:上面的例子中使用this._set(“open”, open)更新属性值,它会自动触发所有通过watch()方法注册的句柄来监听值得变化。
生命周期
上面为属性自定义的setters(通过setXXXAttr或者直接映射方式)在组件创建的时候就会被应用到组件上面。
另外自定义的setters在用户调用set(‘name’,value)的时候也会被调用。
注意,setters过程的调用发生在postCreate()之前,buildRendering()后。所以你要确保自定义的setters方法中的代码不能依赖在postCreate()方法内或者此方法后执行的操作。
但是有一些情况,例如_setDisabledAttr() 方法通常希望在组件执行startup()的时候被调用,这就和Dojo的setters执行的时机不相符,所以需要有一些特别处理的代码来处理这类情况。
例如:
dojo.declare("my.Thinger", _WidgetBase, { value:9, buildRendering: function(){ this.inherited(arguments); this.multiplier = 3; }, _setValueAttr: function(value){ this.value = value * this.multiplier; } });
如果在postCreate()中定义multiplier变量,那么在执行set()的时候就会失败。
容器(Container)
通常一个组件在使用的时候都会填充一些内容。例如:
<button data-dojo-type="dijit/form/Button">press me</button>
一般情况下,non-behavioral组件(类似于上面的例子,只是将新建一个DOM树替换上面的<button>节点)需要将<button>的内部DOM树拷贝到组件的新的DOM树里面。
完成拷贝过程的附着点被称作containerNode。换而言之,你可以通过myButton.containerNode.innerHTML访问节点的innerHTML,即为press me。
对于一个组件,只要你在模板中声明data-dojo-attach-point=”containerNode”,混入类(_TemplatedMixin)会自动完成这个过程。
讲了这么多,下面我们就利用templateString属性指定模板方式定义一个组件(注意,通常情况下模板的定义会放到一个单独的文件中,我们可以通过dojo.text!获取模板的内容)。
如下代码所示:
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="../dijit/themes/claro/claro.css"> <script>dojoConfig = {parseOnLoad: false}</script> <script src='../dojo/dojo.js'></script> <script> require([ "dojo/_base/declare", "dojo/parser", "dojo/ready", "dijit/_WidgetBase", "dijit/_TemplatedMixin" ], function (declare, parser, ready, _WidgetBase, _TemplatedMixin) { declare("MyButton", [_WidgetBase, _TemplatedMixin], { // 只是定义了一个containerNode的附着点,并未显式的将innerHTML复制到模板的innerHTML中 // 具体实现可以参考_TemplateMixin.js中的_fillContent方法 templateString: "<button data-dojo-attach-point='containerNode'></button>" }); ready(function () { // Call the parser manually so it runs after our widget is defined, and page has finished loading parser.parse(); }); }); </script> </head> <body class="claro"> <button data-dojo-type="MyButton">press me</button> </body> </html>
创建扩展的附着点(Creating extension points)
比方说,你已经写了一个组件,你希望点击这个组件的时候可以进行一些操作。你希望开发人员在不修改你组件代码的情况下可以自定义组件的事件,或者添加一些额外的处理。
来看一下如何实现这样的情况,我们先看一下dijit/form/Button是如何实现它的点击事件的。注意,我们需要区分DOM事件(DOM节点上的事件)和组件事件(发生在组件的事件,组件就是DOM树的结合体)。DOM节点的点击事件可能会触发组件的点击事件,但是你只希望你的组件在可用情况下(“enabled” state)可以触发点击事件。
- 在你的HTML模板中,你想为HTML元素添加DOM事件,你可以像下面这样为元素添加data-dojo-attach-event。参看下面dijit包中Button的处理(请参看Button.html文件)
<div class="dijit dijitReset dijitLeft dijitInline" data-dojo-attach-event="ondijitclick:_onButtonClick,onmouseenter:_onMouse,onmouseleave:_onMouse,onmousedown:_onMouse" ...
onclick是指这个DIV的DOM事件。_onButtonClick是Button.js中定义的普通的JS方法。通过这个普通的JS方法(并非DOM原生的事件处理方法),为开发人员提供事件处理的接口。
- 自定义组件的JS文件中,需要定义并实现data-dojo-attach-event中声明的JS方法,并作为一个扩展的附着点让开发人员覆盖。
在Button.js中你可以找到如下代码:
// 译者注:在1.10版本中已经不使用这个方法,通过lang、focurs等类进行进一步的封装
_onButtonClick: function( /*Event*/ e){ ... // Trust me, _onClick calls this._onClick }, _onClick: function( /*Event*/ e){ ... return this.onClick(e); }, onClick: { // nothing here: the extension point! }
它的运行机制是:第一个方法中(_onButtonClick),组件的所有onClick事件都会指向Button组件自定义的_onButtonClick方法,这个方法又会转向调用_onClick方法,这个方法会进行一些组件相关处理,然后会触发onClick方法,这个方法开发人员可以进行重写。
- _onButtonClick:方法内的处理总会执行,并且会触发_onClick();
- _onClick :方法内的处理总是会执行,并且会触发onClick();
- onClick :方法是一个空方法,不进行任何处理,供开发人员自己实现。
这样处理的原因:这样设计的原理在于将组件特有的一些处理进行封装,让开发人员专注于组件针对的业务处理。
第三个方法(onClick),为开发人员提供了在不改变组件原有代码的基础上进行事件的自定义。
-
在有些HTML页面中,开发人员会这样写一个Button:
<button data-dojo-type="dijit/form/Button" onClick="alert('Woohoo! I'm using the extension point "onClick"!!');">press me</button>
或者下面的方式:
<div data-dojo-type="dijit/form/Button"> <script type="dojo/method" data-dojo-event="onClick" data-dojo-args="evt"> alert('Woohoo! I'm using the extension point "onClick"!!'); </script> press me </div>
这样的话,每当用户点击这个组件的时候(这里说的是最顶级的DIV元素),_onButtonClick 和_onClick 就会执行,并且会弹出一个对话框,实现了为Button组件添加自定义的处理。
-
如果你不想重写组件扩展的onClick方法,但仍然想自定义一些处理,你也可以通过使用type=”dojo/connect”替换type=”dojo/method”来实现。
总结:
- data-dojo-event属性中命名的事件不是DOM原生的事件(就像data-dojo-event=”onClick”这样),要记住:onClick 只是一个普通的JS方法。(Dojo在这里做了一个详细的说明)
- 开发人员如何辨别哪些事件可以重写或者进行绑定呢?这是非常头痛的。第一:你必须查看这个组件以及它父类的内部实现,或者父类的父类。。。这一点都不好用;第二:dojo命名有一些不规范,“_”有些时候表示私有的方法,有些时候又表示受保护的方法(TODO:移动到一个单独的页面)。
非常有用的self-scoping功能
有两套可用的功能提供给所有的组件,用来简化组件之间的关联:
- connect/disconnect
- New in 1.4 subscribe/unsubscribe
这两个功能处理的机制类似。但是有两点需要注意:1、连接的目标方法只能在目标组件内部执行,访问的数据范围只能是组件内部数据;2、这些connections/subscriptions 会在组件生命周期的destroy()阶段会被清理。
可访问性(Accessibility)
下面这些连接描述了如何针对智障人士开发这些组件:
变化模板引擎(Alternate Templating Engines)
Dojo为组件设计有备用的模板语法,可以让你在模板HTML中进行编写条件语句或者其他有特色的处理。
- DTL:DTL (currently the top page from the above link has been copied to)
- Mustache:只是一个备用方案
常见问题
为组件模板添加附加功能时遇到问题时,要首先确保在引入依赖的对象列表时的所有依赖混入类(_TemplatedMixin, _WidgetsInTemplate)要在 _WidgetBase之后引入,这个确保了混入类的混入方法继承自_WidgetBase提供的方法,而不是其他的。
例如下面这个例子:
require(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin"], function(declare, _WidgetBase, _TemplatedMixin){ declare("SomeModule", [_TemplatedMixin, _WidgetBase], { }); });
应该修改成:
require(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin"], function(declare, _WidgetBase, _TemplatedMixin){ declare("SomeModule", [_WidgetBase, _TemplatedMixin], { }); });
参考文献:
- Declaring a widget in markup
- Widgets in templates are discussed on the dijit._WidgetsInTemplateMixin page
- Example: File Upload Dialog Box
- Dropdowns and Popups
参考资料:
Dojo 1.6 官方教程翻译:创建基于模板的小部件(Widget) :http://blog.csdn.net/dojotoolkit/article/details/6583515
能力有限,如果有纰漏之处,请指点。谢谢。