Skip to content

Commit

Permalink
feat(gestures): finish all gestures
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc committed Oct 23, 2023
1 parent 57f68b8 commit 263ed60
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 77 deletions.
9 changes: 7 additions & 2 deletions apps/test-app/src/app/drag/drag.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import {
injectDrag,
NgxDrag,
provideZonelessGesture,
type NgxDragState,
type NgxInjectDrag,
} from 'ngxtension/gestures';

const dragHandler = (from: WritableSignal<Vector2>) => {
return ({ target, active, offset: [ox, oy], cdr }: NgxDragState) => {
return ({
target,
active,
offset: [ox, oy],
cdr,
}: NgxInjectDrag['state']) => {
const el = target as HTMLElement;
from.set([ox, oy]);
el.style.transform = `translate(${ox}px, ${oy}px)`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
---
title: Drag Gesture
title: Gesture
description: ngxtension/gestures
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

## Usage

There are various ways we can utilize Drag Gesture from `ngxtension/gestures`
There are various ways we can utilize the Gesture from `ngxtension/gestures`

### `NgxDrag`
The examples show using `NgxDrag`, for `DragGesture`, but they can apply to other gestures as well.

### Directive

```ts
import { NgxDrag, type NgxDragState } from 'ngxtension/gestures';
import { NgxDrag, type NgxInjectDrag } from 'ngxtension/gestures';
```

`NgxDrag` is a directive that we can use to attach on any element to capture that element's drag events.
Expand All @@ -35,7 +37,7 @@ import { NgxDrag, type NgxDragState } from 'ngxtension/gestures';
templateUrl: './app.html',
})
export class App {
onDrag(state: NgxDragState) {
onDrag(state: NgxInjectDrag['state']) {
// fire every time a drag event happens
}
}
Expand All @@ -57,7 +59,7 @@ We can also use `NgxDrag` on a Host element by leveraging `hostDirectives`
})
export class DraggableBox {
@HostListener('ngxDrag', ['$event'])
onDrag(state: NgxDragState) {
onDrag(state: NgxInjectDrag['state']) {
// fire every time a drag event happens
}
}
Expand Down
10 changes: 5 additions & 5 deletions docs/src/content/docs/utilities/Gesture/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ sidebar:
`ngxtension/gestures` is a collection of Gestures that let you bind mouse and touch events.

- [x] Drag
- [ ] Move
- [ ] Hover
- [ ] Scroll
- [ ] Wheel
- [ ] Pinch
- [x] Move
- [x] Hover
- [x] Scroll
- [x] Wheel
- [x] Pinch
- [ ] Multiple gestures

### Comparison to Angular CDK
Expand Down
71 changes: 7 additions & 64 deletions libs/ngxtension/gestures/src/drag.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,30 @@
import {
ChangeDetectorRef,
DestroyRef,
Directive,
ElementRef,
EventEmitter,
Injector,
Input,
NgZone,
Output,
effect,
inject,
runInInjectionContext,
signal,
type OnInit,
} from '@angular/core';
import {
DragGesture,
type DragConfig,
type DragState,
type EventTypes,
type Handler,
} from '@use-gesture/vanilla';
import { assertInjector } from 'ngxtension/assert-injector';
import { injectZonelessGesture } from './zoneless-gesture';

type DragHandler = Handler<'drag', EventTypes['drag']>;
export type NgxDragState = Parameters<DragHandler>[0] & {
cdr: ChangeDetectorRef;
};
type NgxDragHandler = (state: NgxDragState) => ReturnType<DragHandler>;

export function injectDrag(
handler: NgxDragHandler,
{
injector,
zoneless,
config = () => ({}),
}: { injector?: Injector; zoneless?: boolean; config?: () => DragConfig } = {}
) {
injector = assertInjector(injectDrag, injector);
return runInInjectionContext(injector, () => {
const zonelessGesture = injectZonelessGesture();
const host = inject(ElementRef) as ElementRef<HTMLElement>;
const zone = inject(NgZone);
const cdr = inject(ChangeDetectorRef);

zoneless ??= zonelessGesture;

const ngHandler = (state: DragState) => {
return handler(Object.assign(state, { cdr }) as NgxDragState);
};
import { DragGesture } from '@use-gesture/vanilla';
import { createGesture, type GestureInfer } from './gesture';

const dragGesture = zoneless
? zone.runOutsideAngular(
() => new DragGesture(host.nativeElement, ngHandler)
)
: new DragGesture(host.nativeElement, ngHandler);

effect(() => {
if (zoneless) {
zone.runOutsideAngular(() => {
dragGesture.setConfig(config());
});
} else {
dragGesture.setConfig(config());
}
});

inject(DestroyRef).onDestroy(dragGesture.destroy.bind(dragGesture));
});
}
export const injectDrag = createGesture('drag', DragGesture);
export type NgxInjectDrag = GestureInfer<typeof injectDrag>;

@Directive({
selector: '[ngxDrag]',
standalone: true,
})
export class NgxDrag implements OnInit {
private config = signal<DragConfig>({});
@Input('ngxDragConfig') set _config(config: DragConfig) {
private config = signal<NgxInjectDrag['config']>({});
@Input('ngxDragConfig') set _config(config: NgxInjectDrag['config']) {
this.config.set(config);
}
@Input('ngxDragZoneless') zoneless?: boolean;
@Output() ngxDrag = new EventEmitter<NgxDragState>();
@Output() ngxDrag = new EventEmitter<NgxInjectDrag['state']>();

private injector = inject(Injector);

Expand Down
98 changes: 98 additions & 0 deletions libs/ngxtension/gestures/src/gesture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
ChangeDetectorRef,
DestroyRef,
ElementRef,
NgZone,
effect,
inject,
type Injector,
type Type,
} from '@angular/core';
import type {
EventTypes,
GestureKey,
Handler,
UserGestureConfig,
} from '@use-gesture/vanilla';
import type { Recognizer } from '@use-gesture/vanilla/dist/declarations/src/Recognizer';
import { assertInjector } from 'ngxtension/assert-injector';
import { injectZonelessGesture } from './zoneless-gesture';

type InjectGestureFn<
TGestureKey extends GestureKey,
TGestureHandler extends Handler<TGestureKey, EventTypes[TGestureKey]>,
TGestureConfig extends UserGestureConfig[TGestureKey]
> = {
(
handler: (
state: Parameters<TGestureHandler>[0] & { cdr: ChangeDetectorRef }
) => ReturnType<TGestureHandler>,
options?: {
injector?: Injector;
zoneless?: boolean;
config?: () => TGestureConfig;
}
): void;
};

export type GestureInfer<TInjectGesture extends (...args: any[]) => void> =
TInjectGesture extends InjectGestureFn<
infer _GestureKey,
infer _GestureHandler,
infer GestureConfig
>
? {
key: _GestureKey;
handlerParameters: Parameters<TInjectGesture>[0];
state: Parameters<Parameters<TInjectGesture>[0]>[0];
config: GestureConfig;
}
: never;

export function createGesture<
TGestureKey extends GestureKey,
TRecognizer extends Recognizer<TGestureKey>
>(key: TGestureKey, gesture: Type<TRecognizer>) {
type GestureHandler = Handler<TGestureKey, EventTypes[TGestureKey]>;
type GestureConfig = UserGestureConfig[TGestureKey];

return function _injectGesture(
handler,
{ injector, config = () => ({}), zoneless } = {}
) {
return assertInjector(_injectGesture, injector, () => {
const zonelessGesture = injectZonelessGesture();
const host = inject(ElementRef) as ElementRef<HTMLElement>;
const zone = inject(NgZone);
const cdr = inject(ChangeDetectorRef);

zoneless ??= zonelessGesture;

const ngHandler = (state: Parameters<GestureHandler>[0]) => {
return handler(Object.assign(state, { cdr }));
};

const gestureInstance = zoneless
? zone.runOutsideAngular(
() => new gesture(host.nativeElement, ngHandler)
)
: new gesture(host.nativeElement, ngHandler);

effect(() => {
if (zoneless) {
zone.runOutsideAngular(() => {
// @ts-expect-error
gestureInstance.setConfig(config());
});
} else {
// @ts-expect-error
gestureInstance.setConfig(config());
}
});

inject(DestroyRef).onDestroy(
gestureInstance.destroy.bind(gestureInstance)
);
});
} as InjectGestureFn<TGestureKey, GestureHandler, GestureConfig>;
}
38 changes: 38 additions & 0 deletions libs/ngxtension/gestures/src/hover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Directive,
EventEmitter,
Injector,
Input,
Output,
inject,
signal,
type OnInit,
} from '@angular/core';
import { HoverGesture } from '@use-gesture/vanilla';
import { createGesture, type GestureInfer } from './gesture';

export const injectHover = createGesture('hover', HoverGesture);
export type NgxInjectHover = GestureInfer<typeof injectHover>;

@Directive({
selector: '[ngxHover]',
standalone: true,
})
export class NgxHover implements OnInit {
private config = signal<NgxInjectHover['config']>({});
@Input('ngxHoverConfig') set _config(config: NgxInjectHover['config']) {
this.config.set(config);
}
@Input('ngxHoverZoneless') zoneless?: boolean;
@Output() ngxHover = new EventEmitter<NgxInjectHover['state']>();

private injector = inject(Injector);

ngOnInit(): void {
injectHover(this.ngxHover.emit.bind(this.ngxHover), {
injector: this.injector,
zoneless: this.zoneless,
config: this.config,
});
}
}
5 changes: 5 additions & 0 deletions libs/ngxtension/gestures/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export * from './drag';
export * from './hover';
export * from './move';
export * from './pinch';
export * from './scroll';
export * from './wheel';
export * from './zoneless-gesture';
38 changes: 38 additions & 0 deletions libs/ngxtension/gestures/src/move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Directive,
EventEmitter,
Injector,
Input,
Output,
inject,
signal,
type OnInit,
} from '@angular/core';
import { MoveGesture } from '@use-gesture/vanilla';
import { createGesture, type GestureInfer } from './gesture';

export const injectMove = createGesture('move', MoveGesture);
export type NgxInjectMove = GestureInfer<typeof injectMove>;

@Directive({
selector: '[ngxMove]',
standalone: true,
})
export class NgxMove implements OnInit {
private config = signal<NgxInjectMove['config']>({});
@Input('ngxMoveConfig') set _config(config: NgxInjectMove['config']) {
this.config.set(config);
}
@Input('ngxMoveZoneless') zoneless?: boolean;
@Output() ngxMove = new EventEmitter<NgxInjectMove['state']>();

private injector = inject(Injector);

ngOnInit(): void {
injectMove(this.ngxMove.emit.bind(this.ngxMove), {
injector: this.injector,
zoneless: this.zoneless,
config: this.config,
});
}
}
38 changes: 38 additions & 0 deletions libs/ngxtension/gestures/src/pinch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Directive,
EventEmitter,
Injector,
Input,
Output,
inject,
signal,
type OnInit,
} from '@angular/core';
import { PinchGesture } from '@use-gesture/vanilla';
import { createGesture, type GestureInfer } from './gesture';

export const injectPinch = createGesture('pinch', PinchGesture);
export type NgxInjectPinch = GestureInfer<typeof injectPinch>;

@Directive({
selector: '[ngxPinch]',
standalone: true,
})
export class NgxPinch implements OnInit {
private config = signal<NgxInjectPinch['config']>({});
@Input('ngxPinchConfig') set _config(config: NgxInjectPinch['config']) {
this.config.set(config);
}
@Input('ngxPinchZoneless') zoneless?: boolean;
@Output() ngxPinch = new EventEmitter<NgxInjectPinch['state']>();

private injector = inject(Injector);

ngOnInit(): void {
injectPinch(this.ngxPinch.emit.bind(this.ngxPinch), {
injector: this.injector,
zoneless: this.zoneless,
config: this.config,
});
}
}
Loading

0 comments on commit 263ed60

Please sign in to comment.