uNmAnNeR/imaskjs

Compatible with React Native

yordis opened this issue · 23 comments

Yes, this is not the only one, factory.js also seems to be incompatible because of circular dependencies.
I could investigate it later to do something.

@yordis I fixed this.
But probably control/input.js should be reimplemented for React Native in places where view element fires events and interacts with masked.
I am not very familiar with React Native, help needed here.

@uNmAnNeR I can help you out with the RN but I need to know from you which events would you need from the inputs for iMask to work. Make a checklist on the comment and I will dig in

@yordis ok, here it is:

el.addEventListener('keydown', ...)
el.addEventListener('input', ...)
el.addEventListener('drop', ...)  // just disables
el.addEventListener('click', ...)
el.addEventListener('change' ...)
// ...and remove counterparts

el.selectionStart
el.selectionEnd
el.setSelectionRange(...);
get/set el.value
document.activeElement

@uNmAnNeR I will port my iMask to the React Native code today so I will have to deal with this.

@yordis How did this end up going?

@yordis @mjsisley
Some thoughts on this. I have not used RN at all, but looking on RN API superficially i think it is possible to use React mixin from imask React plugin and inside it wrap RN element to fit the UIElement API.
But is it possible to bind multiple listeners to events in RN?

@mjsisley sorry for the delay.

I didn't end up using it for the Mobile app so I don't face the issues.

@uNmAnNeR I think that in RN everything is about callbacks, it doesn't seem that the Native elements use EventEmitter for those things.

@yordis Sure, but i think it is possible to do like this:

// RN -> React Plugin Mixin wraps RNTextInput to DOMlikeInput -> pass to IMask
// e.g
class TextInputAdapter {
  constructor (nativeElement) {
    this.nativeElement = nativeElement;
  }
  addEventListener(ev, handler) {
    if (ev === 'input') this.nativeElement.onChangeText(handler);
  }
  // ...
}

@uNmAnNeR

class Input extends React.Component {
  // all the states happen here as well
  // unless specific Adapter needs to handle
  // some internals before letting this component 
  // to know about it
  state = {}
  
  render() {
   const InputAdapter = this.props.adapter

   const inputProps = {
    // this is a well defined protocol for any Input Adapter
    // for example
    onChange(x: string): void
    // every Adapter HAVE to call the same set of data
    // on the web normally the events give you `event` object
    // and in Native it will give you the actual `text`
    // both have to send back the same data structure
   }

   return (
    <InputAdapter  {...inputProps}/>
   )
  }

  // here is where all the callbacks management happens
  // no data management happens inside the adapters
  // example

  onChange(alwaysExpectTheText) {
    // do what you need to do
  }
}

// package name: imask-react-dom

class IMaskDOMInput extends Component {
  // do whatever the DOM API requires you to do

  // the onChange needs to clean up the data before
  // sending to the callback because the DOM event
  handleOnChange(event) {
    const text = event.target.value
    this.props.onChange(text)
  }
}

class IMaskInput extends Component {
  render() {
    <Input
      {...props }
      // notice this
      adapter={IMaskDOMInput}
    >
  }
}

// package name: imask-react-native

class IMaskNativeInput extends Component {
  // do whatever the DOM API requires you to do

  // the onChange needs to clean up the data before
  // sending to the callback because the DOM event
  handleOnChange(text) {
    this.props.onChange(text)
  }
}

class IMaskInput extends Component {
  render() {
    <Input
      {...props }
      // notice this
      adapter={IMaskNativeInput}
    >
  }
}

@uNmAnNeR something around that API design.

Normally you will have 2 packages, each package the ONLY thing it defines is the Adapter you will be using and both of them will have the same HOC so you could use the same component name in no matter which platform.

Each adapter do whatever it needs to be able to communicate with the Internal HOC that actually manage the iMask interactions needs.

I dont like this

addEventListener(ev, handler) {
    if (ev === 'input') this.nativeElement.onChangeText(handler);
  }

because the component where you are doing that have to know about both platforms, using the Adapter approach your HOC Input do not care about event registration or callbacks that is a detail managed by the adapter so this makes the code more clear.

Probably the Input that I am refererring is this one https://github.com/uNmAnNeR/imaskjs/blob/gh-pages/src/controls/input.js

just make it agnostic of the low level components and just manage the iMask interactions with React I guess

@yordis But your Input extends React.Component.
As i understand to be trully agnostic on a low level adapter should be defined in terms of element, and current controls/input.js will use it.
So I need to rewrite UIElement interface and make 2 adapters for HTMLInput and RN TextInput.
Something like this:

interface UIElement {
  value (string) // ok
  selectionStart (): number  // ok
  selectionEnd (): number   // ok
  setSelectionRange (function (number, number): void)  // ok
  // addEventListener (function (string, Function): void) // not ok, will be split to:
  onInput ()
  onKeyDown ()
  onDrop ()
  onClick ()
  onChange ()
  
  // removeEventListener (function (string, Function): void) // not ok, will be replaced with passing null to methods above
}

Well, I think that you can't use that class on React but you could make it work as long as you let the React component to know about the data changes but even that the way you would pass down the callbacks it is another concern as well, normally you would use HOC Higher Order Component for do these type of practices.

Definitely you could use that class under InputDOM because it is how the web works but I dont see that working for the RN that easy but probably is because I dont know the full internals.

This is how I see it

Web

Vanilla: UIElement (this does DOM stuff in a Web way)  ---> uses ---> iMask
React: Input (HOC) ---> uses ---> iMaskDOMInput (HOC) --- uses ---> UIElement
React Native: Input (HOC) ---> uses ---> iMaskNativeInput (HOC) ---> uses ---> iMask

As you notice React DOM Input could use the UIElement because it is just DOM stuff but I dont see that working for RN.

I thikn you can't do this.nativeElement.onChangeText(handler) and things like mutating the Component directly, React have specific way to do things.

In the case of the Web maybe is different because you can always just ask for the native element and register callbacks and stuff like that directly but I dont think that exists on RN but please dig in more.

@yordis hi again! :)

I hacked a little on RN. I supposed to end up with something like:

import React from 'react';
import { StyleSheet, Text, View, TextInput } from 'react-native';
import { IMaskMixin } from 'react-imask';


class Adapter {
  constructor (el) {
    this.el = el;
  }

  addEventListener (event, handler) {
    // switch (event) {
    //   case 'keydown': return this.el.onKeyPress(handler);
    //   case 'input': return this.el.onChange(handler);
    // }
  }

  removeEventListener (event) {
    // TODO
  }

  selectionStart () {
    return 0;
    // return this.el.selection.start;
  }

  selectionEnd () {
    return 0;
    // return this.el.selection.end;
  }

  setSelectionRange (start, end) {
    // this.el.selection = {start, end};
  }

  get value () {
    return 'asd';
    // return this.el.value;
  }

  set value (value) {
    // this.el.value = value;
  }
}

const MaskedTextInput = IMaskMixin(({inputRef, ...props}) => (
  <TextInput
    {...props}
    ref={el => inputRef(new Adapter(el))}
  />
));

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <MaskedTextInput
          style={{
            height: 40,
            width: 100,
            borderColor: 'gray',
            borderWidth: 1
          }}
          mask='000'
          editable = {true}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

But RN element in ref is not editable, thanks to FLUX. Every change or event should be handled inside component render. Because of this the only way to change something is to update internal state. So I have not percieved yet how do it right and does HOC make sense with RN or it should be complete component like MaskedInput.

@yordis Hello!
If you are still interested in please take a look.
I've just released react-native-imask and added an example.
Cursor is flickering... events are not always fired... I dont know how to do it better, if it's possible at all.
May be after resolving this RN-issue I will try to remake plugin.
I also tried uncontrolled behavior using setNativeProps, but other issues appeared.

Merged in master. Plugin was released with version 0.0.x and then made private to prevent version updating.
On iOS it works not so bad, despite flickering with lazy=false. On Android cursor is jumping back on input because of fireing onSelectionChange event during rerendering. Probably this is a bug in RN.
Currently development is frozen and waiting for fixing:
facebook/react-native#19505
facebook/react-native#18874
facebook/react-native#18221

@uNmAnNeR Is it ready then?

@yordis it's not production ready yet.

@yordis @uNmAnNeR Is it ready then?

will be released as is, does not make sense to wait one more year for bug fixing in RN

@uNmAnNeR did you ever found a fix for the rerender bug for react native?

@TNAJanssen I tried to make it work a long time ago, but with no luck. Too many things did not work as expected on different systems, so I gave up. If someone can restore my faith with some good PR, I'd be happy to continue supporting it.