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.
- How does a form component share with an Anchor component
sections
The data? - How does a form component scroll to inform an Anchor?
- 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
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?
- Actively click on an anchor point that is highlighted
- 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!