├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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