Skip to content

Commit

Permalink
chore(behaviors): centralize behavior trigger code (#726)
Browse files Browse the repository at this point in the history
Moving the behavior trigger code out of `hyper-ref` into a shared
location to allow access by other components

Asana: https://app.asana.com/0/1204008699308084/1205741965789631/f
  • Loading branch information
hgray-instawork authored Oct 17, 2023
1 parent c156d5c commit 077e3a5
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 95 deletions.
101 changes: 26 additions & 75 deletions src/core/hyper-ref/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,24 @@
*
*/

import * as Behaviors from 'hyperview/src/services/behaviors';
import * as Dom from 'hyperview/src/services/dom';
import * as Events from 'hyperview/src/services/events';
import * as Namespaces from 'hyperview/src/services/namespaces';
import * as Render from 'hyperview/src/services/render';
import {
ACTIONS,
BEHAVIOR_ATTRIBUTES,
LOCAL_NAME,
NAV_ACTIONS,
PRESS_TRIGGERS,
TRIGGERS,
UPDATE_ACTIONS,
} from 'hyperview/src/types';
import { ATTRIBUTES, PRESS_TRIGGERS_PROP_NAMES } from './types';
import type {
HvComponentOnUpdate,
HvComponentOptions,
NavAction,
PressTrigger,
StyleSheet,
StyleSheets,
Trigger,
UpdateAction,
} from 'hyperview/src/types';
import type { PressHandlers, PressPropName, Props, State } from './types';
import React, { PureComponent } from 'react';
Expand All @@ -37,10 +33,12 @@ import {
Text,
TouchableOpacity,
} from 'react-native';
import { PRESS_TRIGGERS_PROP_NAMES } from './types';
import VisibilityDetectingView from 'hyperview/src/VisibilityDetectingView';
import { XMLSerializer } from '@instawork/xmldom';
import { X_RESPONSE_STALE_REASON } from 'hyperview/src/services/dom/types';
import { createTestProps } from 'hyperview/src/services';

/**
* Wrapper to handle UI events
* Stop propagation and prevent default client behavior
Expand Down Expand Up @@ -107,7 +105,9 @@ export default class HyperRef extends PureComponent<Props, State> {

updateStyle = () => {
// Retrieve and cache style
const styleAttr = this.props.element.getAttribute(ATTRIBUTES.HREF_STYLE);
const styleAttr = this.props.element.getAttribute(
BEHAVIOR_ATTRIBUTES.HREF_STYLE,
);
this.style = styleAttr
? styleAttr.split(' ').map(s => this.props.stylesheets.regular[s])
: null;
Expand All @@ -116,7 +116,7 @@ export default class HyperRef extends PureComponent<Props, State> {
onEventDispatch = (eventName: string) => {
const behaviorElements = Dom.getBehaviorElements(this.props.element);
const onEventBehaviors = behaviorElements.filter(e => {
if (e.getAttribute(ATTRIBUTES.TRIGGER) === TRIGGERS.ON_EVENT) {
if (e.getAttribute(BEHAVIOR_ATTRIBUTES.TRIGGER) === TRIGGERS.ON_EVENT) {
const currentAttributeEventName:
| string
| null
Expand All @@ -138,7 +138,7 @@ export default class HyperRef extends PureComponent<Props, State> {
return false;
});
onEventBehaviors.forEach(behaviorElement => {
const handler = this.createActionHandler(
const handler = Behaviors.createActionHandler(
behaviorElement,
this.props.onUpdate,
);
Expand All @@ -160,63 +160,16 @@ export default class HyperRef extends PureComponent<Props, State> {
});
};

createActionHandler = (
behaviorElement: Element,
onUpdate: HvComponentOnUpdate,
) => {
const action =
behaviorElement.getAttribute(ATTRIBUTES.ACTION) || NAV_ACTIONS.PUSH;
if (Object.values(NAV_ACTIONS).indexOf(action as NavAction) >= 0) {
return (element: Element) => {
const href = behaviorElement.getAttribute(ATTRIBUTES.HREF);
const targetId = behaviorElement.getAttribute(ATTRIBUTES.TARGET);
const showIndicatorId = behaviorElement.getAttribute(
ATTRIBUTES.SHOW_DURING_LOAD,
);
const delay = behaviorElement.getAttribute(ATTRIBUTES.DELAY);
onUpdate(href, action, element, { delay, showIndicatorId, targetId });
};
}
if (
action === ACTIONS.RELOAD ||
Object.values(UPDATE_ACTIONS).indexOf(action as UpdateAction) >= 0
) {
return (element: Element) => {
const href = behaviorElement.getAttribute(ATTRIBUTES.HREF);
const verb = behaviorElement.getAttribute(ATTRIBUTES.VERB);
const targetId = behaviorElement.getAttribute(ATTRIBUTES.TARGET);
const showIndicatorIds = behaviorElement.getAttribute(
ATTRIBUTES.SHOW_DURING_LOAD,
);
const hideIndicatorIds = behaviorElement.getAttribute(
ATTRIBUTES.HIDE_DURING_LOAD,
);
const delay = behaviorElement.getAttribute(ATTRIBUTES.DELAY);
const once = behaviorElement.getAttribute(ATTRIBUTES.ONCE);
onUpdate(href, action, element, {
behaviorElement,
delay,
hideIndicatorIds,
once,
showIndicatorIds,
targetId,
verb,
});
};
}
// Custom behavior
return (element: Element) =>
onUpdate(null, action, element, { behaviorElement, custom: true });
};

getBehaviorElements = (trigger: Trigger): Element[] => {
return this.behaviorElements.filter(
e => e.getAttribute(ATTRIBUTES.TRIGGER) === trigger,
e => e.getAttribute(BEHAVIOR_ATTRIBUTES.TRIGGER) === trigger,
);
};

getStyle = (): StyleSheet | null | undefined => {
const styleAttr = this.props.element.getAttribute(ATTRIBUTES.HREF_STYLE);
const styleAttr = this.props.element.getAttribute(
BEHAVIOR_ATTRIBUTES.HREF_STYLE,
);
return styleAttr
? styleAttr.split(' ').map(s => this.props.stylesheets.regular[s])
: null;
Expand All @@ -233,24 +186,21 @@ export default class HyperRef extends PureComponent<Props, State> {
);
loadBehaviors = loadBehaviors.concat(loadStaleBehaviors);
}
loadBehaviors.forEach(behaviorElement => {
const handler = this.createActionHandler(
behaviorElement,

if (loadBehaviors.length > 0) {
Behaviors.triggerBehaviors(
this.props.element,
loadBehaviors,
this.props.onUpdate,
);
if (behaviorElement.getAttribute(ATTRIBUTES.IMMEDIATE) === 'true') {
handler(this.props.element);
} else {
setTimeout(() => handler(this.props.element), 0);
}
});
}
};

TouchableView = ({ children }: { children: JSX.Element }): JSX.Element => {
const behaviors = this.behaviorElements.filter(
e =>
PRESS_TRIGGERS.indexOf(
(e.getAttribute(ATTRIBUTES.TRIGGER) ||
(e.getAttribute(BEHAVIOR_ATTRIBUTES.TRIGGER) ||
TRIGGERS.PRESS) as PressTrigger,
) >= 0,
);
Expand All @@ -268,10 +218,11 @@ export default class HyperRef extends PureComponent<Props, State> {

behaviors.forEach(behaviorElement => {
const trigger =
behaviorElement.getAttribute(ATTRIBUTES.TRIGGER) || TRIGGERS.PRESS;
behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.TRIGGER) ||
TRIGGERS.PRESS;
const triggerPropName =
PRESS_TRIGGERS_PROP_NAMES[trigger as PressTrigger];
const handler = this.createActionHandler(
const handler = Behaviors.createActionHandler(
behaviorElement,
this.props.onUpdate,
);
Expand Down Expand Up @@ -387,7 +338,7 @@ export default class HyperRef extends PureComponent<Props, State> {
return children;
}
const refreshHandlers = behaviors.map(behaviorElement =>
this.createActionHandler(behaviorElement, this.props.onUpdate),
Behaviors.createActionHandler(behaviorElement, this.props.onUpdate),
);
const onRefresh = () => refreshHandlers.forEach(h => h(this.props.element));

Expand All @@ -412,7 +363,7 @@ export default class HyperRef extends PureComponent<Props, State> {
// the DOM might have been mutated since.
this.getBehaviorElements(TRIGGERS.VISIBLE)
.map(behaviorElement =>
this.createActionHandler(behaviorElement, this.props.onUpdate),
Behaviors.createActionHandler(behaviorElement, this.props.onUpdate),
)
.forEach(h => h(this.props.element));
};
Expand All @@ -422,7 +373,7 @@ export default class HyperRef extends PureComponent<Props, State> {
// and the internal state needs to be reset.
const id =
this.props.element.getAttribute('id') ||
Object.values(ATTRIBUTES)
Object.values(BEHAVIOR_ATTRIBUTES)
.reduce((acc: string[], name: string) => {
const value = this.props.element.getAttribute(name);
return value ? [...acc, `${name}:${value}`] : acc;
Expand Down
18 changes: 0 additions & 18 deletions src/core/hyper-ref/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,6 @@ export type State = {
refreshing: boolean;
};

export const ATTRIBUTES = {
ACTION: 'action',
DELAY: 'delay',
EVENT_NAME: 'event-name',
HIDE_DURING_LOAD: 'hide-during-load',
HREF: 'href',
HREF_STYLE: 'href-style',
IMMEDIATE: 'immediate',
NEW_VALUE: 'new-value',
ONCE: 'once',
SHOW_DURING_LOAD: 'show-during-load',
TARGET: 'target',
TRIGGER: 'trigger',
VERB: 'verb',
} as const;

export type Attribute = typeof ATTRIBUTES[keyof typeof ATTRIBUTES];

export type PressHandlers = {
onLongPress?: () => void;
onPressIn?: () => void;
Expand Down
86 changes: 84 additions & 2 deletions src/services/behaviors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
*/

import * as Dom from 'hyperview/src/services/dom';
import type { HvComponentOnUpdate, UpdateAction } from 'hyperview/src/types';
import { ACTIONS } from 'hyperview/src/types';
import {
ACTIONS,
BEHAVIOR_ATTRIBUTES,
NAV_ACTIONS,
UPDATE_ACTIONS,
} from 'hyperview/src/types';
import type {
HvComponentOnUpdate,
NavAction,
UpdateAction,
} from 'hyperview/src/types';
import { shallowCloneToRoot } from 'hyperview/src/services';

/**
Expand Down Expand Up @@ -109,6 +118,9 @@ export const performUpdate = (
return shallowCloneToRoot(targetElement);
};

/**
* Trigger all behaviors matching the given name
*/
export const trigger = (
name: string,
element: Element,
Expand Down Expand Up @@ -138,3 +150,73 @@ export const trigger = (
});
});
};

/**
* Trigger a set of pre-filtered behaviors
*/
export const triggerBehaviors = (
element: Element,
behaviors: Element[],
onUpdate: HvComponentOnUpdate,
) => {
behaviors.forEach(behaviorElement => {
const handler = createActionHandler(behaviorElement, onUpdate);
if (
behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.IMMEDIATE) === 'true'
) {
handler(element);
} else {
setTimeout(() => handler(element), 0);
}
});
};

export const createActionHandler = (
behaviorElement: Element,
onUpdate: HvComponentOnUpdate,
) => {
const action =
behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.ACTION) ||
NAV_ACTIONS.PUSH;
if (Object.values(NAV_ACTIONS).indexOf(action as NavAction) >= 0) {
return (element: Element) => {
const href = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.HREF);
const targetId = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.TARGET);
const showIndicatorId = behaviorElement.getAttribute(
BEHAVIOR_ATTRIBUTES.SHOW_DURING_LOAD,
);
const delay = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.DELAY);
onUpdate(href, action, element, { delay, showIndicatorId, targetId });
};
}
if (
action === ACTIONS.RELOAD ||
Object.values(UPDATE_ACTIONS).indexOf(action as UpdateAction) >= 0
) {
return (element: Element) => {
const href = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.HREF);
const verb = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.VERB);
const targetId = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.TARGET);
const showIndicatorIds = behaviorElement.getAttribute(
BEHAVIOR_ATTRIBUTES.SHOW_DURING_LOAD,
);
const hideIndicatorIds = behaviorElement.getAttribute(
BEHAVIOR_ATTRIBUTES.HIDE_DURING_LOAD,
);
const delay = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.DELAY);
const once = behaviorElement.getAttribute(BEHAVIOR_ATTRIBUTES.ONCE);
onUpdate(href, action, element, {
behaviorElement,
delay,
hideIndicatorIds,
once,
showIndicatorIds,
targetId,
verb,
});
};
}
// Custom behavior
return (element: Element) =>
onUpdate(null, action, element, { behaviorElement, custom: true });
};
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,23 @@ export type BehaviorRegistry = {
[key: string]: HvBehavior;
};

// https://hyperview.org/docs/reference_behavior_attributes
export const BEHAVIOR_ATTRIBUTES = {
ACTION: 'action',
DELAY: 'delay',
EVENT_NAME: 'event-name',
HIDE_DURING_LOAD: 'hide-during-load',
HREF: 'href',
HREF_STYLE: 'href-style',
IMMEDIATE: 'immediate',
NEW_VALUE: 'new-value',
ONCE: 'once',
SHOW_DURING_LOAD: 'show-during-load',
TARGET: 'target',
TRIGGER: 'trigger',
VERB: 'verb',
} as const;

// https://hyperview.org/docs/reference_behavior_attributes#trigger
export const TRIGGERS = Object.freeze({
DESELECT: 'deselect',
Expand Down

0 comments on commit 077e3a5

Please sign in to comment.