Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WebView] Add props for overriding native component #10946

Closed
wants to merge 13 commits into from

Conversation

cbrevik
Copy link
Contributor

@cbrevik cbrevik commented Nov 15, 2016

Motivation
Just like with #10105, the motivation is to make WebView more extensible.

In order to extend ReactWebViewManager (Android) with your own custom logic in a new class, you will have to override getName, and refer to this name in the JavaScript-code. And that's fine.

But, the problem is that you will have to duplicate the entire file for WebView.android.js in order to refer to the new custom native component by name with requireNativeComponent (since it is hard-coded to 'RCTWebView'). It's also necessary to do so in order to pass through any custom events or props you may want to set on the custom native WebView.

All of the above is also true for iOS.

What I'm trying to solve with this PR is give the ability to extend native WebView logic, while being able to re-use and extend existing WebView JavaScript-logic. This is done by adding a new nativeComponent prop on WebView. I've also added nativeComponentProps so custom props / events can be set on the custom native WebView.

Test plan

Let's say I've extended ReactWebViewManager with my own CustomWebViewManager in my Android-project (overriding getName so it returns "RCTCustomWebView"), and this new class may emit a onSomethingHappened event.
Then I can implement my custom WebView as follows in JavaScript:

import React, { Component, PropTypes } from 'react';
import {
    WebView
} from 'react-native';

import requireNativeComponent from 'requireNativeComponent';

class CustomWebView extends Component {
    constructor(props) {
        super(props);
    }

    static propTypes = {
        ...WebView.propTypes,
        onSomethingHappened: PropTypes.func,
    };

    onSomethingHappenedEvent = (event) => {
        var { onSomethingHappened } = this.props;
        onSomethingHappened && onSomethingHappened(event);
    }

    render() {
        return (
            <WebView 
              {...this.props} 
              nativeComponent={RCTCustomWebView} 
              nativeComponentProps={{ onSomethingHappened: this.onSomethingHappenedEvent }} 
            />
        );
    }
}

var RCTCustomWebView = requireNativeComponent('RCTCustomWebView', CustomWebView);

module.exports = CustomWebView;

@facebook-github-bot
Copy link
Contributor

By analyzing the blame information on this pull request, we identified @jacobp100 and @spicyj to be potential reviewers.

@facebook-github-bot facebook-github-bot added GH Review: review-needed CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. labels Nov 15, 2016
@@ -160,6 +160,18 @@ class WebView extends React.Component {
* @platform android
*/
allowUniversalAccessFromFileURLs: PropTypes.bool,

/**
* Override the native component used to render the WebView. Enables a custom native

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

@@ -334,6 +334,18 @@ class WebView extends React.Component {
* to tap them before they start playing. The default value is `true`.
*/
mediaPlaybackRequiresUserAction: PropTypes.bool,

/**
* Override the native component used to render the WebView. Enables a custom native

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

Copy link
Contributor

@jacobp100 jacobp100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice way to handle extending native components.

@cbrevik
Copy link
Contributor Author

cbrevik commented Nov 16, 2016

This could in principle be done with more React Native components, to make it easier for the community to re-use and extend native components.

@mkonicek
Copy link
Contributor

Interesting approach to reusing the WebView.*.js. Have you tried it out on iOS by extending the WebView in Obj-C?

Why do you need to extend the Android WebView, however? If there are bugs or missing features would you be up for contributing them upstream?

I'm curious why we haven't had a use case like this with any of the components (Text, TextInput etc.) so far. If there are very few people who need to extend the native code a reasonable tradeoff might be for you to fork the WebView.*.js so we don't add two extra props to the public API very few people would use.

@cbrevik
Copy link
Contributor Author

cbrevik commented Nov 23, 2016

@mkonicek

I've tested on iOS as well, and this approach works there as well.

You'll have to duplicate RCTWebViewManager.m, since the exported macro-defined methods/properties (with RCT_REMAP_VIEW_PROPERTY, RCT_EXPORT_METHOD, etc) are only picked up from the subclass, and not from the parent. So you cannot inherit the props from RCTWebViewManager, which is a shame.
But, you will not have to duplicate RCTWebView.m, and can easily enough inherit and extend that class.

One issue I've found in WebView.ios.js is that the RCTWebViewManager is also referenced by name, and is used when deciding to startLoadWithResult or not. This could be remedied though if we did like with the rest of the exported methods from the RCTWebViewManager and call startLoadWithResult with dispatchViewManagerCommand.

I don't really see the need to reference the ViewManager directly, or maybe I am missing something?


Why not just contribute upstream?

There might be features which are a bit too specific to a single use-case, like with for example #9949.
In my specific case I have some sensitive logic for handling inactivity in the WebView, which doesn't seem to fit upstream either.

I don't think this is satisfactory resolved by just forking the logic. It is difficult to maintain in that case, and hard to keep up-to-date with each new RN version. Also the complexity of it is a bit too high for junior RN developers on my team, and I'd dread to hand off the project to someone else in that case.

See for example this discussion on extending core components. Or this issue with regards to duplication of code. Coincidentally both by @alinz so maybe he has some input here?

This might not be necessary for simple components (Text, TextInput, etc). But for more complex components, or those that need a lot of customization, this would help a lot I think.

@jacobp100
Copy link
Contributor

@cbrevik Would you be able to look into what would need to happen to get extending the iOS webview to be as easy as Android’s (without manually copying over the props)? I ask so we don’t implement this solution, and then have to introduce breaking changes later to accommodate iOS.

@cbrevik
Copy link
Contributor Author

cbrevik commented Nov 23, 2016

Well I suspect the issue is with this line in RCTComponentData, where the class_copyMethodList method only retrieves the child methods.

We could potentially reflect (and maybe loop through) the inheritance tree with superclass, and retrieve the parents methods as well.

I'll test it later this evening.

@cbrevik
Copy link
Contributor Author

cbrevik commented Nov 24, 2016

@jacobp100

Just a preliminary test, but it seems like if we change that line to something like:

  Class managerClass = [_managerClass class];
  while (managerClass != [RCTViewManager class]) {
    Method *methods = class_copyMethodList(object_getClass(managerClass), &count);
    // snipped away props for-loop
    free(methods);
    managerClass = [managerClass superclass];
  }

Then it will loop through and add the parents' props as well, and then extending the ViewManager becomes very easy:

#import "RCTCustomWebViewManager.h"
#import "RCTCustomWebView.h"
#import "UIView+React.h"

@interface RCTCustomWebViewManager () <RCTWebViewDelegate>
@end

@implementation RCTCustomWebViewManager
{
}

RCT_EXPORT_MODULE()

- (UIView *)view
{
  RCTCustomWebView *webView = [RCTCustomWebView new];
  webView.delegate = self;
  return webView;
}

RCT_EXPORT_VIEW_PROPERTY(onSomethingHappened, RCTDirectEventBlock)

@end

@jacobp100
Copy link
Contributor

Neat! Really excited about this.

I think we need some examples in UIExplorer and documentation—but I'm happy to help with that if you need!

@cbrevik
Copy link
Contributor Author

cbrevik commented Nov 24, 2016

Absolutely, help would be appreciated! Not sure if the RCTComponentData change belongs in its own PR though?

Or maybe not? I suppose both changes would be connected, we wouldn't be able to provide a good example in UIExplorer without landing this PR first.

@lacker
Copy link
Contributor

lacker commented Feb 8, 2017

Hey @jacobp100 are you still down to follow through on that generous offer to help out with the docs on this PR? ;-)

@jacobp100
Copy link
Contributor

I could certainly do that! @cbrevik, the snippet where you copy over the superclass methods, where does that go?

@cbrevik
Copy link
Contributor Author

cbrevik commented Feb 9, 2017

@jacobp100 I can create a new PR for that snippet, it probably needs re-testing first since RCTComponentData has changed.

@jacobp100
Copy link
Contributor

I know it's been some time—just wondering if you wanted to continue with this? I can do docs if you give me code examples! @cbrevik

@cbrevik
Copy link
Contributor Author

cbrevik commented May 3, 2017

@jacobp100 I've had this PR in the back of my mind. Code examples shouldn't venture too far from the original issue comment and the example there. I can take a look this week, and resolve conflicts etc.

@jacobp100
Copy link
Contributor

@cbrevik I understood how to do it on the JS side, I wasn't able to get anything working on ObjC (and I didn't even try Java)

@cbrevik
Copy link
Contributor Author

cbrevik commented May 9, 2017

@jacobp100 Aha my bad, I'll try to put together an example repository for you. The Java-side is pretty trivial, Objective-C demands cloning the RCTWebViewManager as previously stated (until we have the ability to traverse the superclass inheritance tree).

@cbrevik
Copy link
Contributor Author

cbrevik commented May 18, 2017

@jacobp100 has written docs (ad98f85) for this new feature. So I think this PR is good to go for another review @lacker

@ptilli
Copy link

ptilli commented May 18, 2017

Great job guys!

I hope this feature would be extended to other components (especially Text ;;)) as well!!

@shergin
Copy link
Contributor

shergin commented May 30, 2017

@jacobp100 @cbrevik 👍 If you could present the changes in RCTComponentData.m (iOS) as separate PR, I will definitely review it and most likely merge. (Same probably true for Android.)
I would love to have this functionality at least in iOS but I have no time to tackle it myself.

@shergin shergin self-assigned this May 30, 2017
@shergin shergin self-requested a review May 30, 2017 02:59
@shergin shergin added Android Platform: iOS iOS applications. labels May 30, 2017
@cbrevik
Copy link
Contributor Author

cbrevik commented May 30, 2017

@shergin Great! I'll open the two other PRs today 👍 This PR (and especially the docs) will be dependent on the other two PRs landing then.

@niztal
Copy link

niztal commented Jun 13, 2017

Can someone merge it? I see all checks, tests and reviewes passed, all left is push the merge button.

It's blocking for other PRs to be merged.
#12807

@shergin
Copy link
Contributor

shergin commented Jun 26, 2017

Nope, it cannot be merged until we figure out (and implement) the right design of module inheritance. See discussion in #14260.
I plan to tackle this problem relatively soon (~this summer) anyway.
Or, @cbrevik, do you interested in implementing the approach roughtly descibed in #14260 (comment) ? 😄

@cbrevik
Copy link
Contributor Author

cbrevik commented Jun 27, 2017

Sure @shergin, something like this cbrevik@e71c4ed?

Bit unsure on how to re-establish inheritance with the base "ViewManager" name since NativeProps seems to be exported with the "View" name instead. (E.g. UIManager.RCTView.NativeProps.)

Maybe you know of a better way? Don't think looping through is the best approach.

Edit: Or, possibly need to loop through anyway if we want to support a longer module inheritance chain? RCTWebViewManager -> RCTViewManager -> RCTBaseViewManager?

@shergin
Copy link
Contributor

shergin commented Jun 27, 2017

@cbrevik We definitely want to support long inheritance chain.
I also think we have to avoid looping through UIManager; can we infer key from viewConfig.BaseManager?

@cbrevik
Copy link
Contributor Author

cbrevik commented Jun 27, 2017

@shergin Not sure, it's this bit of hacky code which determines the exported module name. Don't think we want to replicate that on the JS side.

I could extract that bit of code into its own method in RCTComponentData though, to also export the parent module name in the viewConfig. Then we could do something like UIManager[viewConfig.baseModuleName].

@shergin
Copy link
Contributor

shergin commented Jun 27, 2017

@cbrevik viewConfig.baseModuleName 👍

@cbrevik
Copy link
Contributor Author

cbrevik commented Jun 28, 2017

@shergin I've landed on an implementation which supports longer inheritance chains. I'll test more, then open a PR.

@shergin
Copy link
Contributor

shergin commented Jun 29, 2017

@cbrevik Omg, omg! That's awesome!!1 Please, go ahead and open PR (even if it is not finished, we will discuss approach there)!

Current implementation will break Android (and all other platforms), right? Can we easily implement baseModuleName for Android or we should temporary workaround it on JS side?

@cbrevik
Copy link
Contributor Author

cbrevik commented Jun 29, 2017

@shergin I can open a PR a bit later then.

I don't think it will break Android since that platform already supports inheritance (so you don't have to merge in anything). That might need a it of testing though to verify.

As far as other platforms, UWP etc. we will have to find out.

Edit: Might be mistaken about Android. Doing a diff between them it seems like you inherit most, but not all of the props. Question is if we should merge the non-inherited ones at all? If the underlying ViewManager does not export them itself, why should they be merged on the JS-side?

facebook-github-bot pushed a commit that referenced this pull request Jul 10, 2017
Summary:
**Motivation**
This is a re-worked version of #14260, by shergin's suggestion.

For iOS, if you want to inherit from a native ViewManagers, your custom ViewManager will not automatically export the parents' props. So the only way to do this today, is to basically copy/paste the parent ViewManager-file, and add your own custom logic.

With this PR, this is made more extensible by exporting the `baseModuleName` (i.e. the iOS `superclass` of the ViewManager), and then using that value to re-establish the inheritance relationship in `requireNativeComponent`.

**Test plan**
I've run this with a test project, and it works fine there. But needs more testing.

Opened this PR as [per shergin's suggestion](#10946 (comment)) though, so we can discuss approach.

**Discussion**
* Android already supports inheritance, so this change should be compatible with that. But, not every prop available on `UIManager.RCTView.NativeProps` is actually exported by every ViewManager. So should `UIManager.RCTView.NativeProps` still be merged with `viewConfig.NativeProps`, even if the individual ViewManager does not export/use them to begin with?
* Does this break other platforms? [UWP](https://github.com/Microsoft/react-native-windows)?
Closes #14775

Differential Revision: D5392953

Pulled By: shergin

fbshipit-source-id: 5212da616acfba50cc285e2997d183cf8b2cd09f
@cbrevik
Copy link
Contributor Author

cbrevik commented Jul 12, 2017

Now that #14775 is landed, we're getting closer to landing this one as well.

To make the API for this functionality a bit more general, I'm thinking of changing the signature from:

<WebView 
  nativeComponent={RCTCustomWebView} 
  nativeComponentProps={{ onSomethingHappened: this.onSomethingHappenedEvent }} 
/>

To:

<WebView 
  nativeConfig={{
    component: RCTCustomWebView,
    props: { onSomethingHappened: this.onSomethingHappenedEvent }
  }} 
/>

Makes the API a bit cleaner, and also easier to extend with more options. For example if you want to send in a custom ViewManager as well (which it seems like the iOS WebView might require):

<WebView 
  nativeConfig={{
    component: RCTCustomWebView,
    props: { onSomethingHappened: this.onSomethingHappenedEvent },
    viewManager: CustomWebViewManager
  }} 
/>

Shouldn't be much work to make this change. Thoughts?

@jacobp100
Copy link
Contributor

The extensibility of the new way definitely looks like an improvement!

facebook-github-bot pushed a commit that referenced this pull request Jul 13, 2017
…ivate

Summary:
**Motivation**

See discussion in #10946. The motivation is to make `ReactWebViewManager` more extensible.

Re-using logic from the ReactWebViewManager when implementing your own custom WebView is a pain since so much of the logic is set as `private`.

This PR makes for easier extension/overriding of behavior, and less duplication of code, since most of the methods/fields are set as `protected` instead.

I've also made some "create" methods for the `WebView` and `WebViewBridge` so they can more easily be overridden.

**Test plan**
The test plan is the same as the other PR (#10946). I've made an simple test app which extends `RCTWebViewManager`: https://github.com/cbrevik/overrideWebview

See [CustomWebViewManager.java](https://github.com/cbrevik/overrideWebview/blob/master/android/app/src/main/java/com/overridewebview/CustomWebViewManager.java) for a simple implementation.

CC shergin (#10946 (comment))
Closes #14261

Differential Revision: D5413922

Pulled By: shergin

fbshipit-source-id: d2f6d478f2a147e2e7b5e05c195a8b28a0a3d576
@cbrevik
Copy link
Contributor Author

cbrevik commented Jul 14, 2017

Opened a new PR #15016 for this feature, since the implementation has changed over time. Closing this one in favor of that.

@cbrevik cbrevik closed this Jul 14, 2017
facebook-github-bot pushed a commit that referenced this pull request Sep 19, 2017
Summary:
Opening a new PR for #10946 (see discussion there).

This PR builds upon #14775 (iOS ViewManager inheritance) and #14261 (more extensible Android WebView).

**Motivation**
When `WebView.android.js` and `WebView.ios.js` use `requireNativeComponent`, they are hard-coded to require `RCTWebView`. This means if you want to re-use the same JS-logic, but require a custom native WebView-implementation, you have to duplicate the entire JS-code files.

The same is true if you want to pass through any custom events or props, which you want to set on the custom native `WebView`.

What I'm trying to solve with this PR is to able to extend native WebView logic, and being able to re-use and extend existing WebView JS-logic.

This is done by adding a new `nativeConfig` prop on WebView. I've also moved the  extra `requireNativeComponent` config to `WebView.extraNativeComponentConfig` for easier re-use.

**Test plan**
jacobp100 has been kind enough to help me with docs for this new feature. So that is part of the PR and can be read for some information.

I've also created an example app which demonstrates how to use this functionality: https://github.com/cbrevik/webview-native-config-example

If you've implemented the native side as in the example repo above, it should be fairly easy to use from JavaScript like this:
```javascript
import React, { Component, PropTypes } from 'react';
import { WebView, requireNativeComponent, NativeModules } from 'react-native';
const { CustomWebViewManager } = NativeModules;

export default class CustomWebView extends Component {
  static propTypes = {
    ...WebView.propTypes,
    finalUrl: PropTypes.string,
    onNavigationCompleted: PropTypes.func,
  };

  _onNavigationCompleted = (event) => {
    const { onNavigationCompleted } = this.props;
    onNavigationCompleted && onNavigationCompleted(event);
  }

  render() {
    return (
      <WebView
        {...this.props}
        nativeConfig={{
          component: RCTCustomWebView,
          props: {
            finalUrl: this.props.finalUrl,
            onNavigationCompleted: this._onNavigationCompleted,
          },
          viewManager: CustomWebViewManager
        }}
      />
    );
  }
}

const RCTCustomWebView = requireNativeComponent(
  'RCTCustomWebView',
  CustomWebView,
  WebView.extraNativeComponentConfig
);
```

As you see, you require the custom native implementation at the bottom, and send in that along with any custom props with the `nativeConfig` prop on the `WebView`. You also send in the `viewManager` since iOS requires that for `startLoadWithResult`.

**Discussion**
As noted in the original PR, this could in principle be done with more React Native components, to make it easier for the community to re-use and extend native components.
Closes #15016

Differential Revision: D5701280

Pulled By: hramos

fbshipit-source-id: 6c3702654339b037ee81d190c623b8857550e972
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Platform: iOS iOS applications.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants