React Native - ListView Scroll To Nested Child Ref?
Asked Answered
T

1

7

I am using ListView to display a list of comments and possibly subcomments if they exist on a comment. I'm trying to scroll to a specific subcomment via its ref, but I am not able to get it to work. I used 3 components (boiled down below) to accomplish this:

1. Comments

import React, { Component } from 'react'
import { TouchableOpacity, ListView, View, Text } from 'react-native'
import CommentRow from './commentRow'

const ds = new ListView.DataSource({ rowHasChanged: ( r1, r2 ) => r1.id !== r2.id });
const commentsDataSource = [
  {id: '1', body: 'comment 1'},{id: '2', body: 'comment 2'},{id: '3', body: 'comment 3'},{id: '4', body: 'comment 4'},{id: '5', body: 'comment 5'},{id: '6', body: 'comment 6'},{id: '7', body: 'comment 7'},{id: '8', body: 'comment 8'},{id: '9', body: 'comment 9'},{id: '10', body: 'comment 10'},
  {id: '11', body: 'comment 11'},{id: '12', body: 'comment 12', hasSubComments: true},{id: '13', body: 'comment 13'},{id: '14', body: 'comment 14'},{id: '15', body: 'comment 15'},{id: '16', body: 'comment 16'},{id: '17', body: 'comment 17'},{id: '18', body: 'comment 18'},{id: '19', body: 'comment 19'},{id: '20', body: 'comment 20'}
];

export default class Comments extends Component {
    constructor(props) {
      super(props);

      this.state = {
        dataSource: ds.cloneWithRows(commentsDataSource)
      };
    }

    scrollToSubCommentRef(ref) {
      this.rowz[ref].measure((ox, oy, width, height, px, py) => {
        const offsetY = oy;
        this.refs.mainListView.scrollTo({ y: offsetY })
      });
    }

    render() {
      return (
        <View>
            <TouchableOpacity style={{backgroundColor: 'red', padding: 50}}
                              onPress={() => this.scrollToSubCommentRef('subComment_10')}>
                <Text>Scroll to subComment_10!</Text>
            </TouchableOpacity>

            <ListView ref="mainListView"
                      renderRow={comment => <CommentRow comment={comment} />}
                      dataSource={this.state.dataSource}
                      enableEmptySections={true} />
        </View>
      )
    }
}

2. CommentRow

import React, { Component } from 'react';
import { View } from 'react-native'
import CommentListItem from './commentListItem'

export default class CommentRow extends Component {
    render() {
      const comment = this.props.comment;

      return (
        <View key={`comment_${comment.id}`} style={{overflow: 'hidden'}}>
          <CommentListItem comment={comment} />
        </View>
      )
    }
}

3. CommentListItem

import React, { Component } from 'react'
import { View, Text } from 'react-native'

const subComments = [
  {id: '1', body: 'subcomment 1'},{id: '2', body: 'subcomment 2'},{id: '3', body: 'subcomment 3'},{id: '4', body: 'subcomment 4'},{id: '5', body: 'subcomment 5'},{id: '6', body: 'subcomment 6'},{id: '7', body: 'subcomment 7'},{id: '8', body: 'subcomment 8'},{id: '9', body: 'subcomment 9'},{id: '10', body: 'subcomment 10'},
  {id: '11', body: 'subcomment 11'},{id: '12', body: 'subcomment 12'},{id: '13', body: 'subcomment 13'},{id: '14', body: 'subcomment 14'},{id: '15', body: 'subcomment 15'},{id: '16', body: 'subcomment 16'},{id: '17', body: 'subcomment 17'},{id: '18', body: 'subcomment 18'},{id: '19', body: 'subcomment 19'},{id: '20', body: 'subcomment 20'},
  {id: '21', body: 'subcomment 21'},{id: '22', body: 'subcomment 22'},{id: '23', body: 'subcomment 23'},{id: '24', body: 'subcomment 24'},{id: '25', body: 'subcomment 25'},{id: '26', body: 'subcomment 26'},{id: '27', body: 'subcomment 27'},{id: '28', body: 'subcomment 28'},{id: '29', body: 'subcomment 29'},{id: '30', body: 'subcomment 30'},
  {id: '31', body: 'subcomment 31'},{id: '32', body: 'subcomment 32'},{id: '33', body: 'subcomment 33'},{id: '34', body: 'subcomment 34'},{id: '35', body: 'subcomment 35'},{id: '36', body: 'subcomment 36'},{id: '37', body: 'subcomment 37'},{id: '38', body: 'subcomment 38'},{id: '39', body: 'subcomment 39'},{id: '40', body: 'subcomment 40'},
  {id: '41', body: 'subcomment 41'},{id: '42', body: 'subcomment 42'},{id: '43', body: 'subcomment 43'},{id: '44', body: 'subcomment 44'},{id: '45', body: 'subcomment 45'},{id: '46', body: 'subcomment 46'},{id: '47', body: 'subcomment 47'},{id: '48', body: 'subcomment 48'},{id: '49', body: 'subcomment 49'},{id: '50', body: 'subcomment 50'},
  {id: '51', body: 'subcomment 51'},{id: '52', body: 'subcomment 52'},{id: '53', body: 'subcomment 53'},{id: '54', body: 'subcomment 54'},{id: '55', body: 'subcomment 55'},{id: '56', body: 'subcomment 56'},{id: '57', body: 'subcomment 57'},{id: '58', body: 'subcomment 58'},{id: '59', body: 'subcomment 59'},{id: '60', body: 'subcomment 60'},
  {id: '61', body: 'subcomment 61'},{id: '62', body: 'subcomment 62'},{id: '63', body: 'subcomment 63'},{id: '64', body: 'subcomment 64'},{id: '65', body: 'subcomment 65'},{id: '66', body: 'subcomment 66'},{id: '67', body: 'subcomment 67'},{id: '68', body: 'subcomment 68'},{id: '69', body: 'subcomment 69'},{id: '70', body: 'subcomment 70'}
];

export default class CommentListItem extends Component {
  rowz = []; // to hold subComment refs for scroll access

  subCommentsList = () => {
    return subComments.map((subComment, i) => {
      return (
        <View ref={i => this.rowz["subComment_"+subComment.id] = i} key={"subComment_"+subComment.id}>
          <Text>{subComment.body}</Text>
        </View>
      );
    });
  }

  render() {
    const comment = this.props.comment;

    return (
      <View>
        <Text>{comment.body}</Text>
        {comment.hasSubComments && this.subCommentsList()}
      </View>
    )
  }
}

In the parent component #1 I tried to scroll to a subComment via its ref of subComment_10, but measure gives an undefined error. I understand this.rowz doesn't exist in #1 just in #3 where the subComments map iterates over each subComment and assigns it to the rowz array (I just realized it does not assign the subComment_idhere to the rowz array for some reason).

So how can we fix the ref assignment issue in the #3 map so the rowz array gets a list of all the subComment refs so we can scroll to them? And how can we get the TouchableOpacity with this.scrollToSubCommentRef('subComment_10') in #1 to scroll the mainListView to subComment_10?

UPDATE

With the provided solution, the ref is passed to the rowz array successfully, but as you'll notice, it does not scroll to subComment_10, instead it scrolls to the bottom of comment 10. It should scroll to the top of subComment_10 so that it is the top most visible subComment on click of the TouchableHighlight:

enter image description here

Topping answered 3/2, 2017 at 3:56 Comment(6)
In this part: subCommentsList = () => { ref={subComment => this.rows["subComment_"+subComment.id] = subComment} can you tell whether subComment in params and ref function subComment param get mixed?Arnoldoarnon
Not sure what you mean, but the rows array in the top of #3 holds the list of subcomments when assigned by the ref function you mentioned, so the ref subcomment_10 exists in the rows array.Topping
subComments.map has a parameter that is the same name with the ref anonymous arrow function. I suspected that may cause a conflict. But you're assuring that functions are working as expected right?Arnoldoarnon
What about that rows = [] array. Where and how did you define it? because this.rows is something different than just rows = [] without prefix, also its a forbidden use in ecma 6.Arnoldoarnon
I've updated the code/question above so it's easily reproducible. I also changed rows to rowz I thought it may conflict with something, but I realized that it does not assign the refs to rowz when I log it. You may have more experience and know where the issue is :)Topping
Thank you, reproducing the error made it clearer. I posted the answer, let me know if anything goes wrongArnoldoarnon
A
2

OK, I ran your edited code and figured out what you're missing. The refz array is created locally in CommentListItem class, therefore you can't access it from parent classes. However, since you will be doing all the navigation from parent class, passing a prop array to bottom most level, and filling it there would be a better approach. This way you won't get the this.rowz is undefined error and run your code as expected.

export default class Comments extends Component {
constructor(props) {
  super(props);
  this.rowz = []
  this.state = {
    dataSource: ds.cloneWithRows(commentsDataSource)
  };
}

scrollToSubCommentRef(ref) {
  this.rowz[ref].measure((ox, oy, width, height, px, py) => {
    const offsetY = oy;
    this.refs.mainListView.scrollTo({ y: offsetY })
  });
}

render() {
  return (
    <View>
        <TouchableOpacity style={{backgroundColor: 'red', padding: 50}}
                          onPress={() => this.scrollToSubCommentRef('subComment_10')}>
            <Text>Scroll to subComment_10!</Text>
        </TouchableOpacity>

        <ListView ref="mainListView"
                  renderRow={comment => <CommentRow refArr={this.rowz} comment={comment} />}
                  dataSource={this.state.dataSource}
                  enableEmptySections={true} />
    </View>
  )
}
}

Here in Comments class, we pass the array, (this.rowz) that we created in constructor, to CommentsRow class ~

<CommentRow refArr={this.rowz} comment={comment} />

In CommentRow class, we will just pass what we had from parent class,

export default class CommentRow extends Component {
    render() {
      const comment = this.props.comment;

      return (
        <View key={`comment_${comment.id}`} style={{overflow: 'hidden'}}>
          <CommentListItem refArr={this.props.refArr} comment={comment} />
        </View>
      )
    }
}

Right here:

<CommentListItem refArr={this.props.refArr} comment={comment} />

And finally, in CommentListItem class, to fill our array, we can simply call this.props.refArr.push()

export default class CommentListItem extends Component {
  rowz = []; // to hold subComment refs for scroll access

  subCommentsList = () => {
    return subComments.map((subComment, i) => {
      return (
        <View ref={i => this.props.refArr["subComment_"+subComment.id] = i} key={"subComment_"+subComment.id}>
          <Text>{subComment.body}</Text>
        </View>
      );
    });
  }

  render() {
    const comment = this.props.comment;

    return (
      <View>
        <Text>{comment.body}</Text>
        {comment.hasSubComments && this.subCommentsList()}
      </View>
    )
  }
}

As you may see clearer here:

<View ref={i => this.props.refArr["subComment_"+subComment.id] = i} key={"subComment_"+subComment.id}>

It just runs and scrolls smoothly when touchable is pressed. I skipped the import parts in snippets above.

Arnoldoarnon answered 6/2, 2017 at 6:29 Comment(8)
Thanks for the solution, it is half way there (rowz array has the refs now), but the scroll position is incorrect. I added an update to the question with a visual showing the remaining scroll position issue.Topping
Can you log what you measure for each subcomment? Also try to scroll lets say sub_comment20, what's the delta of distance, does it match row height? I believe that's something you can figure out by testing your scrollToSubCommentRef function.Arnoldoarnon
I believe it's your function and measurements that fail. This may help: #34750924Arnoldoarnon
The thing is, the function and measurements works fine when it's the comment that has the ref in the array, scrolls to the correct comment, but it's the subComment it does not scroll to correctly, hence why I created this question.Topping
I will try to free up some space and take another look tomorrow.Arnoldoarnon
I got it :) In the measure function I had to calculate the offsetY of the parent comment and add that value to the offsetY of the subComment, so it definitely needs the sum of both to scroll to the correct position. Thank you so much for all your help @Enie Jakiro!Topping
Glad you figured that out :)Arnoldoarnon
Don't mutate props!! It's better to pass a function as a prop to change state or even just a property on parent. More clean and predictable. i.e. on child: this.props.addChild(childId) and on parent in addChild: this.childrefs.push(childId)Truck

© 2022 - 2024 — McMap. All rights reserved.