Add declarataive event handlers.

Now inside the <template> of a SkyElement you can use
on-eventName="method" on any element to add event listeners.

For example you can write <sky-button on-click="handleClick">
and then define handleClick(event) on the element class that
contains the button.

In adding this and tests I also realized that property bindings
were not setup on the initial call to bind(), which is now
fixed in this patch (See change to Node.prototype.bind).

R=eseidel@google.com, rafaelw@chromium.org

Review URL: https://codereview.chromium.org/812713005
diff --git a/sky/examples/widgets/index.sky b/sky/examples/widgets/index.sky
index 88e63ce..d102f85 100644
--- a/sky/examples/widgets/index.sky
+++ b/sky/examples/widgets/index.sky
@@ -16,8 +16,9 @@
   </style>
 
   <sky-box title='Buttons'>
-    <sky-button id='button'>Button</sky-button>
+    <sky-button id='button' on-click='handleClick'>Button</sky-button>
     <div>highlight: {{ myButton.highlight }}</div>
+    <div>clickCount: {{ clickCount }}</div>
   </sky-box>
 
   <sky-box title='Checkboxes'>
@@ -47,6 +48,10 @@
   attached() {
     this.myButton = this.shadowRoot.getElementById('button');
     this.myCheckbox = this.shadowRoot.getElementById('checkbox');
+    this.clickCount = 0;
+  }
+  handleClick(event) {
+    this.clickCount++;
   }
 }.register();
 </script>
diff --git a/sky/framework/sky-element/TemplateBinding.sky b/sky/framework/sky-element/TemplateBinding.sky
index 65d9bb5..1000094 100644
--- a/sky/framework/sky-element/TemplateBinding.sky
+++ b/sky/framework/sky-element/TemplateBinding.sky
@@ -14,7 +14,7 @@
     return;
   }
 
-  observable.open(function(value) {
+  this[name] = observable.open(function(value) {
     self[name] = value;
   });
 
@@ -544,6 +544,16 @@
   return parseMustaches(v == '' ? '{{}}' : v, name, el, prepareBindingFn);
 }
 
+function addEventHandler(element, name, method) {
+  element.addEventListener(name, function(event) {
+    var scope = element.ownerScope;
+    var host = scope.host;
+    var handler = host && host[method];
+    if (handler instanceof Function)
+      return handler.call(host, event);
+  });
+}
+
 function parseAttributeBindings(element, prepareBindingFn) {
   var bindings = [];
   var ifFound = false;
@@ -560,6 +570,13 @@
       continue;
     }
 
+    if (name.startsWith('on-')) {
+      if (!bindings.eventHandlers)
+        bindings.eventHandlers = new Map();
+      bindings.eventHandlers.set(name.substring(3), value);
+      continue;
+    }
+
     var tokens = parseMustaches(value, name, element,
                                 prepareBindingFn);
     if (!tokens)
@@ -618,6 +635,12 @@
       clone.setDelegate_(delegate);
   }
 
+  if (bindings.eventHandlers) {
+    bindings.eventHandlers.forEach(function(handler, eventName) {
+      addEventHandler(clone, eventName, handler);
+    });
+  }
+
   processBindings(clone, bindings, model, instanceBindings);
   return clone;
 }
diff --git a/sky/tests/framework/templates-expected.txt b/sky/tests/framework/templates-expected.txt
new file mode 100644
index 0000000..c79c875
--- /dev/null
+++ b/sky/tests/framework/templates-expected.txt
@@ -0,0 +1,7 @@
+Running 3 tests
+ok 1 SkyElement templates should stamp when the element is inserted
+ok 2 SkyElement templates should connect data binding
+ok 3 SkyElement templates should connect event handlers
+3 tests
+3 pass
+0 fail
diff --git a/sky/tests/framework/templates.sky b/sky/tests/framework/templates.sky
new file mode 100644
index 0000000..c0a9fa6
--- /dev/null
+++ b/sky/tests/framework/templates.sky
@@ -0,0 +1,54 @@
+<sky>
+<import src="/sky/tests//resources/chai.sky" />
+<import src="/sky/tests/resources/mocha.sky" />
+<import src="/sky/tests/resources/test-element.sky" as="TestElement" />
+
+<div id="sandbox"></div>
+
+<script>
+describe("SkyElement templates", function() {
+  var element;
+  var sandbox = document.getElementById("sandbox");
+
+  beforeEach(function() {
+    element = new TestElement();
+  });
+  afterEach(function() {
+    element.remove();
+  });
+
+  it("should stamp when the element is inserted", function() {
+    assert.isNull(element.shadowRoot);
+    sandbox.appendChild(element);
+    assert.instanceOf(element.shadowRoot, ShadowRoot);
+    assert.ok(element.shadowRoot.getElementById("inside"));
+  });
+
+  it("should connect data binding", function(done) {
+    sandbox.appendChild(element);
+    var inside = element.shadowRoot.getElementById("inside");
+    Promise.resolve().then(function() {
+      assert.equal(inside.textContent, 10);
+      assert.equal(inside.attr, 10);
+      element.value = 20;
+    }).then(function() {
+      assert.equal(inside.textContent, 20);
+      assert.equal(inside.attr, 20);
+      done();
+    }).catch(function(e) {
+      done(e);
+    });
+  });
+
+  it("should connect event handlers", function() {
+    sandbox.appendChild(element);
+    var inside = element.shadowRoot.getElementById("inside");
+    inside.dispatchEvent(new CustomEvent("wrong-event"));
+    assert.isNull(element.lastEvent);
+    var event = new CustomEvent("test-event");
+    inside.dispatchEvent(event);
+    assert.equal(element.lastEvent, event);
+  });
+});
+</script>
+</sky>
\ No newline at end of file
diff --git a/sky/tests/resources/test-element.sky b/sky/tests/resources/test-element.sky
new file mode 100644
index 0000000..c852cd1
--- /dev/null
+++ b/sky/tests/resources/test-element.sky
@@ -0,0 +1,23 @@
+<!--
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+-->
+<import src="/sky/framework/sky-element/sky-element.sky" as="SkyElement" />
+
+<sky-element name="test-element">
+<template>
+  <div id="inside" on-test-event="handleEvent" attr="{{ value }}">{{ value }}</div>
+</template>
+<script>
+module.exports = class extends SkyElement {
+  created() {
+    this.lastEvent = null;
+    this.value = 10;
+  }
+  handleEvent(event) {
+    this.lastEvent = event;
+  }
+}.register();
+</script>
+</sky-element>