Zero, introduce

This article mainly introduces the implementation of waterfall flow layout of website page, mainly including:

  1. What is a waterfall stream
  2. Implementation principle of waterfall flow
  3. Waterfall flow usage scenarios
  4. What are the problems with the implementation & how can they be solved
  5. Extensible usage scenarios

What is waterfall flow

Waterfall flow, also known as waterfall flow layout, is a popular web page layout. The visual representation is a jagged multi-column layout of elements of equal width and varying height, which is continuously loaded down as the page scrolls, with new elements attached to the shortest column.

The realization principle of waterfall flow

Waterfall flow is essentially looking for the lowest height column in each column and adding new elements after that column. As long as there are new elements that need to be arranged, it continues looking for the lowest height column in all the columns and adding subsequent elements to the lowest height column.

Diagram of base waterfall flow

So why do we always look for the smallest column?

First look at the order in Figure 1. The top of the first row of elements will be the same height, arranged in order at the top. After the first row is full, the second row will be arranged from left to right. However, this arrangement can easily lead to one column being too long or one column being too short.

To solve the problem that the columns in Figure 1 May be too long or too short, we arrange the elements in the shortest column as shown in Figure 2.

3. The use scenario of waterfall flow

Waterfalls flow slide will not stop the emergence of new things, attracted you to constantly explore down, the clever use of visual hierarchy, the line of sight of arbitrary flows to alleviate visual fatigue, using this scheme can extend the user vision, improve the user viscosity, suitable for those who browse through, with no purpose usage scenarios, like shopping, walk while see, Therefore, it is more suitable for the scene of pictures, goods and information. Many e-commerce related websites use waterfall stream to carry.

The mogujie PC Waterfall effect above is an extension of the basic waterfall stream, with other non-waterfall content inserted into one or more of the top columns of the waterfall stream.

This article introduces the four-column implementation scenario for this extended waterfall flow, which applies to the following basic scenarios:

What are the problems in the implementation of waterfall flow & how to solve them

  1. How to insert non-waterfall stream content?
  2. How to find the smallest height of all columns?
  3. How to render waterfall flow?

Vue implements waterfall flow

We use the Vue framework to implement waterfall flow, and some of its built-in properties make our waterfall flow implementation much easier.

  • The height of each column can be easily obtained by ref, and the minimum height column can be calculated by comparison algorithm.
  • After the minimum height column is obtained, the next element to be inserted is placed in the columnList of the minimum height column, and the element is rendered by manipulating the data.
  • Use Vue’s named slot to insert other non-waterfall content at the top of the waterfall stream.
  • Watch monitors element rendering to determine whether to continue rendering and request more element data.

How to insert non-waterfall stream content

Non-waterfall flow elements are passed as the content of the parent component to the waterfall flow child component through the named slot of the Vue.

  • The parent component is associated with the named slot via slot attributes on the HTML template, and the non-waterfall stream content is provided to the child component as the content of the named slot.
  • The named slots are first-col, second-col, third-col, last-col, and merge-slot, which represent the first, second, third, fourth, and merged columns respectively.
  • The child component uses the slot name to determine which column to place non-waterfall stream content in. If a slot exists, the contents it carries are inserted into the top position.
  • Because of the particularity of the merged column, if it is included, the merged column will be absolutely positioned at the top, and the corresponding column of the waterfall flow will be moved down. The parent component passes merged column parameters to the children: merge(to determine whether the merged columns are included), mergeHeight(the height of the merged columns), mergeColunms(which 2 columns are merged).

Code implementation

<! -- Parent component -->
<div class="parent">
    <Waterfall :merge=true :mergeHeight=800 mergeColumns=[2, 3]>
        <template slot="first-col">
            <! The first column... -->
        </template>
        <template slot="second-col">
            <! The second column... -->
        </template>
        <template slot="third-col">
            <! Column 3... -->
        </template>
        <template slot="last-col">
            <! Column 4... -->
        </template>
        <template slot="merge-col">
            <! Merging content... -->
        </template>
    </Waterfall>
</div>
Copy the code
<! -- Subcomponent (waterfall) --> Waterfall
<div class="child">
    <! -- column 1 -->
    <div ref="column1" :style="{marginTop: merge && mergeColumns.indexOf(1) > -1 ? mergeHeight + 'px':''}">
        <template v-if="$slots['first-col']">
            <slot name="first-col"></slot>
        </template>
        <template v-for="(item, index) in columnList1">
            <! The first column of waterfall flow content... -->
        </template>
    </div>
    <! -- column 2 -->
    <div ref="column2" :style="{marginTop: merge && mergeColumns.indexOf(2) > -1 ? mergeHeight + 'px':''}">
        <template v-if="$slots['second-col']">
            <slot name="second-col"></slot>
        </template>
        <template v-for="(item, index) in columnList2">
            <! The second column of waterfall flow content... -->
        </template>
    </div>
    <! -- column 3 -->
    <div ref="column3" :style="{marginTop: merge && mergeColumns.indexOf(3) > -1 ? mergeHeight + 'px':''}">
        <template v-if="$slots['third-col']">
            <slot name="third-col"></slot>
        </template>
        <template v-for="(item, index) in columnList3">
            <! Column 3 waterfall flow content... -->
        </template>
    </div>
    <! -- column 4 -->
    <div ref="column4" v-if="is4Columns">
        <template v-if="$slots['last-col']">
            <slot name="last-col"></slot>
        </template>
        <template v-for="(item, index) in columnList4">
            <! Column 4 waterfall flow content... -->
        </template>
    </div>
    <! Merge block non-waterfall stream content -->
    <div class="column-merge" v-if="merge" :style="{left: (mergeColumns[0] - 1)*330 + 'px'}">
        <slot name="merge-col"></slot>
    </div>
</div>
Copy the code

How to find the smallest height of all columns

A ref is defined for each column, and the height of the current column is obtained by ref. If there is a merged block above the column, the height should be added to the height of the merged block. Then, the height of the four columns is compared to the minimum height, and the corresponding column number is calculated by the minimum height.

Code implementation

Column1, column2, column3, column4 represent the first, second, third, and fourth columns, respectively
let columsHeight = [this.$refs.column1.offsetHeight, this.$refs.column2.offsetHeight, this.$refs.column3.offsetHeight, this.$refs.column4.offsetHeight]

// If the merged block is included, the height is updated. The column height under the merged block increases the height of the merged block
if(this.merge){
    // If there are merged columns, the height of the column under the merged column is added to the height of the merged content.
    columsHeight[0] = this.mergeColumns.indexOf(1) > - 1 ? columsHeight[0] + this.mergeHeight : columsHeight[0];
    columsHeight[1] = this.mergeColumns.indexOf(2) > - 1 ? columsHeight[1] + this.mergeHeight : columsHeight[1];
    columsHeight[2] = this.mergeColumns.indexOf(3) > - 1 ? columsHeight[2] + this.mergeHeight : columsHeight[2];
		columsHeight[3] = this.mergeColumns.indexOf(4) > - 1 ? columsHeight[3] + this.mergeHeight : columsHeight[3];
}

// Get the minimum height of each column
let minHeight = Math.min.apply(null, columsHeight);

// The height of the column is the smallest
this.getMinhIndex(columsHeight, minHeight).then(minIndex= > {
	 // Render load logic
});

// Get the height minimum index function
getMinhIndex(arr, value){
    return new Promise((reslove) = > {
        let minIndex = 0;
        for(let i in arr){
            if(arr[i] == value){ minIndex = i; reslove(minIndex); }}}); }Copy the code

How to render waterfall flow

Waterfall flows are commonly used in infinite drop-down loading or loading data volume is very big, and contains many image element, so usually don’t one-time get all the data, also won’t one-time will get data rendered to the page, otherwise easy to cause the page caton affect user experience, so when rendering is done, when to request data is the key.

When rendering

Select render area as scrolling height + 1.5 times of visual area height, which can not only prevent the user from scrolling to the bottom of the white screen, but also prevent rendering too much affect the user experience. Continue rendering elements if the height of the minimum column – scroll height < viewable area height * 1.5, otherwise not continue rendering.

When to request data

Continue to request more data when the number of rendered elements + the estimated number of elements that can be displayed in the viewable area > the number of requested elements to prevent wasted requests. If the number of loaded elements + the estimated number of elements that can be displayed on a screen > the number of elements received on all requests, the next request is triggered to fetch more data.

Waterfall flow rendering core idea

  • Monitor the roll to determine if the rendering conditions are met, and if so, start rendering.
  • Define a rendering index named renderIndex, and renderIndex + 1 after each element is rendered. Monitor the changes of renderIndex in real time and judge whether it meets rendering and data request conditions.
  • After getting the minimum height column index, insert the next element into that column and trigger renderIndex + 1 for the next render judgment.

Code implementation

data() {
    return {
        columnList1: [].// List of elements in the first column
        columnList2: [],
        columnList3: [].columnList4: [].renderIndex: - 1.// Render the item number
        isRendering: false.// Whether rendering is in progress
        itemList: [], // List of all elements
        isEnd: false
    };
}

watch: {
    renderIndex(value) {

        // The current scrollbar height
        const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;

        // Minimum column height - roll height < 1.5 times the viewable area height
        if (renderMinTop - scrollTop < winHeight * 1.5) {
            this.renderWaterfall();
        }

        // The number of loaded elements + one screen can display the estimated number of elements > the number of elements obtained by all requests
        if (loadedItemNum + canShowItemNum > this.itemList.length && !this._requesting && !this.isEnd) {
            // Request waterfall stream data
            this.getData();
        }
    }
}


scroll() {

    // The current scrollbar height
    const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;

    // Check the height of the bottom
    const bottomDetectionTop = this.$refs.bottomDetection.offsetTop;

    const tempLastScrollTop = lastScrollTop; // lastScrollTop: height of last scroll
    lastScrollTop = scrollTop;

    if (tempLastScrollTop === - 1) {
        this.renderWaterfall();
    }

    // If you scroll down, see if you need to continue rendering
    if (scrollTop > tempLastScrollTop) {
        if (bottomDetectionTop - tempLastScrollTop < winHeight * 1.5&&!this.isRendering) {
            this.renderWaterfall();
        }
    }

}

renderWaterfall() {

    // If there is no data yet, all data has been rendered, and rendering is in progress, no render calculation operation will be performed
    if (this.itemList.length === 0 || this.renderIndex >= this.itemList.length - 1 || this.isRendering) {
        if (this.renderIndex === this.feedList.length - 1&&!this._requesting && !this.isEnd) {
            this.getData();
        }
        return;
    }
    this.isRendering = true;

    /*** *** get the minimum height code ***/

    this.getMinhIndex(columnsHeight, minHeight).then(minIndex= > {

        const key = `columnList${minIndex + 1}`;
        let itemData = this.itemList[this.renderIndex + 1];
        this[key] = this[key].concat(itemData);
        this.$nextTick((a)= > {
            this.renderIndex = this.renderIndex + 1;
            this.isRendering = false;
        });
    });
}
Copy the code

Five, extensible use scenarios

In order to use waterfall streams flexibly, it was designed with extensions in mind. As you can see from the HTML template code, the contents of named slots can be placed in any column without limitation, so it can be extended to the following scenarios.