“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

Preface πŸ–Ό ️

I’ve been playing with some widgets with VUe3 and TS recently and found that one of the most common requirements in development is to render lists of data. Now when I re-study, I find that many design specifications and logic in learning VUe2 are not properly considered.

Therefore, write this article to document the data list rendering and global header design in component design.

Let’s learn ~πŸ™†

πŸ“»δΈ€, ColumnList List data rendering

1. The design draft is known first

Before we look at the functionality implementation, let’s take a look at the prototype diagram to see what the list of data we want to implement looks like. As shown below:

You can first understand, we will be in the club to achieve the effect of the content.

2. Data conception

After understanding the specific effect drawing, now we have to start to work!

The first thing we need to think about is what kind of data does this component need?

The data required by this component is the unique ID of each row of data, the title, avatar, and the corresponding text description of each title.

Analysis is done, we are now in vue3 project under the SRC | components folder to create a new file, named ColumnList. Vue. Then write the business code. The specific code is as follows:

<template>
	<div></div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
// write an interface with ts to store the attributes of the list data
export interface ColumnProps {
  id: number; title: string; avatar? : string; description: string; }export default defineComponent({
  name: 'ColumnList'.props: {
    // Assign the contents of the interface to the list array to receive data from the parent component
    list: {
      type: Array as PropType<ColumnProps[]>,
      required: true}}})</script>
<style lang="scss" scoped>
  
</style>

Copy the code

3. View data binding

Now, after conceiving the data, we do not have any data to render, which is equivalent to an empty ColumnList. But we already have the property content of the interface, so let’s bind the data to the view first. The specific code is as follows:

<template>
  <div class="row">
    <div v-for="column in columnList" :key="column.id" class="col-4 mb-3">
      <div class="card h-100 shadow-sm">
        <div class="card-body text-center">
          <img :src="column.avatar" :alt="column.title" class="rounded-circle border border-light w-25 my-3">
          <h5 class="title">{{column.title}}</h5>
          <p class="card-text text-left">{{column.description}}</p>
          <a href="#" class="btn btn-outline-primary">Enter the column</a>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export interface ColumnProps {
  id: number; title: string; avatar? : string; description: string; }export default defineComponent({
  name: 'ColumnList'.props: {
    list: {
      type: Array as PropType<ColumnProps[]>,
      required: true}}})</script>
<style lang="scss" scoped>
  
</style>

Copy the code

Note: HERE I use the bootstrap style library, so I do not do too much writing CSS, you can go to the official Chinese document to view, you can also do your own style design.

At this point, we have completed our first round of data binding. Next we pass the data in the parent component.

4. Data transfer

We do this in app.vue in the SRC folder of the vue3 project. The specific code is as follows:

<template>
  <div class="container">
    <column-list :list="list"></column-list>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
// Import bootstrap in the root file
import 'bootstrap/dist/css/bootstrap.min.css'
// Introduce child components
import ColumnList, { ColumnProps } from './components/ColumnList.vue'

// Create interface data for child components
const testData: ColumnProps[] = [
  {
    id: 1.title: 'test1 column'.description: 'Js is known to be a weakly typed language with few specifications. This can make it hard to spot bugs until the project is live, and once the project is live, bugs are popping up. So, over the past two years, TS has quietly risen. This column will introduce some learning notes about TS. '
    avatar: 'https://img0.baidu.com/it/u=3101694723, 748884042 & FM = 26 & FMT = auto&gp = 0. JPG'
  },
  {
    id: 2.title: 'test2 column'.description: 'Js is known to be a weakly typed language with few specifications. This can make it hard to spot bugs until the project is live, and once the project is live, bugs are popping up. So, over the past two years, TS has quietly risen. This column will introduce some learning notes about TS. '.avatar: 'https://img0.baidu.com/it/u=3101694723, 748884042 & FM = 26 & FMT = auto&gp = 0. JPG'}]export default defineComponent({
  name: 'App'.components: {
    ColumnList
  },
  setup () {
    return {
      list: testData
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Copy the code

Now, let’s take a look at how the browser works:

As you can see, through the above code writing, the normal transfer of data and successful operation.

5. Head scratching

Look at this, feel the design of the whole component is pretty perfect. But, have you ever thought of a special case where there’s a row of data coming from the back end that doesn’t have the avatar value in it? At this point, if we haven’t thought through all the possible scenarios beforehand, the program can easily get an error.

So one of the things we need to do is, when we don’t get the avatar data, we need to add an initial image to it to keep the list consistent.

Alter table columnlist. vue alter table columnlist. vue

<template>
  <div class="row">
    <div v-for="column in columnList" :key="column.id" class="col-4 mb-3">
      <div class="card h-100 shadow-sm">
        <div class="card-body text-center">
          <img :src="column.avatar" :alt="column.title" class="rounded-circle border border-light w-25 my-3">
          <h5 class="title">{{column.title}}</h5>
          <p class="card-text text-left">{{column.description}}</p>
          <a href="#" class="btn btn-outline-primary">Enter the column</a>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
// write an interface with ts to store the attributes of the list data
export interface ColumnProps {
  id: number; title: string; avatar? : string; description: string; }export default defineComponent({
  name: 'ColumnList'.props: {
    // Assign the contents of the interface to the list array to receive data from the parent component
    list: {
      type: Array as PropType<ColumnProps[]>,
      required: true}},// Pass props to setup
  setup(props) {
    const columnList = computed(() = > {
      // Iterate over each line of the list array data
      return props.list.map(column= > {
        // When the current row does not have an avatar
        if(! column.avatar) {// Assign the initialization image
          column.avatar = require('@/assets/logo.png')}return column
      })
    })
    return {
      columnList
    }
  }
})
</script>

<style lang="scss" scoped>
  
</style>

Copy the code

Going ahead, we’ll delete testData from app. vue. The specific code is as follows:

<template>
  <div class="container">
    <column-list :list="list"></column-list>
  </div>
</template>

<script lang="ts">

// Create interface data for child components
const testData: ColumnProps[] = [
  {
    id: 1.title: 'test1 column'.description: 'Js is known to be a weakly typed language with few specifications. This can make it hard to spot bugs until the project is live, and once the project is live, bugs are popping up. So, over the past two years, TS has quietly risen. This column will introduce some learning notes about TS. '
    / / avatar: 'https://img0.baidu.com/it/u=3101694723, 748884042 & FM = 26 & FMT = auto&gp = 0. JPG'
  },
  {
    id: 2.title: 'test2 column'.description: 'Js is known to be a weakly typed language with few specifications. This can make it hard to spot bugs until the project is live, and once the project is live, bugs are popping up. So, over the past two years, TS has quietly risen. This column will introduce some learning notes about TS. '.avatar: 'https://img0.baidu.com/it/u=3101694723, 748884042 & FM = 26 & FMT = auto&gp = 0. JPG'}]Copy the code

If you go to the avatar line in testData, we annotate the avatar property of the first data. Now, let’s look at what the browser looks like:

As you can see, in the absence of the Avatar attribute, the browser automatically displays our pre-initialized image as expected. As a result, does it feel much more extensible, both in terms of component architecture and code logic architecture?

☎️ 2. GlobalHeader GlobalHeader

1. Look at the design draft first

Having written the columnList component, we augment this design approach with a new component. Let’s write a new component, GlobalHeader, which is the GlobalHeader. Let’s take a look at the renderings we’re going to implement. See below for details:

2. Data conception

Once we’ve seen the concrete renderings, we’ll also start thinking about what data this component will need.

The data required by this component is for each user, so each user has its own unique ID, followed by the user name, and finally whether to log in to isLogin.

Analysis is done, we are now in vue3 project under the SRC | components folder to create a new file, named GlobalHeader. Vue. Then write the business code. The specific code is as follows:

<template>
	<div></div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'

// write an interface with ts to store the attributes of the list data
//name and id plus? Indicates that this parameter is optional
export interface UserProps{
    isLogin: boolean; name? : string; id? : number; }export default defineComponent({
  name: 'GlobalHeader'.props: {
    // Assign the contents of the interface to the user object to receive data from the parent component
    user: {
      type: Object as PropType<UserProps>,
      required: true}}})</script>
<style lang="scss" scoped>
  
</style>

Copy the code

3. View data binding

Now that the data is conceived, let’s bind it to the view. The specific code is as follows:

<template>
  <nav class="navbar navbar-dark bg-primary justify-content-between mb-4 px-4">
        <a class="navbar-brand" href="#">Column on Monday</a>
        <ul v-if=! "" user.isLogin" class="list-inline mb-0">
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">The login</a></li>
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">registered</a></li>
        </ul>
        <ul v-else class="list-inline mb-0">
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">Welcome you {{user. The name}}</a></li>
        </ul>
    </nav>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
export interface ColumnProps {
  id: number; title: string; avatar? : string; description: string; }export default defineComponent({
  name: 'ColumnList'.props: {
    list: {
      type: Array as PropType<ColumnProps[]>,
      required: true}}})</script>
<style lang="scss" scoped>
  
</style>

Copy the code

4. Data transfer

For now, let’s do the data transfer in app.vue under the SRC folder in the vue3 project. The specific code is as follows:

<template>
  <div class="container">
    <global-header :user="user"></global-header>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
// Import bootstrap in the root file
import 'bootstrap/dist/css/bootstrap.min.css'
// Introduce child components
import GlobalHeader, { UserProps } from './components/GlobalHeader.vue'

// Create interface data for child components
const currentUser: UserProps = {
  isLogin: false.name: 'Monday'
}

export default defineComponent({
  name: 'App'.components: {
    GlobalHeader
  },
  setup () {
    return {
      user: currentUser
    }
  }
})
</script>

<style lang="scss" scoped>

</style>

Copy the code

The current state of isLogin is set to false. Now, let’s take a look at how the browser works:

As you can see, the current state is false, so the login and register buttons are displayed on the right side of the header, as expected.

Now let’s change the status of isLogin to true as follows:

const currentUser: UserProps = {
  isLogin: true.name: 'Monday'
}
Copy the code

Now let’s take a look at the browser display, as shown below:

Now, you can see that when isLogin is true, the user successfully logged in. So on the right side of the header is the welcome word Monday, as we expected.

πŸ“Έ 3. Dropdown menu

After reading the above content, if there is a doubt in your mind, the drop-down menu on the right side of our header has not been implemented. Take your time, let’s design this component.

1. Basic functions of components

Let’s start by designing the basic functions of this component. First of all in vue3 project SRC | components folder, a new one. The vue file, named Dropdown can. Vue. Then write the code of the file, the specific code is as follows:

<template>
    <div class="dropdown">
        <! -- Dropdown menu title -->
        <a href="#" class="btn btn-outline-light my-2dropdown-toggle" @click.prevent="toggleOpen()">
            {{title}}
        </a>
        <! Dropdown menu contents -->
        <ul class="dropdown-menu" :style="{ display: 'block' }" v-if="isOpen">
          <li class="dropdown-item">
            <a href="#">New articles</a>
          </li>
          <li class="dropdown-item">
            <a href="#">Edit data</a>
          </li>
        </ul>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'

export default defineComponent({
  name: 'Dropdown'.props: {
    title: {
      type: String.required: true}},setup() {
    const isOpen = ref(false)
    // Click to open the menu
    const toggleOpen = () = >{ isOpen.value = ! isOpen.value }return {
      isOpen,
      toggleOpen
    }
  }
})
</script>

<style lang="scss" scoped>

</style>

Copy the code

Moving on, let’s modify the globalHeader. vue file we just created. The specific code is as follows:

<template>
    <nav class="navbar navbar-dark bg-primary justify-content-between mb-4 px-4">
        <a class="navbar-brand" href="#">Column on Monday</a>
        <ul v-if=! "" user.isLogin" class="list-inline mb-0">
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">The login</a></li>
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">registered</a></li>
        </ul>
        <ul v-else class="list-inline mb-0">
            <li class="list-inline-item">
                <dropdown :title=${user.name} '"></dropdown>
            </li>
        </ul>
    </nav>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import Dropdown from './Dropdown.vue'

export interface UserProps{
    isLogin: boolean; name? : string; id? : number; }export default defineComponent({
  name: 'GlobalHeader'.components: {
    Dropdown
  },
  props: {
    user: {
      type: Object as PropType<UserProps>,
      required: true}}})</script>

<style lang="scss" scoped>

</style>

Copy the code

Now, let’s take a look at the browser display:

2. Customize menu contents DropdownItem

We have now completed the basic functionality of the component. But as the observant observer has noticed, there is no way to customize the drop-down menu because it is written as fixed. Another problem is that we can’t close the menu when we click on other areas, so this is grounded, so the user experience doesn’t seem to be as good. So, when there’s a need, we fulfill it. Now, let’s solve the two problems mentioned above.

Similarly, the SRC in the vue3 projects | components folder. Add a vue file, named DropdownItem. Vue. The specific code is as follows:

<template>
    <li
    class="dropdown-option"
    :class="{'is-disabled': disabled}"
    >
        <! -- Define a slot for the parent component -->
        <slot></slot>
    </li>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  props: {
    // Disable the status attribute
    disabled: {
      type: Boolean.default: false}}})</script>

<style>
/* Note: * represents all elements */ under both classes
.dropdown-option.is-disabled * {
    color: #6c757d;
    /* To prevent it from clicking, set pointer-events to None */
    pointer-events: none;
    background: transparent;
}
</style>

Copy the code

Vue: dropdown. vue: dropdown. vue: dropdown. vue

<template>
    <div class="dropdown">
        <! -- Dropdown menu title -->
        <a href="#" class="btn btn-outline-light my-2dropdown-toggle" @click.prevent="toggleOpen()">
            {{title}}
        </a>
        <! Dropdown menu contents -->
        <ul v-if="isOpen" class="dropdown-menu" :style="{ display: 'block' }">
            <slot></slot>
        </ul>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref  } from 'vue'

export default defineComponent({
  name: 'Dropdown'.props: {
    title: {
      type: String.required: true}},setup() {
    const isOpen = ref(false)
    // Click to open the menu
    const toggleOpen = () = >{ isOpen.value = ! isOpen.value }return {
      isOpen,
      toggleOpen
    }
  }
})
</script>

<style lang="scss" scoped>

</style>

Copy the code

From the above code we can see, put the slot in dropdown-menu.


Now, for the final step, let’s import it into the GlobalHeader.vue file. The specific code is as follows:

<template>
    <nav class="navbar navbar-dark bg-primary justify-content-between mb-4 px-4">
        <a class="navbar-brand" href="#">Column on Monday</a>
        <ul v-if=! "" user.isLogin" class="list-inline mb-0">
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">The login</a></li>
            <li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">registered</a></li>
        </ul>
        <ul v-else class="list-inline mb-0">
            <li class="list-inline-item">
                <dropdown :title=${user.name} '">
                    <drop-down-item><a href="#" class="dropdown-item">New articles</a></drop-down-item>
                    <drop-down-item disabled><a href="#" class="dropdown-item">Edit data</a></drop-down-item>
                    <drop-down-item><a href="#" class="dropdown-item">Logged out</a></drop-down-item>
                </dropdown>
            </li>
        </ul>
    </nav>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import Dropdown from './Dropdown.vue'
import DropDownItem from './DropDownItem.vue'

export interface UserProps{
    isLogin: boolean; name? : string; id? : number; }export default defineComponent({
  name: 'GlobalHeader'.components: {
    Dropdown,
    DropDownItem
  },
  props: {
    user: {
      type: Object as PropType<UserProps>,
      required: true}}})</script>

<style lang="scss" scoped>

</style>

Copy the code

At this point, let’s take a look at the browser display:

As you can see, the edit material has turned gray and cannot be clicked or jumped to. At the same time, the contents of the custom menu are also displayed.

Here, we are done with this step! Let’s go on to refine this component to make it a better user experience.

3. Components are automatically hidden by clicking the external area

You can think of the usual scene when you click on each major website, when you click on the external area of the menu, the component will automatically hide. So, let’s implement this.

First of all, there are two tasks that need to be done:

  • inonMountedWhen addingclickEvents in theonUnmountedWill delete the event;
  • getDropdown ηš„ DOMElement to determine whether the clicked content is contained by the element.

Next we locate the dropdown. vue file and continue to upgrade. The specific code is as follows:

<template>
    <div class="dropdown" ref="dropdownRef">
        <a href="#" class="btn btn-outline-light my-2 dropdown-toggle" @click.prevent="toggleOpen()">
            {{title}}
        </a>
        <ul v-if="isOpen" class="dropdown-menu" :style="{ display: 'block' }">
            <slot></slot>
        </ul>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'

export default defineComponent({
  name: 'DropDown'.props: {
    title: {
      type: String.required: true}},setup() {
    const isOpen = ref(false)
    // Get dow node of ref
    const dropdownRef = ref<null | HTMLElement>(null)
    const toggleOpen = () = >{ isOpen.value = ! isOpen.value }const handler = (e: MouseEvent) = > {
      if (dropdownRef.value) {
        // Use contains to determine whether it contains another DOW node
        // When you click on a node that is not in the current region and the menu is open
        if(! dropdownRef.value.contains(e.targetas HTMLElement) && isOpen.value) {
          isOpen.value = false
        }
      }
    }
    onMounted(() = > {
      document.addEventListener('click', handler)
    })
    onUnmounted(() = > {
      document.removeEventListener('click', handler)
    })
    return {
      isOpen,
      toggleOpen,
      dropdownRef,
      handler
    }
  }
})
</script>

<style lang="scss" scoped>

</style>

Copy the code

Now let’s take a look at the browser display:

As you can see, after tweaking the dropdown logic, as expected, when we click on the outside of the component, the component also hides itself.

4. Custom functions

At this point, the GlobalHeader component as a whole is relatively perfect. The dropdown.vue code looks a little redundant when designing the Dropdown component.

At this point we can consider taking the logic out of it and putting it into a custom function. Let’s implement it

Start by creating a new folder in the SRC folder of the vue3 project called hooks. Then create a new file under hooks called useclickoutside.ts. UseClickOutside:

import { ref, onMounted, onUnmounted, Ref } from "vue";

const useClickOutside = (elementRef: Ref<null | HTMLElement>) = > {
    const isClickOutside = ref(false)
    const handler = (e: MouseEvent) = > {
        if (elementRef.value){
            if(elementRef.value.contains(e.target as HTMLElement)){
                isClickOutside.value = true   
            }else{
                isClickOutside.value = false
            }
        }
    }
    onMounted( () = > {
        document.addEventListener('click', handler)
    })
    onUnmounted(() = > {
        document.removeEventListener('click', handler)
    })
    return isClickOutside
}

export default useClickOutside
Copy the code

With the code out of the way, we continue to simplify dropdown.vue. The specific code is as follows:

<template>
    <div class="dropdown" ref="dropdownRef">
        <a href="#" class="btn btn-outline-light my-2 dropdown-toggle" @click.prevent="toggleOpen()">
            {{title}}
        </a>
        <ul v-if="isOpen" class="dropdown-menu" :style="{ display: 'block' }">
            <slot></slot>
        </ul>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'
import useClickOutside from '.. /hooks/useClickOutside'

export default defineComponent({
  name: 'DropDown'.props: {
    title: {
      type: String.required: true}},setup() {
    const isOpen = ref(false)
    // Get dow node of ref
    const dropdownRef = ref<null | HTMLElement>(null)
    const toggleOpen = () = >{ isOpen.value = ! isOpen.value }const isClickOutside = useClickOutside(dropdownRef)
    if (isOpen.value && isClickOutside) {
      isOpen.value = false
    }
    watch(isClickOutside, () = > {
      if (isOpen.value && isClickOutside.value) {
        isOpen.value = false}})return {
      isOpen,
      toggleOpen,
      dropdownRef
    }
  }
})
</script>

<style lang="ts" scoped>

</style>

Copy the code

The browser displays the following information:

As you can see, the final display is the same. However, by pulling the code logic away, our overall component design looks better and more extensible.

5. Joint effect

Finally, we combine GlobalHeader with receiving, which we have learned above, to see our integrative effect. See below for details:

This is how the ColumnList and GlobalHeader components are implemented. I don’t know if you still want it ~

There will continue to be articles on component design

πŸ›’ 4. Conclusion

The design of the GlobalHeader and ColumnList components is now complete. When designing components, consider their extensibility. If a component feels like it’s not being reused at the time it’s written, you might want to think about what’s wrong with it. Ask yourself why, and ask yourself if this component could be better removed.

That’s all for this article! If you have any questions or the article is wrong, please leave a message in the comment section or add my wechat to communicate in the background of the public account

  • Pay attention to the public number Monday laboratory, the first time to pay attention to learning dry goods, more selected columns for you to unlock ~

  • If this article is useful to you, be sure to leave your footprints

  • See you next time! πŸ₯‚ πŸ₯‚ πŸ₯‚

🐣 Egg One More Thing

Review of Basic Knowledge

  • Vuejs basics
  • Ts Basics

The software is recommended

Axure RP 9 is one of the best drawing software for this article

Axure RP is designed for lo-fi prototyping and is extremely developer friendly. Rich control library and animation interaction, can meet most of the needs of daily drawing.

Amway wave

πŸ‘‹ πŸ‘‹ πŸ‘‹