Master react (#18998)

* chore(): bump to beta 8

* fix(): IonFabButton href fix

* fix(react): support components with href attributes

* fix(): Prep work to break router out

* fix(): breaking react-router and react-core into own packages

* chore(): moving view stuff out of react-core

* chore(): dev build 8-1

* chore(): update to react beta 8

* chore(): fixes to deps

* fix(): removing IonAnchor in favor of IonRouterLink

* chore(): beta 9 release

* refactor(react): treeshake, minify, api

* wip

* fix(): react dev builds

* fix(): fixes to get app builds working again

* fix(): removing tgz file

* feat(): adding platform helper methods

* fix(): don't map attributes to props

* chore(): add test app

* feat(): copy css folder from core

* chore(): move rollup node resolve to devDependencies

* fix(): expose setupConfig()

* perf(): improve treeshaking

* fix(): removing crypto from generateUniqueId

* fix(): adding missing rollup dp

* fix(): test cleanup and fixes to make tests pass

* chore(): moving react to packages folder

* fix(): fixing react build due to move to packages

* feat(): adding missing IonInfiniteScrollContent component

* chore(): add automated testing using cypress

* fix(): adding option onDidDismiss to controller components

* 0.0.10 react

* wip

* fix(): removing deprecated React calls

* fix(): exporting setupConfig from core

* chore(): bump to 4.8.0-rc.0

* chore(): updating test-app deps and fixing test

* chore(): updates to react readme
This commit is contained in:
Manu MA
2019-08-13 22:24:44 +02:00
committed by Ely Lucas
parent 0b1e23f754
commit 930b271a4a
224 changed files with 16337 additions and 1734 deletions

View File

@ -0,0 +1,81 @@
import { RouterDirection } from '@ionic/core';
import { NavContext, NavContextState } from '@ionic/react';
import { Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { ViewManager } from './ViewManager';
interface NavManagerProps extends RouteComponentProps {
findViewInfoByLocation: (location: HistoryLocation) => any;
findViewInfoById: (id: string) => any;
};
interface NavManagerState extends NavContextState {};
export class NavManager extends React.Component<NavManagerProps, NavManagerState> {
listenUnregisterCallback: UnregisterCallback;
constructor(props: NavManagerProps) {
super(props);
this.state = {
goBack: this.goBack.bind(this),
hasIonicRouter: () => true,
getHistory: this.getHistory.bind(this),
getLocation: this.getLocation.bind(this),
navigate: this.navigate.bind(this),
getViewManager: this.getViewManager.bind(this),
currentPath: this.props.location.pathname
}
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
this.setState({
currentPath: location.pathname
})
});
}
componentWillUnmount() {
if(this.listenUnregisterCallback) {
this.listenUnregisterCallback();
}
}
goBack(defaultHref?: string) {
const { view: leavingView } = this.props.findViewInfoByLocation(this.props.location);
if (leavingView) {
const { view: enteringView } = this.props.findViewInfoById(leavingView.prevId);
if (enteringView) {
this.props.history.replace(enteringView.routeData.match.url, { direction: 'back' });
} else {
defaultHref && this.props.history.replace(defaultHref, { direction: 'back' });
}
} else {
defaultHref && this.props.history.replace(defaultHref, { direction: 'back' });
}
}
getHistory() {
return this.props.history as any;
}
getLocation() {
return this.props.location as any;
}
navigate(path: string, direction?: RouterDirection) {
this.props.history.push(path, { direction });
}
getViewManager() {
return ViewManager;
}
render() {
return (
<NavContext.Provider value={this.state}>
{this.props.children}
</NavContext.Provider>
);
}
}

View File

@ -0,0 +1,35 @@
import React, { ReactNode } from 'react';
import { NavDirection } from '@ionic/core';
import { ViewItem } from './ViewItem';
export interface ViewStack {
routerOutlet: HTMLIonRouterOutletElement;
activeId?: string,
views: ViewItem[]
}
export interface ViewStacks {
[key: string]: ViewStack;
}
export interface RouteManagerContextState {
hideView: (viewId: string) => void;
viewStacks: ViewStacks;
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
removeViewStack: (stack: string) => void;
renderChild: (item: ViewItem) => void;
transitionView: (enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) => void;
}
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
viewStacks: {},
hideView: () => { navContextNotFoundError(); },
setupIonRouter: () => { navContextNotFoundError() },
removeViewStack: () => { navContextNotFoundError(); },
renderChild: () => { navContextNotFoundError(); },
transitionView: () => { navContextNotFoundError(); }
});
function navContextNotFoundError() {
console.error('IonReactRouter not found, did you add it to the app?')
}

View File

@ -0,0 +1,329 @@
import { NavDirection } from '@ionic/core';
import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { BrowserRouter, BrowserRouterProps, match, matchPath, Redirect, Route, RouteComponentProps, RouteProps, withRouter } from 'react-router-dom';
import { generateUniqueId } from '../utils';
import { NavManager } from './NavManager';
import { RouteManagerContext, RouteManagerContextState, ViewStack, ViewStacks } from './RouteManagerContext';
import { ViewItem } from './ViewItem';
interface RouterManagerProps extends RouteComponentProps { }
interface RouteManagerState extends RouteManagerContextState { }
interface IonRouteData {
match: match<{ tab: string }> | null;
childProps: RouteProps;
}
class RouteManager extends React.Component<RouterManagerProps, RouteManagerState> {
listenUnregisterCallback: UnregisterCallback | undefined;
activeViewId?: string;
prevViewId?: string;
constructor(props: RouterManagerProps) {
super(props);
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
this.state = {
viewStacks: {},
hideView: this.hideView.bind(this),
setupIonRouter: this.setupIonRouter.bind(this),
removeViewStack: this.removeViewStack.bind(this),
renderChild: this.renderChild.bind(this),
transitionView: this.transitionView.bind(this)
};
}
hideView(viewId: string) {
const viewStacks = Object.assign({}, this.state.viewStacks);
const { view } = this.findViewInfoById(viewId, viewStacks);
if (view) {
view.show = false;
view.key = generateUniqueId();
this.setState({
viewStacks
});
}
}
historyChange(location: HistoryLocation, action: HistoryAction) {
this.setActiveView(location, action);
}
findViewInfoByLocation(location: HistoryLocation, viewStacks: ViewStacks) {
let view: ViewItem<IonRouteData> | undefined;
let match: IonRouteData["match"] | null | undefined;
let viewStack: ViewStack | undefined;
const keys = Object.keys(viewStacks);
keys.some(key => {
const vs = viewStacks[key];
return vs.views.some(x => {
const matchProps = {
exact: x.routeData.childProps.exact,
path: x.routeData.childProps.path || x.routeData.childProps.from,
component: x.routeData.childProps.component
};
match = matchPath(location.pathname, matchProps)
if (match) {
view = x;
viewStack = vs;
return true;
}
return false;
});
})
const result = { view, viewStack, match };
return result;
}
findViewInfoById(id: string, viewStacks: ViewStacks) {
let view: ViewItem<IonRouteData> | undefined;
let viewStack: ViewStack | undefined;
const keys = Object.keys(viewStacks);
keys.some(key => {
const vs = viewStacks[key];
view = vs.views.find(x => x.id === id);
if (view) {
viewStack = vs;
return true;
} else {
return false;
}
});
return { view, viewStack };
}
setActiveView(location: HistoryLocation, action: HistoryAction) {
const viewStacks = Object.assign({}, this.state.viewStacks);
const { view: enteringView, viewStack: enteringViewStack, match } = this.findViewInfoByLocation(location, viewStacks);
let direction: NavDirection = location.state && location.state.direction;
if (!enteringViewStack) {
return;
}
const { view: leavingView } = this.findViewInfoById(this.activeViewId!, viewStacks);
if (leavingView && leavingView.routeData.match!.url === location.pathname) {
return;
}
if (enteringView) {
/**
* If the page is being pushed into the stack by another view,
* record the view that originally directed to the new view for back button purposes.
*/
if (!enteringView.show && action === 'PUSH') {
enteringView.prevId = leavingView && leavingView.id;
}
enteringView.show = true;
enteringView.mount = true;
enteringView.routeData.match = match!;
enteringViewStack.activeId = enteringView.id;
this.activeViewId = enteringView.id;
if (leavingView) {
this.prevViewId = leavingView.id
if (leavingView.routeData.match!.params.tab === enteringView.routeData.match.params.tab) {
if (action === 'PUSH') {
direction = direction || 'forward';
} else {
direction = direction || 'back';
leavingView.mount = false;
}
}
/**
* If the leaving view is a Redirect, take it out of the rendering phase.
*/
if(leavingView.element.type === Redirect) {
leavingView.mount = false;
leavingView.show = false;
}
if (leavingView.element.type === Route && leavingView.element.props.render) {
if (leavingView.element.props.render().type === Redirect) {
leavingView.mount = false;
leavingView.show = false;
}
} else if (leavingView.element.type === Redirect) {
leavingView.mount = false;
leavingView.show = false;
}
}
this.setState({
viewStacks
}, () => {
const enteringEl = enteringView.ref && enteringView.ref.current ? enteringView.ref.current : undefined;
const leavingEl = leavingView && leavingView.ref && leavingView.ref.current ? leavingView.ref.current : undefined;
this.transitionView(
enteringEl!,
leavingEl!,
enteringViewStack.routerOutlet,
leavingEl && leavingEl.innerHTML !== '' ? direction : undefined!) // Don't animate from an empty view
});
}
}
componentWillUnmount() {
this.listenUnregisterCallback && this.listenUnregisterCallback();
}
setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
const views: ViewItem[] = [];
let activeId: string | undefined;
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
views.push(createViewItem(child, this.props.history.location));
});
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
function createViewItem(child: React.ReactElement<any>, location: HistoryLocation) {
const viewId = generateUniqueId();
const key = generateUniqueId();
const element = child;
const matchProps = {
exact: child.props.exact,
path: child.props.path || child.props.from,
component: child.props.component
};
const match: IonRouteData['match'] = matchPath(location.pathname, matchProps);
const view: ViewItem<IonRouteData> = {
id: viewId,
key,
routeData: {
match,
childProps: child.props
},
element,
mount: true,
show: !!match,
ref: React.createRef()
};
if (!!match) {
activeId = viewId;
};
return view;
}
}
registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, location: HistoryLocation) {
this.setState((prevState) => {
const prevViewStacks = Object.assign({}, prevState.viewStacks);
prevViewStacks[stack] = {
activeId: activeId,
views: stackItems,
routerOutlet
};
return {
viewStacks: prevViewStacks
};
}, () => {
const { view: activeView } = this.findViewInfoById(activeId!, this.state.viewStacks);
if (activeView) {
this.prevViewId = this.activeViewId;
this.activeViewId = activeView.id;
const direction = location.state && location.state.direction;
const { view: prevView } = this.findViewInfoById(this.prevViewId!, this.state.viewStacks);
this.transitionView(
activeView.ref!.current!,
prevView && prevView.ref!.current || undefined!,
routerOutlet,
direction);
}
});
};
removeViewStack(stack: string) {
const viewStacks = Object.assign({}, this.state.viewStacks);
delete viewStacks[stack];
this.setState({
viewStacks
});
}
renderChild(item: ViewItem<IonRouteData>) {
const component = React.cloneElement(item.element, {
computedMatch: item.routeData.match
});
return component;
}
findActiveView(views: ViewItem[]) {
let view: ViewItem<IonRouteData> | undefined;
views.some(x => {
const match = matchPath(this.props.location.pathname, x.routeData.childProps)
if (match) {
view = x;
return true;
}
return false;
});
return view;
}
transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) {
/**
* Super hacky workaround to make sure ionRouterOutlet is available
* since transitionView might be called before IonRouterOutlet is fully mounted
*/
if (ionRouterOuter && ionRouterOuter.componentOnReady) {
this.commitView(enteringEl, leavingEl, ionRouterOuter, direction);
} else {
setTimeout(() => {
this.transitionView(enteringEl, leavingEl, ionRouterOuter, direction);
}, 10);
}
}
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) {
await ionRouterOuter.commit(enteringEl, leavingEl, {
deepWait: true,
duration: direction === undefined ? 0 : undefined,
direction: direction,
showGoBack: direction === 'forward',
progressAnimation: false
});
if (leavingEl && (enteringEl !== leavingEl)) {
/**
* add hidden attributes
*/
leavingEl.classList.add('ion-page-hidden');
leavingEl.setAttribute('aria-hidden', 'true');
}
}
render() {
return (
<RouteManagerContext.Provider value={this.state}>
<NavManager {...this.props}
findViewInfoById={(id: string) => this.findViewInfoById(id, this.state.viewStacks)}
findViewInfoByLocation={(location: HistoryLocation) => this.findViewInfoByLocation(location, this.state.viewStacks)}
>
{this.props.children}
</NavManager>
</RouteManagerContext.Provider>
);
}
};
const RouteManagerWithRouter = withRouter(RouteManager);
RouteManagerWithRouter.displayName = 'RouteManager';
export class IonReactRouter extends React.Component<BrowserRouterProps> {
render() {
const { children, ...props } = this.props;
return (
<BrowserRouter {...props}>
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
</BrowserRouter>
);
}
}

View File

@ -0,0 +1,89 @@
import React from 'react';
import { IonLifeCycleContext } from '@ionic/react';
type Props = React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
interface InternalProps extends React.HTMLAttributes<HTMLElement> {
forwardedRef?: React.RefObject<HTMLElement>,
};
type ExternalProps = Props & {
ref?: React.RefObject<HTMLElement>
};
interface StackViewState {
ref: any;
}
class ViewInternal extends React.Component<InternalProps, StackViewState> {
context!: React.ContextType<typeof IonLifeCycleContext>;
constructor(props: InternalProps) {
super(props);
this.state = {
ref: null
}
}
componentDidMount() {
const { forwardedRef } = this.props;
this.setState({ ref: forwardedRef });
if (forwardedRef && forwardedRef.current) {
forwardedRef.current.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
forwardedRef.current.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
forwardedRef.current.addEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
forwardedRef.current.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
}
}
componentWillUnmount() {
const { forwardedRef } = this.props;
if (forwardedRef && forwardedRef.current) {
forwardedRef.current.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
forwardedRef.current.removeEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
forwardedRef.current.removeEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
forwardedRef.current.removeEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
}
}
ionViewWillEnterHandler() {
this.context.ionViewWillEnter();
}
ionViewDidEnterHandler() {
this.context.ionViewDidEnter();
}
ionViewWillLeaveHandler() {
this.context.ionViewWillLeave();
}
ionViewDidLeaveHandler() {
this.context.ionViewDidLeave();
}
render() {
const { className, children, forwardedRef, ...rest } = this.props;
const { ref } = this.state;
return (
<div
className={className ? `ion-page ${className}` : 'ion-page'}
ref={forwardedRef as any}
{...rest}
>
{ref && children}
</div>
)
}
static get contextType() {
return IonLifeCycleContext;
}
}
function forwardRef(props: InternalProps, ref: React.RefObject<HTMLElement>) {
return <ViewInternal forwardedRef={ref} {...props} />;
}
forwardRef.displayName = 'View';
export const View = /*@__PURE__*/React.forwardRef<HTMLElement, ExternalProps>(forwardRef as any);

View File

@ -0,0 +1,10 @@
export interface ViewItem<RouteData = any> {
id: string;
key: string;
element: React.ReactElement<any>;
ref?: React.RefObject<HTMLElement>;
routeData: RouteData;
prevId?: string;
mount: boolean;
show: boolean;
}

View File

@ -0,0 +1,58 @@
import React from 'react';
import { IonLifeCycleContext, DefaultIonLifeCycleContext } from '@ionic/react';
import { RouteManagerContext } from './RouteManagerContext';
interface StackItemManagerProps {
id: string;
mount: boolean;
}
interface StackItemManagerState {
show: boolean;
}
export class ViewItemManager extends React.Component<StackItemManagerProps, StackItemManagerState> {
ionLifeCycleContext = new DefaultIonLifeCycleContext();
_isMounted = false;
context!: React.ContextType<typeof RouteManagerContext>;
constructor(props: StackItemManagerProps) {
super(props)
this.state = {
show: true
};
this.ionLifeCycleContext.onComponentCanBeDestroyed(() => {
if (!this.props.mount) {
if (this._isMounted) {
this.setState({
show: false
}, () => {
this.context.hideView(this.props.id);
});
}
}
});
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { show } = this.state;
return (
<IonLifeCycleContext.Provider value={this.ionLifeCycleContext}>
{show && this.props.children}
</IonLifeCycleContext.Provider>
)
}
static get contextType() {
return RouteManagerContext;
}
}

View File

@ -0,0 +1,75 @@
import React from 'react';
import { generateUniqueId } from '../utils';
import { View } from './View';
import { ViewItemManager } from './ViewItemManager';
import { RouteManagerContext } from './RouteManagerContext';
declare global {
namespace JSX {
interface IntrinsicElements {
'ion-router-outlet': any;
}
}
}
type ViewManagerProps = {
id?: string;
};
type ViewManagerState = {}
export class ViewManager extends React.Component<ViewManagerProps, ViewManagerState> {
containerEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
context!: React.ContextType<typeof RouteManagerContext>;
id: string;
constructor(props: ViewManagerProps) {
super(props);
this.id = this.props.id || generateUniqueId();
}
componentDidMount() {
this.context.setupIonRouter(this.id, this.props.children, this.containerEl.current!);
}
componentWillUnmount() {
this.context.removeViewStack(this.id);
}
render() {
const context = this.context;
const viewStack = context.viewStacks[this.id];
const activeId = viewStack ? viewStack.activeId : '';
const views = (viewStack || { views: [] }).views.filter(x => x.show);
return (
<ion-router-outlet data-id={this.id} ref={this.containerEl}>
{views.map((item) => {
let props: any = {};
if (item.id === activeId) {
props = {
'className': ' ion-page-invisible'
};
}
return (
<ViewItemManager
id={item.id}
key={item.key}
mount={item.mount}
>
<View
ref={item.ref}
{...props}
>
{this.context.renderChild(item)}
</View>
</ViewItemManager>
);
})}
</ion-router-outlet>
);
}
static get contextType() {
return RouteManagerContext;
}
}

View File

@ -0,0 +1,3 @@
export * from './Router';
export * from './ViewManager';