Skip to content

Instantly share code, notes, and snippets.

@menosprezzi
Last active August 17, 2019 18:41
Show Gist options
  • Save menosprezzi/9d4de98960d343a4283e268073402b6c to your computer and use it in GitHub Desktop.
Save menosprezzi/9d4de98960d343a4283e268073402b6c to your computer and use it in GitHub Desktop.
Stencil's Component Behavior
import { ComponentInterface } from '@stencil/core';
import { extendMethod } from '../lang/extend-method';
/**
* Represents a Component that implements a Behavior Modification API.
*/
export interface ComponentBase extends ComponentInterface {
/**
* The main native nativeElement from the component.
* @Element
*/
host: HTMLElement;
/**
* The unload lifecycle hook.
* Note that Stencil only calls componentDidUnload if it is declared in the component.
*/
componentDidUnload: () => void;
/**
* If this behavior is applied to a native element.
*/
native?: boolean;
}
/**
* Represents a Component's Behavior Modification.
*/
export class ComponentBehavior<T extends ComponentBase> {
/**
* The internal component that this instance is attached.
*/
component: T;
/**
* A hook called before the component load.
*/
beforeAttach?(): Promise<any> | void;
/**
* A hook called after the component load.
*/
attach?(): Promise<any> | void;
/**
* A hook called before the component destroy.
*/
detach?(): Promise<any> | void;
constructor(component: T) {
this.component = component;
if (this.beforeAttach) { this.beforeAttach(); }
// Native Elements support
if (this.component.native) {
console.warn('Attaching a behavior to a native element', this.component);
this.component.host.remove = () => {
this.component.componentDidUnload();
Element.prototype.remove.apply(this.component.host);
};
setTimeout(() => this.component.componentDidLoad(), 0);
}
extendMethod(this.component, 'componentDidLoad', componentDidLoad => {
if (componentDidLoad) { componentDidLoad(); }
if (this.attach) { this.attach(); }
});
extendMethod(this.component, 'componentDidUnload', componentDidUnload => {
if (componentDidUnload) { componentDidUnload(); }
if (this.detach) { this.detach(); }
});
}
}
import { Component, Element, Event, EventEmitter, Prop, Watch } from '@stencil/core';
import { FocusBehavior, FocusableComponent } from '../../../behaviors/focus-behavior';
import { Bind } from '../../../utils/lang/bind';
/**
* Layout webcomponent.
*/
@Component({
tag: 'ac-layout',
styleUrl: 'ac-layout.scss',
shadow: false
})
export class AcLayout implements FocusableComponent {
@Element() host: HTMLAcLayoutElement;
focusBehavior = new FocusBehavior(this);
focusTarget: HTMLElement;
hasFocus: boolean;
/**
* Collapse a nav drawer.
*/
@Prop({ mutable: true, reflectToAttr: true }) collapsed: 'nav-left';
@Event() contentScroll: EventEmitter<{top: number, left: number}>;
@Watch('collapsed')
collapsedDidUpdate() {
this.hasFocus = !!this.collapsed;
}
componentDidLoad() {
this.focusTarget = this.host.querySelector('.ac-layout__nav-left-container ac-navdrawer');
}
componentDidUnload() {}
whenBlur(element) {
console.log(element, element.dataset);
if (!element.dataset.navdrawer && this.collapsed) {
this.collapsed = null;
}
}
@Bind
private handleContentScroll(ev) {
this.contentScroll.emit({ top: ev.target.scrollTop, left: ev.target.scrollLeft });
}
hostData() {
return {
class: {
[`ac-layout--${this.collapsed}-collapsed`]: !!this.collapsed,
}
};
}
render() {
return [
<div class="ac-layout__nav-left-container">
<slot name="nav-left" />
</div>,
<div class="ac-layout__content-container">
<slot name="header" />
<div class="ac-layout__content-scroll" onScroll={this.handleContentScroll}>
<slot name="content" />
</div>
</div>
];
}
}
/**
* Extend a method functionality, applying monkeypatch method.
* @link https://www.audero.it/blog/2016/12/05/monkey-patching-javascript/
* @param target The target object.
* @param method The name of the method that will be extended.
* @param extend The method's patch.
*/
export function extendMethod(target: any, method: string, extend: (original: Function, args?: any[]) => any) {
const original = target[method];
target['__' + method + '__patch_list'] = target['__' + method + '__patches'] || [];
const patchList = target['__' + method + '__patch_list'];
patchList.push(extend);
target[method] = function(...args) {
let lastResult;
for (const patch of patchList) {
lastResult = patch.bind(this)(original ? original.bind(this) : null, args);
}
return lastResult;
};
}
import debug from 'debug/src/browser';
import { ComponentBase, ComponentBehavior } from '../utils/stencil/component-behavior';
const log = debug('solar:FocusBehavior');
/**
* Implements a Focus logic in a component, providing a control for blur clicks.
*/
export class FocusBehavior extends ComponentBehavior<FocusableComponent> {
/**
* Check if a target node branch has a data-toggle that match the host id.
*/
static checkBypassNode(focusElt, target) {
let isBypassNode = false;
if (focusElt.id) {
const bypassNode = document.body
.querySelector(`[data-toggle="${focusElt.id}"]`);
isBypassNode = bypassNode ? bypassNode.contains(target) : false;
}
return isBypassNode;
}
/**
* Filter all the clicks in the body and calls the `whenBlur` from the component if match an outside click.
* @param ev A Click Event.
*/
private handleBodyClick = (ev: any) => {
if (this.component.hasFocus) {
const focusElt = this.component.focusTarget || this.component.host;
const isClickingOutsideTheTarget = ev.target.closest(focusElt.tagName.toLowerCase()) !== focusElt;
if (isClickingOutsideTheTarget && !FocusBehavior.checkBypassNode(focusElt, ev.target)) {
log('Clicked outside', focusElt);
this.component.whenBlur(ev.target);
}
}
};
/**
* Setup the event listener to the body.
*/
attach() {
document.body.addEventListener('click', this.handleBodyClick);
}
/**
* Remove the event listener to the body.
*/
detach() {
document.body.removeEventListener('click', this.handleBodyClick);
}
}
/**
* Represents a component that implements the focus logic.
*/
export interface FocusableComponent extends ComponentBase {
/**
* The instance of the behavior applied in the component.
*/
focusBehavior: FocusBehavior;
/**
* Called when the behavior detects a click outside of the component.
*/
whenBlur: (element: HTMLElement) => void;
/**
* Used to control the focus state.
*/
hasFocus: boolean;
/**
* The target to be checked. If it is null, the host will be used.
*/
focusTarget?: HTMLElement;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment