Borrowed from two time-tested wheels: fastClick and Better-Scroll, mind can stop there. This article is absolutely original, hand, clear thinking, knowledge is not difficult, not suitable for big guy to watch, thank you.

First of all, I do not work for Ali, nor have I been to Ali for an interview. This was given to me by a friend in a wechat group. My current ability cannot meet Ali’s requirements. But people have no dream is better than salt fish, if you have the ability or want to try. If there are any shortcomings in this article, please do not mock, just point out the shortcomings, thank you. Code word is not easy, and see and cherish, reproduced please indicate the source. Original blog, if violate the interests of your company, please private letter I delete. If you like it, ask for a thumbs up and a Star on Github.

The title is as follows:

And that’s about it, the analysis is to make a city selection component, The function or requirement is to locate the current city, use localstorage to store the last location of the city and the recently selected city, according to the input of the letter or text screen to find the city, the data to the page is also a father and son transfer reference problem, the page using Flex layout.

I do it in my spare time, and the success is as follows:

  

I only made this component, the function of passing parameters to the page has not been done, can be done with the parent component.

Knowledge points:

A brief description of my city selection components and the following knowledge points:

1. The background

I use Node.js up a background service, the use of express framework, complete to meet my needs. My data source is the city address of a website (please contact me to delete it if it infringes), and the data is as follows:

    {
      "id": 151,
      "name": "Anshan"."pinyin": "anshan"."acronym": "as"."rank": "C"."firstChar": "A"
    }Copy the code

I called a location interface of a wave on the Node end as my location service, and returned the data. When there is a problem or the interface is not obtained, the location will be returned to Beijing. The specific code is:

// Get the city data, city for me to crawl the information app.get('/'.function(req, res) { res.send(city); res.end() }); // Call sina's interface to return location app.get('/nowcity'.function (req, res) {
    let getIpInfo = function (cb) {
        var url = 'http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json';
        http.get(url, function (res) {
            var code = res.statusCode;
            if (code == 200) {
                res.on('data'.function (data) {
                    try {
                        cb(JSON.parse(data));
                    } catch (err) {
                        console.log(err)
                    }
                });
            }
        }).on('error'.function(e){
            cb({
                city: "Beijing",
                country: "China",
                province: "Beijing"}})}); getIpInfo(function (msg) {
        let nowcity = msg
        res.send(nowcity)
        res.end()
    })
});Copy the code

2. Vue scaffold

This component is based on vUE framework, I use VUE-CLI scaffolding to build, this piece of knowledge is not much to describe, refer to my blog “VUE environment build and create the first Vuejs file”.

3.stylus

This time I used stylus, a CSS preprocessor.

NPM install stylus –save-dev, NPM install stylus-loader –save-dev, Then use

To introduce a separate stylus file use @import ‘~common/stylus/css.styl’.

4. Dependence of this project

In this project, in addition to installing the dependency on Stylus, I also introduced three dependencies of Better Scroll, Fastclick and AXIOS.

Better Scroll is the best mobile scroll library I have ever seen, with clear documentation and clear thinking. Fastclick is used to handle mobile click events with a 300 ms delay. As for Axios, I think you all know that axios is a Promise-based HTTP library that can be used in browsers and Node.js.

5. Use of VUE components

In this project, I built six functional components, which are search box component, search page component, location component, sidebar component, popup component and city display component. There are also two basic components, the scroll component and the city component.

The urban component is introduced by:

// Import the file import Search from'components/Search'
import Scroll from 'base/Scroll.vue'
import PositionBox from 'components/PositionBox'
import CityList from 'components/CityList'
import NavList from 'components/NavList'
import MaskBox from 'components/MaskBox'
import SearchList from 'components/SearchList'// Then register components in the parent component: {'search': Search,
    'scroll': Scroll,
    'position-box': PositionBox,
    'nav-list': NavList,
    'city-list': CityList,
    'mask-box': MaskBox,
    'search-list': SearchList} // use < search@txtdata ="searchText" :clearText="clearSearch"></search>Copy the code

6. Send parameters to parent and child components

It is very simple for a parent component to pass arguments to a child component, as in the case of a search box component:

    <search @txtdata="searchText" :clearText="clearSearch"></search>Copy the code

The parent component simply needs to :clearText=”clearSearch”, where clearSearch is the information to be passed in and clearText is the name to be received by the child component.

In the child component, use the props property to pass the parameter:

// Props: {clearText: Boolean} // Props: {clearText: {type: Boolean,
      default:false}}Copy the code

$emit to send a child to its parent using this.$emit:

// Clicking the list triggers an event that changes the locationCopy the code
this.$emit(‘txtdata’, this.searchText)
Copy the code

In the above code txtData is the name of the content passed to the parent component and this.searchText is the parameter. @txtData =”searchText” :

// Search box content searchText (text) {// text is the parameter passed}Copy the code

7. Delay the operation

When dealing with Ajax on the front end, we generally want to reduce interaction to improve performance and efficiency. In the search box component, we use the associative search function, here I use the re to achieve. So when you’re typing, you want to interact while you’re typing (you don’t want the browser going through arrays or Ajax all the time). Here I use a timing function to complete the delay effect:

if(this.timer) {clearTimeout(this.timer) // Clear timer} this.timer =setTimeout(() => {
   this.$emit('txtdata', this.searchText)
}, 300)Copy the code

In this code, I bind the KeyUp event, which means that whenever a button is raised in 300 milliseconds, the event clears the previous timer, and a new timer is generated. If there is no input in 300 milliseconds, the timer fires, passing parameters to the parent component.

8. Regular

Regex used to be my biggest headache until I read a lot of documents and blogs patiently.

export function getSearchList (text, list) {
  letReg1 = /^\w+$/glet reg2 = new RegExp(`^${text}`, 'g') // Check template textlet reg3 = new RegExp('^[\\u4E00-\\u9FFF]{1,}$'.'g') // Check for Chinese charactersletResList = [] // When text is a letterif (text.match(reg1)) {
    for (let i = 0, len1 = list.length; i < len1; i++) {
      for (letj = 0, len2 = list[i][1].length; j < len2; J++) {// filters those that satisfy this reif (list[i][1][j].pinyin.match(reg2)) {
          resList.push(list[i][1][j])
        }
      }
    }
  } else{/ / same as aboveif (reg3.test(text)) {
      for (let i = 0, len1 = list.length; i < len1; i++) {
        for (let j = 0, len2 = list[i][1].length; j < len2; j++) {
          if (list[i][1][j].name.match(reg2)) {
            resList.push(list[i][1][j])
          }
        }
      }
    }
  }
  return resList
}Copy the code

Var reg=new RegExp(‘<%[^%>]+%>’,’g’); var reg=/<%[^%>]%>/g, Because I’m using the template statement this time, which is the constructor, and the last g stands for global.

    

Var reg1 = /[0123456789] var reg2 = /[0-9] var reg2 = /[0-9] Var reg3 = /[a-z]/ // Matches a character var reg3 = /[A-za-z0-9]/ / Matches a character. This character can be any Chinese character var reg4 = /[\\u4E00-\ u9FFF]/Copy the code

We can also introduce a beginning and end constraint:

^ Starting with XXX
$ Ends in XXX
\b Word boundaries
\B Non-word boundary

Quantifiers:

character meaning
? Zero or one occurrence (maximum one occurrence)
+ Appear once or more (at least once)
* Zero or more occurrences (any occurrence)
{n} A n time
{n,m} N to m occurrences
{n,} At least n times

9.this.$refs

In general, to get a DOM element, you need document.querySelector (“.input1”) to get the DOM node, and then get the value of input1. With ref binding, we don’t need to fetch the DOM node. Instead, we bind input1 to the input above and call it in $refs. Then call this.$refs.input1 in javascript to reduce the cost of retrieving dom nodes.

<div ref="wrapper" class="scroll"> </div>$refs('wrapper') represents the divCopy the code

10.slot

A slot is literally a “slot”, presumably a place to put components or DOM structures. The child component template must contain at least one

socket, otherwise the content of the parent component will be discarded. When the child component template only has a slot with no attributes, the entire content fragment passed in by the parent component is inserted into the DOM where the slot is, replacing the slot label itself. Anything originally in the

tag is considered alternate content. Alternate content compiles within the scope of the child component and is displayed only if the host element is empty and there is no content to insert.

Suppose the my-Component component has the following template:

<div> <h2> I am the subcomponent's title </h2> <slot> is only displayed if there is nothing to distribute. </slot> </div>Copy the code

Parent component template:

<div> <h1> I'm the title of the parent component </h1> <my-component> <p> Here's some initial content </p> <p> Here's more initial content </p> </my-component> </div>Copy the code

Render result:

< div > < h1 > my father is the title of the component < / h1 > < div > < h2 > I am a child component of title < / h2 > < p > this is some of the initial content < / p > < p > this is more of the initial content < / p > < / div > < / div >Copy the code

Slot of this project:

<! -- parent component --> <scroll :data="citylist" ref="suggest" :probeType="3" :listenScroll="true" @distance="distance" @scrollStore="scrollStore">
      <div>
        <position-box :chooseCity="chooseCity" :orientate="nowCity" :historyCityArr="historyCityArr" @changeCity="changeCity"></position-box>
        <city-list :citylist="citylist" :elementIndex="elementIndex" @positionCity="changeCity" @singleLetter="singleLetter"></city-list> </div> </scroll> <! <div ref="wrapper" class="scroll">
    <slot></slot>
</div>Copy the code

11. The use of better – scroll

this.scroll = new BScroll(this.$refs.wrapper, {
        probeType: this.probeType,
        scrollY: true// Click:true// Whether the browser issued click or BetterScroll issued click can be used to determine whether the event._constructed or bs issued clicktrue
        momentum: true// Whether to enable bounce when fast sliding:falseDeceleration: 0.001, // The greater the rolling momentum, the faster the animation will be. Deceleration is recommended. 300, momentumLimitDistance: 15, momentumLimitDistance: 15, momentumLimitDistance: 15, momentumLimitDistance: 15, momentumLimitDistance: 15, momentumLimitDistance: 15, momentumLimitDistance: 15, momentumLimitDistance: 15Copy the code

The better Scroll is used by building an Scroll object where a DOM node, this.$refs.wrapper, must be bound. Add some attributes to it for customization.

In this project, we used three methods of Bscroll:

refresh()

Parameters: no

Returned value: None

Function: Recalculate the better scroll, when the DOM structure changes must be called to ensure that the scrolling effect is normal.

scrollTo(x, y, time, easing)

Parameter: Returned value: none

{Number} x coordinates (unit: px)

{Number} y coordinates (unit: px)

{Number} time Duration of scrolling animation execution (ms)

{Object} Easing is not recommended. If you want to ease it, refer to ease.js in the source code

Action: Scroll to the specified position

scrollToElement(el, time, offsetX, offsetY, easing)

Parameter: Returned value: none

DOM | {String} el scroll to a target element, if it is a String, internal will try to call querySelector into a DOM object. (HERE I used this.$refs)

{Number} time Duration of scrolling animation execution (ms)

{Number | Boolean} offsetX transverse offset relative to the target element, if set to true, then rolled into the center of the target element

{Number | Boolean} offsetY longitudinal axis offset relative to the target element, if set to true, then rolled into the center of the target element

{Object} Easing is not recommended. If you want to ease it, refer to ease.js in the source code

Action: Scrolls to the specified target element.

12.localstorage

Localstorage is like ROM, while sessionStorage is like RAM.

In this project, localstorage is operated using setItem and getItem:

localStorage.setItem('historyCityArr', arr)
localStorage.getItem('historyCityArr')Copy the code

13. The transition of the transition

Similar to adding an animation effect during unit rendering and removal.

    <transition name="flag">
      <div class="nowFlag" v-if="flag">{{flagText}}</div>
    </transition>Copy the code

Flag-leave-active transition all 1s. Flag-leave-to opacity 0Copy the code

To a paragraph is interpreted as adding an exit (remove) transition that changes the opacity from 1 to 0 in one second.

14.stop&prevent

Calling event.preventDefault() or event.stopPropagation() in the event handler is a very common requirement. While this could easily be done in a method, it’s better if the method has pure data logic instead of dealing with DOM event details. To solve this problem, vue.js provides event modifiers for V-Ons. As mentioned earlier, modifiers are represented by an instruction suffix beginning with a dot.

. Stop Prevents events from bubbling

. Prevent Prevents default events

.capture blocks event capture

. Once Triggers only once

Business Part:

1. Search box component

The HTML code is as follows: the parent component passes information to the child component about whether to clear the content (for changing the search page after clicking the search page option), and the child component passes the content to the parent component when the keyUP event is triggered.

<! -- parent component --> <search @txtdata="searchText" :clearText="clearSearch"></search> <! Subcomponent --> <div class="search-box">
    <div class="ipt-box">
      <input type="text" class="ipt" placeholder="City Name/Pinyin" @keydown="entry()" v-model="searchText" />
      <div class="icon-box">
        <i class="iconfont icon-sousuo icon"></i>
      </div>
    </div>
</div>Copy the code

// subcomponent js methods: {// delay searchentry () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      this.timer = setTimeout(() => {
        this.$emit('txtdata', this.searchText)}, 300)}}, watch: {// clear search content clearText (val) {if (val) {
        this.searchText = ' '
        this.entry()
      }
    }
  }Copy the code

There is an effect of reducing interaction and computation when passing up, which is achieved using timers, as described above.

2. Locate components

<! -- Parent module --> <position-box :chooseCity="chooseCity" :orientate="nowCity" :historyCityArr="historyCityArr" @changeCity="changeCity"></position-box> <! Subcomponent module --> <div class="position-box">
    <div class="choose"> <span> You have selected: {{chooseCity}}</span> </div> <div class="hostory"> <p> Locate/recently accessed </p> <div class="citybox">
        <button @click="changeCity(orientate)">
          <i class="iconfont icon-dingwei icon"></i>{{orientate}}
        </button>
        <button @click="changeCity(item)" v-for="item in historyCityArr" :key="item">{{item}}</button>
      </div>
    </div>
    <div class="hot"> <p> Hot city </p> <div class="citybox">
        <button v-for="city in hotCitys" :key="city" @click="changeCity(city)">{{city}}</button>
      </div>
    </div>
  </div>Copy the code

In this section, the initial loading of the page triggers two events: locating and reading the history view stored in localStorage.

axios.get('http://localhost:1234/nowcity').then((res) => {
        this.nowCity = res.data.city
        if(! this.choiceCity && ! this.choiceCityName) { this.choiceCity = this.nowCity this.choiceCityName = this.nowCity } }, () => { this.nowCity ='Beijing'
        if(! this.choiceCity && ! this.choiceCityName) { this.choiceCity = this.nowCity this.choiceCityName = this.nowCity } })Copy the code

Part of the positioning logic is simple, nothing more than to obtain data, if not the default is Beijing.

Localstorage’s data processing is in this component:

    setHistory (arr) {
      localStorage.setItem('historyCityArr', arr)}, // fetch it locallygetHistory () {
      let history = localStorage.getItem('historyCityArr')
      if (!history) {
        this.historyCityArr = []
      } else {
        this.historyCityArr = history.split(', '}}, // save to local, view the citysetCity (name) {
      localStorage.setItem('seeCity', name)}, // Fetch from local, and view the citygetCity () {
      let name = localStorage.getItem('seeCity')
      if(! name) { this.choiceCity =' '
        this.choiceCityName = ' '
      } else {
        this.choiceCity = name
        this.choiceCityName = name
      }
    }Copy the code

When a city change is seen, two setItem events (either an array or a string) are fired so that getItem can retrieve data when this is turned on. When the page is first loaded, two GET events will be sent, and the data will be passed to the location module for rendering. The information that gets from get is a string, and when we get it we’re going to turn it into an array.

3. Page city component

<! -- Parent module --> <city-list: cityList ="citylist" :elementIndex="elementIndex" @positionCity="changeCity" @singleLetter="singleLetter"></city-list> <! Subcomponent module --> <div class="lists">
    <div v-for="citys in citylist" :key="citys[0]" :dataNum="citys[1].length">
      <p class="city-title" :ref="citys[0]">{{citys[0]}}</p>
      <p class="city-item" v-for="city in citys[1]" :key="city.id" @click="changeCity(city.name)">{{city.name}}</p>
    </div>
  </div>Copy the code

This component alone is a very simple one that only displays the render information and clicks on the city option to pass up the value of the city information. Add the function to pass the DOM node up after adding the nav in the right column:

SingleLetter (dom) {this.$refs.suggest.scrollToElement(dom, 200, false.false} // elementIndex (val) {if (val === 'top') {
     return false
   }
   this.$emit('singleLetter', this.$refs[val][0])
}Copy the code

After the parent component obtains the city DOM node information uploaded by the city component, it triggers the Bscroll scrollToElement method and scrolls to the corresponding position within 0.2 seconds.

4. Popover components

This component is triggered when a city is selected by clicking (and the city is not already viewed).

<! -- Parent module --> <mask-box v-if="maskShow" :message="maskMessage" @chooseing="chooseResult"></mask-box> <! Subcomponent module --> <div class="mask-box">
    <div class="mask-body"></div>
    <div class="btn-box">
      <div class="message">
        <p>{{message}}</p>
      </div>
      <div class="btn-left" @click="chooseTrue()"> <p> Determines </p> </div> <div class="btn-right" @click="chooseFalse()"</p> </div> </div>Copy the code

The JS part is very simple

  chooseTrue () {
      this.$emit('chooseing'.true)},chooseFalse () {
      this.$emit('chooseing'.false)}Copy the code

Values are passed up depending on which button is clicked. The parent component fires an event that scrolls the page to the top when the passed value is true.

ChooseResult (res) {if(! Res) {this.maskclose () // do not switch, only close popover}else {
        this.choiceCityName = this.choiceCity
        this.local()
        this.associationShow = false// Close the search box (in the search state) this.clearSearch =true// Clear the word in the input box (in the search state) // Scroll to the top this when confirmed.$refs.suggest.scrollTo(0, 0, 200)
        this.maskClose()
      }
    }Copy the code

5. Search the list component

However, the logic code is also relatively simple, using the above re, not much to explain.

<! Parent module --> < Transition name="list">
      <search-list v-if="associationShow" :searchListContent="searchListContent" @changeName="changeCity"></search-list> </transition> <! Subcomponent module --> <div class="listbody">
    <scroll :data="searchListContent">
      <div>
        <city-item :searchListContent="searchListContent" @changeName="changeCity"></city-item>
      </div>
    </scroll>
  </div>Copy the code

The component is only used to display and select a city by clicking. The function of the component is the same as that of the three components, but there is no Bscroll event.

6. The nav component on the right

<! -- parent module --> <nav-list :navList="cityIndexList" @toElement="toElement" :flagText="flagText"></nav-list> <! Subcomponent module --> <div class="navbody">
   <div class="navList" @touchstart.stop.prevent="start" @touchmove.stop.prevent="move">
      <div :class="navClass(item)" :data-name="item" v-for="item in navList" :key="item">
        {{item}}
      </div>
   < /div>
</div>Copy the code

This part of HTML code is relatively small, but it has the most linkage with other components, such as clicking the letter on the nav to scroll the page city component to the corresponding position, sliding on the page city component to continue scrolling, etc.

In the function of clicking the letter on the nav to scroll the page city component to its location, clicking triggers the TouchStart event:

    start (e) {
      let item = handleDomData(e.target, 'data-name')
      this.touch.start = e.touches[0].pageY
      this.touch.startIndex = getIndex(this.navList, item)
      this.scrollToElement(item)
    }Copy the code

The position of the first click provides the starting height for subsequent slides, and the scrollToElement event is triggered to pass up the value and scroll the parent component to the corresponding position.

In the sliding implementation of the page city component continues to scroll this function in, trigger the touchMove event:

    move (e) {
      this.touch.end = e.touches[0].pageY
      let distance = this.touch.end - this.touch.start
      this.touch.endIndex = Math.min(Math.max(this.touch.startIndex + Math.floor((distance + 10) / 20), 0), 22)
      this.scrollToElement(this.navList[this.touch.endIndex])
    }Copy the code

Calculate the current letter by the distance in the scrolling process, and upload the changed letter, so that the parent component scroll to the corresponding position.

In this component, we introduce two JS functions, handleDomData and getIndex in Start

// Get or assign a dom attributeexport function handleDomData (el, name, val) {
  if (val) {
    return el.setAttribute(name, val)
  } else {
    returnEl.getattribute (name)}} // Get the index of each letter in the arrayexport function getIndex (arr, query) {
  let key
  arr.map((val, index) => {
    if (val === query) {
      key = index
      return false}})return key
}Copy the code

7. (non-component) letter display card

This little thing is not a component, but it does something, so it’s there. The code is very simple, is to accept two parameters, whether to display and display what:

    <transition name="flag">
      <div class="nowFlag" v-if="flag">{{flagText}}</div>
    </transition>Copy the code

Whether to display this parameter comes from three events with the scroll base component:

// Listen for scroll eventsif(this.listenscroll) {// This.scroll. On ('scrollStart', () => {
          this.$emit('scrollStore'.true})} this.scroll. On ('scroll', (pos) => {
          this.$emit('distance', Math.abs(pos.y))
          this.$emit('scrollStore'.true})} this.scroll.on('scrollEnd', () => {
          this.$emit('scrollStore'.false)})}Copy the code

ListenScroll is not called on the search list, so it defaults to false and is only passed true on the main page. When triggered, monitor the activity of scroll component, for example, upload true at the beginning of scroll, pass true during scroll, pass false at the end to control the display and hide of cards.

The words on the card are calculated according to the scrolling distance:

// Display the word distance (val) {for (let i = 0, len = this.arrHeight.length; i < len; i++) {
        if (val < this.arrHeight[i]) {
          this.flagText = this.cityIndexList[i]
          return false}}} // Height array source // Calculates the height of each part of the linkexport function getDistance (arr) {
      let titleHeight = 30
      let itemHeight = 35
      let distanceArr = []
      arr.map((item) => {
        distanceArr.push(titleHeight + itemHeight * item[1].length)
      })
      return distanceArr
    }Copy the code

The resulting letters are passed into the navList component in addition to being used in the card, making the difference between the current letter styles.

Conclusion:

The form of the city selection component is applied to various apps and websites. It is the realization form of the city selection function after the second-level linkage of provinces and cities. There is a lot of logic, most of which is mentioned above.

The project is also uploaded to Github at github.com/lunlunshiwo… (There is also a direct scrolling version without sidebar: github.com/lunlunshiwo… To replace the corresponding file). Node.playdate.js (github.com/lunlunshiwo…); node.playdate.js (github.com/lunlunshiwo…); Then run vue-cli (NPM run dev).

For how to start the two services, refer to CMD and Power Shell.

Code words are not easy, and look at and cherish.

Original blog, if violate the interests of your company, please private letter I delete.

If you like it, ask for a thumbs up and a Star on Github.