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>
<div
id="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"
/>
<polyline
class="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>
<div
class="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 @Component
template: `
@for (user of users; track user.id) {
<p>{{ user.name }}</p>
}
`,
//in class
users = [{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);
}