necolas/react-native-web

Alert: implementation

necolas opened this issue ยท 30 comments

Kif11 commented

@necolas can you please merge this changes.

For now I do something like this to workaround this.

const alertTitle = 'Foutmelding'
const alertText = 'Geen toegang tot locatiedata'
if (Platform.OS === 'web') {
   alert(alertText)
} else {
    Alert.alert(alertTitle, alertText)
}

Browser alert() is blocking, so it's not recomended to use. Not sure why it's not deprecated yet...

Does react-native-web supports Alert already?

arqex commented

What do you think about implementing it using react portals? We can inject the node in the dom and use a portal to show/hide.

We can ship simple styling for it and create a way to let the developer customize the appearance and the appearing/hiding transition.

What I've done so far.
one file for default react native alert, and one file for react web alert.
Alert. js

import { Alert } from 'react-native'

export const StdAlert = (title, desc, onPress = () => { }) => {
  Alert.alert(
    title,
    desc,
    [
      { text: 'OK', onPress: () => onPress() }
    ],
    { cancelable: false }
  )
}

export const BinaryAlert = (title, desc, onPositivePress = () => {}, onNegativePress = () => {}) => {
  Alert.alert(
    title,
    desc,
    [
      { text: 'Sim', onPress: () => onPositivePress() },
      { text: 'Nรฃo', onPress: () => onNegativePress() }
    ],
    { cancelable: false }
  )
}

and for web, Alert.web.js

export const StdAlert = (title, desc, onPress = () => {}) => {
  alert(`${title}\n${desc}`)
  if (onPress) onPress()
}

export const BinaryAlert = (title, desc, onPositivePress = () => {}, onNegativePress = () => {}) => {
  const res = window.confirm(`${title}\n${desc}`)
  if (res) onPositivePress()
  else onNegativePress()
}

Then use it as following:

import {StdAlert, BinaryAlert} from ...

StdAlert('title', 'desc', ()=>action())
BinaryAlert('title', 'desc', ()=>positiveAction(), ()=>negativeAction())

@vlimag , how about dependencies which use the Alert from react native ? (#1366)

@joan-saum As a provisory solution you should probably fork the dependency, change as necessary - following the example above - and publish to npm to use it.
Hopefully React native alert will be available at some time on React Native Web

I just created my own alert popup for web, using native-base - works pretty well

https://gist.github.com/tonypee/f3ebb3a6f89e6d73255a5823092b24c6

it needs to be instantiated in the root of the app too

very quick and dirty polyfill I whipped up if anyone else got super stuck by this:

import { Alert, Platform } from 'react-native'

const alertPolyfill = (title, description, options, extra) => {
    const result = window.confirm([title, description].filter(Boolean).join('\n'))

    if (result) {
        const confirmOption = options.find(({ style }) => style !== 'cancel')
        confirmOption && confirmOption.onPress()
    } else {
        const cancelOption = options.find(({ style }) => style === 'cancel')
        cancelOption && cancelOption.onPress()
    }
}

const alert = Platform.OS === 'web' ? alertPolyfill : Alert.alert

export default alert

Usage:

Before:

import { Alert } from 'react-native'
Alert.alert(
    ...
)

After:

import alert from './alert'
alert(
    ...
)

Thank you @joshbalfour.

Here's roughly the same in TypeScript for those who need it. Written as a singleton in order to be able to implement AlertStatic symmetrically from React Native and call it via Alert.alert.

// Alert.web.ts
import { AlertButton, AlertStatic } from 'react-native';

class WebAlert implements Pick<AlertStatic, 'alert'> {
  public alert(title: string, message?: string, buttons?: AlertButton[]): void {
    if (buttons === undefined || buttons.length === 0) {
      window.alert([title, message].filter(Boolean).join('\n'));
      return;
    }

    const result = window.confirm([title, message].filter(Boolean).join('\n'));

    if (result === true) {
      const confirm = buttons.find(({ style }) => style !== 'cancel');
      confirm?.onPress?.();
      return;
    }

    const cancel = buttons.find(({ style }) => style === 'cancel');
    cancel?.onPress?.();
  }
}

export const Alert = new WebAlert();
// Alert.ts
export { Alert } from 'react-native';

Several things to note:

  1. Providing no buttons argument manifests a basic alert. Providing buttons presents a dialogue.
  2. The fourth options argument is not implemented as it pertains to Android cancellation behaviour only, which has no browser analogue.
  3. The AlertStatic interface implies an additional prompt method, but the React Native docs suggest that it hasn't yet been implemented (or that it's just undocumented, for now).

EDIT: Added Alert.ts to indicate how it should be imported.

Browser alert() is blocking, so it's not recomended to use. Not sure why it's not deprecated yet...

It's meant to block on purpose!

The Modal PR is merged in react-native-web, that means the Alert implementation will be a lot easier to implement. I'll see if I can find some time to make a PR for this

I just did an experiment with the Modal in the canary version to implement a the Alert API on the web: https://codesandbox.io/s/alert-implementation-z4eoq?file=/src/App.js.
This would require the React DOM to be available before Alert.alert can be called.

I'm not sure how we could handle this better
ezgif-5-b66429171338

@necolas Is it a requirement of the web version of the Alert.api to be available before React is mounted?

Before React is mounted? What does that mean?

In my implementation the would have to be included somewhere in the three, I don't know if that's possible in the AppRegistry.

So if you would could Alert.alert() before the <AlertRoot /> has rendered it would fail, I presume we should implement the Alert.alert outside of the component tree but that would be hard since <Modal> would not be available.

I think I can workaround the issue I described above, but is it possible to add extra components in the root of a react-native-web app, like an <AlertRoot /> or extra Context providers?

I now have rewritten the Alert proposal to add support for Alerts outside components.
https://codesandbox.io/s/alert-implementation-z4eoq?file=/src/Alert.js

It would still need something like ReactDOM.render(<AlertRoot />) when react-native-web registers the app

update: fixed a bug where multiple Alerts would open

Do you think the Codesandbox will be good enough for a PR if I

@RichardLindhout yeah something like that would be a good start

Do you think the Codesandbox will be good enough for a PR if I

I hate materialways design. It's like a plague, I think that at least, we should be able to modify it.

Hi, is there any update on this?
Would love to see a web version for the Alert.alert / .prompt in the same imperative approach.

And more user-friendly / good looking than plain old browsers' window.alert / window.prompt.

@ezekiel747 there is react-native-paper-alerts that looks pretty good on web.

@zhigang1992 react-native-paper-alerts is a reasonable solution, thanks for that pointer. I just looked it over and went ahead with an integration of it in https://github.com/invertase/react-native-firebase-authentication-example

If you're not using react-native-paper then it probably isn't attractive, but if you are - definitely worth a look - mind the styles applied in public/index.html to avoid a keyboard hiding issue if you use prompts with TextInput in the Alerts, but if you're just doing text and buttons, zero issues.

hi there! is there any update on this?

can I help on something? are there any pull request with this working progress?

pjk5 commented

While not an actual solution for this issue, I was able to make React Native's Alert work on multiple platforms by utilizing the sweetalert2 library for the web.

I've included my code below for anyone who might find it useful. Native Alert API is called for the native platforms and the code falls back to sweetalert2 for the web. The API is almost identical to the native Alert, with one addition to the AlertButton type to enable button type props for sweetalert2.

It seems that the prompt method could also be implemented with sweetalert2 quite easily, but I didn't have the need for that yet.

You can test it out on Snack.


Install sweetalert2:
npm install sweetalert2

Alert.web.ts:

import {
  type AlertType,
  type AlertButton,
  type AlertOptions,
} from 'react-native'

import Swal from 'sweetalert2'

export type CustomAlertButton = AlertButton & {
  swalType?: 'deny' | 'cancel' | 'confirm'
}

class Alert {
  static alert(
    title: string,
    message?: string,
    buttons?: CustomAlertButton[],
    options?: AlertOptions
  ): void {
    const confirmButton = buttons
      ? buttons.find((button) => button.swalType === 'confirm')
      : undefined
    const denyButton = buttons
      ? buttons.find((button) => button.swalType === 'deny')
      : undefined
    const cancelButton = buttons
      ? buttons.find((button) => button.swalType === 'cancel')
      : undefined

    Swal.fire({
      title: title,
      text: message,
      showConfirmButton: !!confirmButton,
      showDenyButton: !!denyButton,
      showCancelButton: !!cancelButton,
      confirmButtonText: confirmButton?.text,
      denyButtonText: denyButton?.text,
      cancelButtonText: cancelButton?.text,
    }).then((result) => {
      if (result.isConfirmed) {
        if (confirmButton?.onPress !== undefined) {
          confirmButton.onPress()
        }
      } else if (result.isDenied) {
        if (denyButton?.onPress !== undefined) {
          denyButton.onPress()
        }
      } else if (result.isDismissed) {
        if (cancelButton?.onPress !== undefined) {
          cancelButton.onPress()
        }
      }
    })
  }

  static prompt(
    title: string,
    message?: string,
    callbackOrButtons?: ((text: string) => void) | CustomAlertButton[],
    type?: AlertType,
    defaultValue?: string,
    keyboardType?: string
  ): void {
    throw new Error('Not implemented.')
  }
}

export default Alert

Alert.ts

import {
  Alert as RNAlert,
  type AlertOptions,
  type AlertButton,
  type AlertType,
} from 'react-native'

type CustomAlertButton = AlertButton & {
  swalType?: 'deny' | 'cancel' | 'confirm'
}

export interface ExtendedAlertStatic {
  alert: (
    title: string,
    message?: string,
    buttons?: CustomAlertButton[],
    options?: AlertOptions
  ) => void
  prompt: (
    title: string,
    message?: string,
    callbackOrButtons?: ((text: string) => void) | CustomAlertButton[],
    type?: AlertType,
    defaultValue?: string,
    keyboardType?: string
  ) => void
}

const Alert: ExtendedAlertStatic = RNAlert as ExtendedAlertStatic

export default Alert

I'm creating a multiplatform. simple for now

https://github.com/mensonones/AnywhereAlertConfirm