השוואת גבהים בריאקט - חלק 2

19/12/2018

ריאקט מאפשרת כתיבת קוד גנרי באמצעות פיצול המערכת לפקדים, פקד מיכל יכלול את הקוד הגנרי והפקדים שבתוכו יושפעו מהמיכל. נזכרתי במבנה הזה כשרציתי להפוך את קוד השוואת הגבהים שכתבתי פה לפני כמה ימים לקוד גנרי יותר. אני כמעט מרוצה מהתוצאה.

1. השוואת גבהים בין פקדים

לפני מספר ימים הראיתי כאן קוד שמשווה גבהים של פקדים שונים בריאקט באמצעות הפעלת פונקציית jQuery אחרי כל שינוי. כך זה נראה בקודפן:

הקוד יוצר 4 תיבות וכשאתם מקלידים טקסט באחת התיבות הגובה של כל ה-4 משתנה בהתאמה.

השאלה הבאה- איך אפשר לפרק את הקוד הזה למספר פקדים, כך שאפשר יהיה לשנות גובה של כל רכיב ביישום?

2. הפקד SameHeight

הדרך הריאקטית לחבר בין מספר פקדים היא באמצעות רכיב חיצוני אליהם שיוכל להשפיע על כולם. אנחנו מכירים את זה מספריות ניהול State אבל אפשר ליישם את התבנית גם בקטן למשל באמצעות רכיב מיכל שעוטף את הרכיבים שבתוכו ומשפיע עליהם.

הייתי שמח לשים כל תיבה ברכיב משלה שיראה בערך כך:

class Box extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: '' };
  }

  setText(box, value) {
    this.setState(oldState => ({
      text: value,
    }));
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.text}
          onChange={(e) => this.setText('text', e.target.value)} 
          />
        <h1>{this.state.text}</h1>
      </div>
    );
  }
}

ואת התיבות כולן לשים ברכיב מרכזי בצורה הבאה:

class App extends React.Component {
  render() {
    return (
      <div className="container">
        <SameHeight>
          <Box />
          <Box />
          <Box />
          <Box />
        </SameHeight>
      </div>
    );
  }
}

הרכיב SameHeight שנרצה תכף לכתוב אותו יצטרך לתקן את הגבהים של כל התיבות.

3. יצירת קשר מהמיכל לאלמנטים פנימיים בקופסאות

בשביל שהמיכל SameHeight יוכל להשפיע על הגבהים של אלמנט פנימי בתוך אלמנט Box הוא צריך להחזיק ref אל האלמנט הפנימי הזה. הבעיה שהמיכל שלנו לא יודע מראש כמה ילדים יהיו לו ולכן עליו להחזיק מערך דינמי של References. ילדים חדשים שיצטרפו יוסיפו פריטים למערך זה, וילדים שיוסרו מה DOM יצטרכו להימחק מהמערך.

בהנחה שיש לנו מערך מתאים נוכל לכתוב את הפונקציה fixHeights ברכיב SameHeight באופן הבא:

  fixHeights() {
    let maxHeight = 0;

    $(this.myRefs).each(function(idx, el){

      if ($(el).height() > maxHeight) {
        maxHeight = $(el).height();
        console.log(maxHeight);
      }
    });

    $(this.myRefs).css('min-height', maxHeight);
  }

המשימה הקשה יותר היא ליצור את המערך ולנהל מה קורה כשהילדים של SameHeight משתנים. בשביל זה אנחנו נצטרך להשתמש ב ref וב Callback Functions, והרעיון הוא כזה:

הפקד SameHeight יגדיר עבור כל אחד מהילדים שלו פונקציית ref שתפקידה לשמור את ה ref במערך ואם הילד עוזב את ה DOM למחוק את ה ref מהמערך. בגלל ש ref צריך לקבל פונקציה תהיה לנו פונקציה שמחזירה פונקציה כזו, ונפעיל אותה על כל ילד בנפרד. כך יכולה להיראות אותה פונקציה ראשית:

  watchChildHeight() {
    let _ref = null;

    return (ref) => {
      if ((ref === null) && (_ref !== null)) {
        const idx = this.myRefs.indexOf(_ref);
        if (idx >= 0) {
          this.myRefs.splice(idx, 1);
        }
      } else {
        _ref = ref;
        this.myRefs.push(ref);
      }
    }
  }

כשאלמנט חדש מצטרף ל DOM ריאקט יפעיל אותה עם האלמנט ואנחנו נשמור אותו במערך. כשאלמנט יוצא מה DOM ריאקט יפעיל אותה עם null ואז נדע למחוק את האלמנט שהוספנו בעבר.

את התוצאה של פונקציית watchChildHeight נרצה להעביר לכל אחד מהילדים ולכן פונקציית render של SameHeight תיראה כך:

  render() {
    return React.Children.map(this.props.children, (child) => (
      React.cloneElement(
        child,
        {
          refMaker: this.watchChildHeight(),
          fixHeights: this.fixHeights.bind(this),
        },
      )
    ));
  }

ובצד של הילדים נרצה לקחת את הפונקציה שקיבלנו ב refMaker ולהשתמש בה בתור ה ref של האלמנט שאנחנו רוצים להשוות את הגובה שלו, מה שאומר ש render של Box בעצם יראה כך:

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.text}
          onChange={(e) => this.setText('text', e.target.value)} 
          />
        <h1 ref={this.props.refMaker}>{this.state.text}</h1>
      </div>
    );
  }

יצרנו חיבור ישיר מ SameHeight לאלמנטי ה h1 שנמצאים בתוך אלמנטי ה Box שלנו, ואנחנו מאפשרים גם לילדים מסוגים אחרים להצטרף למשחק כל עוד הם יעבירו את האלמנט שרוצים להשוות גובה שלו דרך ה ref.

4. הודעה על עדכון מ Box ל SameHeight

החלק שאני פחות מרוצה ממנו בפיתרון הוא העדכון ההפוך- כלומר כל פעם ש Box משנה את ה State שלו גם גובה האלמנטים משתנה ולכן צריך להשוות את הגבהים. חשוב לשים לב שאלמנט SameHeight לא יודע על העדכון הזה: שינוי הערך ב State של Box גורם ל render שלו, בלי להשפיע על האלמנטים שמעליו. לכן אנחנו צריכים בצורה יזומה לקרוא לפונקציה fixHeights אחרי כל mount או update של אחד הילדים.

מבחינת הפקד זה אומר ש Box צריך לכלול גם את הפונקציות:

  componentDidUpdate() {
    this.props.fixHeights();
  }

  componentDidMount() {
    this.props.fixHeights();
  }

וזה כמובן החלק הפחות יפה בפיתרון כי היינו רוצים שמנגנון זה יהיה שקוף ל Box עצמו.

ואם הגעתם עד לפה מוזמנים להנות מהקודפן המלא והעובד: