preface

Waterfall flow layout is a relatively popular layout, uneven multi-column structure, not only to save space, but also in the visual display of a well-proportioned and eclectic. This component was developed after finding several open source waterfall components in a business requirement that had some minor problems with their use.

Before you start, you may need to check IntersectionObserver. The core of this API is that it monitors whether a specified card is displayed in the visible area. When a monitored card appears in the visible area, it triggers a callback, performs the comparison logic between columns, and adds data to a column with a lower height.

The basic use

This component has been uploaded to NPM, interested partners can download and use it.

1, install,

npm i waterfall-vue2
Copy the code

2. Use mode

import { Waterfall } from "waterfall-vue2";
Vue.use(Waterfall);

<Waterfall
  :pageData="pageData"
  :columnCount="2"
  :colStyle="{display:'flex',flexDirection:'column',alignItems:'center'}"
  query-sign="#cardItem"
  @wfLoad="onLoad"
  @ObserveDataTotal="ObserveDataTotal"
>
  <template #default="{ item, columnIndex, index}">
    <! -- Slot content good-card: instance component -->
    <good-card :item="item" id="cardItem" />
  </template>
</Waterfall>
Copy the code

3. Basic parameters and events

API

parameter instructions type The default value version
columnCount The number of columns Number 2
pageData Data for the current pageIndex request (non-multi-page cumulative data) Array []
resetSign Reset data (clear each column of data) Boolean false
immediateCheck Immediately check Boolean true
offset The distance threshold for triggering loading, in unit of px String | Number 300
colStyle The style of each column Object {}
querySign Content identifier (querySelectorAll selector) String Item must be

Event

The event name instructions parameter
wfLoad The scrollbar is less than the bottomoffsetTriggered when
ObserveDataTotal Total amount of unrendered data length

Slot

The name of the instructions
default Content of the slot
columnIndex The column where the current content is
item A single data
index The index of the column in which the current data resides

The technical implementation

The difficulties in

Most of the pictures will use lazy loading to achieve, in general, the node content has been loaded, the picture does not appear in the visual area is not loaded. In the case of uncertain image height, how to ensure that the drop of the column in the list is less than a card content height?

The principle of

IntersectionObserver is used to monitor fixed node information. Every time a monitoring node appears in the visible area, IntersectionObserver callback will be triggered, and data insertion will be performed in the callback to compare the height of each column. Fetch a datapool in the listener datapool and place it in the minimum column height datapool list. Each presentation of a data card triggers the loading of a new data card, which is the core idea of the “lazy loading” waterfall stream component.

As shown in the figure below, when card 7 is just displayed in the visible area, the callback of IntersectionObserver will be triggered, and then the insertion function will be executed in the callback logic. In the insertion function, the height of column B is found to be small, and then a data is extracted from the monitoring data pool and put into the data list of column B. Render card 8.

Design plan

1. General waterfall flow arrangement can be divided into two types: column layout and absolute positioning layout. Either way, the difficulty lies in solving the problem of dynamic height of pictures. This time, the column layout is adopted to reduce the calculation of large area card position due to image loading.

2. Create a listening pool to store all unrendered data. The second function is to reduce unnecessary operations after the data pool is fetched.

3. Parameter planning

/ / the number of columns
    columnCount: {
      type: Number.default: 2,},// Data per page
    pageData: {
      type: Array.default: () = >[],},/ / reset
    resetSign: {
      type: Boolean.default: false,},// Check immediately
    immediateCheck: {
      type: Boolean.default: true,},/ / migration
    offset: {
      type: [Number.String].default: 300,},/ / style
    colStyle: {
      type: Object.default: () = >({})},// Query the id
    querySign: {
      type: String.require: true,},Copy the code

3. Function planning

  • GetMinColSign returns the identity of the smallest column
  • CheckObserveDom Checks whether the CURRENT DOM is not monitored, and puts the nodes that are not monitored into the listening range
  • InsetData performs fetching and inserting into column data
  • GetScrollParentNode gets the ancestor scroll element and binds the scroll event
  • Check Rolls to check whether the loading threshold is triggered

4. Flow chart design

practice

PageData implementation

  1. Incoming data into the monitor datapool
  2. If reset to true, clear monitoring data, column data,
  3. Data inserts are triggered each time new data is created
  4. If IntersectionObserver is not compatible, all current data will be equally divided in each column
   pageData(value = []) {
      if(! value.length)return
      if (IntersectionObserver) {
        // Determine whether a reset is required
        if (this.resetSign) {
          // Reset disconnect all current monitoring data
          this.intersectionObserve.disconnect()
          Object.keys(this.colListData).forEach((key) = > {
            this.colListData[key] = []
          })
          this.observeData = [...value]
          this.$nextTick(() = > {
            this.insetData()
          })
        } else {
          this.observeData = [...this.observeData, ...value]
          // Insert data
          this.insetData()
        }
      } else {
        // If IntersectionObserver is not supported, data from each column will be evenly distributed
        const val = (this.observeData = value)
        while (Array.isArray(val) && val.length) {
          let keys = null
          // Minimize uneven data distribution
          if (this.averageSign) {
            keys = Object.keys(this.colListData)
          } else {
            keys = Object.keys(this.colListData).reverse()
          }
          keys.forEach((key) = > {
            const item = val.shift()
            item && this.colListData[key].push(item)
          })
          this.averageSign = !this.averageSign
        }
      }
    }
Copy the code

InsetData implements data insertion functions to ensure that there is only one entry to control data, avoiding multiple executions within the same batch processing cycle.

// Insert data
    insetData() {
      const sign = this.getMinColSign()
      const divData = this.observeData && this.observeData.shift()
      if(! divData || ! sign) {return null
      }
      this.colListData[sign].push(divData)
      this.checkObserveDom()
    },
Copy the code

The getMinColSign implementation gets the least tall column of all the current columns and returns its identity

// Get the minimum height minimum identifier
    getMinColSign() {
      let minHeight = -1
      let sign = null
      Object.keys(this.colListData).forEach((key) = > {
        const div = this.$refs[key][0]
        if (div) {
          const height = div.offsetHeight
          if (minHeight === -1 || minHeight > height) {
            minHeight = height
            sign = key
          }
        }
      })
      return sign
    },
Copy the code

The checkObserveDom implementation adds unmonitored nodes to the monitor

// Check whether the DOM is fully monitored
    checkObserveDom() {
      const divs = document.querySelectorAll(this.querySign)
      if(! divs || divs.length ===0) {
        // Prevent data from being inserted into the DOM without rendering, and the listener function has no data
        setTimeout(() = > {
          // The first data of each new data cannot be monitored and needs to be delayed
          this.insetData()
        }, 100)
      }
      divs.forEach((div) = > {
        if(! div.getAttribute('data-intersectionobserve')) {
          // Avoid repeated listening
          this.intersectionObserve.observe(div)
          div.setAttribute('data-intersectionobserve'.true)}}}Copy the code

ObserveData implementation

  1. Every time the datapool data is empty, change the bottom marker, as long as it prevents scrolling from continuing to bottom, the current data is not finished rendering
  2. The first time the data is null looks for an ancestor scroll element
  3. Each time data changes, events are released to inform the remaining data in the current data pool
  observeData(val) {
      if(! val)return
      if (val.length === 0) {
        if (this.onceSign) {
          // Monitor array data distribution finished, the first ancestor scroll element lookup
          this.onceSign = false
          this.scrollTarget = this.getScrollParentNode(this.$el)
          this.scrollTarget.addEventListener('scroll'.this.check)
        }
        // Data update, modify trigger bottom indicator
        this.emitSign = true
      }
      this.$emit('ObserveDataTotal', val.length)
    }
Copy the code

GetScrollParentNode implements that when the content is not loaded, it cannot accurately find the scrolling ancestor element through overflow attribute. In order to obtain the scrolling ancestor element more accurately, it does not search for the scrolling ancestor element until the content is loaded for the first time

// Get the parent element of the scroll
    getScrollParentNode(el) {
      let node = el
      while(node.nodeName ! = ='HTML'&& node.nodeName ! = ='BODY' && node.nodeType === 1) {
        const parentNode = node.parentNode
        const { overflowY } = window.getComputedStyle(parentNode)
        if (
          (overflowY === 'scroll' || overflowY === 'auto') && parentNode.clientHeight ! = parentNode.scrollHeight ) {return parentNode
        }
        node = parentNode
      }
      return window
    },
Copy the code

The check implementation checks whether load is triggered

// Check () {this.intersectionObserve && this.checkobservedom () // Skip if (! this.emitSign) { return } const { scrollTarget } = this let bounding = { top: 0, bottom: scrollTarget.innerHeight || 0, } the if (this. $refs. Bottom. GetBoundingClientRect) {bounding = this. $refs. Bottom. GetBoundingClientRect ()} / / element in the height of the viewport container let height = bounding.bottom - bounding.top if (! height) { return } const container = scrollTarget.innerHeight || scrollTarget.clientHeight const distance = Bounding.bottom-container-this. _offset if (distance < 0) {// Publish event this.$emit('wfLoad') // publish event trigger modify bottom tag this.emitSign = false } },Copy the code

The last

Above is the whole “lazy loading” waterfall flow component generation process, interested partners can download and use, experience. Or you have a better idea, discuss with each other, common progress.

The source address

Github:github.com/zengxiangfu…

reference

IntersectionObserver:developer.mozilla.org/zh-CN/docs/…