Code address: gitee.com/wyh-19/supe…

Series of articles:

  • Vue + Element Large Forms solution (1)– Overview
  • Vue + Element Large Forms solution (2)– Form splitting
  • Vue + Element Large Forms Solution (3)– Anchor Components (part 1)
  • Vue + Element Large Form Solution (4)– Anchor Component (2)
  • Vue + Element Large Forms solution (5)– Validation identifier
  • Vue + Element Large Forms solution (6)– Automatic identification
  • Vue + Element Large Forms solution (7)– Form form
  • Vue + Element Large Form Solution (8)– Data comparison (1)
  • Vue + Element Large Form Solution (9)– Data comparison (2)
  • Vue + Element Large Forms solution (10)– Form communication and dynamic forms

preface

In the last article on splitting forms, splitting forms is not only a technical requirement, but also a business requirement. In business, the form is divided into multiple sections, and each section has its own title. In this case, an anchor is needed to quickly locate to this section. The anchor component has nothing to do with the form itself, but is an auxiliary tool in this solution, so I design for the independence and versatility of the anchor component.

Consult and formulate requirements

Looking for reference

Element-ui does not have anchor components; Antd is available, but it looks weak and the style is not very good, so we can only make it by ourselves. We need to find a finished product for reference. Baidu Baike’s anchor component looks good and the interaction is reasonable, so I chose it as a reference. As shown below:

Determine the requirements

Anchor points are divided into master node and child node, and there is no recursive implementation of multi-level node (multi-level node has little practical significance, and deep node indentation and style are problems). When the page scrolls, the anchor point component will automatically locate to the corresponding node, when the node is at the edge of the anchor point panel, the node will automatically move to the visual range; Click the node, the page automatically scrolls to the corresponding chapter.

The specific implementation

Draw the UI

Create an Anchor component and introduce it on the page. Since I decided to implement only two tiers of nodes from the start, the data structure does not use children recursion. Template of Anchor is as follows:

<div class="anchor">
    <div v-for="node in sections" :key="node.label" :label="node.index"
         :class="[node.ismain?'anchor-main-node':'anchor-sub-node']">
      {{ node.label }}
    </div>
</div>

Copy the code

The data part is as follows:

sections: [
    { label: 'Basic information'.ismain: true.index: '1' },
    { label: 'Personal Information'.index: '1.1' },
    { label: 'Other Information'.index: '1.2' },
    { label: 'Advanced information'.ismain: true.index: '2' },
    { label: Information 'XXX'.index: '2.1'}]Copy the code

SCSS is as follows:

.anchor-main-node {
  position: relative;
  margin: 8px 0;
  font-size: 14px;
  font-weight: bold;
  color: # 555;
  cursor: pointer;
  &::before {
    content: attr(label);
    margin-left: 6px;
    margin-right: 6px; }}.anchor-sub-node {
  position: relative;
  margin: 8px 0;
  padding-left: 22px;
  font-size: 14px;
  color: # 666;
  cursor: pointer;
  &::before {
    content: attr(label);
    margin-right: 4px; }}Copy the code

The effect is as follows:

Basically satisfied, although there is still a lot of gap from the final effect, don’t worry, now to solve the more important problem.

  1. How does a form component share with an Anchor componentsectionsThe data?
  2. How does a form component scroll to inform an Anchor?
  3. How to notify the form after anchor clicks?

Determine the reference

To solve the above problems, according to the conventional component communication thinking, the form component defines sections data for its own rendering and passes it to the Anchor component. The form component binds the rolling event. During the rolling, the anchor node that should be active is calculated and the activeNode is passed to the Anchor component as props for rendering the active state. The Anchor component binds the click event to notify the form component via $emit to scroll to the corresponding section of the click node.

The biggest problem with this design is that the code logic is scattered, and the form components have to bind to scroll events and respond to anchor click events, which are not relevant to the form’s business. Can all this work be done within the anchor component?

My solution is to pass the DOM of the form to the Anchor component as props. All the work is completed within the Anchor component, and the form component is only responsible for introducing the Anchor component and passing the DOM structure. Since the SECTIONS data is not easily rendered in the form component (it is not possible to iterate and insert subform components in the right place), I abandoned the use of the SECTIONS data and instead defined a set of rules that add specific data-attribute attributes to divs, Data-section means that it is a section, and accordingly an Anchor node is rendered within the anchor point. Data-ismain indicates that it is the primary node, and if it does not have this attribute, it is a child node. In variable naming, section represents chapter, anchor represents anchor point, and the two are one-to-one corresponding and completely consistent in data.

The implementation code in the form component is as follows:

<div ref="pageBlock" class="form-wrapper">
    <el-button type="primary" @click="handleSave">save</el-button>
    <div data-section="Basic Information" data-ismain></div>
    <div data-section="Personal Information"></div>
    <form1 ref="form1" :data="formDataMap.form1" />
    <div data-section="Other Information"></div>
    <div style="width:300px; height:100px; backgoround:#ccc;">A placeholder</div>
    <div data-section="Advanced information" data-ismain></div>
    <div data-section="Company Information"></div>
    <form2 ref="form2" :data="formDataMap.form2" />
    <div class="anchor-wrapper">
        <anchor :page-block="pageBlock" />
    </div>
</div>
Copy the code

SCSS is as follows:

.form-wrapper {
  position: relative;
  width: 100%; // Set the height to small for easier scrollingheight: 280px; 
  padding: 16px;
  overflow-y: auto;
  ::v-deep input {
    width: 280px; }}.anchor-wrapper {
  position: fixed;
  right: 0px;
  width: 220px;
  height: 300px;
  top: 30%;
  transform: translate(0, -50%);
}
div[data-section] {
  position: relative;
  font-size: 14px;
  font-weight: bold;
  color: #5c658d;
  padding: 14px 0;
  margin-left: 34px;
  &::before {
    content: attr(data-section); }}div[data-ismain] {
  font-size: 16px;
  font-weight: bold;
  margin-left: 28px;
  &::after {
    content: ' ';
    position: absolute;
    left: -16px;
    top: 14px;
    width: 4px;
    height: 16px;
    background: #5c658d;
    border-radius: 2px;
}
Copy the code

Js part of the code is as follows:

// Add pageBlock to data :null
mounted() {
    this.pageBlock = this.$refs['pageBlock']}Copy the code

Data parsing

The Anchor component receives pageBlockprops, resolves the data-section element in pageBlock when mounted, and binds the scroll event to the page. However, due to the sequence of the life cycle of the parent and child components, when the Anchor component mounts, the main form is not mounted, and the pageBlock passed in this case is null, so data and binding events cannot be resolved from it. The main form
is changed to
, Make sure pageBlock references the form DOM structure before rendering the anchor component. Add the mounted hook function to the pageBlock property. The code is as follows:

props: {
    pageBlock: HTMLElement
},
data() {
    return {
      sections: []}},mounted() {
    this.sections = this.getSectionsData(this.pageBlock)
    this.pageBlock.addEventListener('scroll'.this.handlePageScroll)
},
beforeDestroy() {
    this.pageBlock.removeEventListener('scroll'.this.handlePageScroll)
},
methods: {
    // Get section information from pageBlock
    getSectionsData(pageBlock){},// The scroll event handler for the page
    handlePageScroll(e) {
      e.stopPropagation()
      this.currentSection = this.getCurrentSection()
    },
    // Calculate the current scroll to the section
    getCurrentSection(){}}Copy the code

The next step is to implement the getSectionsData function, which fetches the data-section element from the dom structure of the form and extracts the information needed to render the anchor point as follows:

getSectionsData(pageBlock) {
  let mainIndex = 0 // The number of the primary node
  let subIndex = 0 // The numeric number of the child node
  // Query the data-section node and convert it to an array
  const sections = Array.from(pageBlock.querySelectorAll('[data-section]'))
  // Map transforms the node array into the final data
  return sections.map((item, index) = > {
    let ismain = false
    if ('ismain' in item.dataset) {
      ismain = true
      mainIndex++
      // Resets subIndex when a new primary node is encountered
      subIndex = 0
    } else {
      subIndex++
    }
    return {
      ismain,
      index: ismain ? mainIndex : `${mainIndex}.${subIndex}`.label: item.dataset.section
    }
  })
}
Copy the code

Test the effect as shown below:

No problem. There is no implementation of the getCurrentSection function, which is the currently highlighted anchor node. When are anchor nodes highlighted?

  1. Actively click on an anchor point that is highlighted
  2. The page scroll is in a section, and the corresponding anchor point of the section is highlighted

The event processing

Now add currentSection: “responsive data to data and modify the template code to add highlighted styles and bind anchor point events as follows:

<div class="anchor">
    <div v-for="node in sections" :key="node.label" :label="node.index"
         :class="[node.ismain?'anchor-main-node':'anchor-sub-node',{'anchor-node-active':currentSection===node.label}]"
         @click="handleClick(node.label)">
      {{ node.label }}
    </div>
</div>

.anchor-node-active {
    color: #38f;
}
Copy the code

The corresponding click event handler is as follows:

handleClick(label) {
  // Set the section corresponding to the current anchor point
  this.currentSection = label
  // Find the DOM of the section
  const section = this.pageBlock.querySelector(`[data-section=${label}] `)
  // Scroll smoothly to the section
  section.scrollIntoView({
      behavior: 'smooth'.block: 'start'})}Copy the code

The test results are normal, as shown below:

Let’s implement the getCurrentSection function, which is the event handler that scrolls the left form. First of all, how do YOU make sure the current chapter is at the top of the window? We can get the scrollTop of the current page, which is how far the page is rolled up, and then compare it to the offsetTop of each chapter to determine who is currently at the top. So add the top attribute to the return value of getSectionsData as follows:

return {
      ismain,
      index: ismain ? mainIndex : `${mainIndex}.${subIndex}`.label: item.dataset.section,
      // Add the top attribute
      top: item.offsetTop
}
Copy the code

The specific judgment code is as follows, and the note is a logical explanation:

getCurrentSection() {
  // scrollTop for the current form
  const currentScrollTop = this.pageBlock.scrollTop
  const sections = this.sections
  const length = sections.length
  let currentSection
  // Compare with the original offsetTop of each node
  for (let i = 0; i < length; i++) {
    // If scrollTop is exactly equal to offsetTop of a node
    // Or scrollTop is between the current judged node and the next node
    // The current node cannot be the last node because the next node is needed
    if (currentScrollTop === sections[i].top ||
      (i < length - 1 &&
        currentScrollTop > sections[i].top &&
        currentScrollTop < sections[i + 1].top)) {
      currentSection = sections[i].label
      break
    } else if (i === length - 1) {
      // If a node is identified, scrollTop is greater than offsetTop of the node
      if (currentScrollTop > sections[i].top) {
        currentSection = sections[i].label
        break}}}return currentSection
}
Copy the code

Performance optimization

Because the scrolling event is triggered too often and the scrollIntoView also generates an event when an anchor point is clicked, the event handler needs to be buffeted, using lodash.debounce. Change to the following code:

mounted() {
    this.sections = this.getSectionsData(this.pageBlock)
    // Try to get the current chapter at initialization
    this.currentSection = this.getCurrentSection()
    this.debouncedPageScrollHandler = debounce(this.handlePageScroll, 100)
    this.pageBlock.addEventListener('scroll'.this.debouncedPageScrollHandler)
},
beforeDestroy() {
    this.pageBlock.removeEventListener('scroll'.this.debouncedPageScrollHandler)
},
Copy the code

Today, I will stop here, and there are some optimizations and upgrades to be completed in the next article. Thank you for reading, and your comments are welcome!