├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── scripts └── clean-package-json-and-write-to-dist.js ├── src ├── index.ts ├── recaptcha │ ├── recaptcha.component.ts │ ├── recaptcha.module.ts │ └── recaptcha.tokens.ts ├── tsconfig.build.json └── tsconfig.es5.json ├── test ├── jest-setup.ts ├── recaptcha.component.spec.ts ├── recaptcha.module.spec.ts └── tsconfig.spec.json ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | parser: 'typescript-eslint-parser' 3 | parserOptions: 4 | sourceType: 'module' 5 | rules: 6 | semi: ['error', 'always'] 7 | quotes: ['error', 'single'] 8 | comma-dangle: ['error', { 9 | arrays: 'always-multiline', 10 | objects: 'always-multiline', 11 | imports: 'always-multiline', 12 | exports: 'always-multiline', 13 | functions: 'always-multiline' 14 | }] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,visualstudiocode 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | 64 | ### VisualStudioCode ### 65 | .vscode/* 66 | !.vscode/settings.json 67 | !.vscode/tasks.json 68 | !.vscode/launch.json 69 | !.vscode/extensions.json 70 | 71 | # End of https://www.gitignore.io/api/node,visualstudiocode 72 | 73 | # Ignore compiled and built files 74 | build/ 75 | dist/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | notifications: 3 | email: false 4 | language: node_js 5 | cache: 6 | yarn: true 7 | directories: 8 | - node_modules 9 | node_js: 10 | - '9' 11 | - '8' 12 | script: 13 | - yarn test 14 | - yarn build 15 | after_success: 16 | - yarn semantic-release 17 | branches: 18 | only: 19 | - master -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.eslintIntegration": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Henry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Angular Google reCAPTCHA

2 | 3 |

4 | Travis 5 | GitHub license 6 | NPM Version 7 | NPM Downloads 8 | Commitizen friendly 9 | semantic-release 10 |

11 | 12 |
13 |

14 | typescriptcourses.com 15 |

16 |
17 | 18 | Google's reCAPTCHA is an awesome, UX-friendly way of ensuring that the users who are submitting your forms are actually humans. 19 | 20 | Angular has fantastic built in forms functionality which makes it easy to write powerful custom components and validation logic. 21 | 22 | This library makes it effortless to combine them! 23 | 24 | If you ever need to reference the full reCAPTCHA docs you can find them here: [reCAPTCHA docs](https://developers.google.com/recaptcha/) 25 | 26 | # Installation 27 | 28 | Head over to the command line and use your favourite package manager to install and save it as a dependency: 29 | 30 | E.g. 31 | 32 | ```bash 33 | npm install --save angular-google-recaptcha 34 | ``` 35 | 36 | or 37 | 38 | ```bash 39 | yarn add angular-google-recaptcha 40 | ``` 41 | 42 | # Usage 43 | 44 | ### 1. If you haven't yet registered your site for use with reCAPTCHA, you'll need to do that next. 45 | 46 | Head over to: 47 | 48 | [https://www.google.com/recaptcha/admin#list](https://www.google.com/recaptcha/admin#list) 49 | 50 | ...and register your site. 51 | 52 | Once you have done that, a **"site key"** will have been generated for you. You need to find this and copy it to you clipboard as it will be important in the next step! 53 | 54 | ### 2. Now all you need to do is pass that site key into the library, and this is done via the `forRoot` convention on the `NgModule` which the library exposes. 55 | 56 | E.g. 57 | 58 | ```ts 59 | import { BrowserModule } from '@angular/platform-browser'; 60 | import { NgModule } from '@angular/core'; 61 | import { ReactiveFormsModule } from '@angular/forms'; 62 | import { RecaptchaModule } from 'angular-google-recaptcha'; 63 | 64 | @NgModule({ 65 | imports: [ 66 | BrowserModule, 67 | ReactiveFormsModule, 68 | RecaptchaModule.forRoot({ 69 | siteKey: 'YOUR_SITE_KEY_HERE', 70 | }), 71 | ], 72 | bootstrap: [AppComponent] 73 | }) 74 | export class AppModule { } 75 | ``` 76 | 77 | As you might expect, you need to be using the `@angular/forms` package (either the `FormsModule` or `ReactiveFormsModule`) within your project, otherwise this library will have nothing to hook into. 78 | 79 | ### 3. With everything wired up, you can now use the `` component! 80 | 81 | E.g. For reactive forms 82 | 83 | ```ts 84 | import { Component } from '@angular/core'; 85 | import { FormControl } from '@angular/forms'; 86 | 87 | @Component({ 88 | selector: 'app', 89 | template: ` 90 | 95 | ` 96 | }) 97 | export class AppComponent { 98 | myRecaptcha = new FormControl(false); 99 | 100 | onScriptLoad() { 101 | console.log('Google reCAPTCHA loaded and is ready for use!') 102 | } 103 | 104 | onScriptError() { 105 | console.log('Something went long when loading the Google reCAPTCHA') 106 | } 107 | } 108 | ``` 109 | 110 | E.g. For template-driven forms 111 | 112 | ```ts 113 | import { Component } from '@angular/core'; 114 | 115 | @Component({ 116 | selector: 'app', 117 | template: ` 118 | 123 | ` 124 | }) 125 | export class AppComponent { 126 | myRecaptcha: boolean 127 | 128 | onScriptLoad() { 129 | console.log('Google reCAPTCHA loaded and is ready for use!') 130 | } 131 | 132 | onScriptError() { 133 | console.log('Something went long when loading the Google reCAPTCHA') 134 | } 135 | } 136 | ``` 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-google-recaptcha", 3 | "version": "0.0.0-semantically-released", 4 | "author": "James Henry ", 5 | "repository": "github:JamesHenry/angular-google-recaptcha", 6 | "description": "Easily use Google's reCAPTCHA within your Angular forms", 7 | "keywords": ["angular", "google", "recaptcha", "form"], 8 | "license": "MIT", 9 | "module": "angular-google-recaptcha.es5.js", 10 | "es2015": "angular-google-recaptcha.js", 11 | "typings": "angular-google-recaptcha.d.ts", 12 | "peerDependencies": { 13 | "@angular/core": "^4.0.1 || ^5.0.0", 14 | "@angular/forms": "^4.0.1 || ^5.0.0", 15 | "rxjs": "^5.3.0", 16 | "zone.js": "^0.8.5" 17 | }, 18 | "scripts": { 19 | "test": "jest", 20 | "prebuild": "rm -rf build/ dist/", 21 | "transpile:es5": "ngc -p src/tsconfig.es5.json", 22 | "transpile:es2015": "ngc -p src/tsconfig.build.json", 23 | "bundle:es5": "rollup -c -o dist/angular-google-recaptcha.es5.js", 24 | "bundle:es2015": "rollup -c -o dist/angular-google-recaptcha.js", 25 | "build:es5": "npm run transpile:es5 && npm run bundle:es5", 26 | "build:es2015": "npm run transpile:es2015 && npm run bundle:es2015", 27 | "build": "npm run build:es5 && npm run build:es2015", 28 | "postbuild": 29 | "rsync -a --exclude=*.js build/ dist && cp README.md dist/README.md && rm -rf build", 30 | "precommit": "npm test && lint-staged", 31 | "cz": "git-cz", 32 | "clean-package-json-and-write-to-dist": 33 | "node ./scripts/clean-package-json-and-write-to-dist.js", 34 | "semantic-release": 35 | "semantic-release pre && npm run clean-package-json-and-write-to-dist && npm publish ./dist && semantic-release post" 36 | }, 37 | "jest": { 38 | "preset": "jest-preset-angular", 39 | "setupTestFrameworkScriptFile": "/test/jest-setup.ts", 40 | "testRegex": "/test/.*\\.spec\\.ts$", 41 | "globals": { 42 | "ts-jest": { 43 | "tsConfigFile": "test/tsconfig.spec.json" 44 | }, 45 | "__TRANSFORM_HTML__": true 46 | } 47 | }, 48 | "lint-staged": { 49 | "src/**/*": ["prettier-eslint --write", "git add"] 50 | }, 51 | "config": { 52 | "commitizen": { 53 | "path": "./node_modules/cz-conventional-changelog" 54 | } 55 | }, 56 | "devDependencies": { 57 | "@angular/common": "^4.0.1", 58 | "@angular/compiler": "^4.0.1", 59 | "@angular/compiler-cli": "^4.0.1", 60 | "@angular/core": "^4.0.1", 61 | "@angular/forms": "^4.0.1", 62 | "@angular/platform-browser": "^4.0.1", 63 | "@angular/platform-browser-dynamic": "^4.0.1", 64 | "@types/jest": "^21.1.6", 65 | "cz-conventional-changelog": "^2.1.0", 66 | "eslint": "^4.11.0", 67 | "husky": "^0.14.3", 68 | "jest": "^21.2.1", 69 | "jest-preset-angular": "^4.0.1", 70 | "lint-staged": "^5.0.0", 71 | "prettier": "^1.8.2", 72 | "prettier-eslint-cli": "^4.4.0", 73 | "rollup": "^0.41.6", 74 | "rxjs": "^5.3.0", 75 | "semantic-release": "^8.2.0", 76 | "typescript": "^2.2.2", 77 | "typescript-eslint-parser": "^9.0.0", 78 | "zone.js": "^0.8.5" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency 4 | */ 5 | external: ['@angular/core', '@angular/forms'], 6 | /** 7 | * The single-file output from the Angular Compiler 8 | * is our input to rollup 9 | */ 10 | entry: 'build/angular-google-recaptcha.js', 11 | format: 'es', 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/clean-package-json-and-write-to-dist.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const pkg = require('../package.json'); 4 | const finalPkg = {}; 5 | const finalKeys = [ 6 | 'name', 7 | 'version', 8 | 'author', 9 | 'repository', 10 | 'description', 11 | 'keywords', 12 | 'license', 13 | 'module', 14 | 'es2015', 15 | 'typings', 16 | 'peerDependencies', 17 | ]; 18 | 19 | for (const key of Object.keys(pkg)) { 20 | if (!finalKeys.includes(key)) { 21 | continue; 22 | } 23 | finalPkg[key] = pkg[key]; 24 | } 25 | 26 | fs.writeFileSync('./dist/package.json', JSON.stringify(finalPkg, null, 2)); 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recaptcha/recaptcha.module'; 2 | -------------------------------------------------------------------------------- /src/recaptcha/recaptcha.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ControlValueAccessor, 3 | AbstractControl, 4 | NgControl, 5 | } from '@angular/forms'; 6 | import { 7 | Inject, 8 | Output, 9 | EventEmitter, 10 | Component, 11 | ViewChild, 12 | ElementRef, 13 | Self, 14 | Optional, 15 | OnInit, 16 | NgZone, 17 | ChangeDetectorRef, 18 | Injectable, 19 | OnDestroy, 20 | ChangeDetectionStrategy, 21 | } from '@angular/core'; 22 | 23 | import { RECAPTCHA_CONFIG } from './recaptcha.tokens'; 24 | import { RecaptchaModuleConfig } from './recaptcha.module'; 25 | 26 | export interface InjectAndLoadScriptConfig { 27 | scriptSrc: string; 28 | onLoadCallback(): void; 29 | onErrorCallback(err: ErrorEvent): void; 30 | } 31 | 32 | @Injectable() 33 | export class ScriptLoaderService { 34 | injectAndLoadScript(config: InjectAndLoadScriptConfig) { 35 | const script = document.createElement('script'); 36 | script.src = config.scriptSrc; 37 | script.async = true; 38 | script.defer = true; 39 | script.onload = () => config.onLoadCallback(); 40 | script.onerror = err => config.onErrorCallback(err); 41 | document.body.appendChild(script); 42 | } 43 | } 44 | 45 | @Component({ 46 | selector: 'recaptcha', 47 | changeDetection: ChangeDetectionStrategy.OnPush, 48 | template: ` 49 |
50 | `, 51 | providers: [ScriptLoaderService], 52 | }) 53 | export class RecaptchaComponent 54 | implements OnInit, OnDestroy, ControlValueAccessor { 55 | @Output() scriptLoad = new EventEmitter(); 56 | @Output() scriptError = new EventEmitter(); 57 | 58 | @ViewChild('container') container: ElementRef; 59 | 60 | private readonly GLOBAL_ON_LOAD_CALLBACK_NAME = '___recaptchaOnLoadCallback___'; 61 | private onChange: (val: true | false) => void; 62 | private onTouched: () => void; 63 | private activeRecaptchaId: string; 64 | private recaptchaAPI: { 65 | render: (elementId: string, opts: any) => string; 66 | getResponse: (widgetId: string) => any; 67 | }; 68 | 69 | constructor( 70 | @Inject(RECAPTCHA_CONFIG) private recaptchaConfig: RecaptchaModuleConfig, 71 | @Self() 72 | @Optional() 73 | private controlDir: NgControl, 74 | private scriptLoaderService: ScriptLoaderService, 75 | private zone: NgZone, 76 | private cd: ChangeDetectorRef, 77 | ) { 78 | this.controlDir.valueAccessor = this; 79 | } 80 | 81 | ngOnInit() { 82 | const control = this.controlDir.control; 83 | if (!control) { 84 | return; 85 | } 86 | this.setGlobalHandlers(); 87 | this.injectGoogleRecaptchaScript(); 88 | /** 89 | * Only one validator (specifically our one below) makes sense for this Control, so we just overwrite 90 | * whatever was previously set 91 | */ 92 | control.setValidators((ctrl: AbstractControl) => { 93 | if (typeof this.activeRecaptchaId === 'undefined' || !this.recaptchaAPI) { 94 | return { 95 | invalidRecaptcha: true, 96 | }; 97 | } 98 | const recaptchaResponse = this.recaptchaAPI.getResponse( 99 | this.activeRecaptchaId, 100 | ); 101 | if (!recaptchaResponse) { 102 | return { 103 | invalidRecaptcha: true, 104 | }; 105 | } 106 | return null; 107 | }); 108 | control.updateValueAndValidity(); 109 | } 110 | 111 | ngOnDestroy() { 112 | this.unsetGlobalHandlers(); 113 | } 114 | 115 | /** 116 | * There is currently no way to programmatically set the value of 117 | * a visible reCAPTCHA, so this is a noop 118 | */ 119 | writeValue(val: any): void {} 120 | 121 | /** 122 | * Required method of the ControlValueAccessor interface, we register the callback 123 | * function that should be called whenever the model value changes 124 | */ 125 | registerOnChange(fn: (val: any) => void): void { 126 | this.onChange = fn; 127 | } 128 | 129 | /** 130 | * Required method of the ControlValueAccessor interface, we register the callback 131 | * function that should be called whenever the control is "touched" 132 | */ 133 | registerOnTouched(fn: () => void): void { 134 | this.onTouched = fn; 135 | } 136 | 137 | /** 138 | * Unfortunately we have to register a global handler for the onload 139 | * event from the recaptcha lib 140 | */ 141 | private setGlobalHandlers(): void { 142 | (window as any)[this.GLOBAL_ON_LOAD_CALLBACK_NAME] = () => { 143 | /** 144 | * Make it easier to add type information to, and work with, the recaptcha lib 145 | * by storing a single reference to it 146 | */ 147 | this.recaptchaAPI = (window as any).grecaptcha; 148 | this.renderRecaptcha(); 149 | }; 150 | } 151 | 152 | private unsetGlobalHandlers(): void { 153 | delete (window as any)[this.GLOBAL_ON_LOAD_CALLBACK_NAME]; 154 | } 155 | 156 | /** 157 | * Create a