低代码-栅格系统

889 阅读2分钟

功能概览

低代码设计中最重要的一种布局方式就是栅格布局,将物料拖拽至设计器中,会根据其设定的span之类的值自适应填充位置,与之相关的场景比如表单设计、excel打印设计等

  • 表单设计
  • excel打印设计

方案选择

flex

下图为antd的栅格方案:

flex弹性布局是一个很不错的方案,简单、上手快,只要按照antd的Row、Col那样实现一个布局栅格系统系统,根据节点的栅格大小去动态添加对应的class,似乎就能cover住咱们的功能,但是想要满足excel打印设计那种,有行合并的需求的话,就只能含泪跟flex说拜拜了 orz。

grid

在寻找解决方案的途中,偶然看到了一个网站 CSS Grid Generator ,你可以按照自己的需求动态去生成想要的grid样式配置,比如要实现上图的excel布局

效果图

代码

.parent {
/*指定一个容器采用网格布局  */ 
display: grid;
/*每一列的列宽 repeat相当一个遍历,减少代码 24列,每列1/24 */  
grid-template-columnsrepeat(241fr);
/*每一行的行高 repeat同上 */ 
grid-template-rowsrepeat(31fr);
/*置列与列的间隔*/  
grid-column-gap0px;
/*置行与行的间隔*/    
grid-row-gap0px;
}

/*实现核心 grid-area属性指定项目放在哪一个区域*/
/*grid-area: <row-start> / <column-start> / <row-end> / <column-end>*/
/*row-start行开始的位置 column-start列开始的位置*/
/*row-end行结束的位置 column-end列结束的位置*/
.div1 { grid-area1 / 1 / 4 / 5; }
.div2 { grid-area1 / 5 / 2 / 25; }
.div3 { grid-area2 / 5 / 4 / 15; }
.div4 { grid-area2 / 15 / 3 / 25; }
.div5 { grid-area3 / 15 / 4 / 25; }

Ops!这不就是瞌睡遇上枕头,冥冥中就感觉他一定能解决我的需求,想要为我所用,于是乎简单去了解下相关的文档,相关注释见上面代码

API设计

根据grid的属性,我们需要两个栅格布局组件,一个是Container去负责计算出children正确的grid-area,GridItem就根据生成的grid-area去渲染出我们节点,伪代码如下。

function () {
  return (
    <Container columns={DEFAULT_SYSTEM_COL}>
      { schema.map((node) => {
        const {id, col, row} = node.props;
        return (
          <GridItem
            key={id}
            col={col}
            row={row}
          >
              <Node />
          </GridItem>
        )
      })}
  )
}
</Container>

代码实现

interface IContainer {
  columnsnumber;
  gap?: number;
  rows?: number;
}


interface IGridItem {
  rownumber;
  colnumber;
  border?: boolean;
  // 这里的full需要解释下:由于我们的思路是自适应铺满,
  // 但是有的时候就是需要一个元素占一整行,但又不铺满的效果,
  // 此刻就需要用full去标记
  full?: boolean; 
  gridArea?: string;
}

const GridItemReact.FC<IGridItem> = props => {
  return (
    <div
      className={cns('brick-grid-system-item', { border: props.border })}
      style={{ gridArea: props.gridArea }}
    >
      {props.children}
    </div>
  );
};

const ContainerReact.FC<IContainer> & {
  GridItemtypeof GridItem;
}  = props => {
  const { columns, gap, rows, children } = props;

  const renderChildren = () => {
    // how to calc child's gridArea
  };

  const containerStyle = useMemo(() => {
    const style = {
      gridTemplateColumns`repeat(${columns}, 1fr)`,
      gap`${gap || 0}px`,
    };
    if (!isUndefined(rows)) {
      style['gridTemplateRows'] = `repeat(${rows}, 1fr)`;
    }
    return style;
  }, [columns, rows, gap]);

  return (
    <div className="brick-grid-system" style={containerStyle}>
      {renderChildren()}
    </div>
  );
};
export default Container;

其实上面代码都很简单,现在唯一要做的就是 how to calc child's gridArea,这步搞定咱们的栅格系统就差不多竣工了。

实现renderChildren

说实话,这里走了不少弯路,最后的效果都不如人意,尝试多次之后,想到了用二维矩阵的方式去对应我们的布局。

  • 根据grid的行列配置,生成对应的二维数组, 如24列 3行的grid就是[Array(24) * 3], 用0填充整个二维数组,初始效果如下

  • 把我们创建好的二维数组想象成画布,被children占位之后,填充对应的index,就会生成一个和效果图相对应的二维数组,是不是有那味了

所以我们现在需要做的就是动态遍历children,根据每个child的 col、row、full值去动态的计算二维数组中能放下child的位置,并填充我们的二维矩阵,这样就能计算出每个child所需要的gridArea属性了

  • renderChildren
export type Matrix = Array<Array<number>>;

function renderChildren() {
 // 这里我们没有规定matrix的高度,动态更新,节省内存
  const matrixMatrix = [];
   return React.Children.map(
      children,
      (child: React.ReactElement, index: number) => {
        const { col, row, full } = child.props;
        // 动态扩张高度 尽量大于需要的row
        addMatrixData(matrix, row, columns);
        // 计算出最第一个可填充的位置
        let points = findAvailable(matrix, row, col, columns, full);
        // 设置矩阵内容
        setMatrixData(matrix, points, index + 1, full, columns);
        // 转换成gridArea
        const gridArea = getGridAreaFromPoints(points);
        // 返回children
        return React.cloneElement(child, { gridArea });
      },
    );
}
  • addMatrixData 為矩陣添加數據
function addMatrixData(
  arr: Matrix,
  rows: number,
  cols: number,
  value = 0,
) {
  for (let i = 0; i < rows; i++) {
    arr.push(Array.from({ length: cols }).fill(value) as Array<number>);
  }
}
  • findAvailable 找到矩陣中最符合的位置(核心)

思路:现在我们有了一个二维矩阵,里面的值要么是没有被填充的部分(值为0),要么是被填充的部分(值为节点index,或者任何真值,用index只是为了更形象地展示被填充的情况),还有节点的rows(行高)和cols(列高),是否占用整行。查找开始前,我们声明两个变量 accRows和accCols分别表示遍历过程中符合条件的连续区间大小,如果 accRows === rows && accCols === cols 条件满足,则表示找到了我们想要的位置,如一格3行4列的元素,如果在二维数组中找到一块3 * 4 的连续未被填充的区间,就是我们想要的位置。

function findAvailable(
  matrix: Matrix,
  rows: number,
  cols: number,
  columns: number,
  full: boolean = false
) {
  // 两层遍历,外面代表二维数组的行遍历,里面则是列遍历
  let accRows = 0,accCols = 0;
  // rowStart 行开始的位置,外层循环且accRows为0的时候会被赋值, 每次内循环如果accCols不等于cols则清零,rowStart会从下一行开始
  // colStart 列开始的位置 默认从0
  // colEnd 列结束的位置
  let rowStart,colStart = 0,colEnd;
  for (let r = 0; r < matrix.length; r++) {
    // accRows为0说明查找开始或上一轮查找失败 初始化rowStart为外循环的指针
    if (accRows === 0) {
      rowStart = r;
    }
    for (let c = colStart; c < columns; c++) {
      // 判断可用的列数 如果小于元素的cols则就是装不下,重置colStart 结束本次内循环
      const usableCols = columns - colStart;
      if ((cols > usableCols) || full && (usableCols < columns)) {
        colStart = 0;
        break;
      }
      // 如果accCols、accRows都为0 开始或上一轮查找失败 初始化colStart为当前内循环的指针
      if (accCols === 0 && accRows === 0) {
        colStart = c;
      }
      // 当前元素的值
      const val = matrix[r][c];
      if (!val) {
      // 未被占用的情况 accCols累加1
        accCols += 1;
        if (accCols === cols) {
        // 如果 accCols === cols则说明找到了一行连续区间,记录下colEnd
          colEnd = c;
          break;
        }
      } else {
      // 被占用的情况 重置accCols
        accCols = 0;
      }
    }
    // 内存循环结束,外层循环继续
    if (accCols === cols) {
    // 内循环满足条件后跳出来的情况
    // accRows累加1
      accRows += 1;
      if (accRows === rows) {
      // 满足条件的区间被找到,返回对应值,结束所有遍历
        return { rowStart, colStart, rowEnd: r, colEnd };
      }
      // 重置accCols
      accCols = 0;
    } else {
    // 未找到一行连续区间,重置rowStart和accRows
      rowStart = r;
      accRows = 0;
    }
  }
  return null;
}
  • setMatrixData 設置矩陣内容
export type Points = {
  rowStart: number;
  colStart: number;
  rowEnd: number;
  colEnd: number;
};

function setMatrixData(
  matrix: Matrix,
  points: Points,
  value = 1,
  full = false,
  columns = DEFAULT_SYSTEM_COL,
) {
  for (let r = points.rowStart; r <= points.rowEnd; r++) {
    for (let c = points.colStart; c <= (full ? columns : points.colEnd); c++) {
      matrix[r][c] = value;
    }
  }
}
  • getGridAreaFromPoints 生成gridArea
function getGridAreaFromPoints(points: Points) {
  return `${points.rowStart + 1} / ${points.colStart + 1} / ${points.rowEnd +
    2} / ${points.colEnd + 2}`;
}

这样通过renderChildren渲染出来的children就有了我们需要的gridArea样式,完结撒花,心情舒畅~