├── protractor.conf.js ├── .gitignore ├── bower.json ├── src ├── modules │ └── ce-bind.module.js └── directives │ ├── ce-one-way │ ├── test │ │ ├── elements │ │ │ ├── my-input.html │ │ │ ├── my-person.html │ │ │ └── my-list.html │ │ ├── object-spec.js │ │ ├── string-spec.js │ │ ├── string-spec.html │ │ ├── object-spec.html │ │ ├── array-spec.html │ │ ├── array-spec.js │ │ ├── controller-alias-spec.js │ │ └── controller-alias-spec.html │ └── ce-one-way.directive.js │ └── ce-interpolated │ ├── test │ ├── elements │ │ ├── my-input.html │ │ ├── friend-name.html │ │ ├── my-person.html │ │ └── my-list.html │ ├── ng-if-spec.js │ ├── object-spec.js │ ├── string-spec.js │ ├── ng-if-spec.html │ ├── object-spec.html │ ├── string-spec.html │ ├── array-spec.html │ ├── array-spec.js │ ├── multiple-instances-spec.js │ ├── multiple-instances-spec.html │ ├── ng-repeat-spec.html │ └── ng-repeat-spec.js │ └── ce-interpolated.directive.js ├── package.json ├── .travis.yml ├── dist ├── ce-bind.min.js └── ce-bind.js ├── gulpfile.js └── README.md /protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = {}; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | npm-debug.log -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-custom-elements", 3 | "description": "Angular 1.x directive to hold all yr Custom Element bindings together 😁", 4 | "private": true, 5 | "main": "ce-bind.module.js", 6 | "authors": [ 7 | "Rob Dodson " 8 | ], 9 | "license": "Apache-2.0", 10 | "homepage": "https://github.com/robdodson/angular-custom-elements", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "devDependencies": { 19 | "angular": "^1.5.8", 20 | "polymer": "Polymer/polymer#^1.6.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/ce-bind.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | angular.module('robdodson.ce-bind', []); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-custom-elements", 3 | "version": "2.1.0", 4 | "description": "Angular 1.x directive to hold all yr Custom Element bindings together 😁", 5 | "main": "dist/ce-bind.js", 6 | "scripts": { 7 | "test": "gulp" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/robdodson/angular-custom-elements.git" 12 | }, 13 | "author": "", 14 | "license": "Apache-2.0", 15 | "bugs": { 16 | "url": "https://github.com/robdodson/angular-custom-elements/issues" 17 | }, 18 | "homepage": "https://github.com/robdodson/angular-custom-elements#readme", 19 | "devDependencies": { 20 | "del": "^2.2.1", 21 | "eslint": "^3.1.1", 22 | "gulp": "^3.9.1", 23 | "gulp-concat": "^2.6.0", 24 | "gulp-connect": "^5.0.0", 25 | "gulp-protractor": "^3.0.0", 26 | "gulp-rename": "^1.2.2", 27 | "gulp-uglify": "^2.0.0", 28 | "gulp-umd": "^0.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | dist: trusty 4 | addons: 5 | apt: 6 | sources: 7 | - google-chrome 8 | packages: 9 | - google-chrome-stable 10 | node_js: 11 | - '6' 12 | - '4' 13 | before_script: 14 | - npm install -g bower 15 | - bower install 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | - sleep 5 19 | script: 20 | - npm test 21 | deploy: 22 | provider: npm 23 | email: "lets.email.rob@gmail.com" 24 | on: 25 | tags: true 26 | api_key: 27 | secure: H1MS92RKEv67t+RvxtT/3YQ80XV0ySNysGn7i1jqmz0hUX6v3Pgc0+tvoOIgN97SeC8BNQi/jQYyiIuLaKhqeyEqXL/Tvk/MSXxd4qNHDAHJ30wtxgWkfKWDXHIp20h6sEzo5FyAMW6RVceI8Jmi2hjz2lKLOtTTNxppYwuNQMUFhEmNQhXXORp0cLt1Mdc8USB48crSkkHk4RZR4o+hIvuqZoAG0/j8nOzum+Sd/6bVbZwJgLKZGuapi21LhC7W3Lcit4zrrWlB1aXG7c54RvDi1X2n+0J59gU7gv0olBO11ig9guZIPDT/8ZxTq4yYXact+zI8rMFRavlEY5o1aBrAGrfyrpYuMKv1AOuwGP5SjHYRHBhBX92Y5aAOb24SVHlaqgjwpocGqeKiInwSeJ4WKCsEB/gp3PiGJMYKIMYdNcq5KzzPQ14DaKIMXBX2WVrWNGEwMa4UjOH9qu8yD/cFNcCeJ34nY1YDx4lf9+9BkNtbEyziUrSXZvgNfKBgPUWwVvR+wWp/LBbO0O4DYYdbRxoPPVofE5YeV4Alqtk3msuUrx400p3jbad0I9THnM8QfsgEKUtElesS+jTKXkZXiRmA05QODDDBK46fGTkKTKBA9lzdDq2oslnhwev/RWJwM+Y96gZPiJGpmhe/vyRElES2Bmr5LeWSdezi/n0= 28 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/elements/my-input.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 34 | 35 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/elements/my-input.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 35 | 36 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/elements/friend-name.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 22 | 36 | 37 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/elements/my-person.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 37 | 38 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/elements/my-person.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 38 | 39 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/elements/my-list.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 21 | 39 | 40 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/elements/my-list.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 21 | 40 | 41 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/ng-if-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('interpolated, ng-if', function() { 19 | it('should receive binding as ng-if is toggled', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-interpolated/test/ng-if-spec.html'); 21 | 22 | var ngString = element(by.id('ng-string')); 23 | var wcString = element(by.id('wc-string')); 24 | var ngCheckbox = element(by.id('ng-checkbox')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngCheckbox.click(); 28 | expect(ngString.getText()).toEqual('Hello, from Angular!'); 29 | expect(wcString.getText()).toEqual('Hello, from Angular!'); 30 | 31 | wcButton.click(); 32 | expect(ngString.getText()).toEqual('String changed in Polymer'); 33 | expect(wcString.getText()).toEqual('String changed in Polymer'); 34 | }); 35 | }); -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/object-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('one-way, objects', function() { 19 | it('should have the same sub-property', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/object-spec.html'); 21 | 22 | var ngObject; 23 | var wcObject; 24 | var ngButton = element(by.id('ng-button')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngObject = element(by.id('ng-object')); 28 | wcObject = element(by.id('wc-object')); 29 | expect(ngObject.getText()).toEqual('Lisa'); 30 | expect(wcObject.getText()).toEqual('Lisa'); 31 | 32 | ngButton.click(); 33 | expect(ngObject.getText()).toEqual('Joe'); 34 | expect(wcObject.getText()).toEqual('Joe'); 35 | 36 | wcButton.click(); 37 | expect(ngObject.getText()).toEqual('Alex'); 38 | expect(wcObject.getText()).toEqual('Alex'); 39 | }); 40 | }); -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/object-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('interpolated, objects', function() { 19 | it('should have the same sub-property', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-interpolated/test/object-spec.html'); 21 | 22 | var ngObject; 23 | var wcObject; 24 | var ngButton = element(by.id('ng-button')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngObject = element(by.id('ng-object')); 28 | wcObject = element(by.id('wc-object')); 29 | expect(ngObject.getText()).toEqual('Lisa'); 30 | expect(wcObject.getText()).toEqual('Lisa'); 31 | 32 | ngButton.click(); 33 | expect(ngObject.getText()).toEqual('Joe'); 34 | expect(wcObject.getText()).toEqual('Joe'); 35 | 36 | wcButton.click(); 37 | expect(ngObject.getText()).toEqual('Alex'); 38 | expect(wcObject.getText()).toEqual('Alex'); 39 | }); 40 | }); -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/string-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('one-way, strings', function() { 19 | it('should have the same string', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/string-spec.html'); 21 | 22 | var ngString; 23 | var wcString; 24 | var ngButton = element(by.id('ng-button')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngString = element(by.id('ng-string')); 28 | wcString = element(by.id('wc-string')); 29 | expect(ngString.getText()).toEqual('Hello, from Angular!'); 30 | expect(wcString.getText()).toEqual('Hello, from Angular!'); 31 | 32 | ngButton.click(); 33 | expect(ngString.getText()).toEqual('String changed in Angular'); 34 | expect(wcString.getText()).toEqual('String changed in Angular'); 35 | 36 | wcButton.click(); 37 | expect(ngString.getText()).toEqual('String changed in Polymer'); 38 | expect(wcString.getText()).toEqual('String changed in Polymer'); 39 | }); 40 | }); -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/string-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('interpolated, strings', function() { 19 | it('should have the same string', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-interpolated/test/string-spec.html'); 21 | 22 | var ngString; 23 | var wcString; 24 | var ngButton = element(by.id('ng-button')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngString = element(by.id('ng-string')); 28 | wcString = element(by.id('wc-string')); 29 | expect(ngString.getText()).toEqual('Hello, from Angular!'); 30 | expect(wcString.getText()).toEqual('Hello, from Angular!'); 31 | 32 | ngButton.click(); 33 | expect(ngString.getText()).toEqual('String changed in Angular'); 34 | expect(wcString.getText()).toEqual('String changed in Angular'); 35 | 36 | wcButton.click(); 37 | expect(ngString.getText()).toEqual('String changed in Polymer'); 38 | expect(wcString.getText()).toEqual('String changed in Polymer'); 39 | }); 40 | }); -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/ng-if-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | Toggle message: 29 | 30 |
{{main.greeting}}
31 | 32 |
33 | 34 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/object-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

Angular object is: {{main.person.name}}

29 | 30 | 31 |
32 | 33 | 43 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/string-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | Angular string is: {{main.greeting}} 29 | 30 | 31 |
32 | 33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/array-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |

Angular array is:

30 |
31 | {{n}} 32 |
33 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /dist/ce-bind.min.js: -------------------------------------------------------------------------------- 1 | !function(n,e){"function"==typeof define&&define.amd?define(["angular"],e):"object"==typeof exports?module.exports=e(require("angular")):n.returnExports=e(n.angular)}(this,function(n){function e(e){return{restrict:"A",scope:!1,compile:function(r,t){var i={};for(var o in t)if(n.isString(t[o])){var c=t[o].match(/\{\{\s*([\.\w]+)\s*\}\}/);c&&(i[o]=e(c[1]))}return function(e,r,t){function o(r){var o,c,a,u;o=t.$normalize(r.type.substring(0,r.type.indexOf("-changed"))),o in i&&(c=r.detail&&r.detail.path?r.target.get(r.detail.path.split(".")[0]):r.detail.value,u=i[o],setter=u.assign,a=u(e),!n.equals(c,a)&&n.isFunction(setter)&&e.$evalAsync(function(e){n.isArray(c)?setter(e,c):n.isObject(c)?Object.assign(a,c):setter(e,c)}))}function c(n){return n.replace(/[A-Z]/g,function(n){return"-"+n.toLowerCase()})}for(var a in i)r[0].addEventListener(c(a)+"-changed",o);e.$on("$destroy",function(){for(var n in i)r[0].removeEventListener(c(n)+"-changed",o)})}}}}function r(){return{restrict:"A",scope:!1,compile:function(e,r){function t(n){var e=u(n);if(e)return e.controllerAlias}function i(n){var e=u(n);if(e)return e.handler}function o(n){var e=c(n);return e.replace("on-","")}function c(n){return n.replace(/[A-Z]/g,function(n){return"-"+n.toLowerCase()})}function a(n,e,r,t,i){var o=function(e){n.$evalAsync(t.bind(i,e))};return e.addEventListener(r,o),function(){e.removeEventListener(r,o)}}var u=function(){var n={};return function(e){if(n[e])return n[e];var r=e.match(/((.*)\.)?(\w*)\((.*)\)/);return n[e]={handler:r[3],controllerAlias:r[2]}}}();return function(e,r,c){function u(n,c){var u=i(n),s=t(n),d=e[s]||e,l=o(c),v=a(e,r[0],l,d[u],d);f.push(v)}function s(t,i){e.$watch(t,function(e){n.isArray(e)?r[0][i]=e.slice(0):n.isObject(e)?r[0][i]=Object.assign({},e):r[0][i]=e},!0)}var f=[];for(var d in c)n.isString(c[d])&&""!==c[d]&&("on"===d.substr(0,2)&&c[d].indexOf("(")!==-1?u(c[d],d):s(c[d],d));e.$on("$destroy",function(){f.forEach(function(n){n()})})}}}}return n.module("robdodson.ce-bind",[]),n.module("robdodson.ce-bind").directive("ceInterpolated",["$parse",e]),n.module("robdodson.ce-bind").directive("ceOneWay",r),"robdodson.ce-bind"}); -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/string-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 51 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/object-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 50 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/array-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 53 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/array-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('one-way, arrays', function() { 19 | it('should have the same array', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/array-spec.html'); 21 | 22 | var ngArrayItems; 23 | var wcArrayItems; 24 | var ngButton = element(by.id('ng-button')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngArrayItems = element.all(by.css('.ng-array-item')); 28 | ngArrayItems.last().getText().then(function(text) { 29 | expect(text).toEqual('Alice'); 30 | }); 31 | wcArrayItems = element.all(by.css('.wc-array-item')); 32 | wcArrayItems.last().getText().then(function(text) { 33 | expect(text).toEqual('Alice'); 34 | }); 35 | 36 | ngButton.click(); 37 | ngArrayItems = element.all(by.css('.ng-array-item')); 38 | ngArrayItems.last().getText().then(function(text) { 39 | expect(text).toEqual('Paul'); 40 | }); 41 | wcArrayItems = element.all(by.css('.wc-array-item')); 42 | wcArrayItems.last().getText().then(function(text) { 43 | expect(text).toEqual('Paul'); 44 | }); 45 | 46 | wcButton.click(); 47 | ngArrayItems = element.all(by.css('.ng-array-item')); 48 | ngArrayItems.last().getText().then(function(text) { 49 | expect(text).toEqual('Sam'); 50 | }); 51 | wcArrayItems = element.all(by.css('.wc-array-item')); 52 | wcArrayItems.last().getText().then(function(text) { 53 | expect(text).toEqual('Sam'); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/array-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('interpolated, arrays', function() { 19 | it('should have the same array', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-interpolated/test/array-spec.html'); 21 | 22 | var ngArrayItems; 23 | var wcArrayItems; 24 | var ngButton = element(by.id('ng-button')); 25 | var wcButton = element(by.id('wc-button')); 26 | 27 | ngArrayItems = element.all(by.css('.ng-array-item')); 28 | ngArrayItems.last().getText().then(function(text) { 29 | expect(text).toEqual('Alice'); 30 | }); 31 | wcArrayItems = element.all(by.css('.wc-array-item')); 32 | wcArrayItems.last().getText().then(function(text) { 33 | expect(text).toEqual('Alice'); 34 | }); 35 | 36 | ngButton.click(); 37 | ngArrayItems = element.all(by.css('.ng-array-item')); 38 | ngArrayItems.last().getText().then(function(text) { 39 | expect(text).toEqual('Paul'); 40 | }); 41 | wcArrayItems = element.all(by.css('.wc-array-item')); 42 | wcArrayItems.last().getText().then(function(text) { 43 | expect(text).toEqual('Paul'); 44 | }); 45 | 46 | wcButton.click(); 47 | ngArrayItems = element.all(by.css('.ng-array-item')); 48 | ngArrayItems.last().getText().then(function(text) { 49 | expect(text).toEqual('Sam'); 50 | }); 51 | wcArrayItems = element.all(by.css('.wc-array-item')); 52 | wcArrayItems.last().getText().then(function(text) { 53 | expect(text).toEqual('Sam'); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var del = require('del'); 3 | var concat = require('gulp-concat'); 4 | var umd = require('gulp-umd'); 5 | var uglify = require('gulp-uglify'); 6 | var rename = require('gulp-rename'); 7 | var connect = require('gulp-connect'); 8 | var protractor = require('gulp-protractor').protractor; 9 | 10 | var webdriver_standalone = require('gulp-protractor').webdriver_standalone; 11 | var webdriver_update = require('gulp-protractor').webdriver_update; 12 | gulp.task('webdriver_standalone', webdriver_standalone); 13 | gulp.task('webdriver_update', webdriver_update); 14 | 15 | gulp.task('test', ['webdriver_update'], function() { 16 | return new Promise(function(resolve, reject) { 17 | connect.server({ port: 8000 }); 18 | // Called when the tests either complete or error 19 | // This function will kill the server and tell the gulp process 20 | // to either abort (if tests failed) or carry on 21 | function handleEnd(err) { 22 | connect.serverClose(); 23 | if (err) { 24 | return reject(err); 25 | } else { 26 | return resolve(); 27 | } 28 | } 29 | gulp.src(['**/*-spec.js']) 30 | .pipe(protractor({ 31 | configFile: 'protractor.conf.js', 32 | args: ['--baseUrl', 'http://127.0.0.1:8000'] 33 | })) 34 | .on('error', handleEnd) 35 | .on('close', handleEnd); 36 | }); 37 | }); 38 | 39 | gulp.task('clean', function() { 40 | return del(['dist']); 41 | }); 42 | 43 | gulp.task('build', ['clean', 'test'], function() { 44 | return gulp.src([ 45 | 'src/modules/ce-bind.module.js', 46 | 'src/directives/ce-interpolated/ce-interpolated.directive.js', 47 | 'src/directives/ce-one-way/ce-one-way.directive.js' 48 | ]) 49 | .pipe(concat('ce-bind.js')) 50 | .pipe(umd({ 51 | dependencies: function(file) { 52 | return [{ name: 'angular' }]; 53 | }, 54 | exports: function(file) { 55 | return "'robdodson.ce-bind'"; 56 | }, 57 | namespace: function(file) { 58 | return 'returnExports'; 59 | } 60 | })) 61 | .pipe(gulp.dest('dist')) 62 | .pipe(uglify()) 63 | .pipe(rename('ce-bind.min.js')) 64 | .pipe(gulp.dest('dist')); 65 | }); 66 | 67 | gulp.task('default', ['build']); -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/multiple-instances-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('interpolated, multiple instances', function() { 19 | it('should only update the instance that changed', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-interpolated/test/multiple-instances-spec.html'); 21 | 22 | var ngString2 = element.all(by.css('.ng-string')).get(1); 23 | var ngString3 = element.all(by.css('.ng-string')).last(); 24 | 25 | var wcString2 = element.all(by.css('#wc-string')).get(1); 26 | var wcString3 = element.all(by.css('#wc-string')).last(); 27 | 28 | var ngButton = element(by.id('ng-button')); 29 | var wcButton = element.all(by.css('#wc-button')).get(1); 30 | 31 | expect(ngString2.getText()).toEqual('Second string from Angular'); 32 | expect(wcString2.getText()).toEqual('Second string from Angular'); 33 | expect(ngString3.getText()).toEqual('Third string from Angular'); 34 | expect(wcString3.getText()).toEqual('Third string from Angular'); 35 | 36 | ngButton.click(); 37 | expect(ngString2.getText()).toEqual('Second string from Angular, updated!'); 38 | expect(wcString2.getText()).toEqual('Second string from Angular, updated!'); 39 | expect(ngString3.getText()).toEqual('Third string from Angular, updated!'); 40 | expect(wcString3.getText()).toEqual('Third string from Angular, updated!'); 41 | 42 | wcButton.click(); 43 | expect(ngString2.getText()).toEqual('String changed in Polymer'); 44 | expect(wcString2.getText()).toEqual('String changed in Polymer'); 45 | expect(ngString3.getText()).toEqual('Third string from Angular, updated!'); 46 | expect(wcString3.getText()).toEqual('Third string from Angular, updated!'); 47 | }); 48 | }); -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/multiple-instances-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
Angular string is: {{main.first}}
29 |
Angular string is: {{main.second}}
30 |
Angular string is: {{main.third}}
31 | 32 | 33 | 34 | 35 |
36 | 37 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/ng-repeat-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | Angular friend is: {{friend.name}} 31 | 38 | 39 | 41 | 42 |
43 |
44 | Parent scope friend is: 45 |
46 | 47 | 56 | 57 | -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/controller-alias-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('one-way, use given controller alias', function() { 19 | 20 | it('should have the same sub-property, with no ctrl alias', function() { 21 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/controller-alias-spec.html'); 22 | runTestFor('#test1'); 23 | }); 24 | 25 | it('should have the same sub-property, with simple "ctrl" alias', function() { 26 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/controller-alias-spec.html'); 27 | runTestFor('#test2'); 28 | }); 29 | 30 | it('should have the same sub-property, with "$ctrl" alias', function() { 31 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/controller-alias-spec.html'); 32 | runTestFor('#test3'); 33 | }); 34 | 35 | it('should have the same sub-property, with "$$$_ctrl42$$$" alias', function() { 36 | browser.get('http://127.0.0.1:8000/src/directives/ce-one-way/test/controller-alias-spec.html'); 37 | runTestFor('#test4'); 38 | }); 39 | }); 40 | 41 | function runTestFor(selector) { 42 | var ngObject; 43 | var wcObject; 44 | var ngButton = element(by.css(selector + ' > button')); 45 | 46 | // TODO: Deep selector /deep/ has been deprecated. Find another way. 47 | var wcButton = element(by.css(selector + ' /deep/ #wc-button')); 48 | 49 | 50 | ngObject = element(by.css(selector + ' > p > span')); 51 | wcObject = element(by.css(selector + ' /deep/ #wc-object')); 52 | expect(ngObject.getText()).toEqual('Lisa'); 53 | expect(wcObject.getText()).toEqual('Lisa'); 54 | 55 | ngButton.click(); 56 | expect(ngObject.getText()).toEqual('Joe'); 57 | expect(wcObject.getText()).toEqual('Joe'); 58 | 59 | wcButton.click(); 60 | expect(ngObject.getText()).toEqual('Alex'); 61 | expect(wcObject.getText()).toEqual('Alex'); 62 | } 63 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/test/ng-repeat-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | describe('interpolated, ng-repeat', function() { 19 | it('should update the value in the local and parent scopes', function() { 20 | browser.get('http://127.0.0.1:8000/src/directives/ce-interpolated/test/ng-repeat-spec.html'); 21 | 22 | var ngItem = element.all(by.css('.ng-repeat-item')).last(); 23 | var wcItem = element.all(by.css('.wc-repeat-item')).last(); 24 | var parentItem = element(by.id('parent-item')); 25 | var wcButton = element.all(by.css('.wc-button')).last(); 26 | var ngButton = element.all(by.css('.ng-button')).last(); 27 | 28 | ngItem.getText().then(function(text) { 29 | expect(text).toEqual('Alice'); 30 | }); 31 | wcItem.getText().then(function(text) { 32 | expect(text).toEqual('Alice'); 33 | }); 34 | expect(parentItem.getAttribute('value')).toEqual('Alice'); 35 | 36 | ngButton.click(); 37 | ngItem.getText().then(function(text) { 38 | expect(text).toEqual('Alice-ng'); 39 | }); 40 | wcItem.getText().then(function(text) { 41 | expect(text).toEqual('Alice-ng'); 42 | }); 43 | expect(parentItem.getAttribute('value')).toEqual('Alice-ng'); 44 | 45 | wcButton.click(); 46 | ngItem.getText().then(function(text) { 47 | expect(text).toEqual('Alice-ng-poly'); 48 | }); 49 | wcItem.getText().then(function(text) { 50 | expect(text).toEqual('Alice-ng-poly'); 51 | }); 52 | expect(parentItem.getAttribute('value')).toEqual('Alice-ng-poly'); 53 | 54 | parentItem.sendKeys('-abc'); 55 | ngItem.getText().then(function(text) { 56 | expect(text).toEqual('Alice-ng-poly-abc'); 57 | }); 58 | wcItem.getText().then(function(text) { 59 | expect(text).toEqual('Alice-ng-poly-abc'); 60 | }); 61 | expect(parentItem.getAttribute('value')).toEqual('Alice-ng-poly-abc'); 62 | }); 63 | }); -------------------------------------------------------------------------------- /src/directives/ce-one-way/test/controller-alias-spec.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |

Angular object is: {{person.name}}

32 | 33 | 36 |
37 | 38 |
39 | 40 | 41 |

Angular object is: {{ctrl.person.name}}

42 | 43 | 46 |
47 | 48 |
49 | 50 | 51 |

Angular object is: {{$ctrl.person.name}}

52 | 53 | 56 |
57 | 58 |
59 | 60 | 61 |

Angular object is: {{$$$_ctrl42$$$.person.name}}

62 | 63 | 66 |
67 | 68 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-custom-elements 2 | 3 | [![Build Status](https://travis-ci.org/robdodson/angular-custom-elements.svg?branch=master)](https://travis-ci.org/robdodson/angular-custom-elements) 4 | 5 | Angular 1.x directive to hold all yr Custom Element bindings together 😁 6 | 7 | *note: This is still experimental so use at your own risk* 8 | 9 | ## Install 10 | 11 | ``` 12 | npm install --save angular-custom-elements 13 | ``` 14 | 15 | ## Usage 16 | 17 | - Include the `dist/ce-bind.(min).js` script in your page. 18 | - Add `robdodson.ce-bind` as a module dependency to your app. 19 | - **For interpolated/two-way bindings**: Add the `ce-interpolated` directive to 20 | any Custom Element or Polymer Element to keep your interpolated bindings in sync. 21 | 22 | ```html 23 |
24 | {{main.greeting}} 25 | 26 |
27 | ``` 28 | 29 | - **For one-way bindings**: Add the `ce-one-way` directive to any Custom 30 | Element or Polymer Element, to keep its one-way bindings in sync. 31 | 32 | ```js 33 | app.component('fooComponent', { 34 | template: ` 35 |

Angular string is: {{$ctrl.str}}

36 | 39 | 40 | ` 41 | ``` 42 | 43 | ## How does it work? 44 | 45 | ### Interpolated bindings 46 | 47 | Polymer's two-way binding system is event based. Anytime a bindable property 48 | changes it fires an event named: `[property]-changed`. For example, a two-way 49 | bindable property named `foo` would fire a `foo-changed` event. 50 | 51 | This means we can listen for the `*-changed` events coming off of an element, 52 | and take the new value and pass it into our scope using `$evalAsync`. 53 | 54 | This also means you could write your own Custom Elements that didn't use Polymer 55 | and so long as they fired a `[property]-changed` event, and the 56 | `event.detail.value` contained the new value, it would also work. 57 | 58 | ### One-way bindings 59 | 60 | For Angular 1.5 style one-way bindings, we look at the Input, e.g. 61 | `friend="$ctrl.person"`, set the property on the Custom Element using the value 62 | from `$ctrl.person`, and create a watcher to update the Custom Element anytime 63 | the `$ctrl.person` property changes. 64 | 65 | For Outputs, we look for any attribute starting with `on-` and create an event 66 | listener which triggers the corresponding handler in our Angular controller. 67 | E.g. `on-person-changed="$ctrl.updatePerson($event)"` will listen for the 68 | `person-changed` event and call the controller's `updatePerson` method, 69 | passing the event object to it. The controller can then take the value of 70 | `event.detail` and choose what to do with it. Because Custom Elements typically 71 | communicate to the outside world using Events, this binding will **only** create 72 | event listeners. This means you cannot use the Angular 1.5 approach of creating 73 | a callback with named arguments: 74 | 75 | ``` 76 | // This will NOT work. The argument will be ignored and the handler 77 | // will always be called with the event object 78 | on-person-changed="$ctrl.updatePerson({name: 'Bob'})" 79 | ``` 80 | 81 | Instead, treat these as regular event listeners and use the value(s) passed 82 | via `event.detail`. 83 | 84 | ## How is this different from other Polymer + Angular adapters? 85 | 86 | The two adapters I've found are 87 | [angular-bind-polymer](https://github.com/eee-c/angular-bind-polymer) and 88 | [ng-polymer-elements](https://gabiaxel.github.io/ng-polymer-elements/). Both are 89 | very cool but they have limitations which this project (hopefully) fixes. 90 | 91 | **angular-bind-polymer** relies on Mutation Observers to notify the scope when an 92 | element's attributes change. This only works if the element chooses to serialize 93 | its internal state back to strings and reflect them to attributes. Most Polymer 94 | elements do not do this, meaning they can't be used with angular-bind-polymer. 95 | 96 | **ng-polymer-elements** attempts to create directives for specific attributes 97 | exposed by Polymer elements but this becomes a bit of an arms race as every time 98 | an element creates a new attribute/property then `ng-polymer-elements` needs to 99 | be updated. It also relies on `Object.observe` which has been removed from the 100 | platform, so an additional polyfill is required. 101 | -------------------------------------------------------------------------------- /src/directives/ce-interpolated/ce-interpolated.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @desc Enable two-way/interpolated data bindings for Angular + Custom Elements 20 | * @example 21 | */ 22 | angular 23 | .module('robdodson.ce-bind') 24 | .directive('ceInterpolated', ['$parse', ceInterpolated]); 25 | 26 | // Make Angular 1.x interpolated bindings work. 27 | // Finds interpolated bindings and sets up event listeners 28 | // to hear when the underlying Polymer property updates. 29 | // Because Polymer's interpolated binding system is event based 30 | // we can listen for the [prop]-changed event dispatched 31 | // by a Polymer element and apply the new value to the 32 | // controller's scope. 33 | // This also works for vanilla Custom Elements so long as 34 | // they dispatch a [prop]-changed event where 35 | // event.detail.value equals the new value 36 | function ceInterpolated($parse) { 37 | return { 38 | restrict: 'A', 39 | scope: false, 40 | compile: function($element, $attrs) { 41 | var attrMap = {}; 42 | 43 | for (var prop in $attrs) { 44 | if (angular.isString($attrs[prop])) { 45 | var _match = $attrs[prop].match(/\{\{\s*([\.\w]+)\s*\}\}/); 46 | if (_match) { 47 | attrMap[prop] = $parse(_match[1]); 48 | } 49 | } 50 | } 51 | 52 | return function($scope, $element, $attrs) { 53 | 54 | function applyChange(event) { 55 | var attributeName, newValue, oldValue, getter; 56 | // Figure out what changed by the event type 57 | // Convert the event from dash-case to camelCase with $normalize 58 | // So we can get it out of the attrMap 59 | attributeName = $attrs.$normalize( 60 | event.type.substring(0, event.type.indexOf('-changed')) 61 | ); 62 | 63 | if (attributeName in attrMap) { 64 | // When you modify an array or object using Polymer's set methods, 65 | // the `prop-changed` event's detail will contain a `path` property; 66 | // in that case the `value` is the value at that path. 67 | if (event.detail && event.detail.path) { 68 | newValue = event.target.get(event.detail.path.split('.')[0]); 69 | } else { 70 | newValue = event.detail.value; 71 | } 72 | getter = attrMap[attributeName]; 73 | setter = getter.assign; 74 | oldValue = getter($scope); 75 | 76 | if (!angular.equals(newValue, oldValue) && angular.isFunction(setter)) { 77 | $scope.$evalAsync(function($scope) { 78 | if (angular.isArray(newValue)) { 79 | // FIXME: This is probably not going to work if we're 80 | // mutating the array inside of an ng-repeat. Probably 81 | // need to mutate the original array being referenced 82 | // so we don't shadow the property on the prototype by 83 | // assigning a new object 84 | setter($scope, newValue); 85 | } else if (angular.isObject(newValue)) { 86 | Object.assign(oldValue, newValue); 87 | } else { 88 | setter($scope, newValue); 89 | } 90 | }); 91 | } 92 | } 93 | } 94 | 95 | // Convert Angular camelCase property to dash-case 96 | function denormalize(str) { 97 | return str.replace(/[A-Z]/g, function(c) { 98 | return '-' + c.toLowerCase(); 99 | }); 100 | } 101 | 102 | for (var prop in attrMap) { 103 | $element[0].addEventListener(denormalize(prop) + '-changed', applyChange); 104 | } 105 | 106 | $scope.$on('$destroy', function() { 107 | for (var prop in attrMap) { 108 | $element[0].removeEventListener(denormalize(prop) + '-changed', applyChange); 109 | } 110 | }); 111 | } 112 | } 113 | }; 114 | } -------------------------------------------------------------------------------- /src/directives/ce-one-way/ce-one-way.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @desc Enable unidirectional data bindings for Angular 1.5 components 20 | * @example 23 | 24 | */ 25 | angular 26 | .module('robdodson.ce-bind') 27 | .directive('ceOneWay', ceOneWay); 28 | 29 | // Make Angular 1.5, one-way bindings work. 30 | // Data is always copied before it is passed into the 31 | // child to prevent the child from modifying state in 32 | // the parent. Because Polymer/Custom Elements don't 33 | // have a notion of passing callbacks in, an Output 34 | // is treated as a regular event handler and passed 35 | // the CustomEvent dispatched by the element 36 | function ceOneWay() { 37 | return { 38 | restrict: 'A', 39 | scope: false, 40 | compile: function($element, $attrs) { 41 | 42 | // parse an expression and returns its metadata 43 | var parse = (function () { 44 | var cache = {}; 45 | 46 | return function parse(expression) { 47 | if (cache[expression]) { 48 | return cache[expression]; 49 | } 50 | 51 | var matchedPattern = expression.match(/((.*)\.)?(\w*)\((.*)\)/); 52 | 53 | // matchedPattern[0] = expression 54 | // matchedPattern[1] = controller alias (with dot) i.e. "$ctrl." or "undefined" for non-aliased controllers 55 | // matchedPattern[2] = controller alias (without dot) i.e. "$ctrl" or "undefined" for non-aliased controllers 56 | // matchedPattern[3] = event handler 57 | // matchedPattern[4] = event handler params (i.e. "$event, a, b, c") 58 | 59 | return cache[expression] = { 60 | handler: matchedPattern[3], 61 | controllerAlias: matchedPattern[2] 62 | }; 63 | }; 64 | })(); 65 | 66 | // Find the controller alias associated with the $scope 67 | function getCtrlAlias(expression) { 68 | var parsedExpression = parse(expression); 69 | if (parsedExpression) { 70 | return parsedExpression.controllerAlias; 71 | } 72 | } 73 | 74 | // Find the event handler associated with the $ctrl 75 | function getHandler(expression) { 76 | var parsedExpression = parse(expression); 77 | if (parsedExpression) { 78 | return parsedExpression.handler; 79 | } 80 | } 81 | 82 | // Remove Angular's camelCasing of event names and 83 | // strip on- prefix 84 | function getEvent(expression) { 85 | var event = denormalize(expression); 86 | return event.replace('on-', ''); 87 | } 88 | 89 | // Convert Angular camelCase property to dash-case 90 | function denormalize(str) { 91 | return str.replace(/[A-Z]/g, function(c) { 92 | return '-' + c.toLowerCase(); 93 | }); 94 | } 95 | 96 | // Setup event handler and return a deregister function 97 | // to be used during $destroy 98 | function createHandler($scope, element, event, handler, ctrlScope) { 99 | var listener = function(e) { 100 | $scope.$evalAsync(handler.bind(ctrlScope, e)); 101 | } 102 | element.addEventListener(event, listener); 103 | return function() { 104 | element.removeEventListener(event, listener); 105 | } 106 | } 107 | 108 | return function($scope, $element, $attrs) { 109 | // Store event handler remover functions in 110 | // here and use on $destroy 111 | var cleanup = []; 112 | 113 | // Setup an event handler to act as an output 114 | // Since elements communicate to the outside world 115 | // using events, we'll simulate angular's '&' 116 | // output callbacks using regular event handlers 117 | function makeOutput(handlerName, eventName) { 118 | var handler = getHandler(handlerName); 119 | var ctrlAlias = getCtrlAlias(handlerName); 120 | var ctrlScope = ($scope[ctrlAlias] || $scope); 121 | var event = getEvent(eventName); 122 | var removeHandler = createHandler( 123 | $scope, 124 | $element[0], 125 | event, 126 | ctrlScope[handler], 127 | ctrlScope 128 | ); 129 | cleanup.push(removeHandler); 130 | } 131 | 132 | // Setup a watcher on the controller property 133 | // and create a copy when setting data on the 134 | // element so it can't mutate the parent's data 135 | // TODO: Don't do a deep watch. Differentiate 136 | // based on object type and use watchCollection 137 | function makeInput(ctrlProp, elProp) { 138 | $scope.$watch(ctrlProp, function(value) { 139 | if (angular.isArray(value)) { 140 | $element[0][elProp] = value.slice(0); 141 | } else if (angular.isObject(value)) { 142 | $element[0][elProp] = Object.assign({}, value); 143 | } else { 144 | $element[0][elProp] = value; 145 | } 146 | }, true); 147 | } 148 | 149 | // Iterate over element attributes and look for one way 150 | // inputs or outputs 151 | for (var prop in $attrs) { 152 | if (angular.isString($attrs[prop]) && $attrs[prop] !== '') { 153 | // Look for an Output like 154 | // ==> on-foo="doBar()" 155 | // ==> on-foo="$ctrl.doBar()" 156 | // Note that angular's $attr object will camelCase things beginning 157 | // with "on-". So on-foo becomes onFoo 158 | if (prop.substr(0, 2) === 'on' && $attrs[prop].indexOf('(') !== -1) { 159 | makeOutput($attrs[prop], prop); 160 | } else { 161 | makeInput($attrs[prop], prop); 162 | } 163 | } 164 | } 165 | 166 | // Listen for $destroy event and remove all event 167 | // listeners. $watchers should be automatically removed 168 | // so don't need to do any work there 169 | $scope.$on('$destroy', function() { 170 | cleanup.forEach(function(removeFn) { 171 | removeFn(); 172 | }); 173 | }); 174 | 175 | }; 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /dist/ce-bind.js: -------------------------------------------------------------------------------- 1 | ;(function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['angular'], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(require('angular')); 6 | } else { 7 | root.returnExports = factory(root.angular); 8 | } 9 | }(this, function(angular) { 10 | /** 11 | * 12 | * Copyright 2016 Google Inc. All rights reserved. 13 | * 14 | * Licensed under the Apache License, Version 2.0 (the "License"); 15 | * you may not use this file except in compliance with the License. 16 | * You may obtain a copy of the License at 17 | * 18 | * http://www.apache.org/licenses/LICENSE-2.0 19 | * 20 | * Unless required by applicable law or agreed to in writing, software 21 | * distributed under the License is distributed on an "AS IS" BASIS, 22 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | * See the License for the specific language governing permissions and 24 | * limitations under the License. 25 | */ 26 | 27 | angular.module('robdodson.ce-bind', []); 28 | /** 29 | * 30 | * Copyright 2016 Google Inc. All rights reserved. 31 | * 32 | * Licensed under the Apache License, Version 2.0 (the "License"); 33 | * you may not use this file except in compliance with the License. 34 | * You may obtain a copy of the License at 35 | * 36 | * http://www.apache.org/licenses/LICENSE-2.0 37 | * 38 | * Unless required by applicable law or agreed to in writing, software 39 | * distributed under the License is distributed on an "AS IS" BASIS, 40 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 41 | * See the License for the specific language governing permissions and 42 | * limitations under the License. 43 | */ 44 | 45 | /** 46 | * @desc Enable two-way/interpolated data bindings for Angular + Custom Elements 47 | * @example 48 | */ 49 | angular 50 | .module('robdodson.ce-bind') 51 | .directive('ceInterpolated', ['$parse', ceInterpolated]); 52 | 53 | // Make Angular 1.x interpolated bindings work. 54 | // Finds interpolated bindings and sets up event listeners 55 | // to hear when the underlying Polymer property updates. 56 | // Because Polymer's interpolated binding system is event based 57 | // we can listen for the [prop]-changed event dispatched 58 | // by a Polymer element and apply the new value to the 59 | // controller's scope. 60 | // This also works for vanilla Custom Elements so long as 61 | // they dispatch a [prop]-changed event where 62 | // event.detail.value equals the new value 63 | function ceInterpolated($parse) { 64 | return { 65 | restrict: 'A', 66 | scope: false, 67 | compile: function($element, $attrs) { 68 | var attrMap = {}; 69 | 70 | for (var prop in $attrs) { 71 | if (angular.isString($attrs[prop])) { 72 | var _match = $attrs[prop].match(/\{\{\s*([\.\w]+)\s*\}\}/); 73 | if (_match) { 74 | attrMap[prop] = $parse(_match[1]); 75 | } 76 | } 77 | } 78 | 79 | return function($scope, $element, $attrs) { 80 | 81 | function applyChange(event) { 82 | var attributeName, newValue, oldValue, getter; 83 | // Figure out what changed by the event type 84 | // Convert the event from dash-case to camelCase with $normalize 85 | // So we can get it out of the attrMap 86 | attributeName = $attrs.$normalize( 87 | event.type.substring(0, event.type.indexOf('-changed')) 88 | ); 89 | 90 | if (attributeName in attrMap) { 91 | // When you modify an array or object using Polymer's set methods, 92 | // the `prop-changed` event's detail will contain a `path` property; 93 | // in that case the `value` is the value at that path. 94 | if (event.detail && event.detail.path) { 95 | newValue = event.target.get(event.detail.path.split('.')[0]); 96 | } else { 97 | newValue = event.detail.value; 98 | } 99 | getter = attrMap[attributeName]; 100 | setter = getter.assign; 101 | oldValue = getter($scope); 102 | 103 | if (!angular.equals(newValue, oldValue) && angular.isFunction(setter)) { 104 | $scope.$evalAsync(function($scope) { 105 | if (angular.isArray(newValue)) { 106 | // FIXME: This is probably not going to work if we're 107 | // mutating the array inside of an ng-repeat. Probably 108 | // need to mutate the original array being referenced 109 | // so we don't shadow the property on the prototype by 110 | // assigning a new object 111 | setter($scope, newValue); 112 | } else if (angular.isObject(newValue)) { 113 | Object.assign(oldValue, newValue); 114 | } else { 115 | setter($scope, newValue); 116 | } 117 | }); 118 | } 119 | } 120 | } 121 | 122 | // Convert Angular camelCase property to dash-case 123 | function denormalize(str) { 124 | return str.replace(/[A-Z]/g, function(c) { 125 | return '-' + c.toLowerCase(); 126 | }); 127 | } 128 | 129 | for (var prop in attrMap) { 130 | $element[0].addEventListener(denormalize(prop) + '-changed', applyChange); 131 | } 132 | 133 | $scope.$on('$destroy', function() { 134 | for (var prop in attrMap) { 135 | $element[0].removeEventListener(denormalize(prop) + '-changed', applyChange); 136 | } 137 | }); 138 | } 139 | } 140 | }; 141 | } 142 | /** 143 | * 144 | * Copyright 2016 Google Inc. All rights reserved. 145 | * 146 | * Licensed under the Apache License, Version 2.0 (the "License"); 147 | * you may not use this file except in compliance with the License. 148 | * You may obtain a copy of the License at 149 | * 150 | * http://www.apache.org/licenses/LICENSE-2.0 151 | * 152 | * Unless required by applicable law or agreed to in writing, software 153 | * distributed under the License is distributed on an "AS IS" BASIS, 154 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 155 | * See the License for the specific language governing permissions and 156 | * limitations under the License. 157 | */ 158 | 159 | /** 160 | * @desc Enable unidirectional data bindings for Angular 1.5 components 161 | * @example 164 | 165 | */ 166 | angular 167 | .module('robdodson.ce-bind') 168 | .directive('ceOneWay', ceOneWay); 169 | 170 | // Make Angular 1.5, one-way bindings work. 171 | // Data is always copied before it is passed into the 172 | // child to prevent the child from modifying state in 173 | // the parent. Because Polymer/Custom Elements don't 174 | // have a notion of passing callbacks in, an Output 175 | // is treated as a regular event handler and passed 176 | // the CustomEvent dispatched by the element 177 | function ceOneWay() { 178 | return { 179 | restrict: 'A', 180 | scope: false, 181 | compile: function($element, $attrs) { 182 | 183 | // parse an expression and returns its metadata 184 | var parse = (function () { 185 | var cache = {}; 186 | 187 | return function parse(expression) { 188 | if (cache[expression]) { 189 | return cache[expression]; 190 | } 191 | 192 | var matchedPattern = expression.match(/((.*)\.)?(\w*)\((.*)\)/); 193 | 194 | // matchedPattern[0] = expression 195 | // matchedPattern[1] = controller alias (with dot) i.e. "$ctrl." or "undefined" for non-aliased controllers 196 | // matchedPattern[2] = controller alias (without dot) i.e. "$ctrl" or "undefined" for non-aliased controllers 197 | // matchedPattern[3] = event handler 198 | // matchedPattern[4] = event handler params (i.e. "$event, a, b, c") 199 | 200 | return cache[expression] = { 201 | handler: matchedPattern[3], 202 | controllerAlias: matchedPattern[2] 203 | }; 204 | }; 205 | })(); 206 | 207 | // Find the controller alias associated with the $scope 208 | function getCtrlAlias(expression) { 209 | var parsedExpression = parse(expression); 210 | if (parsedExpression) { 211 | return parsedExpression.controllerAlias; 212 | } 213 | } 214 | 215 | // Find the event handler associated with the $ctrl 216 | function getHandler(expression) { 217 | var parsedExpression = parse(expression); 218 | if (parsedExpression) { 219 | return parsedExpression.handler; 220 | } 221 | } 222 | 223 | // Remove Angular's camelCasing of event names and 224 | // strip on- prefix 225 | function getEvent(expression) { 226 | var event = denormalize(expression); 227 | return event.replace('on-', ''); 228 | } 229 | 230 | // Convert Angular camelCase property to dash-case 231 | function denormalize(str) { 232 | return str.replace(/[A-Z]/g, function(c) { 233 | return '-' + c.toLowerCase(); 234 | }); 235 | } 236 | 237 | // Setup event handler and return a deregister function 238 | // to be used during $destroy 239 | function createHandler($scope, element, event, handler, ctrlScope) { 240 | var listener = function(e) { 241 | $scope.$evalAsync(handler.bind(ctrlScope, e)); 242 | } 243 | element.addEventListener(event, listener); 244 | return function() { 245 | element.removeEventListener(event, listener); 246 | } 247 | } 248 | 249 | return function($scope, $element, $attrs) { 250 | // Store event handler remover functions in 251 | // here and use on $destroy 252 | var cleanup = []; 253 | 254 | // Setup an event handler to act as an output 255 | // Since elements communicate to the outside world 256 | // using events, we'll simulate angular's '&' 257 | // output callbacks using regular event handlers 258 | function makeOutput(handlerName, eventName) { 259 | var handler = getHandler(handlerName); 260 | var ctrlAlias = getCtrlAlias(handlerName); 261 | var ctrlScope = ($scope[ctrlAlias] || $scope); 262 | var event = getEvent(eventName); 263 | var removeHandler = createHandler( 264 | $scope, 265 | $element[0], 266 | event, 267 | ctrlScope[handler], 268 | ctrlScope 269 | ); 270 | cleanup.push(removeHandler); 271 | } 272 | 273 | // Setup a watcher on the controller property 274 | // and create a copy when setting data on the 275 | // element so it can't mutate the parent's data 276 | // TODO: Don't do a deep watch. Differentiate 277 | // based on object type and use watchCollection 278 | function makeInput(ctrlProp, elProp) { 279 | $scope.$watch(ctrlProp, function(value) { 280 | if (angular.isArray(value)) { 281 | $element[0][elProp] = value.slice(0); 282 | } else if (angular.isObject(value)) { 283 | $element[0][elProp] = Object.assign({}, value); 284 | } else { 285 | $element[0][elProp] = value; 286 | } 287 | }, true); 288 | } 289 | 290 | // Iterate over element attributes and look for one way 291 | // inputs or outputs 292 | for (var prop in $attrs) { 293 | if (angular.isString($attrs[prop]) && $attrs[prop] !== '') { 294 | // Look for an Output like 295 | // ==> on-foo="doBar()" 296 | // ==> on-foo="$ctrl.doBar()" 297 | // Note that angular's $attr object will camelCase things beginning 298 | // with "on-". So on-foo becomes onFoo 299 | if (prop.substr(0, 2) === 'on' && $attrs[prop].indexOf('(') !== -1) { 300 | makeOutput($attrs[prop], prop); 301 | } else { 302 | makeInput($attrs[prop], prop); 303 | } 304 | } 305 | } 306 | 307 | // Listen for $destroy event and remove all event 308 | // listeners. $watchers should be automatically removed 309 | // so don't need to do any work there 310 | $scope.$on('$destroy', function() { 311 | cleanup.forEach(function(removeFn) { 312 | removeFn(); 313 | }); 314 | }); 315 | 316 | }; 317 | } 318 | } 319 | } 320 | 321 | return 'robdodson.ce-bind'; 322 | })); 323 | --------------------------------------------------------------------------------