Skip to content

Instantly share code, notes, and snippets.

@lukebrandonfarrell
Created August 13, 2019 08: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 lukebrandonfarrell/0d1ce7213bfb0289c85bb13e6a827def to your computer and use it in GitHub Desktop.
Save lukebrandonfarrell/0d1ce7213bfb0289c85bb13e6a827def to your computer and use it in GitHub Desktop.
Funnels TypeScript implementation for RMN
/**
* @author Luke Brandon Farrell
* @description Funnels allow us to re-use screens with react-native-navigation
* and build more complex patterns. In the context of this utility a stack
* refers to a single screen, where as a funnel refres to a collection of screens.
*/
import { Platform } from "react-native";
import { Navigation, Layout, Options } from "react-native-navigation";
import * as NavigationLayouts from "react-native-navigation-layouts";
import _get from "lodash/get";
import _isArray from "lodash/isArray";
import _isInteger from "lodash/isInteger";
import _isString from "lodash/isString";
import _remove from "lodash/remove";
import _isNil from "lodash/isNil";
import _has from "lodash/has";
import _size from "lodash/size";
import _isEmpty from "lodash/isEmpty";
import _findIndex from "lodash/findIndex";
import _castArray from "lodash/castArray";
import { _set } from "./lodash-extension";
enum NavigationMethodsType {
pop = "pop",
popTo = "popTo",
popToRoot = "popToRoot",
push = "push",
setRoot = "setRoot",
showModal = "showModal",
showOverlay = "showOverlay",
dismissModal = "dismissModal",
dismissOverlay = "dismissOverlay",
dismissAllModals = "dismissAllModals",
}
interface StackInterface {
layout: Layout | string,
navigate: NavigationMethodsType,
disabled?: boolean,
options?: Options,
pointer: number,
passProps?: object,
componentId?: string,
targetComponentId?: string,
branch: Array<StackInterface>
}
/**
* Keeps track of the funnel location
*
* @type {number}
*/
let pointer : number = 0;
/**
* Keeps track of all stacks in the funnel
*
* @type {Array}
*/
let funnel : Array<StackInterface> = [];
class Funnels {
/**
* Creates a stack
*
* @param {array} data
*/
public create(data : Array<StackInterface>) {
funnel = [];
pointer = 0;
if (_isArray(data)) {
if (_isEmpty(funnel)) {
// Populate the stack
data.map(value => {
if(!value.disabled){
funnel.push({
pointer: 0,
branch: [],
...value
});
}
});
}
}
return this;
}
/**
* Consumes the stack
*
* @param data
*/
public navigate(data : StackInterface) {
/*
* If the plain object contains a name / layout, then
* we want to perform plain navigation, it it does
* not contain name / layout and stack exists, we
* navigate from the stack
*/
const isPlainNavigation = _isEmpty(funnel);
if (isPlainNavigation) {
this.routeTo(data);
} else {
/*
* Adds data passed via the navigate method
* to the current stack object, this is
* important for having access to componentId
* on each screen.
*/
funnel[pointer] = {
...funnel[pointer],
...data
};
const currentStack = funnel[pointer - 1];
const nextStack = funnel[pointer];
/*
* If there is a branch key in this
* 'stack', then we have created a
* sub stack, meaning we need to
* navigate inside it until exited.
*/
if (!_isEmpty(_get(currentStack, "branch"))) {
const { branch: branchStack, pointer: branchPointer } = currentStack;
this.routeTo({
...branchStack[branchPointer],
...data
});
currentStack["pointer"] = currentStack["pointer"] + 1;
} else {
/*
* We automatically exit the stack if we get
* to the end of it, the exit method does the
* same as the routeTo method, although it
* empties the stack and removes it from async
* storage.
*/
if (pointer === _size(funnel) - 1) {
this.exit(nextStack);
} else {
this.routeTo(nextStack);
pointer = pointer + 1;
}
}
}
return this;
}
/**
* Branching a funnel will create sub funnel in this parent funnel
* to allow the consumer to return to the parent funnel when
* needed.
*
* @param data
*/
public branch(data : Array<StackInterface>) {
/*
* If stack is not empty, we add the
* stack inside this stack, to build
* a sub-stack.
*/
if (!_isEmpty(funnel)) {
const currentStack = funnel[pointer - 1];
currentStack["branch"] = data.map(value => value);
currentStack["pointer"] = 0;
}
return this;
}
/**
* Moves the pointer one back in the funnel.
*
* A stack has to be passed as data param
* to perform navigation.
*
* @param data
*/
public back(data : StackInterface) {
if (!_isNil(funnel)) {
const currentStack : StackInterface = funnel[pointer - 1];
if (_has(currentStack, "branch")) {
currentStack["pointer"] = currentStack["pointer"] - 1;
if (currentStack["pointer"] === 0) {
currentStack["branch"] = [];
}
} else {
pointer = pointer - 1;
}
}
this.routeTo(data);
return this;
}
/**
* Exits the stack
*
* This will reset the funnel.
*
* @param layout - optional navigation on revoke
*/
public exit(layout : StackInterface) : void {
funnel = [];
pointer = 0;
if (!_isNil(layout)) {
this.routeTo(layout);
}
}
/**
* Uses RNN methods to navigate
*
* @param data
*/
private routeTo(data : StackInterface) : void {
const {
layout,
passProps,
options,
navigate: type,
componentId,
targetComponentId
} = data;
if(!_isNil(componentId)){
if (type === NavigationMethodsType.push) {
if (_isString(layout)) {
Navigation.push(
componentId,
NavigationLayouts.component(layout, passProps, options)
);
} else {
Navigation.push(componentId, layout);
}
} else if (type === "pop") {
Navigation.pop(componentId);
} else if (type === "popTo") {
if(!_isNil(targetComponentId)){
Navigation.popTo(targetComponentId);
}
} else if (type === "popToRoot") {
Navigation.popToRoot(componentId);
} else if (type === "setRoot") {
if (Platform.OS === "android") {
Navigation.dismissAllModals();
}
if (_isString(layout)) {
Navigation.setRoot(
NavigationLayouts.root(NavigationLayouts.stack([NavigationLayouts.component(layout, passProps, options)]))
);
} else {
Navigation.setRoot(NavigationLayouts.root(layout));
}
} else if (type === "showModal") {
Navigation.showModal(NavigationLayouts.component(layout));
} else if (type === "dismissModal") {
Navigation.dismissModal(componentId, options);
} else if (type === "dismissAllModals") {
Navigation.dismissAllModals(options);
} else if (type === "showOverlay") {
Navigation.showOverlay(NavigationLayouts.component(layout));
} else if (type === "dismissOverlay") {
Navigation.dismissOverlay(componentId);
}
}
}
}
export default new Funnels();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment