diff --git a/.eslintignore b/.eslintignore
index 577eb860..dfb2b8a2 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,4 +2,7 @@ dist
node_modules
package-lock.json
*.md
-*.min.js
\ No newline at end of file
+*.min.js
+
+content/blog/**/*
+public/content/blog/**/*
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/angular.json b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/angular.json
new file mode 100644
index 00000000..7a03df95
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/angular.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "demo": {
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "prefix": "app",
+ "schematics": {},
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/demo",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.app.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "extractCss": true,
+ "namedChunks": false,
+ "aot": true,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "demo:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "demo:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "demo:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.spec.json",
+ "karmaConfig": "src/karma.conf.js",
+ "styles": [
+ "styles.css"
+ ],
+ "scripts": [],
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "src/tsconfig.app.json",
+ "src/tsconfig.spec.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "demo"
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/package.json b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/package.json
new file mode 100644
index 00000000..0a44f86f
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "angular",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "9.1.0",
+ "@angular/common": "^9.1.0",
+ "@angular/compiler": "^9.1.0",
+ "@angular/core": "^9.1.0",
+ "@angular/forms": "^9.1.0",
+ "@angular/platform-browser": "^9.1.0",
+ "@angular/platform-browser-dynamic": "^9.1.0",
+ "@angular/router": "^9.1.0",
+ "core-js": "^3.6.4",
+ "rxjs": "^6.5.4",
+ "tslib": "^1.10.0",
+ "zone.js": "^0.10.3"
+ },
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "test": "ng test",
+ "lint": "ng lint",
+ "e2e": "ng e2e"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "~0.10.0",
+ "@angular/cli": "~7.0.2",
+ "@angular/compiler-cli": "~7.0.0",
+ "@angular/language-service": "~7.0.0",
+ "@types/node": "~8.9.4",
+ "@types/jasmine": "~2.8.8",
+ "@types/jasminewd2": "~2.0.3",
+ "codelyzer": "~4.5.0",
+ "jasmine-core": "~2.99.1",
+ "jasmine-spec-reporter": "~4.2.1",
+ "karma": "~3.0.0",
+ "karma-chrome-launcher": "~2.2.0",
+ "karma-coverage-istanbul-reporter": "~2.0.1",
+ "karma-jasmine": "~1.1.2",
+ "karma-jasmine-html-reporter": "^0.2.2",
+ "protractor": "~5.4.0",
+ "ts-node": "~7.0.0",
+ "tslint": "~5.11.0",
+ "typescript": "~3.1.1"
+ }
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.css b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.css
new file mode 100644
index 00000000..b7ef084c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.css
@@ -0,0 +1,3 @@
+p {
+ font-family: Lato;
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.html b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.html
new file mode 100644
index 00000000..217ff5a2
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.html
@@ -0,0 +1 @@
+
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.ts b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.ts
new file mode 100644
index 00000000..d3dd0075
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'my-app',
+ templateUrl: './app.component.html',
+ styleUrls: [ './app.component.css' ]
+})
+export class AppComponent {
+ name = 'Angular';
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.module.ts b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.module.ts
new file mode 100644
index 00000000..3b12e29b
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/app.module.ts
@@ -0,0 +1,14 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { FormsModule } from '@angular/forms';
+
+import { AppComponent } from './app.component';
+import { HelloComponent } from './hello.component';
+import { ExampleInputComponent } from './example-input/example-input.component';
+
+@NgModule({
+ imports: [ BrowserModule, FormsModule ],
+ declarations: [ AppComponent, HelloComponent, ExampleInputComponent ],
+ bootstrap: [ AppComponent ]
+})
+export class AppModule { }
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/example-input/example-input.component.css b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/example-input/example-input.component.css
new file mode 100644
index 00000000..a63e51c1
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/example-input/example-input.component.css
@@ -0,0 +1,67 @@
+.inputContainer {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ }
+
+.inputLabel {
+ font-size: 1.2rem;
+ margin-top: 0;
+ margin-bottom: 0.4rem;
+}
+
+.inputInput {
+ color: #132a58;
+ padding: 1rem 0.5rem;
+ flex-grow: 1;
+ display: block;
+ border: 1px solid rgba(0, 51, 153, 0.2);
+ border-radius: 8px;
+ background: transparent;
+}
+
+.hiddenMessage.hideTheMessage {
+ opacity: 0;
+ height: 0;
+}
+
+.hiddenMessage {
+ animation: shake 2s ease-in-out;
+ animation-iteration-count:infinite;
+ opacity: 1;
+ transition: opacity 200ms ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hiddenMessage {
+ animation: none;
+ transition: none;
+ }
+}
+
+@keyframes shake {
+ 10%, 90% {
+ transform: translate3d(-2px, -2px, 0);
+ }
+
+ 20%, 80% {
+ transform: translate3d(2px, 0px, 0);
+ }
+
+ 30%, 50%, 70% {
+ transform: translate3d(-4px, 0, 0);
+ }
+
+ 40%, 60% {
+ transform: translate3d(4px, 4px, 0);
+ }
+}
+
+.visually-hidden {
+ position: absolute !important;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap; /* added line */
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/example-input/example-input.component.ts b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/example-input/example-input.component.ts
new file mode 100644
index 00000000..514712a6
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/example-input/example-input.component.ts
@@ -0,0 +1,38 @@
+import { Component, Input } from "@angular/core";
+
+@Component({
+ selector: "app-example-input",
+ template: `
+
+ {{ placeholder }}
+
+
+
+ You unlocked the secret unicorn rave!🦄🦄🦄
+
+
+ {{
+ isSecretValue
+ ? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name"
+ : ""
+ }}
+
+ `,
+ styleUrls: ["./example-input.component.css"]
+})
+export class ExampleInputComponent {
+ @Input() placeholder: string;
+ value: any = "";
+
+ get isSecretValue() {
+ return /unicorns/.exec(this.value.toLowerCase());
+ }
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/hello.component.ts b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/hello.component.ts
new file mode 100644
index 00000000..bbc9aa90
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/app/hello.component.ts
@@ -0,0 +1,10 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'hello',
+ template: `Hello {{name}}! `,
+ styles: [`h1 { font-family: Lato; }`]
+})
+export class HelloComponent {
+ @Input() name: string;
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/index.html b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/index.html
new file mode 100644
index 00000000..7462441f
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/index.html
@@ -0,0 +1,8 @@
+
+
+ Angular App
+
+
+ loading
+
+
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/karma.conf.js b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/karma.conf.js
new file mode 100644
index 00000000..b0d5cbe0
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/karma.conf.js
@@ -0,0 +1,32 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, './coverage/my-app'),
+ reports: ['html', 'lcovonly', 'text-summary'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false,
+ restartOnFileChange: true
+ });
+};
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/main.ts b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/main.ts
new file mode 100644
index 00000000..955a7fb1
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/main.ts
@@ -0,0 +1,16 @@
+import './polyfills';
+
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
+ // Ensure Angular destroys itself on hot reloads.
+ if (window['ngRef']) {
+ window['ngRef'].destroy();
+ }
+ window['ngRef'] = ref;
+
+ // Otherwise, log the boot error
+}).catch(err => console.error(err));
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/polyfills.ts b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/polyfills.ts
new file mode 100644
index 00000000..9c15394c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/polyfills.ts
@@ -0,0 +1,71 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/set';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following to support `@angular/animation`. */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+/** Evergreen browsers require these. **/
+// import 'core-js/es6/reflect';
+// import 'core-js/es7/reflect';
+
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+
+/***************************************************************************************************
+ * Zone JS is required by Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+
+/**
+ * Date, currency, decimal and percent pipes.
+ * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
+ */
+// import 'intl'; // Run `npm install --save intl`.
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/styles.css b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/styles.css
new file mode 100644
index 00000000..74f71b07
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/styles.css
@@ -0,0 +1 @@
+/* Add application styles & imports to this file! */
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/tsconfig.app.json b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/tsconfig.app.json
new file mode 100644
index 00000000..f761e8b2
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "main.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.d.ts"
+ ]
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/tsconfig.spec.json b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/tsconfig.spec.json
new file mode 100644
index 00000000..de773363
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/src/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/tsconfig.json b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/tsconfig.json
new file mode 100644
index 00000000..8c4ef3bb
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-unicorns-text-input/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "es2015",
+ "lib": [
+ "es2018",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "fullTemplateTypeCheck": true,
+ "strictInjectionParameters": true
+ }
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/angular.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/angular.json
new file mode 100644
index 00000000..7a03df95
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/angular.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "demo": {
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "prefix": "app",
+ "schematics": {},
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/demo",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.app.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "extractCss": true,
+ "namedChunks": false,
+ "aot": true,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "demo:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "demo:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "demo:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.spec.json",
+ "karmaConfig": "src/karma.conf.js",
+ "styles": [
+ "styles.css"
+ ],
+ "scripts": [],
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "src/tsconfig.app.json",
+ "src/tsconfig.spec.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "demo"
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/package.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/package.json
new file mode 100644
index 00000000..c12842cc
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "angular",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "9.1.9",
+ "@angular/common": "^9.1.4",
+ "@angular/compiler": "9.1.9",
+ "@angular/core": "^9.1.4",
+ "@angular/forms": "^9.1.4",
+ "@angular/platform-browser": "^9.1.4",
+ "@angular/platform-browser-dynamic": "^9.1.9",
+ "@angular/router": "^9.1.4",
+ "rxjs": "^6.5.5",
+ "tslib": "^1.11.1",
+ "zone.js": "^0.10.3"
+ },
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "test": "ng test",
+ "lint": "ng lint",
+ "e2e": "ng e2e"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "~0.10.0",
+ "@angular/cli": "9.0.0-rc.7",
+ "@angular/compiler-cli": "9.0.0-rc.7",
+ "@angular/language-service": "~7.0.0",
+ "@types/node": "~8.9.4",
+ "@types/jasmine": "~2.8.8",
+ "@types/jasminewd2": "~2.0.3",
+ "codelyzer": "~4.5.0",
+ "jasmine-core": "~2.99.1",
+ "jasmine-spec-reporter": "~4.2.1",
+ "karma": "~3.0.0",
+ "karma-chrome-launcher": "~2.2.0",
+ "karma-coverage-istanbul-reporter": "~2.0.1",
+ "karma-jasmine": "~1.1.2",
+ "karma-jasmine-html-reporter": "^0.2.2",
+ "protractor": "~5.4.0",
+ "ts-node": "~7.0.0",
+ "tslint": "~5.11.0",
+ "typescript": "~3.1.1"
+ }
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.css b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.css
new file mode 100644
index 00000000..b7ef084c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.css
@@ -0,0 +1,3 @@
+p {
+ font-family: Lato;
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.html b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.html
new file mode 100644
index 00000000..20dca523
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.html
@@ -0,0 +1,3 @@
+Form Control
+
+The value of the input is: {{control.value}}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.ts
new file mode 100644
index 00000000..b5c7545a
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+import {FormControl, Validators} from '@angular/forms';
+
+@Component({
+ selector: 'my-app',
+ templateUrl: './app.component.html',
+ styleUrls: [ './app.component.css' ]
+})
+export class AppComponent {
+ control = new FormControl('', Validators.required);
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.module.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.module.ts
new file mode 100644
index 00000000..3e69ff34
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/app.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { FormsModule } from '@angular/forms';
+
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { AppComponent } from './app.component';
+import { ExampleInputComponent } from './example-input/example-input.component';
+
+@NgModule({
+ imports: [ ReactiveFormsModule, FormsModule, BrowserModule ],
+ declarations: [ AppComponent, ExampleInputComponent ],
+ bootstrap: [ AppComponent ]
+})
+export class AppModule { }
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.css b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.css
new file mode 100644
index 00000000..589365d6
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.css
@@ -0,0 +1,75 @@
+.inputContainer {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ }
+
+.inputLabel {
+ font-size: 1.2rem;
+ margin-top: 0;
+ margin-bottom: 0.4rem;
+}
+
+.inputInput {
+ color: #132a58;
+ padding: 1rem 0.5rem;
+ flex-grow: 1;
+ display: block;
+ border: 1px solid rgba(0, 51, 153, 0.2);
+ border-radius: 8px;
+ background: transparent;
+}
+
+.redtext {
+ color: red;
+}
+
+.redoutline {
+ border-color: red;
+}
+
+.hiddenMessage.hideTheMessage {
+ opacity: 0;
+ height: 0;
+}
+
+.hiddenMessage {
+ animation: shake 2s ease-in-out;
+ animation-iteration-count:infinite;
+ opacity: 1;
+ transition: opacity 200ms ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hiddenMessage {
+ animation: none;
+ transition: none;
+ }
+}
+
+@keyframes shake {
+ 10%, 90% {
+ transform: translate3d(-2px, -2px, 0);
+ }
+
+ 20%, 80% {
+ transform: translate3d(2px, 0px, 0);
+ }
+
+ 30%, 50%, 70% {
+ transform: translate3d(-4px, 0, 0);
+ }
+
+ 40%, 60% {
+ transform: translate3d(4px, 4px, 0);
+ }
+}
+
+.visually-hidden {
+ position: absolute !important;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap; /* added line */
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.html b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.html
new file mode 100644
index 00000000..1fec360d
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.html
@@ -0,0 +1,20 @@
+
+ {{ placeholder }}
+
+
+
+ You unlocked the secret unicorn
+ rave!🦄🦄🦄
+
+ {{isSecretValue ? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name" : ""}}
+
+You have the following errors: {{errors | json}}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.ts
new file mode 100644
index 00000000..2572c9e1
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/app/example-input/example-input.component.ts
@@ -0,0 +1,114 @@
+import {
+ Component,
+ Input,
+ ChangeDetectorRef,
+ Optional,
+ Self,
+ OnInit
+} from "@angular/core";
+import { ControlValueAccessor, NgControl } from "@angular/forms";
+
+function coerceBooleanProperty(value: any): boolean {
+ return value != null && `${value}` !== "false";
+}
+
+@Component({
+ selector: "app-example-input",
+ templateUrl: "./example-input.component.html",
+ styleUrls: ["./example-input.component.css"]
+})
+export class ExampleInputComponent
+ implements OnInit, ControlValueAccessor {
+ constructor(
+ @Optional() @Self() public ngControl: NgControl,
+ private _changeDetector: ChangeDetectorRef
+ ) {
+ if (ngControl != null) {
+ // Setting the value accessor directly (instead of using
+ // the providers) to avoid running into a circular import.
+ ngControl.valueAccessor = this;
+ }
+ }
+
+ @Input() placeholder: string;
+
+ private _value: any = "";
+ private _disabled: boolean = false;
+
+ /** The method to be called in order to update ngModel */
+ _controlValueAccessorChangeFn: (value: any) => void = () => {};
+
+ /**
+ * onTouch function registered via registerOnTouch (ControlValueAccessor).
+ */
+ onTouched: () => any = () => {};
+
+ get isSecretValue() {
+ return /unicorns/.exec(this._value);
+ }
+
+ ngOnInit() {
+ const control = this.ngControl && this.ngControl.control;
+ if (control) {
+ console.log("ngOnInit", control);
+ // FormControl should be available here
+ }
+ }
+
+ get errors() {
+ const control = this.ngControl && this.ngControl.control;
+ if (control) {
+ return control.touched && control.errors;
+ }
+ return null;
+ }
+
+ /**
+ * Getters and setters for internal values
+ */
+
+ @Input()
+ get value(): any {
+ return this._value;
+ }
+ set value(newValue: any) {
+ if (this._value !== newValue) {
+ // Set this before proceeding to ensure no circular loop occurs with selection.
+ this._value = newValue;
+ }
+ }
+
+ @Input()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+ set disabled(value) {
+ this._disabled = coerceBooleanProperty(value);
+ }
+
+ onChange(event: any) {
+ this._controlValueAccessorChangeFn(event.target.value);
+ }
+
+ /**
+ * Methods from the ControlValueAccessor
+ */
+ writeValue(value: any) {
+ this.value = value;
+ this._changeDetector.markForCheck();
+ }
+
+ registerOnChange(fn: (value: any) => void) {
+ this._controlValueAccessorChangeFn = fn;
+ }
+
+ registerOnTouched(fn: any) {
+ this.onTouched = fn;
+ }
+
+ // Optional
+ setDisabledState(isDisabled: boolean) {
+ this.disabled = isDisabled;
+ this._changeDetector.markForCheck();
+ }
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/index.html b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/index.html
new file mode 100644
index 00000000..7462441f
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/index.html
@@ -0,0 +1,8 @@
+
+
+ Angular App
+
+
+ loading
+
+
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/karma.conf.js b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/karma.conf.js
new file mode 100644
index 00000000..b0d5cbe0
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/karma.conf.js
@@ -0,0 +1,32 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, './coverage/my-app'),
+ reports: ['html', 'lcovonly', 'text-summary'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false,
+ restartOnFileChange: true
+ });
+};
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/main.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/main.ts
new file mode 100644
index 00000000..955a7fb1
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/main.ts
@@ -0,0 +1,16 @@
+import './polyfills';
+
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
+ // Ensure Angular destroys itself on hot reloads.
+ if (window['ngRef']) {
+ window['ngRef'].destroy();
+ }
+ window['ngRef'] = ref;
+
+ // Otherwise, log the boot error
+}).catch(err => console.error(err));
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/polyfills.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/polyfills.ts
new file mode 100644
index 00000000..9c15394c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/polyfills.ts
@@ -0,0 +1,71 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/set';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following to support `@angular/animation`. */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+/** Evergreen browsers require these. **/
+// import 'core-js/es6/reflect';
+// import 'core-js/es7/reflect';
+
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+
+/***************************************************************************************************
+ * Zone JS is required by Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+
+/**
+ * Date, currency, decimal and percent pipes.
+ * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
+ */
+// import 'intl'; // Run `npm install --save intl`.
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/styles.css b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/styles.css
new file mode 100644
index 00000000..74f71b07
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/styles.css
@@ -0,0 +1 @@
+/* Add application styles & imports to this file! */
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/tsconfig.app.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/tsconfig.app.json
new file mode 100644
index 00000000..f761e8b2
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "main.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.d.ts"
+ ]
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/tsconfig.spec.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/tsconfig.spec.json
new file mode 100644
index 00000000..de773363
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/src/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/tsconfig.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/tsconfig.json
new file mode 100644
index 00000000..f3c48687
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-dep-inject/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "es2015",
+ "typeRoots": [
+ "node_modules/@types"
+ ],
+ "lib": [
+ "es2018",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "enableIvy": true,
+ "fullTemplateTypeCheck": true,
+ "strictInjectionParameters": true
+ }
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/angular.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/angular.json
new file mode 100644
index 00000000..7a03df95
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/angular.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "demo": {
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "prefix": "app",
+ "schematics": {},
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/demo",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.app.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "extractCss": true,
+ "namedChunks": false,
+ "aot": true,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "demo:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "demo:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "demo:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.spec.json",
+ "karmaConfig": "src/karma.conf.js",
+ "styles": [
+ "styles.css"
+ ],
+ "scripts": [],
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "src/tsconfig.app.json",
+ "src/tsconfig.spec.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "demo"
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/package.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/package.json
new file mode 100644
index 00000000..c12842cc
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "angular",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "9.1.9",
+ "@angular/common": "^9.1.4",
+ "@angular/compiler": "9.1.9",
+ "@angular/core": "^9.1.4",
+ "@angular/forms": "^9.1.4",
+ "@angular/platform-browser": "^9.1.4",
+ "@angular/platform-browser-dynamic": "^9.1.9",
+ "@angular/router": "^9.1.4",
+ "rxjs": "^6.5.5",
+ "tslib": "^1.11.1",
+ "zone.js": "^0.10.3"
+ },
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "test": "ng test",
+ "lint": "ng lint",
+ "e2e": "ng e2e"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "~0.10.0",
+ "@angular/cli": "9.0.0-rc.7",
+ "@angular/compiler-cli": "9.0.0-rc.7",
+ "@angular/language-service": "~7.0.0",
+ "@types/node": "~8.9.4",
+ "@types/jasmine": "~2.8.8",
+ "@types/jasminewd2": "~2.0.3",
+ "codelyzer": "~4.5.0",
+ "jasmine-core": "~2.99.1",
+ "jasmine-spec-reporter": "~4.2.1",
+ "karma": "~3.0.0",
+ "karma-chrome-launcher": "~2.2.0",
+ "karma-coverage-istanbul-reporter": "~2.0.1",
+ "karma-jasmine": "~1.1.2",
+ "karma-jasmine-html-reporter": "^0.2.2",
+ "protractor": "~5.4.0",
+ "ts-node": "~7.0.0",
+ "tslint": "~5.11.0",
+ "typescript": "~3.1.1"
+ }
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.css b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.css
new file mode 100644
index 00000000..b7ef084c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.css
@@ -0,0 +1,3 @@
+p {
+ font-family: Lato;
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.html b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.html
new file mode 100644
index 00000000..fa0b0c3c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.html
@@ -0,0 +1,6 @@
+Form Control
+
+The value of the input is: {{control.value}}
+ngModel
+
+The value of the input is: {{modelValue}}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.ts
new file mode 100644
index 00000000..7d364a59
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.component.ts
@@ -0,0 +1,12 @@
+import { Component, VERSION } from '@angular/core';
+import {FormControl} from '@angular/forms';
+
+@Component({
+ selector: 'my-app',
+ templateUrl: './app.component.html',
+ styleUrls: [ './app.component.css' ]
+})
+export class AppComponent {
+ control = new FormControl('');
+ modelValue = "";
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.module.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.module.ts
new file mode 100644
index 00000000..3e69ff34
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/app.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { FormsModule } from '@angular/forms';
+
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { AppComponent } from './app.component';
+import { ExampleInputComponent } from './example-input/example-input.component';
+
+@NgModule({
+ imports: [ ReactiveFormsModule, FormsModule, BrowserModule ],
+ declarations: [ AppComponent, ExampleInputComponent ],
+ bootstrap: [ AppComponent ]
+})
+export class AppModule { }
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.css b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.css
new file mode 100644
index 00000000..5649ebc9
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.css
@@ -0,0 +1,67 @@
+.inputContainer {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ }
+
+.inputLabel {
+ font-size: 1.2rem;
+ margin-top: 0;
+ margin-bottom: 0.4rem;
+}
+
+.inputInput {
+ color: #132a58;
+ padding: 1rem 0.5rem;
+ flex-grow: 1;
+ display: block;
+ border: 1px solid rgba(0, 51, 153, 0.2);
+ border-radius: 8px;
+ background: transparent;
+}
+
+.hiddenMessage.hideTheMessage {
+ opacity: 0;
+ height: 0;
+}
+
+.hiddenMessage {
+ animation: shake 2s ease-in-out;
+ animation-iteration-count:infinite;
+ opacity: 1;
+ transition: opacity 200ms ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hiddenMessage {
+ animation: none;
+ transition: none;
+ }
+}
+
+@keyframes shake {
+ 10%, 90% {
+ transform: translate3d(-2px, -2px, 0);
+ }
+
+ 20%, 80% {
+ transform: translate3d(2px, 0px, 0);
+ }
+
+ 30%, 50%, 70% {
+ transform: translate3d(-4px, 0, 0);
+ }
+
+ 40%, 60% {
+ transform: translate3d(4px, 4px, 0);
+ }
+}
+
+.visually-hidden {
+ position: absolute !important;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap; /* added line */
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.html b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.html
new file mode 100644
index 00000000..495f51aa
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.html
@@ -0,0 +1,17 @@
+
+ {{ placeholder }}
+
+
+
+ You unlocked the secret unicorn
+ rave!🦄🦄🦄
+
+ {{isSecretValue ? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name" : ""}}
+
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.ts
new file mode 100644
index 00000000..5b3d545d
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/app/example-input/example-input.component.ts
@@ -0,0 +1,96 @@
+import { Component, forwardRef, Input, ChangeDetectorRef } from '@angular/core';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+
+function coerceBooleanProperty(value: any): boolean {
+ return value != null && `${value}` !== 'false';
+}
+
+/**
+ * Provider Expression that allows your component to register as a ControlValueAccessor. This
+ * allows it to support [(ngModel)] and ngControl.
+ */
+export const EXAMPLE_CONTROL_VALUE_ACCESSOR: any = {
+ /**
+ * Used to provide a `ControlValueAccessor` for form controls.
+ */
+ provide: NG_VALUE_ACCESSOR,
+ /**
+ * Allows to refer to references which are not yet defined.
+ * This is because it's needed to `providers` in the component but references
+ * the component itself. Handles circular dependency issues
+ */
+ useExisting: forwardRef(() => ExampleInputComponent),
+ multi: true
+};
+
+@Component({
+ selector: 'app-example-input',
+ templateUrl: './example-input.component.html',
+ styleUrls: ['./example-input.component.css'],
+ providers: [EXAMPLE_CONTROL_VALUE_ACCESSOR]
+})
+export class ExampleInputComponent implements ControlValueAccessor {
+ @Input() placeholder: string;
+
+ private _value: any = "";
+ private _disabled: boolean = false;
+
+ /** The method to be called in order to update ngModel */
+ _controlValueAccessorChangeFn: (value: any) => void = () => {};
+
+ /**
+ * onTouch function registered via registerOnTouch (ControlValueAccessor).
+ */
+ onTouched: () => any = () => {};
+
+ constructor(private _changeDetector: ChangeDetectorRef) { }
+
+ get isSecretValue() {
+ return /unicorns/.exec(this._value);
+ }
+
+ /**
+ * Getters and setters for internal values
+ */
+
+ @Input()
+ get value(): any { return this._value; }
+ set value(newValue: any) {
+ if (this._value !== newValue) {
+ // Set this before proceeding to ensure no circular loop occurs with selection.
+ this._value = newValue;
+ }
+ }
+
+ @Input()
+ get disabled(): boolean { return this._disabled; }
+ set disabled(value) {
+ this._disabled = coerceBooleanProperty(value);
+ }
+
+ onChange(event: any) {
+ this._controlValueAccessorChangeFn(event.target.value);
+ }
+
+ /**
+ * Methods from the ControlValueAccessor
+ */
+ writeValue(value: any) {
+ this.value = value;
+ this._changeDetector.markForCheck();
+ }
+
+ registerOnChange(fn: (value: any) => void) {
+ this._controlValueAccessorChangeFn = fn;
+ }
+
+ registerOnTouched(fn: any) {
+ this.onTouched = fn;
+ }
+
+ // Optional
+ setDisabledState(isDisabled: boolean) {
+ this.disabled = isDisabled;
+ this._changeDetector.markForCheck();
+ }
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/index.html b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/index.html
new file mode 100644
index 00000000..7462441f
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/index.html
@@ -0,0 +1,8 @@
+
+
+ Angular App
+
+
+ loading
+
+
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/karma.conf.js b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/karma.conf.js
new file mode 100644
index 00000000..b0d5cbe0
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/karma.conf.js
@@ -0,0 +1,32 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, './coverage/my-app'),
+ reports: ['html', 'lcovonly', 'text-summary'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false,
+ restartOnFileChange: true
+ });
+};
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/main.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/main.ts
new file mode 100644
index 00000000..955a7fb1
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/main.ts
@@ -0,0 +1,16 @@
+import './polyfills';
+
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
+ // Ensure Angular destroys itself on hot reloads.
+ if (window['ngRef']) {
+ window['ngRef'].destroy();
+ }
+ window['ngRef'] = ref;
+
+ // Otherwise, log the boot error
+}).catch(err => console.error(err));
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/polyfills.ts b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/polyfills.ts
new file mode 100644
index 00000000..9c15394c
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/polyfills.ts
@@ -0,0 +1,71 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/set';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following to support `@angular/animation`. */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+/** Evergreen browsers require these. **/
+// import 'core-js/es6/reflect';
+// import 'core-js/es7/reflect';
+
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+
+
+/***************************************************************************************************
+ * Zone JS is required by Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+
+/**
+ * Date, currency, decimal and percent pipes.
+ * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
+ */
+// import 'intl'; // Run `npm install --save intl`.
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/styles.css b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/styles.css
new file mode 100644
index 00000000..74f71b07
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/styles.css
@@ -0,0 +1 @@
+/* Add application styles & imports to this file! */
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/tsconfig.app.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/tsconfig.app.json
new file mode 100644
index 00000000..f761e8b2
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "main.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.d.ts"
+ ]
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/tsconfig.spec.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/tsconfig.spec.json
new file mode 100644
index 00000000..de773363
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/src/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/tsconfig.json b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/tsconfig.json
new file mode 100644
index 00000000..f3c48687
--- /dev/null
+++ b/content/blog/angular-components-control-value-accessor/angular-value-accessor-example/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "es2015",
+ "typeRoots": [
+ "node_modules/@types"
+ ],
+ "lib": [
+ "es2018",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "enableIvy": true,
+ "fullTemplateTypeCheck": true,
+ "strictInjectionParameters": true
+ }
+}
\ No newline at end of file
diff --git a/content/blog/angular-components-control-value-accessor/index.md b/content/blog/angular-components-control-value-accessor/index.md
index 399855bc..6d4fda93 100644
--- a/content/blog/angular-components-control-value-accessor/index.md
+++ b/content/blog/angular-components-control-value-accessor/index.md
@@ -70,7 +70,7 @@ export class ExampleInputComponent {
With only a bit of CSS, we have a visually appealing, A11Y friendly, and quirky input component. Look, it even wiggles the unicorns!
-
+
Now, this component is far from feature complete. There's no way to `disable` the input, there's no way to extract data out from the typed input, there's not a lot of functionality you'd typically expect to see from an input component. Let's change that.
@@ -303,7 +303,7 @@ Finally, you can pass these options to `ngModel` and `formControl` (or even `for
If done properly, you should see something like this:
-
+
# Form Control Classes
@@ -407,7 +407,7 @@ export class AppComponent {
}
```
-
+
Not only do you have [a wide range of Angular-built validators at your disposal](https://angular.io/api/forms/Validators), but you're even able to [make your own validator](https://angular.io/api/forms/Validator)!
diff --git a/package-lock.json b/package-lock.json
index 075202bc..ced3cc06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/preact": "^3.2.3",
"@testing-library/user-event": "^14.4.3",
+ "@types/git-branch": "^2.0.5",
"@types/hast": "^3.0.0",
"@types/html-escaper": "^3.0.0",
"@types/jest": "^29.5.2",
@@ -51,6 +52,7 @@
"eslint-plugin-jsx-a11y": "^6.7.1",
"feed": "^4.2.2",
"fuse.js": "^6.6.2",
+ "git-branch": "^2.0.1",
"gray-matter": "^4.0.3",
"hast-util-from-html": "^2.0.1",
"hast-util-has-property": "^3.0.0",
@@ -7111,6 +7113,12 @@
"@types/estree": "*"
}
},
+ "node_modules/@types/git-branch": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/git-branch/-/git-branch-2.0.5.tgz",
+ "integrity": "sha512-gNZyvixgeXQLQnFkMUAO47sBnQBzJ/pPMJNuOZUD4bvh3O6taXjpEbugjz2qBjNz4ZFH2mVQ42WJzImEiio1Qg==",
+ "dev": true
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@@ -11524,6 +11532,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
@@ -12872,6 +12889,18 @@
"node": ">=6"
}
},
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/expect": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.6.1.tgz",
@@ -13340,6 +13369,161 @@
"pkg-dir": "^4.2.0"
}
},
+ "node_modules/findup-sync": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+ "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==",
+ "dev": true,
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/findup-sync/node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==",
+ "dev": true,
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "node_modules/findup-sync/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "dev": true,
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/micromatch/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "dev": true,
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync/node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -13699,6 +13883,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/git-branch": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/git-branch/-/git-branch-2.0.1.tgz",
+ "integrity": "sha512-jMCT1kjXvsUdZKQd2p8E1uZhKsIuR1pnHgcDYQpQiXBtzE9cmYGvOcCSGqqi58x0B9CPS0lUSu/yti866est8g==",
+ "dev": true,
+ "dependencies": {
+ "findup-sync": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -13740,6 +13936,48 @@
"node": ">= 6"
}
},
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -14413,6 +14651,18 @@
"integrity": "sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==",
"dev": true
},
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -23802,6 +24052,15 @@
"node": ">=6"
}
},
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -25386,6 +25645,19 @@
"node": ">=8"
}
},
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
diff --git a/package.json b/package.json
index 2b411bc0..f6f9a98e 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/preact": "^3.2.3",
"@testing-library/user-event": "^14.4.3",
+ "@types/git-branch": "^2.0.5",
"@types/hast": "^3.0.0",
"@types/html-escaper": "^3.0.0",
"@types/jest": "^29.5.2",
@@ -75,6 +76,7 @@
"eslint-plugin-jsx-a11y": "^6.7.1",
"feed": "^4.2.2",
"fuse.js": "^6.6.2",
+ "git-branch": "^2.0.1",
"gray-matter": "^4.0.3",
"hast-util-from-html": "^2.0.1",
"hast-util-has-property": "^3.0.0",
diff --git a/src/utils/markdown/iframes/rehype-transform.ts b/src/utils/markdown/iframes/rehype-transform.ts
index dce5ab49..7a6300f9 100644
--- a/src/utils/markdown/iframes/rehype-transform.ts
+++ b/src/utils/markdown/iframes/rehype-transform.ts
@@ -1,4 +1,5 @@
import { Root, Element } from "hast";
+import { VFile } from "vfile";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
@@ -13,7 +14,9 @@ import type { GetPictureResult } from "@astrojs/image/dist/lib/get-picture";
import probe from "probe-image-size";
import { IFramePlaceholder } from "./iframe-placeholder";
-interface RehypeUnicornIFrameClickToRunProps {}
+interface RehypeUnicornIFrameClickToRunProps {
+ srcReplacements?: Array<(val: string, root: VFile) => string>;
+}
// default icon, used if a frame's favicon cannot be resolved
let defaultPageIcon: Promise;
@@ -120,7 +123,7 @@ type PageInfo = {
icon: GetPictureResult;
};
-async function fetchPageInfo(src: string): Promise {
+export async function fetchPageInfo(src: string): Promise {
// fetch origin url, catch any connection timeout errors
const url = new URL(src);
url.search = ""; // remove any search params
@@ -143,8 +146,8 @@ async function fetchPageInfo(src: string): Promise {
export const rehypeUnicornIFrameClickToRun: Plugin<
[RehypeUnicornIFrameClickToRunProps | never],
Root
-> = () => {
- return async (tree) => {
+> = ({ srcReplacements = [], ...props }) => {
+ return async (tree, file) => {
const iframeNodes: Element[] = [];
visit(tree, (node: Element) => {
if (node.tagName === "iframe") {
@@ -165,6 +168,10 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
...propsToPreserve
} = iframeNode.properties;
+ for (const replacement of srcReplacements) {
+ src = replacement(src as string, file);
+ }
+
width = width ?? EMBED_SIZE.w;
height = height ?? EMBED_SIZE.h;
const info: PageInfo = (await fetchPageInfo(
diff --git a/src/utils/markdown/index.ts b/src/utils/markdown/index.ts
index d34b9bf6..4075a3eb 100644
--- a/src/utils/markdown/index.ts
+++ b/src/utils/markdown/index.ts
@@ -1,6 +1,9 @@
import { Plugin } from "unified";
import rehypeSlug from "rehype-slug-custom-id";
import rehypeRaw from "rehype-raw";
+import { VFile } from "vfile";
+import { resolve, relative, dirname } from "path";
+import * as branch from "git-branch";
import { rehypeTabs } from "./tabs/rehype-transform";
import { rehypeTooltips } from "./tooltips/rehype-transform";
import { rehypeHints } from "./hints/rehype-transform";
@@ -20,6 +23,7 @@ import { rehypeHeaderText } from "./rehype-header-text";
import { rehypeHeaderClass } from "./rehype-header-class";
import { rehypeFileTree } from "./file-tree/rehype-file-tree";
import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform";
+import { siteMetadata } from "../../constants/site-config";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RehypePlugin = Plugin | [Plugin, any];
@@ -67,7 +71,38 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
rehypeHints,
rehypeTooltips,
rehypeAstroImageMd,
- rehypeUnicornIFrameClickToRun,
+ [
+ rehypeUnicornIFrameClickToRun,
+ {
+ srcReplacements: [
+ (val: string, file: VFile) => {
+ const iFrameUrl = new URL(val);
+ if (!iFrameUrl.protocol.startsWith("uu-code:")) return val;
+
+ const contentDir = dirname(file.path);
+ const fullPath = resolve(contentDir, iFrameUrl.pathname);
+
+ const fsRelativePath = relative(process.cwd(), fullPath);
+
+ // Windows paths need to be converted to URLs
+ let urlRelativePath = fsRelativePath.replace(/\\/g, "/");
+
+ if (urlRelativePath.startsWith("/")) {
+ urlRelativePath = urlRelativePath.slice(1);
+ }
+
+ const q = iFrameUrl.search;
+ const currentBranch =
+ process.env.VERCEL_GIT_COMMIT_REF ?? branch.sync();
+ const repoPath = siteMetadata.repoPath;
+ const provider = `stackblitz.com/github`;
+ return `
+ https://${provider}/${repoPath}/tree/${currentBranch}/${urlRelativePath}${q}
+ `.trim();
+ },
+ ],
+ },
+ ] as RehypePlugin,
rehypeUnicornElementMap,
rehypeTwoslashTabindex,
rehypeFileTree,
diff --git a/tsconfig.json b/tsconfig.json
index f3aff09d..11f933cd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -28,5 +28,6 @@
"uu-utils": ["./src/utils"],
"assets/*": ["./src/assets/*"]
}
- }
+ },
+ "exclude": ["./content/blog"]
}