Задать вопрос
monochromer
@monochromer
DIVeloper

Как в React извлекать информацию из children?

Есть такой API для задания настроек таблицы:

<Grid>
  <Column field="ProductID" />
  <Column field="ProductName" />
  <GridToolbar>
    <button onClick={this.saveToPDF}>save to pdf</button>
    <button onClick={this.saveToXLS}>save to xls</button>
  </GridToolbar>
</Grid>

Здесь дочерние компоненты используются не для прямого рендеринга, а для задания настроек таблицы - какие поля выводить из массива данных, ширина колонок и пр. Также есть панель инструментов, где есть, например, кнопки для экспорта данных.

Собственно вопрос - как при фильтровании children понять, какой элемент является Column, какой GridToolbar и т.д.? Есть ли какие-либо хорошие практики для этого? Склоняюсь к тому, чтобы проверять свойство type.name, которое вроде как должно быть названием компонента, т. е. 'Column', 'GridToolbar' и т. д.
  • Вопрос задан
  • 369 просмотров
Подписаться 1 Средний Комментировать
Решения вопроса 2
0xD34F
@0xD34F Куратор тега React
Собственно вопрос - как при фильтровании children понять, какой элемент является Column, какой GridToolbar и т.д.? Есть ли какие-либо хорошие практики для этого? Склоняюсь к тому, чтобы проверять свойство type.name, которое вроде как должно быть названием компонента, т. е. 'Column', 'GridToolbar' и т. д.

Можно просто type, без name. Только, конечно, в этом случае сравнивать надо не со строкой, а с самим компонентом.

Но должны ли это быть именно children?
Есть вариант...
...оформить передачу каждого из элементов через отдельный prop:

While this is less common, sometimes you might need multiple “holes” in a component. In such cases you may come up with your own convention instead of using children:

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

UPD. Вынесено из комментариев:

Мне хочется в React достичь того, что называется слотами в Web Components и Vue.js.

Окей, сделаем компонент слота:

function Slot({ name, content, scope, children = null }) {
  const slotContent = []
    .concat(content || [])
    .filter(n => n.props.slot === name)
    .map(n => n.type === 'template'
      ? [].concat(n.props.children).map(m => m instanceof Function ? m(scope) : m)
      : n
    );

  return slotContent.length
    ? slotContent
    : children;
}

Пример использования - компонент таблицы с возможностью кастомизации внешнего вида столбцов (как заголовков, так и ячеек с данными):

function Table({ columns, data, children }) {
  return (
    <table>
      <thead>
        <tr>{columns.map(n => (
          <th>
            <Slot name={`header.${n.name}`} content={children} scope={n}>
              {n.label}
            </Slot>
          </th>))}
        </tr>
      </thead>
      <tbody>{data.map((row, index) => (
        <tr>{columns.map(({ name }) => (
          <td>
            <Slot name={`cell.${name}`} content={children} scope={{ name, row, index }}>
              {row[name]}
            </Slot>
          </td>))}
        </tr>))}
      </tbody>
    </table>
  );
}

const COLUMNS = [
  { name:  'num', label:  'number' },
  { name:  'str', label:  'string' },
  { name: 'bool', label: 'boolean' },
];

const DATA = [
  { num:  69, str:  'hello, world!!', bool: false },
  { num: 187, str:  'fuck the world', bool:  true },
  { num: 666, str: 'fuck everything', bool:  true },
];

<Table columns={COLUMNS} data={DATA}>

  {/* в один слот можно передавать контент несколько раз */}
  <u slot="header.num" style={{ color:   'red' }}>NUM</u>
  <i slot="header.num" style={{ color: 'green' }}>!!!</i>

  {/* или, за один раз передавать несколько элементов */}
  <template slot="header.bool">
    boolean:
    <input type="checkbox" disabled />
    or
    <input type="checkbox" disabled defaultChecked />
  </template>

  {/* контент слота можно определять динамически */}
  <template slot="cell.bool">
    {({ row }) => <input type="checkbox" disabled defaultChecked={row.bool} />}
  </template>

</Table>

Можно даже сделать, чтобы было похоже на то, что вы показывали в тексте вопроса - добавляем компонент строки таблицы, экземпляры которого будут получать описания столбцов и контент слотов, вот как-то так:

function TableColumn() {
  // да, всё правильно - тут пусто, этот компонент нужен только как место, где будет
  // собрано всё, что относится к столбцу, сам он ничего делать не должен
  return null;
}

function Table({ data, children }) {
  const columns = []
    .concat(children || [])
    .reduce((acc, n) => (
      (n.type === TableColumn) && acc.push(n.props),
      acc
    ), []);

  return (
    <table>
      <thead>
        <tr>{columns.map(({ children, ...n }) => (
          <th>
            <Slot name="header" content={children} scope={n}>
              {n.label}
            </Slot>
          </th>))}
        </tr>
      </thead>
      <tbody>{data.map((row, index) => (
        <tr>{columns.map(({ name, children }) => (
          <td>
            <Slot name="cell" content={children} scope={{ name, row, index }}>
              {row[name]}
            </Slot>
          </td>))}
        </tr>))}
      </tbody>
    </table>
  );
}

<Table data={DATA}>

  <TableColumn name="row-index" label="#">
    <template slot="cell">
      {({ index }) => -~index}
    </template>
  </TableColumn>

  <TableColumn name="num" label="number" />

  <TableColumn name="str" label="string" />

  <TableColumn name="bool">
    <span slot="header" style={{ color: 'red' }}>boolean</span>

    <template slot="cell">
      {({ row }) => <input type="checkbox" disabled defaultChecked={row.bool} />}
    </template>
  </TableColumn>

</Table>
Ответ написан
rockon404
@rockon404 Куратор тега React
Frontend Developer
Мы использовали похожий прием, но с другой целью. Мы передавали в виджет формы всех шагов какого либо процесса как детей и рендерили только текущий.
То, что вы хотите сделать можно реализовать примерно так:
class Grid extends React.Component {
  getContent() {
    const { children } = this.props;

    if (!children) return null;

    return React.Children.map(children, (child, index) => {  // или forEach без return, в зависимости от целей
      const { displayName } = child.type;

      switch (displayName) {
        case 'Column':
          // do something
        default:
          // do something other
      }
    });
  }

  render () {
    return (
      <Wrapper>
       {this.getContent()}
      </Wrapper>
    )
  }
}
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы