Skip to content

Instantly share code, notes, and snippets.

@crutchcorn
Created September 18, 2022 17:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save crutchcorn/e014503fc147f1e200cd9ec9245dda7f to your computer and use it in GitHub Desktop.
Save crutchcorn/e014503fc147f1e200cd9ec9245dda7f to your computer and use it in GitHub Desktop.
A semi-complex code example in "The Framework Field Guide 1: Fundamentals"
@Injectable()
class CloseIfOutSideContext implements OnDestroy {
getCloseIfOutsideFunction = (contextMenu: ElementRef<HTMLElement>, close: EventEmitter<any>) => {
return (e: MouseEvent) => {
const contextMenuEl = contextMenu?.nativeElement;
if (!contextMenuEl) return;
const isClickInside = contextMenuEl.contains(e.target as HTMLElement);
if (isClickInside) return;
close.emit();
}
};
setup(contextMenu: ElementRef<HTMLElement>, close: EventEmitter<any>) {
this.closeIfOutsideOfContext = this.getCloseIfOutsideFunction(contextMenu, close);
document.addEventListener('click', this.closeIfOutsideOfContext);
}
ngOnDestroy() {
document.removeEventListener('click', this.closeIfOutsideOfContext);
this.closeIfOutsideOfContext = () => {};
}
closeIfOutsideOfContext: (e: MouseEvent) => void = () => {};
}
@Component({
selector: 'context-menu',
template: `
<div
#contextMenu
tabIndex="0"
[style]="{
position: 'fixed',
top: y + 20,
left: x + 20,
background: 'white',
border: '1px solid black',
borderRadius: 16,
padding: '1rem'
}"
>
<button (click)="close.emit()">X</button>
This is a context menu
</div>
`,
providers: [CloseIfOutSideContext]
})
export class ContextMenuComponent implements AfterViewInit {
@ViewChild('contextMenu') contextMenu!: ElementRef<HTMLElement>;
@Input() x: number = 0;
@Input() y: number = 0;
@Output() close = new EventEmitter();
constructor(private closeIfOutsideContext: CloseIfOutSideContext) {
}
focus() {
this.contextMenu.nativeElement.focus();
}
ngAfterViewInit() {
this.closeIfOutsideContext.setup(this.contextMenu, this.close);
}
}
@Injectable()
class BoundsContext {
bounds = {
height: 0,
width: 0,
x: 0,
y: 0,
};
contextOrigin: ElementRef | undefined;
resizeListener = () => {
if (!this.contextOrigin) return;
this.bounds = this.contextOrigin.nativeElement.getBoundingClientRect();
};
setup(contextOrigin: ElementRef) {
this.bounds = contextOrigin.nativeElement.getBoundingClientRect();
this.contextOrigin = contextOrigin;
window.addEventListener('resize', this.resizeListener);
}
cleanup() {
window.removeEventListener('resize', this.resizeListener);
this.contextOrigin = undefined;
}
}
@Component({
selector: 'my-app',
template: `
<div [style]="{ marginTop: '5rem', marginLeft: '5rem' }">
<div #contextOrigin (contextmenu)="open($event)">
Right click on me!
</div>
</div>
<context-menu #contextMenu *ngIf="isOpen" [x]="boundsContext.bounds.x" [y]="boundsContext.bounds.y" (close)="close()"></context-menu>
`,
providers: [BoundsContext]
})
export class AppComponent implements AfterViewInit, OnDestroy {
@ViewChild('contextOrigin') contextOrigin!: ElementRef<HTMLElement>;
@ViewChildren('contextMenu') contextMenu!: QueryList<ContextMenuComponent>;
isOpen = false;
constructor(public boundsContext: BoundsContext) {
}
ngAfterViewInit() {
this.boundsContext.setup(this.contextOrigin);
this.contextMenu.changes.forEach(() => {
const isLoaded = this?.contextMenu?.first;
if (!isLoaded) return;
this.contextMenu.first.focus();
});
}
ngOnDestroy() {
this.boundsContext.cleanup();
}
close() {
this.isOpen = false;
}
open(e: UIEvent) {
e.preventDefault();
this.isOpen = true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment