在 Angular 中使用指令实现 Figma 类输入字段

熟悉 Figma 的人会注意到,输入字段支持拖动来增加或减少值。拖动功能非常方便,您无需先单击输入字段,然后输入数字,只需拖动即可轻松获得所需的值。

我们可以使用 Angular 指令构建类似的东西。我们将在此实验中使用 Angular 的所有最新功能。

让我们看看如何构建它。

实际上,我们可以用多种方式来实现这一点。我们将使用指令来构建它。我们将采用一种非常通用的方法来实现这一点。这样,我们可以重复使用逻辑来调整元素或侧边栏的大小等。

Scrubber 指令 - 核心功能

输入的主要逻辑可以提取出来并封装成指令。主要目的是监听鼠标事件,然后将鼠标移动转换为可用的值。更详细地解释一下:

  • 当用户点击鼠标(mousedown 事件)时。
  • 我们开始监听鼠标移动(mousemove 事件)并使用该信息将其转换为可用的值。
  • 当用户释放点击时,我们停止监听器(鼠标释放事件)。
  • 我们将使用“rxjs”来稍微简化逻辑。

    伪代码如下所示。

    const mousedown$ = fromEvent(target, 'mousedown');
    const mousemove$ = fromEvent(document, 'mousemove');
    const mouseup$ = fromEvent(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);
      });

    看看上面的代码,应该很清楚发生了什么。我们基本上保存了初始的“clientX”值,即点击在 X 轴上的位置。一旦我们有了这些信息,当用户移动鼠标时,我们就可以计算从初始起始位置到当前 X 位置的增量。

    我们可以进一步添加更多自定义功能,例如:

  • 灵敏度 - 拖动到最终值的距离由灵敏度决定。灵敏度值越高意味着即使移动幅度不大,最终值也会越大。
  • Step - 设置移动鼠标时的步进间隔。如果 step 值为 1,则最终值以 1 为步长递增/递减。
  • 最小值——将发出的最小值。
  • 最大值——将发出的最大值。
  • 最终的指令如下:

    @Directive({
      selector: "[scrubber]",
    })
    export class ScrubberDirective {
      public readonly scrubberTarget = input.required({
        alias: "scrubber",
      });
    
      public readonly step = model(1);
      public readonly min = model(0);
      public readonly max = model(100);
      public readonly startValue = model(0);
      public readonly sensitivity = model(0.1);
    
      public readonly scrubbing = output();
    
      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(target, "mousedown");
        const mousemove$ = fromEvent(document, "mousemove");
        const mouseup$ = fromEvent(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);
      }
    }

    如何使用 Scrubber 指令

    现在我们已经准备好指令,让我们看看如何实际开始使用它。

    目前,我们已将“scrubberTarget”输入标记为“input.required”,但我们实际上可以将其设为可选,并自动使用指令主机的“elementRef.nativeElement”,其工作原理相同。如果您想要将其他元素设置为目标,则“scrubberTarget”将作为输入公开。

    我们还向主体添加了一个类“resizing”,以便我们可以正确设置调整大小光标。

    .resizing {
      cursor: ew-resize;
      touch-action: none;
      -webkit-user-select: none;
      user-select: none;
    }

    我们已经使用“effect”来启动监听器,这将确保当目标元素发生变化时,我们在新元素上设置监听器。

    观看实际操作

    我们在 Angular 中创建了一个非常简单的 Scrubber 指令,它可以帮助我们构建类似于 Figma 的输入字段。让用户可以非常轻松地与数字输入进行交互。

    代码和演示

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

    与我联系

  • 叽叽喳喳
  • Github
  • 领英
  • 请在评论部分发表你的想法。注意安全❤️

    Buy me a pizza