Before, I wrote an article about the building block theory of front-end development and shared my component design values with everyone.

Yeah, I guess I didn’t see it again

The main reason is that the article is more theoretical, and the cases listed are too simple, so I decided to write an actual combat article.

Since DevUI is a professional component library, we will take the design and development of Carousel Carousel component as an example to share with you how to practice the building block theory.

There are several key ideas in building block theory, among which abstraction and stratification are the most important ones.

1 abstractOut of the heart of the Carousel component

To develop the Carousel component, let’s start with a random scenario that involves the component, such as this one from the Gold Digging campaign page:

Take a look at the components and core interactions of this component.

The components can be intuitively perceived. Carousel components mainly consist of the following three components:

  • The core of theThe content area
  • Of the middle and lower regionsPaging indicator
  • Or soPaging button

The core interaction of the Carousel component is actually a paging function that switches between the current content (paging) by clicking the dot in the paging indicator or clicking the left and right paging buttons.

The core of paging has two parts:

  • The current page number
  • The action of switching page numbers

2. Realize the core paging function of Carousel component

After clear analysis of the core components, we did not worry about drawing interfaces, but first realized the most core paging function, which is uI-independent as well as frame-independent, and we chose Vue3 Composition API to achieve it.

In the live broadcast with the village head teacher before, we have taught you to build a component library from 0 hand by hand. Here are some key steps. For details, you can go to the previous articles and videos.

  1. Initialize a projectVite+Vue3+TypeScript:yarn create vite mini-vue-devui --template=vue-ts
  2. The introduction ofjsxAnd in thevite.config.tsIn the configuration:yarn add -D @vitejs/plugin-vue-jsx
  3. The installationsass:yarn add -D sass

The above three steps should initialize a vue engineering environment that we need, at which point we can design the directory structure to follow the specifications of the Vue DevUI open source component library.

Create the component directory Carousel under SRC/Components and organize the files as follows:

Carousel ├ ─ ─ __tests__ # unit test | └ ─ ─ carousel. Spec. Ts ├ ─ ─ but ts # component entry file └ ─ ─ the SRC # component source ├ ─ ─ carousel. The SCSS # component style ├ ─ ─ ├ ─ sci-imp. Sci-imp. Sci-imp. Sci-imp. Sci-imp. Sci-impCopy the code

We’ll focus on use-page.ts, a Composition API that implements paging for Carousel components.

From this file should be exported a usePage method that exports:

  • The current page numberpageIndex
  • Some paging tool methods, such as the previous pageprevPageAnd on the next pagenextPageEtc.
import { ref } from 'vue'

export default function usePage(defaultPageIndex = 1) {
  // Current page number
  const pageIndex = ref(defaultPageIndex)

  // Skip to the next page
  const setPageIndex = (current: number) = > {
    pageIndex.value = current
  }

  // Jump forward (or back) a few pages at a time
  const jumpPage = (page: number) = > {
    pageIndex.value += page
  }

  / / back
  const prevPage = () = > jumpPage(-1)

  / / the next page
  const nextPage = () = > jumpPage(1)

  return { pageIndex, setPageIndex, jumpPage, prevPage, nextPage }
}
Copy the code

Easy as it may seem, this is the heart of a Carousel/Pagination component.

Let’s see.

We introduced and used the usePage we just created in Carousel.tsx.

import { defineComponent } from 'vue' import usePage from './composables/use-page' export default defineComponent({ name: 'DCarousel', setup() { const { pageIndex, prevPage, NextPage} = usePage(1) return () => {return <div class="devui-carousel"> <button onClick={prevPage}> </span> <button onClick={nextPage}> nextPage </button> </div>}}})Copy the code

Next we use the Carousel component in app.vue:

<script setup lang="ts">
import { DCarousel } from './components/carousel'
</script>

<template>
  <div>
    <img alt="Vue logo" src="./assets/logo.png" />
    <DCarousel></DCarousel>
  </div>
</template>
Copy the code

The effect is as follows:

3. Combine paging with UI

UsePage, which you implemented earlier, is a UI-independent paging function that can be used in any paging scenario, so let’s take a look at how you can combine it with the Carousel component to implement the basic round-cast function.

The general implementation principle of the wheel broadcast graph is as follows:

  • Put the contents of each page together
  • Then only the current content is displayed by controlling the position of the content in the multicast container

TSX adds a container element, Carousel-item-container, to wrap the incoming content.

Then place the default slot contents inside the container.

import { defineComponent, renderSlot, useSlots } from 'vue' import usePage from './composables/use-page' import './carousel.scss' export default defineComponent({ name: 'DCarousel', setup() { const { pageIndex, prevPage, NextPage} = usePage(1) // Get the number of elements in the slot contents const count = useSlots().default().length return () => {return <div <div class="devui-carousel-item-container" style={{width: 1; Count * 100 + '%', // count the number of content elements left: PageIndex. Value - (pageIndex. Value - 1) * 100 + '%', RenderSlot (useSlots(), 'default')}</div> <button onClick={prevPage}> </button> <span> {pageIndex. Value}</span> <button onClick={nextPage}> next </button> </div>}}})Copy the code

Then define some styles in Carousel.scss.

.devui-carousel-item-container {
  display: flex;
  position: relative;

  & > * {
    flex: 1; }}Copy the code

Let’s try it out in app.vue:

<DCarousel>
  <div class="carousel-item">page 1</div>
  <div class="carousel-item">page 2</div>
  <div class="carousel-item">page 3</div>
</DCarousel>
Copy the code

The effect is as follows:

Now that the basic functionality is done, let’s finish off the styling.

Add a container carousel-pagination to the pager and an SVG icon to the page button:

<div class="devui-carousel-pagination"> <button class="arrow arrow-left" onClick={ prevPage }> <svg width="18px" height="18px" viewBox="0 0 16 16"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><polygon Fill ="#293040" fill-rule="nonzero" points="10.7071068 12.2928932 9.29289322 13.7071068 3.58578644 8 9.29289322 2.29289322 10.7071068 3.70710678 6.41421356 8"></polygon></g></ SVG > OnClick ={nextPage}> < SVG width="18px" height="18px" viewBox="0 0 16 16 16" version="1.1"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><polygon fill="#293040" fill-rule="nonzero" Transform ="translate(8.146447, 8.000000) scale(-1, 1) Translate (-8.146447, -8.000000) "points="11.7071068 12.2928932 10.2928932 13.7071068 4.58578644 8 10.2928932 2.29289322 11.7071068 3.70710678 7.41421356 8 "> < / polygon > > < / g < / SVG > < / button > < / div >Copy the code

Add the following styles to carousel.scss:

.devui-carousel {
  position: relative;
  overflow: hidden;
}

.devui-carousel-item-container {
  display: flex;
  position: relative;
  transition: left 500ms ease 0s; // Dynamic effect of content switch & > * {flex: 1; }}.devui-carousel-pagination {
  position: absolute;
  width: 100%;
  top: 50%;
  display: flex;
  justify-content: space-between;
  margin-top: -18px;

  .arrow {
    cursor: pointer;
    width: 36px;
    height: 36px;
    border-radius: 18px;
    background: var(--devui-highlight-overlay, rgba(255.255.255.8));
    box-shadow: var(--devui-shadow-length-hover, 0 4px 16px 0) var(--devui-light-shadow, rgba(0.0.0.1));
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    outline: 0;
    transition: background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out-smooth, cubic-bezier(.645.045.355.1)); // Hover button action &:hover {
      background: var(--devui-area, #f8f8f8);
    }

    &.arrow-left {
      margin-left: 20px;
    }

    &.arrow-right {
      margin-right: 20px; }}}Copy the code

The basic Carousel component is complete!

Effect:

Try replacing the content with a picture of the Nuggets:

<DCarousel style="width: 470px; height: 280px;" > <img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a4dda7abf534e098f04fe0e968b1e0c~tplv-k3u1fbpfcp-zoom-mark-crop-v 2:0:0:940:560.awebp?" height="280" /> <img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e5b0b404fcb44ac9fb1359334186b46~tplv-k3u1fbpfcp-zoom-mark-crop-v 2:0:0:940:560.awebp?" height="280" /> </DCarousel>Copy the code

Effect:

Is it already very close to the rotation chart of the Gold nuggets campaign page?

4 plus pagination indicator

The paging indicator, which displays the current page number as well as paging, is simple to implement.

Add the following first in Carousel.tsx:

import { defineComponent, renderSlot, useSlots } from 'vue' import usePage from './composables/use-page' import './carousel.scss' export default defineComponent({ name: From epage (1) to setup() {const {pageIndex, prevPage, nextPage, setPageIndex} = usePage(1) Const count = useSlots().default().length const indicatorArr = array.from (new Array(count).keys()) Return () => {return <div class="devui-carousel">... <div class=" devui-Carousel-indicator "> {indicatorArr. Map ((item, index) => { return <div class={`devui-carousel-indicator-item${pageIndex.value === index+1 ? ' active' : ''}`} onClick={() => setPageIndex(index + 1)}></div> }) } </div> </div> } } })Copy the code

Then perfect the style:

.devui-carousel-indicator {
  display: flex;
  position: absolute;
  bottom: 12px;
  justify-content: center;
  width: 100%;

  .devui-carousel-indicator-item {
    cursor: pointer;
    width: 6px;
    height: 6px;
    border-radius: 3px;
    margin-right: 8px;
    background: var(--devui-icon-fill, #d3d5d9);

    &.active {
      width: 24px;
      background: var(--devui-list-item-active-bg, #5e7ce0);
      transition: all var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-smooth, cubic-bezier(.645.045.355.1)); // Dynamic effect on indicator dot when switching contents}}}Copy the code

The effect is as follows:

At this point, the Carousel component is fully functional and easy to use, just need to rotate content into the component.

<DCarousel>
  <div class="carousel-item">page 1</div>
  <div class="carousel-item">page 2</div>
  <div class="carousel-item">page 3</div>
</DCarousel>
Copy the code

Five componentslayeredAnd API design

However, you will notice that the Carousel component is not yet customizable. If a developer uses this component:

  • Want to adjustLeft and right pagerThe style and location of
  • Want to adjustPaging indicatorThe style and location of

That is not possible at present.

Remember that the building block theory had an abstract idea, and from this we abstracted Carousel’s core interactions into the usePage composable.

In addition to abstraction, building block theory has a core idea of layering, which can be used to expose the capabilities of components to external users, providing greater flexibility to developers while keeping components simple.

For example, if the user wants to change the position of the paging indicator to be placed outside the main area of the multicast, how do we do that?

Industry component library practices may add an API, such as Element Plus adds an indicator-position API to do this. Set the value to outside and the paging indicator will be outside.

One problem with this is, what if I want to put the paging indicator in the bottom left corner?

Such as station B:

Should I add a bottom-left or something like that to indicator- Position? What if the user wants to put it on top, on the right, or somewhere else?

Rather than constantly adding apis to components, expose the capabilities inside them and let developers lay them out and put them wherever they want.

How do you do that? Let’s give it a try.

5.1 Ion extraction Components

The ion extraction module is divided into three steps:

  1. newcarousel-indicatorChild component that copies the corresponding template content over
  2. copycarousel-indicatorStyles associated with child components
  3. willcarouselneutralizationcarousel-indicatorThe associated code is removed and replaced with child components

5.1.1 Creating subComponents

The first step is to separate the parts that need to be customized from the Carousel component. For example, if we want to customize the Carousel Indicator, we will separate it into a child component, the Carousel – Indicator.

Create a new components directory in Carousel/SRC to store carousel child components.

We will create a carousel-indicator. TSX file under Components, and then copy the carousel.tsx file and indicator related code into this file.

To facilitate carousel-indicator status synchronization with Carousel, for example:

  • When page numbers are switched through the pager, the indicator should also be highlighted accordingly
  • When the page number is switched through the indicator, the content should be switched accordingly

We add a bidirectional binding to the Carousel-Indicator that binds the current page number.

We also need to add a count so that carousel-Indicator can render the specified number of dots.

import { defineComponent } from 'vue'
import './carousel-indicator.scss'

export default defineComponent({
  name: 'DCarouselIndicator',
  props: {
    modelValue: {
      type: Number,
    },
    count: {
      type: Number,
    }
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const indicatorArr = Array.from(new Array(props.count).keys())
    
    return () => {
      return <div class="devui-carousel-indicator">
        {
          indicatorArr.map((item, index) => {
            return <div class={`devui-carousel-indicator-item${props.modelValue === index+1 ? ' active' : ''}`} onClick={() => emit('update:modelValue', index + 1)}></div>
          })
        }
      </div>
    }
  }
})
Copy the code

5.1.2 Copy Styles

The second step is to copy the style and create a new carousel-indicator. SCSS file:

.devui-carousel-indicator{... / / from the carousel.scss}Copy the code

5.1.3 Replacing subcomponents

The third step is to remove the code related to Carousel and Carousel-Indicator and replace it with sub-components.

<div class="devui-carousel-indicator">
  {
    indicatorArr.map((item, index) => {
      return <div class={`devui-carousel-indicator-item${pageIndex.value === index+1 ? ' active' : ''}`} onClick={() => setPageIndex(index + 1)}></div>
    })
  }
</div>
Copy the code

->

<DCarouselIndicator count={count} v-model={pageIndex.value}></DCarouselIndicator>
Copy the code

The effect is the same as before:

5.2 Adding indicator slots

Added indicator slot logic in Carousel:

{
  slots.indicator
  ? slots.indicator()
  : <DCarouselIndicator count={count} v-model={pageIndex.value}></DCarouselIndicator>
}
Copy the code

5.3 Exposing Subcomponents

Expose the DCarouselIndicator in the Carousel /index.ts file.

import { App } from 'vue'
import DCarousel from './src/carousel'
import DCarouselIndicator from './src/components/carousel-indicator'

export { DCarousel, DCarouselIndicator }

export default {
  install(app: App) {
    app.component(DCarousel.name, DCarousel)
    app.component(DCarouselIndicator.name, DCarouselIndicator)
  }
}
Copy the code

We tried using the following components in app.vue:

<script setup lang="ts"> import { DCarousel, DCarouselIndicator, usePage } from './components/carousel' const { pageIndex } = usePage(1) </script> <template> <DCarousel> <div class="carousel-item">page 1</div> <div class="carousel-item">page 2</div> <div class="carousel-item">page 3</div> <template #indicator> <DCarouselIndicator :count="3" v-model="pageIndex" style="justify-content: flex-start; padding-left: 12px;" ></DCarouselIndicator> </template> </DCarousel> </template>Copy the code

We notice that the indicator has been moved to the lower left corner, but clicking on the indicator does not change the content, and clicking on the left and right pagers does not change the highlighting status of the indicator.

What is the reason for this?

5.4 Synchronizing the Status of Parent and Child Components

We find that the state of the indicator and the state of the content switch are not synchronized:

  • The status of the indicator is bound toApp.vueIn thepageIndexvalue
  • The state binding of content switching iscarouselin-componentpageIndexvalue

So to synchronize the two states, you can add a V-Model bidirectional binding to the Carousel component.

import { defineComponent, renderSlot, useSlots, watch, toRefs } from 'vue' import usePage from './composables/use-page' import DCarouselIndicator from './components/carousel-indicator' import './carousel.scss' export default defineComponent({ name: 'DCarousel', components: { DCarouselIndicator, }, props: { modelValue: { type: Number } }, emits: ['update:modelValue'], setup(props, { slots, emit }) { const { modelValue } = toRefs(props) const { pageIndex, Page, nextPage} = usePage(1) const count = useSlots().default().length Watch (modelValue, (newVal: number) => {pageIndex. Value = newVal}) watch(pageIndex, (newVal: number) => { emit('update:modelValue', newVal) }) return () => { return <div class="devui-carousel"> ... <div class="devui-carousel-pagination"> <button class="arrow arrow-left" onClick={() => { emit('update:modelValue', // When switching through the pager, the modelValue needs to be changed synchronously. PrevPage ()}}> < SVG > </button> <button class="arrow arrow-right" onClick={() => { emit('update:modelValue', props.modelValue + 1) nextPage() }}> <svg> </button> </div> ... </div> } } })Copy the code

In app. vue, bind DCarousel and DCarouselIndicator to the same pageIndex

<template> <DCarousel v-model="pageIndex"> // add v-model <div class="carousel-item">page 1</div> <div class="carousel-item">page 2</div> <div class="carousel-item">page 3</div> <template #indicator> <DCarouselIndicator :count="3" v-model="pageIndex" style="justify-content: flex-start; padding-left: 12px;" ></DCarouselIndicator> </template> </DCarousel> </template>Copy the code

Here we go again:

  • Click on the pager and the indicator status changes accordingly
  • Click on the indicator and the page content changes accordingly

5.5 How to customize indicator styles

If the user wants more customization, such as the ability to customize the style of the indicator, change it to the form of little dots like station B.

At this time, our built-in DCarouselIndicator component can no longer meet the needs of users, so we need to make further customization. We need to add default slots for DCarouselIndicator component first.

Added to carousel-indicator. TSX file:

setup(props, { emit, slots }) { const indicatorArr = Array.from(new Array(props.count).keys()) return () => { return <div Class ="devui-carousel-indicator"> {slots.default? Slots.default () // Add default slots: indicatorArr.map((item, index) => { return <div class={`devui-carousel-indicator-item${props.modelValue === index+1 ? ' active' : ''}`} onClick={() => emit('update:modelValue', index + 1)}></div> }) } </div> } }Copy the code

Custom indicators in app.vue:

<script setup lang="ts"> import { DCarousel, DCarouselIndicator, usePage } from './components/carousel' const { pageIndex, From (new Array(3).keys()) // setPageIndex = usePage(1) // setPageIndex page jump for custom indicators const indicatorArr = array.from (new Array(3).keys()) // The array used to render indicator elements </script> <template> <div> <DCarousel V-model ="pageIndex"> <div class="carousel-item"> Page 1</div> <div class="carousel-item">page 2</div> <div class="carousel-item">page 3</div> <template #indicator> <DCarouselIndicator :count="3" v-model="pageIndex" style="justify-content: flex-start; padding-left: 12px;" <div :class="['carousel-indicator-item', pageIndex === item+1? 'active' : "]" v-for="item of indicatorArr" :key="item" @click="setPageIndex(item+1)"  </template> </DCarousel> </div> </template> <style> .carousel-item { text-align: center; line-height: 200px; background: rgb(135, 164, 186); } // Define the style of the indicator. Carousel -indicator-item {position: relative; display: inline-block; width: 8px; height: 8px; margin: 4px; border-radius: 50%; background-color: var(--devui-icon-fill, #d3d5d9); overflow: hidden; cursor: pointer; } .carousel-indicator-item.active { width: 14px; height: 14px; margin: 1px; border-radius: 50%; background-color: #fff; } </style>Copy the code

Effect:

5.7 Want to customize pagers?

Follow the custom pagination designator idea by separating the pager into a sub-component CarouselPagination, exposing it, and adding corresponding slots.

Once the capabilities inside the component are taken out and exposed, there is more room for customization.

Such as:

From our exposed usePage core paging capabilities, users can write their own Pagination paging components, ImagePreview ImagePreview components

CarouselIndicator and CarouselPagination sub-component blocks can be used to splice the desired almost arbitrary revolving lamp effect, which is equivalent to the original integral indivisible Carousel component. Now it is divided into several smaller blocks. Users can make Carousel by themselves, or make a corresponding small building block part and splice it into Carousel to form personalized Carousel components

DevUI is recruiting contributors

Where did Carousel components come from that are so simple, easy to use and flexible?

From our DevUI component library, of course!

Welcome those interested in open source to join our DevUI open source organization. At present, we have the following open source products related to the component library ecology:

  • Ng DevUI:Angular12DevUI component library, an open source front-end solution for enterprise backend products
  • Vue DevUI:Vue3Version DevUI component library based onVite+Vue3+TypeScript+JSXTechnology stack
  • React DevUI:React18Version of the DevUI component library, developed by the communityxiejayReact is created with more than 20 React components.
  • DevUI Admin: flexible and customizable Admin system, based on DevUI component library and design system
  • DevUI Icons: DevUI Icons library
  • DevUI Helper: DevUI code Helper that provides a silky code completion experience.

There are now over 80 contributors to DevUI, and we welcome those of you who love learning, open source, and making friends!

Previous articles are recommended

Building Blocks theory of Front-end development – Do front-end development like building blocks

The story of DevUI open source

Build a warm open source community

DevUI Open Source 2021 annual review