4

Background

I have a parent component which is a collapsing card. When it is past a child component and shows it when it expands, it renders in a scroll box. I tried to adjust the height of the component when it expands by using,

Dimensions.get('window').height

But the problem with that is that it makes the card the page height. I need it to be the height of the child's data plus some padding.

Example

class CardCollapsible extends Component {
    constructor(props) {
        super(props);

        this.state = {
            title: props.title,
            expanded: this.props.expanded,
            animation: new Animated.Value(),
            icon: this.props.icon,
            iconStyles: this.props.iconStyles,
        };

        this.anime = {
            height: new Animated.Value(),
            expanded: false,
            contentHeight: 0,
        };

        this._initContentHeight = this._initContentHeight.bind(this);
        this.getIconStyles = this.getIconStyles.bind(this);
        this.toggle = this.toggle.bind(this);
        this.anime.expanded = props.expanded;
        this.minValue = props.minValue;
        this.openIconStyles = props.openIconStyles;
        this.closedIconStyles = props.closedIconStyles;
    }

    componentWillMount() {
        this.getIconStyles();
    }

    getIconStyles() {
        let iconStyles = null;
        let icon = null;
        if (this.state.expanded) {
            iconStyles = _.extend({}, this.props.closedIconStyles);
            icon = this.props.iconClose;
        } else {
            iconStyles = _.extend({}, this.props.openIconStyles);
            icon = this.props.iconOpen;
        }

        const expanded = !this.state.expanded;
        this.setState({ iconStyles, icon, expanded });
    }

    _initContentHeight(evt) {
        if (this.anime.contentHeight <= 0) {
            this.anime.contentHeight = evt.nativeEvent.layout.height;
            this.anime.height.setValue(this.anime.expanded ? this._getMaxValue() : this._getMinValue());
        }
    }

    _getMaxValue() { return this.anime.contentHeight + 30; }
    _getMinValue() { return this.props.minValue; }

    toggle() {
        this.getIconStyles();

        Animated.timing(this.anime.height, {
            toValue: this.anime.expanded ? this._getMinValue() : this._getMaxValue(),
            duration: 300,
        }).start();
        this.anime.expanded = !this.anime.expanded;
    }

    render() {
        return (
            <Card style={styles.container}>
                <View style={styles.titleContainer}>
                    <CardTitle>{this.state.title}</CardTitle>
                    <TouchableHighlight
                        style={styles.button}
                        onPress={this.toggle}
                        underlayColor="#f1f1f1"
                    >
                        <Icon
                            name={this.state.icon}
                            style={[this.state.iconStyles, styles.icon]}
                        />
                    </TouchableHighlight>
                </View>

                <Animated.View style={{ height: this.anime.height }} onLayout={this._initContentHeight}>
                    <Separator />
                    <CardContent style={styles.CardContent}>
                        {this.props.children}
                    </CardContent>
                </Animated.View>
            </Card>
        );
    }
}

Using the CardCollapsible component.

Data renders in a scroll when card is expanded.

<CardCollapsible>
   <FlatList>...</FlatList>
</CardCollapsible>

Then when changing these lines to

Dimensions.get('window').height

It is taking the height of the screen and will not close.

_initContentHeight(evt) {
        if (this.anime.contentHeight <= 0) {
            this.anime.contentHeight = evt.nativeEvent.layout.height;
            this.anime.height.setValue(this.anime.expanded ? this._getMaxValue() : this._getMinValue());
        }
    }

    _getMaxValue() { return Dimensions.get('window').height; }
    _getMinValue() { return this.props.minValue; }

Question

How can I change the height of the card to be dynamic based on its childs data when rendered?

wuno
  • 9,547
  • 19
  • 96
  • 180

1 Answers1

3

What you want to do is a common approach but tricky, since react does not allow communication from children to parent.

How did I accomplish it? By using ref.measures function. Let's take a look.

Child implementation:

export default class MyChild extends Component {
  render() {
    return (
      <TouchableWithoutFeedback onPress={this._onPress} ref={ref => this._options = ref}>

      </TouchableWithoutFeedback>
    );
  }

  _onPress() {
    this._options.measure((fx, fy, width, height, px, py) => {
      if (this.props.setMeasuresOnParent)
        this.props.setMeasuresOnParent({
          relativeX: fx,
          relativeY: fy,
          absoluteX: px,
          absoluteY: py,
          height,
          width
        });
    });
  }
}

Parent implementation:

<MyChild
  setMeasuresOnParent={measures => this.props.showChildren(children_id?, measures.absoluteX, measures.absoluteY)}
/>

In my case, I used a Button-styled child, but you could also use componentDidMount in child to tell the parent its measures.

Facundo La Rocca
  • 3,786
  • 2
  • 25
  • 47
  • So you are saying that everywhere I want to use a CardCollapsible> component I would have to calculate the height inside the child component. This seems like a lot of extra work. Ideally creating a solution inside the actual collapsible card would be idea. Is there for sure not a solution for this that is possible in React / React Native? – wuno Sep 15 '17 at 17:52
  • There is a new version, called [React Fiber](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0ahUKEwiS4_W44qfWAhXGl5QKHR0rAaYQFggmMAA&url=https%3A%2F%2Fgithub.com%2Facdlite%2Freact-fiber-architecture&usg=AFQjCNFEDdF7UX8altgI9Eg5Y_3V1QZrUA) which tries to solve many of those concerns (It has not beed realesed yet). But I'm not actually aware of any other solution. I mean, there might be more or less beatiful solutions, but all of them will end up asking to children for measures. More info [here](http://isfiberreadyyet.com/) – Facundo La Rocca Sep 15 '17 at 18:01
  • originally when I wrote this, I used this as reference. And it appeared that it would handle such a case, https://stackoverflow.com/a/33771254/2218253 What do you think? – wuno Sep 15 '17 at 18:03
  • It might work in the same way, but you would have to pass it to its parent. The problem is like a circle reference: Parent needs something from its children, but a child cant communicate to its parent directly (and we are mentioning that until child has already rendered measures are unknown). – Facundo La Rocca Sep 15 '17 at 18:09
  • Actually this was a silly error. To get the content height of the child you can do it like I am doing. The problem was calling the separator component in the collapsible part. removing it solved the problem. So in order to solve this, onLayout={this._initContentHeight} takes care of the dynamic height. Update your answer and I will accept it. Include what onLayout={this._initContentHeight} does please. – wuno Sep 15 '17 at 18:50
  • I think you should add your own answer instead since that was the real solution and I will vote it up!!! You have earned those rep points! :-) – Facundo La Rocca Sep 15 '17 at 19:20
  • What is the answer please? – Dimitri Kopriwa Feb 15 '20 at 16:09