Custom action

Actions are essentially element-level lifecycle functions. They are useful in the following ways:

  • Interface with third-party libraries
  • Lazy-loaded images
  • prompt
  • Add custom event handlers

In this application, we want to make the orange box “panning”. It has event handlers for panstart, Panmove, and Panend events, but these are not native DOM events. We have to send them ourselves. First, import the pannable function…

import { pannable } from './pannable.js';
Copy the code

Then use it with the element:

<div class="box"
	use:pannable
	on:panstart={handlePanStart}
	on:panmove={handlePanMove}
	on:panend={handlePanEnd}
	style="Transform: Translate ({$coords.x}px,{$coords.y}px) Rotate ({$coords.x * 0.2}deg)"
></div>
Copy the code

To open the pannable.js file:

export function pannable(node) {
	let x;
	let y;

	function handleMousedown(event) {
		x = event.clientX;
		y = event.clientY;

		node.dispatchEvent(new CustomEvent('panstart', {
			detail: { x, y }
		}));

		window.addEventListener('mousemove', handleMousemove);
		window.addEventListener('mouseup', handleMouseup);
	}

	function handleMousemove(event) {
		const dx = event.clientX - x;
		const dy = event.clientY - y;
		x = event.clientX;
		y = event.clientY;

		node.dispatchEvent(new CustomEvent('panmove', {
			detail: { x, y, dx, dy }
		}));
	}

	function handleMouseup(event) {
		x = event.clientX;
		y = event.clientY;

		node.dispatchEvent(new CustomEvent('panend', {
			detail: { x, y }
		}));

		window.removeEventListener('mousemove', handleMousemove);
		window.removeEventListener('mouseup', handleMouseup);
	}

	node.addEventListener('mousedown', handleMousedown);

	return {
		destroy() {
			node.removeEventListener('mousedown', handleMousedown); }}; }Copy the code

Like a transformation function, an action function takes a Node and some optional arguments and returns an action object. The object can have a destroy function that is called when the element is unloaded.

We want the PanStart event to fire when the user presses the mouse over the element, the Panmove event to fire when the user drags it (using the dx and dy attributes to show how far the mouse has moved), and the Panend event to fire when the user releases the mouse.

Update the Pannable function and try to move the box.

This implementation is for demonstration purposes — a more complete one will also consider touch events.

App. Svelte:

<script>
	import { spring } from 'svelte/motion';
	import { pannable } from './pannable.js';

	const coords = spring({ x: 0.y: 0 }, {
		stiffness: 0.2.damping: 0.4
	});

	function handlePanStart() {
		coords.stiffness = coords.damping = 1;
	}

	function handlePanMove(event) {
		coords.update($coords => ({
			x: $coords.x + event.detail.dx,
			y: $coords.y + event.detail.dy
		}));
	}

	function handlePanEnd(event) {
		coords.stiffness = 0.2;
		coords.damping = 0.4;
		coords.set({ x: 0.y: 0 });
	}
</script>

<style>
	.box{-width: 100px;
		--height: 100px;
		position: absolute;
		width: var(--width);
		height: var(--height);
		left: calc(50% - var(--width) / 2);
		top: calc(50% - var(--height) / 2);
		border-radius: 4px;
		background-color: #ff3e00;
		cursor: move;
	}
</style>

<div class="box"
	use:pannable
	on:panstart={handlePanStart}
	on:panmove={handlePanMove}
	on:panend={handlePanEnd}
	style="Transform: Translate ({$coords.x}px,{$coords.y}px) Rotate ({$coords.x * 0.2}deg)"
></div>
Copy the code

Recommend to go to the official website to experience, the demo is cool. actions

Add parameters

Like transitions and animations, an action can take an argument, and the action function will be called with the element to which it belongs.

In this case, we use a longpress operation that triggers an event with the same name every time the user presses the button for a given duration. Now, if you switch to the longpress.js file, you will see that it is hard-coded to 500ms.

export function longpress(node, duration) {
	let timer;
	
	const handleMousedown = () = > {
		timer = setTimeout(() = > {
			node.dispatchEvent(
				new CustomEvent('longpress')); }, duration); };const handleMouseup = () = > {
		clearTimeout(timer)
	};

	node.addEventListener('mousedown', handleMousedown);
	node.addEventListener('mouseup', handleMouseup);

	return {
		update(newDuration) {
			duration = newDuration;
		},
		destroy() {
			node.removeEventListener('mousedown', handleMousedown);
			node.removeEventListener('mouseup', handleMouseup); }}; }Copy the code

We can change the action function to accept duration as the second argument and pass the duration to the setTimeout call:

Back at app.svelte, we can pass the duration value to the operation:

<button use:longpress={duration}
Copy the code

The complete app.svelte code:

<script>
	import { longpress } from './longpress.js';

	let pressed = false;
	let duration = 2000;
</script>

<label>
	<input type=range bind:value={duration} max={2000} step={100}>
	{duration}ms
</label>

<button use:longpress={duration}
	on:longpress="{() => pressed = true}"
	on:mouseenter="{() => pressed = false}"
>press and hold</button>

{#if pressed}
	<p>congratulations, you pressed and held for {duration}ms</p>
{/if}
Copy the code

This almost works, the event now fires after only 2 seconds. But if you shorten the duration, it still takes two seconds.

To change this, we can add an update method to longpress.js. This function is called whenever an argument changes:

return {
	update(newDuration) {
		duration = newDuration;
	},
	// ...
};
Copy the code

If you need to pass multiple arguments to an operation, combine them into a single object, as in use: longpress={duration, spiciness}}

I recommend going to the official website to try it. adding-parameters-to-actions