ホームページ >ウェブフロントエンド >jsチュートリアル >ディレクティブを使用した Angular の Figma のような入力フィールド

ディレクティブを使用した Angular の Figma のような入力フィールド

Linda Hamilton
Linda Hamiltonオリジナル
2025-01-02 21:00:40799ブラウズ

Figma に詳しい人は、入力フィールドがドラッグによる値の増減をサポートしていることに気づいたでしょう。最初に入力フィールドをクリックしてから数値を入力する代わりに、ドラッグすることで目的の値を簡単に取得できるため、ドラッグ機能は非常に便利です。

Angular ディレクティブを使用してそのようなものを構築できます。この実験では、Angular の最新機能をすべて使用します。

Figma like input field in Angular using Directives

これをどのように構築できるかを見てみましょう。

実際には、これを複数の方法で行うことができます。ディレクティブを使用してこれを構築します。私たちがこれを行う方法は、非常に一般的なアプローチをとることです。こうすることで、要素やサイドバーのサイズ変更などのためにロジックを再利用できます。

スクラバーディレクティブ - コア機能

入力のメインロジックを抽出してディレクティブにカプセル化できます。主な目的は、マウス イベントをリッスンし、マウスの動きを使用可能な値に変換することです。もう少し詳しく説明すると、

  1. ユーザーがマウスをクリックしたとき (マウスダウン イベント)。

  2. マウスの動き (mousemove イベント) の監視を開始し、その情報を使用して使用可能な値に変換します。

  3. ユーザーがクリックを放すと、リスナー (マウスアップ イベント) が停止します。

rxjs を使用してロジックを少し単純化します。

疑似コードは次のようになります。

const mousedown$ = fromEvent<MouseEvent>(target, 'mousedown');
const mousemove$ = fromEvent<MouseEvent>(document, 'mousemove');
const mouseup$ = fromEvent<MouseEvent>(document, 'mouseup');

let startX = 0;
let step = 1;

mousedown$
  .pipe(
     tap((event) => {
       startX = event.clientX; // Initial x co-ordinate where the mouse down happened
    }),
    switchMap(() => mousemove$.pipe(takeUntil(mouseup$))))
  .subscribe((moveEvent) => {
    const delta = startX - moveEvent.clientX;
    const newValue = Math.round(startValueAtTheTimeOfDrag + delta);
  });

上記のコードを見ると、何が起こっているのかがかなり明確になるはずです。基本的に、X 軸上のクリックの位置である clientX の初期値を保存します。この情報を取得すると、ユーザーがマウスを移動したときに、最初の開始位置と現在の X 位置からデルタを計算できます。

次のようなカスタマイズをさらに追加できます。

  1. 感度 - 最終値までのドラッグ距離は感度によって決まります。感度の値が高いほど、動きがそれほど大きくなくても最終的な値が大きくなります。

  2. ステップ - マウスを移動するときのステップ間隔を設定します。ステップ値が 1 の場合、最終値は 1 ずつ増加/減少します。

  3. Min - 出力される最小値。

  4. Max - 出力される最大値。

最終的なディレクティブは次のようになります:

@Directive({
  selector: "[scrubber]",
})
export class ScrubberDirective {
  public readonly scrubberTarget = input.required<HTMLDivElement>({
    alias: "scrubber",
  });

  public readonly step = model<number>(1);
  public readonly min = model<number>(0);
  public readonly max = model<number>(100);
  public readonly startValue = model(0);
  public readonly sensitivity = model(0.1);

  public readonly scrubbing = output<number>();

  private isDragging = signal(false);
  private startX = signal(0);
  private readonly startValueAtTheTimeOfDrag = signal(0);
  private readonly destroyRef = inject(DestroyRef);
  private subs?: Subscription;

  constructor() {
    effect(() => {
      this.subs?.unsubscribe();
      this.subs = this.setupMouseEventListener(this.scrubberTarget());
    });

    this.destroyRef.onDestroy(() => {
      document.body.classList.remove('resizing');
      this.subs?.unsubscribe();
    });
  }

  private setupMouseEventListener(target: HTMLDivElement): Subscription {
    const mousedown$ = fromEvent<MouseEvent>(target, "mousedown");
    const mousemove$ = fromEvent<MouseEvent>(document, "mousemove");
    const mouseup$ = fromEvent<MouseEvent>(document, "mouseup");

    return mousedown$
      .pipe(
        tap((event) => {
          this.isDragging.set(true);
          this.startX.set(event.clientX);
          this.startValueAtTheTimeOfDrag.set(this.startValue());
          document.body.classList.add("resizing");
        }),
        switchMap(() =>
          mousemove$.pipe(
            takeUntil(
              mouseup$.pipe(
                tap(() => {
                  this.isDragging.set(false);
                  document.body.classList.remove("resizing");
                })
              )
            )
          )
        )
      )
      .subscribe((moveEvent) => {
        const delta = moveEvent.clientX - this.startX();
        const deltaWithSensitivityCompensation = delta * this.sensitivity();

        const newValue =
          Math.round(
            (this.startValueAtTheTimeOfDrag() +
              deltaWithSensitivityCompensation) /
              this.step()
          ) * this.step();

        this.emitChange(newValue);
        this.startValue.set(newValue);
      });
  }

  private emitChange(newValue: number): void {
    const clampedValue = Math.min(Math.max(newValue, this.min()), this.max());
    this.scrubbing.emit(clampedValue);
  }
}

スクラバーディレクティブの使用方法

ディレクティブの準備ができたので、実際にそれを使用する方法を見てみましょう。

const mousedown$ = fromEvent<MouseEvent>(target, 'mousedown');
const mousemove$ = fromEvent<MouseEvent>(document, 'mousemove');
const mouseup$ = fromEvent<MouseEvent>(document, 'mouseup');

let startX = 0;
let step = 1;

mousedown$
  .pipe(
     tap((event) => {
       startX = event.clientX; // Initial x co-ordinate where the mouse down happened
    }),
    switchMap(() => mousemove$.pipe(takeUntil(mouseup$))))
  .subscribe((moveEvent) => {
    const delta = startX - moveEvent.clientX;
    const newValue = Math.round(startValueAtTheTimeOfDrag + delta);
  });

現在、scrubberTarget 入力を input.required としてマークしていますが、実際にはこれをオプションにして、ディレクティブのホストの elementRef.nativeElement を自動的に使用することもでき、同様に機能します。別の要素をターゲットとして設定する場合に備えて、scrubberTarget が入力として公開されます。

サイズ変更カーソルを正しく設定できるように、本体にサイズ変更クラスも追加します。

@Directive({
  selector: "[scrubber]",
})
export class ScrubberDirective {
  public readonly scrubberTarget = input.required<HTMLDivElement>({
    alias: "scrubber",
  });

  public readonly step = model<number>(1);
  public readonly min = model<number>(0);
  public readonly max = model<number>(100);
  public readonly startValue = model(0);
  public readonly sensitivity = model(0.1);

  public readonly scrubbing = output<number>();

  private isDragging = signal(false);
  private startX = signal(0);
  private readonly startValueAtTheTimeOfDrag = signal(0);
  private readonly destroyRef = inject(DestroyRef);
  private subs?: Subscription;

  constructor() {
    effect(() => {
      this.subs?.unsubscribe();
      this.subs = this.setupMouseEventListener(this.scrubberTarget());
    });

    this.destroyRef.onDestroy(() => {
      document.body.classList.remove('resizing');
      this.subs?.unsubscribe();
    });
  }

  private setupMouseEventListener(target: HTMLDivElement): Subscription {
    const mousedown$ = fromEvent<MouseEvent>(target, "mousedown");
    const mousemove$ = fromEvent<MouseEvent>(document, "mousemove");
    const mouseup$ = fromEvent<MouseEvent>(document, "mouseup");

    return mousedown$
      .pipe(
        tap((event) => {
          this.isDragging.set(true);
          this.startX.set(event.clientX);
          this.startValueAtTheTimeOfDrag.set(this.startValue());
          document.body.classList.add("resizing");
        }),
        switchMap(() =>
          mousemove$.pipe(
            takeUntil(
              mouseup$.pipe(
                tap(() => {
                  this.isDragging.set(false);
                  document.body.classList.remove("resizing");
                })
              )
            )
          )
        )
      )
      .subscribe((moveEvent) => {
        const delta = moveEvent.clientX - this.startX();
        const deltaWithSensitivityCompensation = delta * this.sensitivity();

        const newValue =
          Math.round(
            (this.startValueAtTheTimeOfDrag() +
              deltaWithSensitivityCompensation) /
              this.step()
          ) * this.step();

        this.emitChange(newValue);
        this.startValue.set(newValue);
      });
  }

  private emitChange(newValue: number): void {
    const clampedValue = Math.min(Math.max(newValue, this.min()), this.max());
    this.scrubbing.emit(clampedValue);
  }
}

エフェクトを使用してリスナーを開始しました。これにより、ターゲット要素が変更された場合に、新しい要素にリスナーを設定できるようになります。

実際の動作を確認してください

Figma like input field in Angular using Directives

Figma と同様の入力フィールドを構築するのに役立つ、非常にシンプルな Scrubber ディレクティブを Angular で作成しました。 Makes は、ユーザーが数値入力を操作するのが非常に簡単です。

コードとデモ

https://stackblitz.com/edit/figma-like-number-input-angular?file=src/scrubber.directive.ts

私とつながってください

  • ツイッター

  • Github

  • リンクトイン

コメントセクションにあなたの意見を追加してください。安全を確保してください❤️

Figma like input field in Angular using Directives

以上がディレクティブを使用した Angular の Figma のような入力フィールドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。