I have a react native application I am building, and for part of it I have implemented my own custom module (in this case a webview). I have it set up and everything works fine, until I render two of the same modules on the same screen.
Once I have had two of the same modules on the same screen, the code in Objective-C which calls the onChange method back to javascript no longer executes and I can longer longer communicate with my module through Javascript.
I've noticed that each instance of my module has a tag or something like that, but I am not sure what I should do to fix this.
Update
Basically I have added some code and I can see that the issue is when the new module is added to the screen, it overrides the callbacks for the previous module. Once the new modules leaves the screen, the callbacks in Objective-C are still "focused" on the most recently rendered module...
Might this have something to do with?
ReactNative.findNodeHandle(this.refs[WEB_VIEW]);
Edit
I have been looking over some open source code and I think I have narrowed down the problem. Basically my module consists of a WebView and a WebViewManager. The WebViews are pretty simple and just display the contents of the web and such, and the manager is responsible for controlling their behavior.
When I want to call a method on the module from React Native, I can do so by calling the method on the WebViewManager like this:
scrollToTop() {
RNTWebViewManager.scrollToTop();
}
In which the manager will then call the corresponding method in Objective-C. However, it seems that when I have more than one instance of a WebView on screen at a time, the manager doesn't know which instance to call, and resorts to calling the most recent instance that was placed on screen.
In the open source project I see that they do something by passings refs around and getting the specific view tags, which is then used by the manager to call on the correct instance of their WebViews, but I am still trying to figure out how to implement this.
Here is the link below for the project I was looking at:
https://github.com/CRAlpha/react-native-wkwebview
Ok so after quite some time and research I have finally figured out the solution. Basically it has to do with making sure the right component is being called from React Native and Objective-C. For example in your react native component you have a function:
executeJavascript(tag,js) {
RNTWebViewManager.executeJavascript(tag,js);
}
And then in your Objective-C implementation of RNTWebViewManager you could have something like this:
RCT_EXPORT_METHOD(executeJavascript:(nonnull NSNumber *)reactTag with:(NSString *)js)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RNWebView *> *viewRegistry) {
RNWebView *view = viewRegistry[reactTag];
[view evaluateJavaScript:js completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (result == nil) { result = #""; }
id outputError = (error) ? error.description : #"";
view.onChange(#{ #"result" : result, #"error": outputError, #"type" : #"javascript" });
}];
}];
}
Notice how the executeJavascript function takes a tag, that is a reference to the current view you are trying to invoke that method on. So basically all you need to do is pass that tag number whenever you want to perform a method on a specific instance, which you can get like so:
getTag() {
return ReactNative.findNodeHandle(this.refs['RNWebView']);
}
Where in this case the ref is 'RNWebView', which would give me back the right tag which I could then use to call the function. Hope I did a decent job of explaining this.
Related
I am developing react native project and I am loading some graphs from server response.
It is a Tab based app and this code is written in first tab.
But, In some use cases that data is not loading to that graph properly.
I have written that code in componentDidMount(), But it will call only once. But, My requirement is I have to call whenever view loaded, That time only render method is calling.
I have tried to add addlistener for navigation, But, Due to its it not navigation stack throwing error.
I have found some solution like below.
componentDidMount() {
}
fetchGraphData = () => {
//some code fetching from DB and redux based on conditions
}
render() {
this.fetchGraphData();
return (
);
}
}
But, This is not good practice as per code standards.
I am not receiving props, But, We are using some graphs which are
loading from data. My requirement is I have to call api fetch data
method after screen load every time.
Any suggestions, I have to call that fetchGraphData() once render method or view loaded.
Your problem is that when you move the 'fetchGraphData' function to a screen with the 'fetchGraphData' function, you must execute it. This problem can be solved by something simpler than I thought.
componentDidMount() {
this.fetchGraphData();
}
You can try rendering again when you move to a screen with a function.
this.props.navigation.push('functionMoveScreen') // Rendering the screen again.
I am trying to create a directive where I animate a fab-button when the view is shown.
The animation works if it is inside ngOnInit, but due to ionic route strategy the animation doesn't work when I leave the page and go back. Putting it in ionViewDidEnter didn't work because I presume that ionViewDidEnter doesn't work inside the directive. So is there any approach I can take to solve this?
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button mode="md" appAnimateFab>
<ion-icon name="create" mode="md"></ion-icon>
</ion-fab-button>
</ion-fab>`
#Directive({
selector: 'ion-fab-button[appAnimateFab]'
})
export class AnimateFabDirective implements OnInit {
constructor(
private animationBuilder: AnimationBuilder,
private element: ElementRef
) { }
ngOnInit() {
}
ionViewDidEnter() {
console.log(this.element);
const factory = this.animationBuilder.build([
style({transform: 'rotate(-45deg)'}),
animate('5s ease-in', style({transform: 'rotate(0deg)'}))
]);
const anim = factory.create(this.element.nativeElement);
anim.play();
}
}
This is an interesting question. I got halfway through writing out a detailed reply yesterday when I realised that you were actually asking about directives and not custom components... so all my research was wrong haha.
Today I have had another look. The tutorials all seem to conveniently miss having a requirement to deal with pages changing backwards and forwards and just lean on ngOnInit.
After scratching my head for a bit I started to wonder how else it could be triggered and I'm thinking: what about the Intersection Observer API?
I really like the way Alligator.io explain things:
Using the Intersection Observer API to Trigger Animations and Transitions
Their example shows the animation being triggered every time you scroll down to view.
If you are flipping pages then it feels like it should trigger as coming into view, but I haven't tested this out with code.
For a more Ionic-focused example with Intersection Observer API, Josh has a tutorial:
Animating List Items in Ionic with the Intersection Observer API | joshmorony - Learn Ionic & Build Mobile Apps with Web Tech
Maybe you can adapt this to use your animation code?
import React from "react";
import { Player } from "#react-native-community/audio-toolkit";
export default class RNAudiotoolkit extends React.Component {
componentDidMount(){
new Player("some_audio_file.mp3").play();
console.log(Player.isPlaying);
}
}
Above is the minimum code I've whittled down to, the audio track does play but, console.log(Player.isPlaying) always returns "false" but the audio file is running. Any input on why this isn't working. I can only suspect it has something to do with MediaStates, but have unsuccessfully gotten anything to work. If you have experience with this package your input is greatly appreciated.
documentation: https://github.com/react-native-community/react-native-audio-toolkit/blob/master/docs/API.md
Edit: Fixed and tested; answer is a combination of my original and mAhMoUdDaFeR's answer.
If you look above the documentation for the play method, you will see the documentation for prepare(Function callback). In it, it states:
...Otherwise the file is prepared when calling play() which may result in a small delay.
This means that if you check the .isPlaying property immediately after calling play() like you are doing, it is not guaranteed that the file is actually playing by time your console.log(Player.isPlaying) executes.
There is also the second issue that .isPlaying is not a static property on the Player class despite how it appears in the docs. It is actually a property of the Player instance that you need to create to play an audio file.
If you want to see that .isPlaying is indeed working correctly, one potential check is to run your code in a callback function passed into .play() as the docs show:
play(Function ?callback)
Start playback.
If callback is given, it is called when playback has started.
So a simple example would be to write your example code like this (saving the created instance and then logging in a callback):
componentDidMount(){
const p = new Player("some_audio_file.mp3").play(() => console.log('in callback', p.isPlaying));
console.log('immediately after play', p.isPlaying);
}
I created a new project to test this and if you run the above code, you'll see the following printed out illustrating the issue:
immediately after play false
in callback true
isPlaying is not a static method in the component Player, so you can't use Player.isPlaying, you can get isPlaying from the created instance (object) of this Player.
Try keeping a reference of the player object and then accessing its children:
this.player = new Player("some_audio_file.mp3").play()
and then log:
console.log(this.player.isPlaying)
I'm working on an app in React Native, and am having trouble accessing props that I feed into a component I made.
If I do console.log(this.props) within the constructor, I can see the props display in the console as desired, however if I put it in any other method, it prints undefined. How can I access the props that are clearly being sent to the component from outside of the constructor method?
You are probably adding new methods that are not binding this.
Check if you are writing the method like this:
myMethod(){
//Code
}
and just change it to:
myMethod = () => {
//Code
}
Edit: Like #Li357 says, these are called arrow functions. Arrow functions don't bind this automatically, and as a consequence receive the this of the surrounding class. In your case it will solve your issue as you want to access the properties of that class but you might want to read about it and how binding works in JS classes.
Another option is to write function.bind() but either way should work.
I am creating a tab component that loads its v-tab-item components dynamically, given an array of configuration objects that consist of tabName, id, and tabContent which is a resource location for the component. I have it successfully loading the components. However, they don't actually initialize (or run their created() methods) until I switch tabs. I just get empty tabs with the correct labels. Using the DOM inspector initially shows just <componentId></componentId>, and then when I switch tabs, those tags are replaced with all of the component's content.
How do I get the dynamic components to initialize as soon as they are loaded?
EDIT: I created a CodePen here:
https://codepen.io/sgarfio/project/editor/DKgQON
But as this is my first CodePen, I haven't yet figured out how to reference other files in the project (i.e. what to set tabContent to so that require.js can load them up). I'm seeing "Access is denied" in the console, which makes it sound like it found the files but isn't allowed to access them, which is weird because all the files belong to the same project. So my CodePen doesn't even work as well as my actual project. But maybe it will help someone understand what I'm trying to do.
Also, after poking around a bit more, I found this:
http://michaelnthiessen.com/force-re-render/
that says I should change the key on the component and that will force the component to re-render. I also found this:
https://v2.vuejs.org/v2/guide/components-dynamic-async.html
Which has a pretty good example of what I'm trying to do, but it doesn't force the async component to initialize in the first place. That's what I need the async components to do - they don't initialize until I switch tabs. In fact they don't even show up in the network calls. Vue is simply generating a placeholder for them.
I got it working! What I ended up doing was to emit an event from the code that loads the async components to indicate that that component was loaded. The listener for that event keeps a count of how many components have been loaded (it already knows how many there should be), and as soon as it receives the right number of these events, it changes the value of this.active (v-model value for the v-tabs component, which indicates which tab is currently active) to "0". I tried this because as I noted before, the async components were loading/rendering whenever I switched tabs. I also have prev/next buttons to set this.active, and today I noticed that if I used the "next" button instead of clicking on a tab, it would load the async components but not advance the tab. I had already figured out how to emit an event from the loading code, so all I had to do at that point was capture the number of loaded components and then manipulate this.active.
I might try to update my CodePen to reflect this, and if I do I'll come back and comment accordingly. For now, here's a sample of what I ended up with. I'm still adding things to make it more robust (e.g. in case the configuration object contains a non-existent component URL), but this is the basic gist of it.
created: function() {
this.$on("componentLoaded", () => {
this.numTabsInitialized++;
if(this.numTabsInitialized == this.numTabs) {
// All tabs loaded; update active to force them to load
this.active = "0";
}
})
},
methods: {
loadComponent: function(config) {
var id = config.id;
var compPath = config.tabContent;
var self = this;
require([compPath], function(comp) {
Vue.component(id, comp);
self.$emit("componentLoaded");
});
}
}