Angular2--高级特性(TODO)

发布于:2025-06-25 ⋅ 阅读:(20) ⋅ 点赞:(0)

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);
}

7.2 HTTP Client

7.3 WebSocket


网站公告

今日签到

点亮在社区的每一天
去签到