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: ` + + +

+ {{ + 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 @@ + + +

+ {{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 @@ + + +

+ {{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"] }