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 abstract
Out 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 the
The content area
- Of the middle and lower regions
Paging indicator
- Or so
Paging 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.
- Initialize a project
Vite
+Vue3
+TypeScript
:yarn create vite mini-vue-devui --template=vue-ts
- The introduction of
jsx
And in thevite.config.ts
In the configuration:yarn add -D @vitejs/plugin-vue-jsx
- The installation
sass
: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 number
pageIndex
- Some paging tool methods, such as the previous page
prevPage
And on the next pagenextPage
Etc.
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 componentslayered
And API design
However, you will notice that the Carousel component is not yet customizable. If a developer uses this component:
- Want to adjust
Left and right pager
The style and location of - Want to adjust
Paging indicator
The 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:
- new
carousel-indicator
Child component that copies the corresponding template content over - copy
carousel-indicator
Styles associated with child components - will
carousel
neutralizationcarousel-indicator
The 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 to
App.vue
In thepageIndex
value - The state binding of content switching is
carousel
in-componentpageIndex
value
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:
Angular12
DevUI component library, an open source front-end solution for enterprise backend products - Vue DevUI:
Vue3
Version DevUI component library based onVite
+Vue3
+TypeScript
+JSX
Technology stack - React DevUI:
React18
Version 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