How to Build a Touch Drag Component with NutUI 3.0: A Step‑by‑Step Vue Guide

This article walks through the design and implementation of NutUI 3.0's Drag component for mobile web, explaining the two main drag techniques, touch event handling, boundary checks, optional axis constraints, and snap‑to‑edge behavior with complete Vue code examples.

JD Retail Technology
JD Retail Technology
JD Retail Technology
How to Build a Touch Drag Component with NutUI 3.0: A Step‑by‑Step Vue Guide

Introduction

NutUI is JD.com’s lightweight mobile‑first Vue component library. Version 3.0 introduces a revamped Drag component that lets developers move elements with finger gestures on mobile browsers. The article explains the motivation for component‑based development and provides a full implementation walkthrough.

Implementation Idea

Two common approaches exist for mobile drag: using position:fixed and updating the element’s coordinates, or applying CSS transform: translate. NutUI’s Drag component adopts the fixed‑position method, which requires updating top and left values during touch interactions.

Principle Analysis

The component relies on touch event coordinates ( clientX/Y, pageX/Y, screenX/Y, layerX/Y, offsetX/Y) to calculate movement. When a touch starts, the initial element offsets and finger positions are recorded; during movement, the delta is applied to the element’s style; on touch end, optional post‑processing such as snapping can be performed.

Touch Events

touchstart : fires when a finger contacts the screen.

touchmove : fires continuously as the finger slides; preventDefault() can stop page scrolling.

touchend : fires when the finger lifts.

The event object contains three touch lists: touches (all active points), targetTouches (points on the event target), and changedTouches (points that changed since the last event).

Detailed Process

1. Initial Setup

const state = reactive({
  elWidth: 0, // element width
  elHeight: 0, // element height
  screenWidth: 0,
  screenHeight: 0,
  startTop: 0,
  startLeft: 0,
  direction: props.direction, // all / x / y
  position: { x: 0, y: 0 },
  boundary: { top: 0, left: 0, right: 0, bottom: 0 }
});

function touchStart(e: TouchEvent) {
  const target = e.currentTarget as HTMLElement;
  const touches = e.touches[0];
  state.startTop = target.offsetTop;
  state.startLeft = target.offsetLeft;
  state.position.x = touches.clientX;
  state.position.y = touches.clientY;
}

2. Moving the Element

function touchMove(e: TouchEvent) {
  e.preventDefault();
  const target = e.currentTarget as HTMLElement;
  if (e.targetTouches.length === 1) {
    const touch = e.targetTouches[0];
    state.nx = touch.clientX - state.position.x;
    state.ny = touch.clientY - state.position.y;
    state.xPum = state.startLeft + state.nx;
    state.yPum = state.startTop + state.ny;
    target.style.left = state.xPum + 'px';
    target.style.top = state.yPum + 'px';
  }
}

3. Boundary Checking

const rightLocation = state.screenWidth - state.elWidth - state.boundary.right;
if (Math.abs(state.xPum) > rightLocation) {
  state.xPum = rightLocation;
} else if (state.xPum <= state.boundary.left) {
  state.xPum = state.boundary.left;
}
if (state.yPum < state.boundary.top) {
  state.yPum = state.boundary.top;
} else if (state.yPum > state.screenHeight - state.elHeight - state.boundary.bottom) {
  state.yPum = state.screenHeight - state.elHeight - state.boundary.bottom;
}

4. Optional Axis Constraint

if (props.direction != 'y') {
  target.style.left = state.xPum + 'px';
}
if (props.direction != 'x') {
  target.style.top = state.yPum + 'px';
}

5. Snap‑to‑Edge Effect

function goLeft(target: HTMLElement) {
  if (state.boundary.left) {
    if (+target.style.left.split('px')[0] > state.boundary.left) {
      target.style.left = +target.style.left.split('px')[0] - 10 + 'px';
      requestAniFrame(() => goLeft(target));
    } else {
      target.style.left = `${state.boundary.left}px`;
    }
  } else {
    if (+target.style.left.split('px')[0] > 10) {
      target.style.left = +target.style.left.split('px')[0] - 10 + 'px';
      requestAniFrame(() => goLeft(target));
    } else {
      target.style.left = '0px';
    }
  }
}
function goRight(target: HTMLElement, rightLocation: number) {
  if (rightLocation - parseInt(target.style.left.split('px')[0]) > 10) {
    target.style.left = parseInt(target.style.left.split('px')[0]) + 10 + 'px';
    requestAniFrame(() => goRight(target, rightLocation));
  } else {
    target.style.left = rightLocation + 'px';
  }
}

Visual Aids

Illustrative diagrams and GIFs demonstrate the touch event flow, the difference between touches, targetTouches, and changedTouches, and the final drag behavior with snapping.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontend developmentVueMobile UITouch EventsnutuiDrag Component
JD Retail Technology
Written by

JD Retail Technology

Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.