Skip to content

Commit

Permalink
Controlled Behavior (#20)
Browse files Browse the repository at this point in the history
* f v

* Push

* 2.1.0-beta.0

* beta 1

* Stuff

* 2.1.1
  • Loading branch information
PuruVJ authored Nov 12, 2021
1 parent a318901 commit a17a2a1
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 30 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,16 @@ Applies a grid on the page to which the element snaps to when dragging, rather t

`Note`: If you're programmatically creating the grid, do not set it to [0, 0] ever, that will stop drag at all. Set it to `undefined` to make it continuous once again.

## position

**type**: `{ x: number; y: number }`

**Default Value**: `undefined`

Controls the position of the element programmatically. Fully reactive.

Read more below in the **Controlled vs Uncontrolled** section.

## cancel

**type**: `string`
Expand Down Expand Up @@ -346,6 +356,32 @@ export type DragBoundsCoords = {
};
```

# Controlled vs Uncontrolled

This is taken straight from React's philosophy(After all, this package is inspired from [react-draggable](https://github.com/react-grid-layout/react-draggable)).

Uncontrolled means your app doesn't control the dragging of the app. Meaning, the user drags the element, it changes position, and you do something with that action. You yourself don't change position of the element or anything. This is the default behavior of this library.

Controlled means your app, using state variables, changes the position of the element, or in simple terms, programmatically drag the element. You basically set the `position` property to `{ x: 10, y: 50 }`(or any other numbers), and voila! yur now controlling the position of the element programmatically 🥳🥳

OFC, this library doesn't go fully **Controlled**. The user can still drag it around even when `position` is set.

So, when you change `position`, the element position changes. However, when the element is dragged by user interaction, `position` is not changed. This is done intentionally, as two-way data binding here isn't possible and also will lead to unexpected behavior. To keep the `position` variable up to date, use the `on:svelte-drag` event to keep your state up to date to the draggable's internal state.

To have it be strictly **Controlled**, meaning it can only be moved programmatically, add the `disabled` option to your draggable element's config

```svelte
<div use:draggable={{ position: { x: 0, y: 10 }, disabled: true }} />
```

Here are a bunch of examples showing controlled behavior 👇

1. [Changing with inputs](https://svelte.dev/repl/e1e707358b37467ba272891715878a1d?version=3.44.1)
2. [Changing with Sliders](https://svelte.dev/repl/6b437a1cdbfc4c748520a72330c6395b?version=3.44.1)
3. [Draggable only through external state, not user input](https://svelte.dev/repl/0eae169f272e41ba9c07ef222ed2bf66?version=3.44.1)
4. [Comes back to original position after drag end](https://svelte.dev/repl/83d3aa8c5e154b7baf1a9c417c217d2e?version=3.44.1)
5. [Comes back to original position with transition](https://svelte.dev/repl/bc84ed4ca22f45acbc28de3e33199883?version=3.44.1)

# Why an action and not a component?

In case you're wondering why this library is an action, and not a component, the answer is simple: Actions usage is much much simpler and elegant than a component for this case could ever be.
Expand Down
10 changes: 5 additions & 5 deletions demo/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/// <reference types="@sveltejs/kit" />

declare namespace svelte.JSX {
interface HTMLAttributes {
'onsvelte-drag:start'?: (e: CustomEvent<null>) => void;
'onsvelte-drag:end'?: (e: CustomEvent<null>) => void;
'onsvelte-drag'?: (e: CustomEvent<{ x: number; y: number }>) => void;
export declare namespace svelte.JSX {
interface HTMLAttributes<T> {
'onsvelte-drag:start'?: (e: CustomEvent<{ offsetX: number; offsetY: number }>) => void;
'onsvelte-drag:end'?: (e: CustomEvent<{ offsetX: number; offsetY: number }>) => void;
'onsvelte-drag'?: (e: CustomEvent<{ offsetX: number; offsetY: number }>) => void;
}
}
47 changes: 26 additions & 21 deletions demo/src/routes/index.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
import { draggable } from '../../../dist';
import { spring, tweened } from 'svelte/motion';
import type { DragOptions } from '../../../dist';
import { bounceIn, sineIn, sineOut } from 'svelte/easing';
// import { draggable } from 'svelte-drag';
// import type { Options } from 'svelte-drag';
Expand All @@ -26,6 +28,9 @@
} as DragOptions;
// $: console.log(options);
let progressY = tweened(0, { easing: sineIn });
let progressX = tweened(0, { easing: sineIn });
</script>

<h1>Welcome to SvelteKit</h1>
Expand Down Expand Up @@ -128,12 +133,7 @@

<br />

<div
use:draggable={options}
on:svelte-drag:start={console.log}
on:svelte-drag:end={console.log}
class="box"
>
<div use:draggable={options} class="box">
hello

<div class="handle">Le handel</div>
Expand All @@ -145,11 +145,13 @@

<div
use:draggable={options}
on:svelte-drag:start={console.log}
on:svelte-drag:end={console.log}
on:svelte-drag={(e) => {
progressX = e.detail.offsetX;
progressY = e.detail.offsetY;
}}
class="box"
>
hello
2nd one

<div class="handle">Le handel</div>
<div class="cancel">Cancel</div>
Expand All @@ -158,20 +160,23 @@
<div class="cancel-2">Cancel 2</div>
</div>

<div
use:draggable={options}
on:svelte-drag:start={console.log}
on:svelte-drag:end={console.log}
class="box"
>
hello
<br /><br /><br /><br /><br /><br />

<div class="handle">Le handel</div>
<div class="cancel">Cancel</div>
<input type="number" bind:value={$progressX} />
<input type="number" bind:value={$progressY} />

<div class="handle-2">Le handel 2</div>
<div class="cancel-2">Cancel 2</div>
</div>
<div
use:draggable={{ controlledPosition: { y: $progressY, x: $progressX } }}
on:svelte-drag={(e) => {
progressX.set(e.detail.offsetX, { duration: 0 });
progressY.set(e.detail.offsetY, { duration: 0 });
}}
on:svelte-drag:end={() => {
$progressX = 0;
$progressY = 0;
}}
class="box"
/>

<style>
:global(body) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "svelte-drag",
"version": "2.0.4",
"version": "2.1.1",
"description": "Svelte port of react-draggable",
"main": "dist/index.cjs",
"module": "dist/index.js",
Expand Down
41 changes: 38 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ export type DragOptions = {
*/
grid?: [number, number];

/**
* Control the position manually with your own state
*
* By default, the element will be draggable by mouse/finger, and all options will work as default while dragging.
*
* But changing the `position` option will also move the draggable around. These parameters are reactive,
* so using Svelte's reactive variables as values for position will work like a charm.
*
*
* Note: If you set `disabled: true`, you'll still be able to move the draggable through state variables. Only the user interactions won't work
*
* Examples:
*
* [Changing with inputs](https://svelte.dev/repl/e1e707358b37467ba272891715878a1d?version=3.44.1)
*
* [Changing with Sliders](https://svelte.dev/repl/6b437a1cdbfc4c748520a72330c6395b?version=3.44.1)
*
* [Draggable only through external state, not user input](https://svelte.dev/repl/0eae169f272e41ba9c07ef222ed2bf66?version=3.44.1)
*
* [Comes back to original position after drag end](https://svelte.dev/repl/83d3aa8c5e154b7baf1a9c417c217d2e?version=3.44.1)
*
* [Comes back to original position with transition](https://svelte.dev/repl/bc84ed4ca22f45acbc28de3e33199883?version=3.44.1)
*/
position?: { x: number; y: number };

/**
* CSS Selector of an element inside the parent node(on which `use:draggable` is applied).
*
Expand Down Expand Up @@ -235,6 +260,8 @@ export const draggable = (node: HTMLElement, options: DragOptions = {}) => {

grid,

position,

cancel,
handle,

Expand Down Expand Up @@ -269,6 +296,8 @@ export const draggable = (node: HTMLElement, options: DragOptions = {}) => {
let dragEl: HTMLElement | undefined;
let cancelEl: HTMLElement | undefined;

let isControlled = !!position;

function fireSvelteDragStopEvent(node: HTMLElement) {
node.dispatchEvent(
new CustomEvent('svelte-drag:end', { detail: { offsetX: translateX, offsetY: translateY } })
Expand Down Expand Up @@ -325,7 +354,7 @@ export const draggable = (node: HTMLElement, options: DragOptions = {}) => {
"Element being dragged can't be a child of the element on which `cancel` is applied"
);

if (dragEl.contains(e.target as HTMLElement) && !cancelEl?.contains(e.target as HTMLElement))
if (dragEl.contains(<HTMLElement>e.target) && !cancelEl?.contains(<HTMLElement>e.target))
active = true;

if (!active) return;
Expand All @@ -335,6 +364,7 @@ export const draggable = (node: HTMLElement, options: DragOptions = {}) => {
bodyOriginalUserSelectVal = document.body.style.userSelect;
document.body.style.userSelect = 'none';
}

// Dispatch custom event
fireSvelteDragStartEvent(node);

Expand Down Expand Up @@ -370,8 +400,6 @@ export const draggable = (node: HTMLElement, options: DragOptions = {}) => {
}

function drag(e: TouchEvent | MouseEvent) {
if (disabled) return;

if (!active) return;

// Apply class defaultClassDragging
Expand Down Expand Up @@ -460,6 +488,13 @@ export const draggable = (node: HTMLElement, options: DragOptions = {}) => {
node.classList.add(defaultClass);

if (dragged) node.classList.add(defaultClassDragged);

if (isControlled) {
xOffset = translateX = options.position?.x ?? translateX;
yOffset = translateY = options.position?.y ?? translateY;

Promise.resolve().then(() => setTranslate(translateX, translateY, node, gpuAcceleration));
}
},
};
};
Expand Down

0 comments on commit a17a2a1

Please sign in to comment.