diff --git a/react/src/components/AlertModal.tsx b/react/src/components/AlertModal.tsx deleted file mode 100644 index 04ee2e0cfcb..00000000000 --- a/react/src/components/AlertModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Components } from '@ionic/core'; - - -const IonAlertModal: React.SFC = () => { - return null; -} - -export default IonAlertModal; - -/* -import IonAlertModal, { Header, SubHeader, Message, Button} from 'IonAlertModal'; - - -
Favorite Added
- - Favorite info - -
-*/ diff --git a/react/src/components/IonActionSheet.tsx b/react/src/components/IonActionSheet.tsx new file mode 100644 index 00000000000..4729ffb8d3d --- /dev/null +++ b/react/src/components/IonActionSheet.tsx @@ -0,0 +1,8 @@ +import { Components } from '@ionic/core'; +import { createControllerComponent } from './createControllerComponent'; +import { Omit } from './types'; + +export type ActionSheetOptions = Omit; + +const IonActionSheet = createControllerComponent('ion-action-sheet', 'ion-action-sheet-controller') +export default IonActionSheet; diff --git a/react/src/components/IonAlert.tsx b/react/src/components/IonAlert.tsx new file mode 100644 index 00000000000..788d9d6e66c --- /dev/null +++ b/react/src/components/IonAlert.tsx @@ -0,0 +1,8 @@ +import { Components } from '@ionic/core'; +import { createControllerComponent } from './createControllerComponent'; +import { Omit } from './types'; + +export type AlertOptions = Omit; + +const IonAlert = createControllerComponent('ion-alert', 'ion-alert-controller') +export default IonAlert; diff --git a/react/src/components/IonLoading.tsx b/react/src/components/IonLoading.tsx new file mode 100644 index 00000000000..027ee7f06e7 --- /dev/null +++ b/react/src/components/IonLoading.tsx @@ -0,0 +1,8 @@ +import { Components } from '@ionic/core'; +import { createControllerComponent } from './createControllerComponent'; +import { Omit } from './types'; + +export type LoadingOptions = Omit; + +const IonActionSheet = createControllerComponent('ion-loading', 'ion-loading-controller') +export default IonActionSheet; diff --git a/react/src/components/IonModal.tsx b/react/src/components/IonModal.tsx new file mode 100644 index 00000000000..322cbc4068f --- /dev/null +++ b/react/src/components/IonModal.tsx @@ -0,0 +1,8 @@ +import { Components } from '@ionic/core'; +import { createControllerComponent } from './createControllerComponent'; +import { Omit } from './types'; + +export type ModalOptions = Omit; + +const IonModal = createControllerComponent('ion-modal', 'ion-modal-controller') +export default IonModal; diff --git a/react/src/components/IonPopover.tsx b/react/src/components/IonPopover.tsx new file mode 100644 index 00000000000..c5c2aad450e --- /dev/null +++ b/react/src/components/IonPopover.tsx @@ -0,0 +1,8 @@ +import { Components } from '@ionic/core'; +import { createControllerComponent } from './createControllerComponent'; +import { Omit } from './types'; + +export type PopoverOptions = Omit; + +const IonPopover = createControllerComponent('ion-popover', 'ion-popover-controller') +export default IonPopover; diff --git a/react/src/components/IonToast.tsx b/react/src/components/IonToast.tsx new file mode 100644 index 00000000000..f1f7d3eef15 --- /dev/null +++ b/react/src/components/IonToast.tsx @@ -0,0 +1,8 @@ +import { Components } from '@ionic/core'; +import { createControllerComponent } from './createControllerComponent'; +import { Omit } from './types'; + +export type ToastOptions = Omit; + +const IonToast = createControllerComponent('ion-toast', 'ion-toast-controller') +export default IonToast; diff --git a/react/src/components/createComponent.ts b/react/src/components/createComponent.ts deleted file mode 100644 index 6973ea24bd6..00000000000 --- a/react/src/components/createComponent.ts +++ /dev/null @@ -1,70 +0,0 @@ -import React, { RefObject } from 'react'; -import ReactDOM from 'react-dom'; - -const dashToPascalCase = (str: string) => str.toLowerCase().split('-').map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join(''); - - -function syncEvent(node: Element, eventName: string, newEventHandler: (e: Event) => any) { - const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); - const eventStore = (node as any).__events || ((node as any).__events = {}); - const oldEventHandler = eventStore[eventNameLc]; - - // Remove old listener so they don't double up. - if (oldEventHandler) { - node.removeEventListener(eventNameLc, oldEventHandler); - } - - // Bind new listener. - if (newEventHandler) { - node.addEventListener(eventNameLc, eventStore[eventNameLc] = function handler(e: Event) { - newEventHandler.call(this, e); - }); - } -} - -export interface IonicReactBaseProps { - ref?: RefObject -} - -export function createReactComponent(tagName: string) { - const displayName = dashToPascalCase(tagName); - - - return class ReactComponent extends React.Component { - constructor(props: T & IonicReactBaseProps) { - super(props); - } - - static get displayName() { - return displayName; - } - - componentDidMount() { - this.componentWillReceiveProps(this.props); - } - componentWillReceiveProps(props: any) { - const node = ReactDOM.findDOMNode(this) as Element | null - - if (node == null) { - return; - } - - Object.keys(props).forEach(name => { - if (name === 'children' || name === 'style') { - return; - } - - if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - syncEvent(node, name.substring(2), props[name]); - } else { - (node as any)[name] = props[name]; - } - }); - } - render() { - const { children, className, ...cProps } = this.props as any; - cProps.class = className || cProps.class; - return React.createElement(tagName, cProps, children); - } - } -} diff --git a/react/src/components/createComponent.tsx b/react/src/components/createComponent.tsx new file mode 100644 index 00000000000..7b0bc658853 --- /dev/null +++ b/react/src/components/createComponent.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { dashToPascalCase, attachEventProps } from './utils'; + +export function createReactComponent(tagName: string) { + const displayName = dashToPascalCase(tagName); + + type IonicReactInternalProps = { + forwardedRef?: React.RefObject; + children?: React.ReactNode; + } + + type IonicReactExternalProps = { + ref?: React.RefObject; + children?: React.ReactNode; + } + + class ReactComponent extends React.Component { + componentRef: React.RefObject; + + constructor(props: T & IonicReactInternalProps) { + super(props); + this.componentRef = React.createRef(); + } + + static get displayName() { + return displayName; + } + + componentDidMount() { + this.componentWillReceiveProps(this.props); + } + + componentWillReceiveProps(props: any) { + const node = ReactDOM.findDOMNode(this); + + if (!(node instanceof HTMLElement)) { + return; + } + + attachEventProps(node, props); + } + + render() { + const { children, forwardedRef, ...cProps } = this.props as any; + cProps.ref = forwardedRef; + + return React.createElement(tagName, cProps, children); + } + } + + function forwardRef(props: T & IonicReactInternalProps, ref: React.RefObject) { + return ; + } + forwardRef.displayName = displayName; + + return React.forwardRef(forwardRef); +} diff --git a/react/src/components/createControllerComponent.tsx b/react/src/components/createControllerComponent.tsx new file mode 100644 index 00000000000..5aad5364cd5 --- /dev/null +++ b/react/src/components/createControllerComponent.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { attachEventProps } from './utils' +import { ensureElementInBody, dashToPascalCase } from './utils'; + +export function createControllerComponent(tagName: string, controllerTagName: string) { + const displayName = dashToPascalCase(tagName); + + type IonicReactInternalProps = { + forwardedRef?: React.RefObject; + children?: React.ReactNode; + show: boolean + } + + return class ReactControllerComponent extends React.Component { + element: E; + controllerElement: C; + + constructor(props: T & IonicReactInternalProps) { + super(props); + } + + static get displayName() { + return displayName; + } + + async componentDidMount() { + this.controllerElement = ensureElementInBody(controllerTagName); + await (this.controllerElement as any).componentOnReady(); + } + + async componentDidUpdate(prevProps: T & IonicReactInternalProps) { + if (prevProps.show !== this.props.show && this.props.show === true) { + const { children, show, ...cProps} = this.props as any; + + this.element = await (this.controllerElement as any).create(cProps); + await (this.element as any).present(); + + attachEventProps(this.element, cProps); + } + if (prevProps.show !== this.props.show && this.props.show === false) { + return await (this.element as any).dismiss(); + } + } + + render(): null { + return null; + } + } +} + diff --git a/react/src/components/index.ts b/react/src/components/index.ts index e5974d58205..18d0dae57d9 100644 --- a/react/src/components/index.ts +++ b/react/src/components/index.ts @@ -2,54 +2,59 @@ import { Components as IoniconsComponents } from 'ionicons'; import { Components } from '@ionic/core'; import { createReactComponent } from './createComponent'; -export { default as AlertModal } from './AlertModal'; +export { default as IonActionSheet } from './IonActionSheet'; +export { default as IonAlert } from './IonAlert'; +export { default as IonLoading } from './IonLoading'; +export { default as IonModal } from './IonModal'; +export { default as IonPopover } from './IonPopover'; +export { default as IonToast } from './IonToast'; -export const IonIcon = createReactComponent('ion-icon'); -export const IonApp = createReactComponent('ion-app'); -export const IonPage = createReactComponent<{}>('ion-page'); -export const IonMenu = createReactComponent('ion-menu'); -export const IonHeader = createReactComponent('ion-header'); -export const IonTitle = createReactComponent('ion-title'); -export const IonNav = createReactComponent('ion-nav'); -export const IonToolbar = createReactComponent('ion-toolbar'); -export const IonButtons = createReactComponent('ion-buttons'); -export const IonSelect = createReactComponent('ion-select'); -export const IonSelectOption = createReactComponent('ion-select-option'); -export const IonButton = createReactComponent('ion-button'); -export const IonContent = createReactComponent('ion-content'); -export const IonList = createReactComponent('ion-list'); -export const IonListHeader = createReactComponent('ion-list-header'); -export const IonItem = createReactComponent('ion-item'); -export const IonLabel = createReactComponent('ion-label'); -export const IonDatetime = createReactComponent('ion-datetime'); -export const IonMenuButton = createReactComponent('ion-menu-button'); -export const IonItemGroup = createReactComponent('ion-item-group'); -export const IonItemDivider = createReactComponent('ion-item-divider'); -export const IonItemSliding = createReactComponent('ion-item-sliding'); -export const IonItemOption = createReactComponent('ion-item-option'); -export const IonItemOptions = createReactComponent('ion-item-options'); -export const IonInput = createReactComponent('ion-input'); -export const IonGrid = createReactComponent('ion-grid'); -export const IonRow = createReactComponent('ion-row'); -export const IonCol = createReactComponent('ion-col'); -export const IonSegment= createReactComponent('ion-segment'); -export const IonSegmentButton= createReactComponent('ion-segment-button'); -export const IonSearchbar= createReactComponent('ion-searchbar'); -export const IonRefresher= createReactComponent('ion-refresher'); -export const IonRefresherContent= createReactComponent('ion-refresher-content'); -export const IonFab= createReactComponent('ion-fab'); -export const IonFabList = createReactComponent('ion-fab-list'); -export const IonFabButton= createReactComponent('ion-fab-button'); -export const IonAvatar = createReactComponent('ion-avatar'); -export const IonCard = createReactComponent('ion-card'); -export const IonCardHeader = createReactComponent('ion-card-header'); -export const IonCardContent = createReactComponent('ion-card-content'); -export const IonTextarea = createReactComponent('ion-textarea'); -export const IonTabs = createReactComponent('ion-tabs'); -export const IonTab = createReactComponent('ion-tab'); -export const IonTabBar = createReactComponent('ion-tab-bar'); -export const IonTabButton = createReactComponent('ion-tab-button'); -export const IonSlides = createReactComponent('ion-slides'); -export const IonSlide = createReactComponent('ion-slide'); -export const IonSplitPane = createReactComponent('ion-split-pane'); -export const IonMenuToggle = createReactComponent('ion-menu-toggle'); +export const IonIcon = createReactComponent('ion-icon'); +export const IonApp = createReactComponent('ion-app'); +export const IonMenu = createReactComponent('ion-menu'); +export const IonHeader = createReactComponent('ion-header'); +export const IonTitle = createReactComponent('ion-title'); +export const IonNav = createReactComponent('ion-nav'); +export const IonToolbar = createReactComponent('ion-toolbar'); +export const IonButtons = createReactComponent('ion-buttons'); +export const IonSelect = createReactComponent('ion-select'); +export const IonSelectOption = createReactComponent('ion-select-option'); +export const IonButton = createReactComponent('ion-button'); +export const IonContent = createReactComponent('ion-content'); +export const IonList = createReactComponent('ion-list'); +export const IonListHeader = createReactComponent('ion-list-header'); +export const IonItem = createReactComponent('ion-item'); +export const IonLabel = createReactComponent('ion-label'); +export const IonDatetime = createReactComponent('ion-datetime'); +export const IonMenuButton = createReactComponent('ion-menu-button'); +export const IonItemGroup = createReactComponent('ion-item-group'); +export const IonItemDivider = createReactComponent('ion-item-divider'); +export const IonItemSliding = createReactComponent('ion-item-sliding'); +export const IonItemOption = createReactComponent('ion-item-option'); +export const IonItemOptions = createReactComponent('ion-item-options'); +export const IonInput = createReactComponent('ion-input'); +export const IonGrid = createReactComponent('ion-grid'); +export const IonRow = createReactComponent('ion-row'); +export const IonCol = createReactComponent('ion-col'); +export const IonSegment= createReactComponent('ion-segment'); +export const IonSegmentButton= createReactComponent('ion-segment-button'); +export const IonSearchbar= createReactComponent('ion-searchbar'); +export const IonRefresher= createReactComponent('ion-refresher'); +export const IonRefresherContent= createReactComponent('ion-refresher-content'); +export const IonFab= createReactComponent('ion-fab'); +export const IonFabList = createReactComponent('ion-fab-list'); +export const IonFabButton= createReactComponent('ion-fab-button'); +export const IonAvatar = createReactComponent('ion-avatar'); +export const IonCard = createReactComponent('ion-card'); +export const IonCardHeader = createReactComponent('ion-card-header'); +export const IonCardContent = createReactComponent('ion-card-content'); +export const IonTextarea = createReactComponent('ion-textarea'); +export const IonTabs = createReactComponent('ion-tabs'); +export const IonTab = createReactComponent('ion-tab'); +export const IonTabBar = createReactComponent('ion-tab-bar'); +export const IonTabButton = createReactComponent('ion-tab-button'); +export const IonSlides = createReactComponent('ion-slides'); +export const IonSlide = createReactComponent('ion-slide'); +export const IonSplitPane = createReactComponent('ion-split-pane'); +export const IonMenuToggle = createReactComponent('ion-menu-toggle'); +export const IonToggle = createReactComponent('ion-toggle'); diff --git a/react/src/components/types.ts b/react/src/components/types.ts new file mode 100644 index 00000000000..ca06d71cf92 --- /dev/null +++ b/react/src/components/types.ts @@ -0,0 +1,2 @@ + +export type Omit = Pick>; diff --git a/react/src/components/utils.ts b/react/src/components/utils.ts new file mode 100644 index 00000000000..892329134a8 --- /dev/null +++ b/react/src/components/utils.ts @@ -0,0 +1,43 @@ + +function syncEvent(node: Element, eventName: string, newEventHandler: (e: Event) => any) { + const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); + const eventStore = (node as any).__events || ((node as any).__events = {}); + const oldEventHandler = eventStore[eventNameLc]; + + // Remove old listener so they don't double up. + if (oldEventHandler) { + node.removeEventListener(eventNameLc, oldEventHandler); + } + + // Bind new listener. + if (newEventHandler) { + node.addEventListener(eventNameLc, eventStore[eventNameLc] = function handler(e: Event) { + newEventHandler.call(this, e); + }); + } +} + +export const dashToPascalCase = (str: string) => str.toLowerCase().split('-').map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join(''); + +export function ensureElementInBody(elementName: string): E { + let element = document.querySelector(elementName); + if (!element) { + element = document.createElement(elementName) as E; + document.body.appendChild(element); + } + return element; +} + +export function attachEventProps(node: E, props: any) { + Object.keys(props).forEach(name => { + if (name === 'children' || name === 'style' || name === 'ref') { + return; + } + + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + syncEvent(node, name.substring(2), props[name]); + } else { + (node as any)[name] = props[name]; + } + }); +} diff --git a/react/test/IonAlert.spec.tsx b/react/test/IonAlert.spec.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/react/tsconfig.json b/react/tsconfig.json index 44d40c75c26..2e4c6de552d 100644 --- a/react/tsconfig.json +++ b/react/tsconfig.json @@ -15,7 +15,7 @@ "outDir": "dist", "removeComments": false, "sourceMap": true, - "jsx": "preserve", + "jsx": "react", "target": "es2015" }, "include": [