1 基础
关于Angular的基础部分,几个核心部分和框架,在之前都写过了。Angular1--Hello-CSDN博客
Angular的几个核心部分和框架:
模板就是组件中的template,对应MVC的V。
组件类就是Component类,对应对应MVC的C。
服务就是service类,对应MVC的M。
这个理解不能说100%对,但是基本成立吧。有了这个概念,后面就好理解多了。
2 复杂一点的游戏
来自官网例程:Playground • Angular
main.ts
import {A11yModule} from '@angular/cdk/a11y';
import {CommonModule} from '@angular/common';
import {Component, ElementRef, ViewChild, computed, signal} from '@angular/core';
import {MatSlideToggleChange, MatSlideToggleModule} from '@angular/material/slide-toggle';
import {bootstrapApplication} from '@angular/platform-browser';const RESULT_QUOTES = [['Not quite right!','You missed the mark!','Have you seen an angle before?','Your measurements are all over the place!','Your precision needs work!',],['Not too shabby.', 'Getting sharper, keep it up!', 'Not perfect, but getting better!'],['Your angles are on point!','Your precision is unparalleled!','Your geometric skills are divine!',"Amazing! You're acute-y!",'Wow! So precise!',],
];const CHANGING_QUOTES = [["I'm such a-cute-y!", "I'm a tiny slice of pi!", "You're doing great!"],["I'm wide open!", 'Keep going!', 'Wow!', 'Wheee!!'],["I'm so obtuse!", 'The bigger the better!', "Life's too short for right angles!", 'Whoa!'],
];function getChangingQuote(rotateValue: number): string {let possibleQuotes = CHANGING_QUOTES[1];if (rotateValue < 110) {possibleQuotes = CHANGING_QUOTES[0];} else if (rotateValue >= 230) {possibleQuotes = CHANGING_QUOTES[2];}const randomQuoteIndex = Math.floor(Math.random() * possibleQuotes.length);return possibleQuotes[randomQuoteIndex];
}function getResultQuote(accuracy: number) {let possibleQuotes = RESULT_QUOTES[1];if (accuracy < 50) {possibleQuotes = RESULT_QUOTES[0];} else if (accuracy >= 85) {possibleQuotes = RESULT_QUOTES[2];}let randomQuoteIndex = Math.floor(Math.random() * possibleQuotes.length);return possibleQuotes[randomQuoteIndex];
}@Component({selector: 'app-root',imports: [CommonModule, MatSlideToggleModule, A11yModule],styleUrl: 'game.css',templateUrl: 'game.html',
})
export class Playground {protected readonly isGuessModalOpen = signal(false);protected readonly isAccessiblePanelOpen = signal(false);protected readonly rotateVal = signal(40);protected readonly goal = signal(85);protected readonly animatedAccuracy = signal(0);protected readonly gameStats = signal({level: 0,totalAccuracy: 0,});protected readonly resultQuote = signal('');private isDragging = false;private currentInteractions: {lastChangedAt: number; face: number; quote: string} = {lastChangedAt: 75,face: 0,quote: "Hi, I'm NG the Angle!",};@ViewChild('staticArrow') staticArrow!: ElementRef;protected readonly totalAccuracyPercentage = computed(() => {const {level, totalAccuracy} = this.gameStats();if (level === 0) {return 0;}return totalAccuracy / level;});protected readonly updatedInteractions = computed(() => {if (this.rotateVal() > 75 &&Math.abs(this.rotateVal() - this.currentInteractions.lastChangedAt) > 70 &&Math.random() > 0.5) {this.currentInteractions = {lastChangedAt: this.rotateVal(),face: Math.floor(Math.random() * 6),quote: getChangingQuote(this.rotateVal()),};}return this.currentInteractions;});constructor() {this.resetGame();}resetGame() {this.goal.set(Math.floor(Math.random() * 360));this.rotateVal.set(40);}getRotation() {return `rotate(${this.rotateVal()}deg)`;}getIndicatorStyle() {return 0.487 * this.rotateVal() - 179.5;}getIndicatorRotation() {return `rotate(${253 + this.rotateVal()}deg)`;}mouseDown() {this.isDragging = true;}stopDragging() {this.isDragging = false;}mouseMove(e: MouseEvent) {const vh30 = 0.3 * document.documentElement.clientHeight;if (!this.isDragging) return;let pointX = e.pageX - (this.staticArrow.nativeElement.offsetLeft + 2.5);let pointY = e.pageY - (this.staticArrow.nativeElement.offsetTop + vh30);let calculatedAngle = 0;if (pointX >= 0 && pointY < 0) {calculatedAngle = 90 - (Math.atan2(Math.abs(pointY), pointX) * 180) / Math.PI;} else if (pointX >= 0 && pointY >= 0) {calculatedAngle = 90 + (Math.atan2(pointY, pointX) * 180) / Math.PI;} else if (pointX < 0 && pointY >= 0) {calculatedAngle = 270 - (Math.atan2(pointY, Math.abs(pointX)) * 180) / Math.PI;} else {calculatedAngle = 270 + (Math.atan2(Math.abs(pointY), Math.abs(pointX)) * 180) / Math.PI;}this.rotateVal.set(calculatedAngle);}adjustAngle(degreeChange: number) {this.rotateVal.update((x) =>x + degreeChange < 0 ? 360 + (x + degreeChange) : (x + degreeChange) % 360,);}touchMove(e: Event) {let firstTouch = (e as TouchEvent).touches[0];if (firstTouch) {this.mouseMove({pageX: firstTouch.pageX, pageY: firstTouch.pageY} as MouseEvent);}}guess() {this.isGuessModalOpen.set(true);const calcAcc = Math.abs(100 - (Math.abs(this.goal() - this.rotateVal()) / 180) * 100);this.resultQuote.set(getResultQuote(calcAcc));this.animatedAccuracy.set(calcAcc > 20 ? calcAcc - 20 : 0);this.powerUpAccuracy(calcAcc);this.gameStats.update(({level, totalAccuracy}) => ({level: level + 1,totalAccuracy: totalAccuracy + calcAcc,}));}powerUpAccuracy(finalAcc: number) {if (this.animatedAccuracy() >= finalAcc) return;let difference = finalAcc - this.animatedAccuracy();if (difference > 20) {this.animatedAccuracy.update((x) => x + 10.52);setTimeout(() => this.powerUpAccuracy(finalAcc), 30);} else if (difference > 4) {this.animatedAccuracy.update((x) => x + 3.31);setTimeout(() => this.powerUpAccuracy(finalAcc), 40);} else if (difference > 0.5) {this.animatedAccuracy.update((x) => x + 0.49);setTimeout(() => this.powerUpAccuracy(finalAcc), 50);} else if (difference >= 0.1) {this.animatedAccuracy.update((x) => x + 0.1);setTimeout(() => this.powerUpAccuracy(finalAcc), 100);} else {this.animatedAccuracy.update((x) => x + 0.01);setTimeout(() => this.powerUpAccuracy(finalAcc), 100);}}close() {this.isGuessModalOpen.set(false);this.resetGame();}getText() {const roundedAcc = Math.floor(this.totalAccuracyPercentage() * 10) / 10;let emojiAccuracy = '';for (let i = 0; i < 5; i++) {emojiAccuracy += roundedAcc >= 20 * (i + 1) ? '🟩' : '⬜️';}return encodeURI(`📐 ${emojiAccuracy} \n My angles are ${roundedAcc}% accurate on level ${this.gameStats().level}. \n\nHow @Angular are you? \nhttps://angular.dev/playground`,);}toggleA11yControls(event: MatSlideToggleChange) {this.isAccessiblePanelOpen.set(event.checked);}
}bootstrapApplication(Playground);
game.html
<div class="wrapper"><div class="col"><h1>Goal: {{ goal() }}º</h1><div id="quote" [class.show]="rotateVal() >= 74">"{{ updatedInteractions().quote }}"</div><divid="angle"(mouseup)="stopDragging()"(mouseleave)="stopDragging()"(mousemove)="mouseMove($event)"(touchmove)="touchMove($event)"(touchend)="stopDragging()"(touchcanceled)="stopDragging()"><div class="arrow" id="static" #staticArrow><div class="center"></div>@if(rotateVal() >= 20) {<div class="svg" [style.transform]="getIndicatorRotation()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75 75"><defs><linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="var(--orange-red)" /><stop offset="50%" stop-color="var(--vivid-pink)" /><stop offset="100%" stop-color="var(--electric-violet)" /></linearGradient></defs><path[style.stroke-dashoffset]="getIndicatorStyle()"class="svg-arrow"stroke="url(#gradient)"d="m64.37,45.4c-3.41,11.62-14.15,20.1-26.87,20.1-15.46,0-28-12.54-28-28s12.54-28,28-28,28,12.54,28,28"/><polylineclass="svg-arrow"stroke="url(#gradient)"points="69.63 36.05 65.29 40.39 60.96 36.05"/></svg></div>}<div class="face"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 103.41 84.33" [class.show]="rotateVal() >= 74">@switch(updatedInteractions().face) {@case(0) {<g><path class="c" d="m65.65,55.83v11c0,7.73-6.27,14-14,14h0c-7.73,0-14-6.27-14-14v-11"/><line class="c" x1="51.52" y1="65.83" x2="51.65" y2="57.06"/><path class="c" d="m19.8,44.06c7.26,7.89,18.83,13,31.85,13s24.59-5.11,31.85-13"/><path class="b" d="m3,14.33c3.35-5.71,9.55-9.54,16.65-9.54,6.66,0,12.53,3.37,16,8.5"/><path class="b" d="m100.3,14.33c-3.35-5.71-9.55-9.54-16.65-9.54-6.66,0-12.53,3.37-16,8.5"/></g>}@case(1) {<g><path class="d" d="m22.11,48.83c-.08.65-.14,1.3-.14,1.97,0,11.94,13.37,21.62,29.87,21.62s29.87-9.68,29.87-21.62c0-.66-.06-1.32-.14-1.97H22.11Z"/><circle cx="19.26" cy="12.56" r="12.37"/><circle cx="84.25" cy="12.56" r="12.37"/><circle class="e" cx="14.86" cy="8.94" r="4.24"/><circle class="e" cx="80.29" cy="8.76" r="4.24"/></g>}@case(2) {<g><circle cx="19.2" cy="12.72" r="12.37"/><circle cx="84.19" cy="12.72" r="12.37"/><circle class="e" cx="14.8" cy="9.09" r="4.24"/><circle class="e" cx="80.22" cy="8.92" r="4.24"/><path class="c" d="m19.45,44.33c7.26,7.89,18.83,13,31.85,13s24.59-5.11,31.85-13"/></g>}@case(3) {<g><path class="b" d="m3.11,14.33c3.35-5.71,9.55-9.54,16.65-9.54,6.66,0,12.53,3.37,16,8.5"/><path class="b" d="m100.41,14.33c-3.35-5.71-9.55-9.54-16.65-9.54-6.66,0-12.53,3.37-16,8.5"/><path class="c" d="m19.91,44.06c7.26,7.89,18.83,13,31.85,13s24.59-5.11,31.85-13"/></g>}@case(4) {<g><circle cx="19.26" cy="12.5" r="12.37"/><circle class="e" cx="14.86" cy="8.88" r="4.24"/><path class="c" d="m19.51,44.11c7.26,7.89,18.83,13,31.85,13s24.59-5.11,31.85-13"/><path class="b" d="m100.08,14.33c-3.35-5.71-9.55-9.54-16.65-9.54-6.66,0-12.53,3.37-16,8.5"/></g>}@default {<g><circle cx="19.14" cy="12.44" r="12.37"/><circle cx="84.13" cy="12.44" r="12.37"/><circle class="e" cx="14.74" cy="8.82" r="4.24"/><circle class="e" cx="80.17" cy="8.64" r="4.24"/><circle class="b" cx="52.02" cy="53.33" r="14"/></g>}}</svg></div></div><divclass="grabbable"[style.transform]="getRotation()"(mousedown)="mouseDown()"(touchstart)="mouseDown()"><div class="arrow" id="moving"></div></div></div></div><div class="col"><div class="overall-stats"><h4>level: {{ gameStats().level + 1 }}</h4><h4>accuracy: {{ totalAccuracyPercentage() > 0 ? (totalAccuracyPercentage() | number : '1.1-1') + '%' : '??' }}</h4><button id="guess" class="gradient-button" (click)="guess()" [disabled]="isGuessModalOpen()"><span></span><span>guess</span></button></div></div>@if(isGuessModalOpen()) {<dialog id="result" cdkTrapFocus><button id="close" (click)="close()">X</button><div class="result-stats"><h2>goal: {{ goal() }}º</h2><h2>actual: {{ rotateVal() | number : '1.1-1' }}º</h2></div><h2 class="accuracy"><span>{{ animatedAccuracy() | number : '1.1-1' }}%</span>accurate</h2><svg class="personified" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119.07 114.91"><g><polyline class="i" points="1.5 103.62 56.44 1.5 40.73 8.68"/><line class="i" x1="59.1" y1="18.56" x2="56.44" y2="1.5"/><polyline class="i" points="1.61 103.6 117.57 102.9 103.74 92.56"/><line class="i" x1="103.86" y1="113.41" x2="117.57" y2="102.9"/><path class="i" d="m12.97,84.22c6.4,4.04,10.47,11.28,10.2,19.25"/></g>@if(animatedAccuracy() > 95) {<g><path class="i" d="m52.68,72.99c-.04.35-.07.71-.07,1.07,0,6.5,7.28,11.77,16.26,11.77s16.26-5.27,16.26-11.77c0-.36-.03-.72-.07-1.07h-32.37Z"/><circle cx="51.13" cy="53.25" r="6.73"/><circle cx="86.5" cy="53.25" r="6.73"/><circle class="g" cx="48.73" cy="51.28" r="2.31"/><circle class="g" cx="84.35" cy="51.18" r="2.31"/></g>} @else if (animatedAccuracy() > 80) {<g><path class="h" d="m52.59,70.26c3.95,4.3,10.25,7.08,17.34,7.08s13.38-2.78,17.34-7.08"/><path class="h" d="m43.44,54.08c1.82-3.11,5.2-5.19,9.06-5.19,3.62,0,6.82,1.84,8.71,4.63"/><path class="h" d="m96.41,54.08c-1.82-3.11-5.2-5.19-9.06-5.19-3.62,0-6.82,1.84-8.71,4.63"/></g>} @else if (animatedAccuracy() > 60) {<g><path class="h" d="m77.38,76.81v5.99c0,4.21-3.41,7.62-7.62,7.62h0c-4.21,0-7.62-3.41-7.62-7.62v-5.99"/><line class="h" x1="69.69" y1="82.25" x2="69.76" y2="77.47"/><path class="h" d="m52.42,70.4c3.95,4.3,10.25,7.08,17.34,7.08s13.38-2.78,17.34-7.08"/><path class="h" d="m43.28,54.21c1.82-3.11,5.2-5.19,9.06-5.19,3.62,0,6.82,1.84,8.71,4.63"/><path class="h" d="m96.24,54.21c-1.82-3.11-5.2-5.19-9.06-5.19-3.62,0-6.82,1.84-8.71,4.63"/></g>} @else if (animatedAccuracy() > 40) {<g><circle cx="51.55" cy="53.15" r="6.73"/><circle cx="86.92" cy="53.15" r="6.73"/><circle class="g" cx="49.15" cy="51.17" r="2.31"/><circle class="g" cx="84.77" cy="51.08" r="2.31"/><line class="h" x1="61.21" y1="76.81" x2="78.15" y2="76.81"/></g>} @else {<g><circle cx="51.55" cy="53.12" r="6.73"/><circle cx="86.92" cy="53.12" r="6.73"/><circle class="g" cx="49.15" cy="51.14" r="2.31"/><circle class="g" cx="84.77" cy="51.05" r="2.31"/><path class="h" d="m84.01,81.41c-2.37-5.86-8.11-10-14.83-10s-12.45,4.14-14.83,10"/></g>}</svg><div>"{{ resultQuote() }}"</div><div class="result-buttons"><button (click)="close()" class="gradient-button"><span></span><span>again?</span></button><a target="_blank" class="gradient-button" [href]="'https://twitter.com/intent/tweet?text=' + getText()"><span></span><span>share<img src="assets/share.svg" aria-hidden="true"></span></a></div></dialog>}<div class="accessibility">@if(isAccessiblePanelOpen()) {<div><button [disabled]="isGuessModalOpen()" (click)="adjustAngle(-25)" aria-label="decrease angle a lot">--</button><button [disabled]="isGuessModalOpen()" (click)="adjustAngle(-5)" aria-label="decrease angle a little">-</button><button [disabled]="isGuessModalOpen()" (click)="adjustAngle(5)" aria-label="increase angle a little">+</button><button [disabled]="isGuessModalOpen()" (click)="adjustAngle(25)" aria-label="increase angle a lot">++</button></div>}<mat-slide-toggle [disabled]="isGuessModalOpen()" id="toggle" color="primary" (change)="toggleA11yControls($event)">Show Accessible Controls</mat-slide-toggle></div>
</div>
game.css
.wrapper {height: 100%;width: 100%;max-width: 1000px;margin: auto;display: flex;justify-content: flex-end;align-items: center;
}.col {width: 100%;display: flex;flex-direction: column;justify-content: space-between;align-items: center;
}.overall-stats {display: flex;flex-direction: column;align-items: center;padding: 1rem;font-size: 1.3rem;user-select: none;
}#goal {font-size: 2rem;
}#quote {margin-top: 10px;opacity: 0;transition: all 0.3s ease;
}#quote.show {opacity: 1;
}.gradient-button {text-decoration: none;color: black;margin: 8px;position: relative;cursor: pointer;font-size: 1rem;border: none;font-weight: 600;width: fit-content;height: fit-content;padding-block: 0;padding-inline: 0;
}.gradient-button span:nth-of-type(1) {position: absolute;border-radius: 0.25rem;height: 100%;width: 100%;left: 0;top: 0;background: linear-gradient(90deg, var(--orange-red) 0%, var(--vivid-pink) 50%, var(--electric-violet) 100%);
}.gradient-button span:nth-of-type(2) {position: relative;padding: 0.75rem 1rem;background: white;margin: 1px;border-radius: 0.2rem;transition: all .3s ease;opacity: 1;display: flex;align-items: center;
}.gradient-button:enabled:hover span:nth-of-type(2),
.gradient-button:enabled:focus span:nth-of-type(2) {opacity: 0.9;
}a.gradient-button:hover span:nth-of-type(2),
a.gradient-button:focus span:nth-of-type(2) {opacity: 0.9;
}.gradient-button:disabled {cursor: not-allowed;color: #969696;
}.gradient-button img {display: inline;height: 0.8rem;margin-left: 4px;
}#angle {height: 60vh;width: 60vh;display: flex;flex-direction: column;justify-content: flex-start;align-items: center;padding: 10px;margin: 10px;
}.grabbable {height: 30vh;width: 25px;position: absolute;cursor: pointer;transform-origin: bottom center;
}.arrow {height: 30vh;width: 4px;background-color: black;position: absolute;
}.arrow::before,
.arrow::after {content: '';position: absolute;top: -4px;left: -6px;height: 20px;transform: rotate(45deg);width: 4px;background-color: black;border-radius: 0px 0px 5px 5px;
}.arrow::after {left: 6px;transform: rotate(-45deg);
}#static > div.center {height: 4px;width: 4px;background-color: black;position: absolute;bottom: -2px;border-radius: 100%;
}#static > div.svg {height: 75px;width: 75px;position: absolute;bottom: -37.5px;left: -35.5px;transform-origin: center;transform: rotate(294deg);
}#static svg .svg-arrow {fill: none;stroke-linecap: round;stroke-miterlimit: 10;stroke-width: 3px;
}#static svg path {stroke-dasharray: 180;
}#moving {transform-origin: bottom center;left: calc(50% - 2px);
}.face svg {position: absolute;height: 13vh;width: 13vh;bottom: 2vh;left: 4vh;opacity: 0;transition: all 0.2s ease;
}.face svg.show {opacity: 1;
}.face svg .b {stroke-width: 6px;
}.face svg .b, .c {stroke-miterlimit: 10;
}.face svg .b, .c, .d {fill: none;stroke: #000;stroke-linecap: round;
}.face svg .e {fill: #fff;
}.face svg .c, .d {stroke-width: 7px;
}.face svg .d {stroke-linejoin: round;
}#result {background-color: white;border-radius: 8px;border: 1px solid #f6f6f6;box-shadow: 0 3px 14px 0 rgba(0,0,0,.2);position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);width: 50%;display: flex;flex-direction: column;justify-content: space-around;align-items: center;padding: 2rem;z-index: 10;
}svg.personified {height: 125px;
}.personified .g {fill: #fff;
}.personified .h {stroke-miterlimit: 10;stroke-width: 4px;
}.personified .h, .personified .i {fill: none;stroke: #000;stroke-linecap: round;
}.personified .i {stroke-linejoin: round;stroke-width: 3px;
} #close {border: none;background: none;position: absolute;top: 8px;right: 8px;font-size: 19px;cursor: pointer;
}.result-stats,
.result-buttons {display: flex;width: 100%;justify-content: center;
}.result-stats > * {margin: 4px 16px;
}.result-buttons {margin-top: 16px;
}.accuracy {font-weight: 700;margin: 1rem;
}.accuracy span {font-size: 4rem;margin-right: 6px;
}#copy {display: none;
}.accessibility {position: fixed;left: 10px;bottom: 10px;
}#toggle {margin-top: 8px;
}.accessibility button {width: 2rem;height: 2rem;font-size: 1rem;border: 2px solid var(--electric-violet);border-radius: 4px;cursor: pointer;margin: 0 4px;background-color: #fff;transition: all 0.3s ease;box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.3607843137);
}.accessibility button:focus:enabled, .accessibility button:hover:enabled {background-color: #e8dbf4;
}.accessibility button:disabled {cursor: not-allowed;background-color: #eee;
}@media screen and (max-width: 650px) {.wrapper {flex-direction: column-reverse;align-items: center;}.overall-stats {align-items: center;margin-bottom: 16px;}#result {box-sizing: border-box;min-width: auto;height: 100%;width: 100%;padding: 20px;top: 0;left: 0;border-radius: 0;transform: none;}
}
效果如下:

从代码可以出,代码的主体还是一个组件。
@Component({selector: 'app-root',imports: [CommonModule, MatSlideToggleModule, A11yModule],styleUrl: 'game.css',templateUrl: 'game.html',
})
export class Playground {
...
}
这里实现没有提取出来,都是放在组件里面,所以挺大的。
这里的函数定义如下:
protected readonly totalAccuracyPercentage = computed(() => {const {level, totalAccuracy} = this.gameStats();if (level === 0) {return 0;}return totalAccuracy / level;});
这里的computed(()是可以实时获取并响应this.gameStats()的变化。
还有一个就是
@ViewChild('staticArrow') staticArrow!: ElementRef;
这个的意思就是在 Angular 组件中获取模板中标记为 #staticArrow 的 DOM 元素或子组件的引用,后续可通过 staticArrow 属性安全地操作该元素或组件实例。
3 组件
组件的标准格式:
@Component({selector: 'app-user',template: `Username: {{ username }}`,imports: [App1],
})
export class User {username = 'youngTech';
}
一个组件基本上就对对应一个显示区域,包含了定义和控制。
组件控制流
事件处理
@Component({...template: `<button (click)="greet()">`
})
class App {greet() {alert("Hi. Greeting!");}
}
这里用双引号做的事件绑定。
在angular的模板中,双引号还有几个作用:
1. 静态属性值(纯字符串),表示就是普通字符串,不要做解析。
<input type="text" placeholder="请输入用户名">
2. 属性绑定(动态值),绑定表达式
<button disabled="{{isDisabled}}">按钮</button>
3. 指令输入(Input Binding),传递指令或组件的输入参数。
<app-child [title]="'固定标题'"></app-child>
4. 事件绑定(Event Binding)
<button (click)="handleClick($event)">点击</button>
5. 特殊场景:模板引用变量,声明模板局部变量。
<input #emailInput type="email">
4 模板
模板中可以增加控制,比如@if:
template: `@if (isLoggedIn) {<span>Yes, the server is running</span>}`,
//in @Componenttemplate: `@for (user of users; track user.id) {<p>{{ user.name }}</p>}`,//in classusers = [{id: 0, name: 'Sarah'}, {id: 1, name: 'Amy'}, {id: 2, name: 'Rachel'}, {id: 3, name: 'Jessica'}, {id: 4, name: 'Poornima'}];
template: `<div [contentEditable]="isEditable"></div>`,
在模板中还可以做到延迟显示:
@defer {<comments />
} @placeholder {<p>Future comments</p>
} @loading (minimum 2s) {<p>Loading comments...</p>
}
效果如下:

在模板中,可以将图片的关键字换成ngSrc:
Dynamic Image:<img [ngSrc]="logoUrl" [alt]="logoAlt" width="320" height="320" />
区别如下:
| 特性 | ngSrc (Angular 指令) | src (原生 HTML) |
|---|---|---|
| 动态绑定 | ✅ 支持 Angular 表达式(如变量、函数调用) | ❌ 直接写死字符串,无法动态绑定 |
| 加载控制 | ✅ 避免无效请求和竞争条件 | ❌ 可能发送 404 或重复请求 |
| 性能优化 | ✅ 可结合懒加载、占位图等策略 | ❌ 无内置优化 |
| 框架集成 | ✅ 与 Angular 变更检测无缝协作 | ❌ 需手动处理动态更新 |
数据绑定
template: `<p>Username: {{ username }}</p><p>{{ username }}'s favorite framework: {{ favoriteFramework }}</p><label for="framework">Favorite Framework1:<input id="framework" type="text" [(ngModel)]="favoriteFramework" /></label>`,
可以看到就是(ngModel)这个。除了ngModel,还有以下模板语法
| 类型 | 语法 / 指令 | 用途说明 | 示例 |
|---|---|---|---|
| 绑定 | [property] | 绑定 HTML 属性 | [src]="imgUrl" |
{{ expression }} | 插值表达式 | {{ user.name }} | |
bind-xxx | 等价于 [xxx] | bind-title="msg" | |
| 事件 | (event) | 监听事件 | (click)="doSomething()" |
on-xxx | 等价于 (xxx) | on-click="save()" | |
| 双向绑定 | [(ngModel)] | 绑定输入与数据 | [(ngModel)]="user.name" |
| 条件结构 | *ngIf | 条件显示 | *ngIf="isLoggedIn" |
| 列表结构 | *ngFor | 遍历数据渲染 | *ngFor="let item of list" |
| 切换结构 | *ngSwitch、*ngSwitchCase | 类似 switch-case | 见下方示例 |
| 样式绑定 | [ngClass] | 动态 class 切换 | [ngClass]="{'active': isActive}" |
[ngStyle] | 动态 style | [ngStyle]="{color: colorVar}" | |
| 属性绑定 | [attr.xxx] | 绑定非标准属性 | [attr.aria-label]="label" |
| 类绑定 | [class.className] | 控制某个类是否启用 | [class.active]="isActive" |
| 样式绑定 | [style.xxx] | 控制某个样式值 | [style.backgroundColor]="color" |
| 内容投影 | <ng-content> | 插槽内容传递 | 用于组件中嵌套插入内容 |
| 模板引用变量 | #var | 在模板中获取 DOM 或组件引用 | <input #nameInput> |
| 管道 | `expression | pipe` | 数据格式转换 |
| 自定义指令 | @Directive | 创建结构/属性指令 | 如:[appHighlight] |
| 表单控件 | [formControl], [formGroup] | 响应式表单语法 | <input [formControl]="nameControl"> |
5 路由
路由就是在angular内根据url切换到不同的组件。最小的路由大概是三个部分。
定义路由模块
app.routes.ts
import {Routes} from '@angular/router';
export const routes: Routes = [];
在主模块中导入
app.config.ts
import {ApplicationConfig} from '@angular/core';
import {provideRouter} from '@angular/router';
import {routes} from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
模板中使用路由
app.ts
import {Component} from '@angular/core';
import {RouterOutlet} from '@angular/router';@Component({selector: 'app-root',template: `<nav><a href="/">Home1</a>|<a href="/user">User</a></nav><router-outlet />`,imports: [RouterOutlet],
})
export class App {}
6 表单
内容都在@angular/forms。
响应式表单ReactiveFormsModule。这个后面还要再看看TODO
template: `<form [formGroup]="profileForm" (ngSubmit)="handleSubmit()"><input type="text" formControlName="name" /><input type="email" formControlName="email" /><button type="submit">Submit</button></form><h2>Profile Form</h2><p>Name: {{ profileForm.value.name }}</p><p>Email: {{ profileForm.value.email }}</p>`,imports: [ReactiveFormsModule],
响应式表单的三大核心能力:
| 能力 | 说明 | 示例 |
|---|---|---|
| 数据驱动 | 表单状态(值、校验)完全由代码控制,与模板解耦 | 通过 formGroup.get('field').value 获取值 |
| 动态字段管理 | 运行时增减字段(如购物车动态添加商品) | 使用 FormArray 动态操作字段 |
| 复杂校验 | 支持跨字段校验、异步校验(如用户名实时查重) | 自定义 ValidatorFn 或异步校验 |
在真实 IoT 或企业后台里,设备管理、配置页面常常字段多且动态——选 Reactive Forms 几乎是“默认选项”。只有最轻量的表单才考虑模板驱动。
7 其它
7.1 注入
就是类似单例工厂类。。
@Injectable({providedIn: 'root',
})
export class CarService {
...
}
@Component({
})
export class App {carService = inject(CarService);
}