Vue Elements
A set of reusable Vue 2.7 form and layout components.
5 min read
Updated
A small library of reusable Vue 2.7 components: form fields, a group box, a popup, a sticky element and a tab view. Each component is reproduced below.
For Vue 2.7
FormField.vue
text
<script>
export default {
props: {
label: {
type: String,
required: true
},
required: Boolean
}
}
</script>
<template>
<div class="form-field">
<div class="form-field__label">
<span class="form-field__label__text" v-text="required ? `${label} *` : label"/>
<div class="form-field__label__buttons">
<slot name="buttons"/>
</div>
</div>
<div class="form-field__input">
<slot name="input"/>
<slot/>
</div>
</div>
</template>
<style lang="scss">
.form-field {
display: flex;
flex-direction: column;
justify-content: center;
gap: .2rem;
position: relative;
width: 100%;
&__label {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
font-weight: 700;
&__text {
flex: 1;
white-space: nowrap;
}
&__buttons {
& > * {
border: none;
color: inherit;
background-color: transparent;
cursor: pointer;
// transform: $transition;
&:disabled {
opacity: .3;
cursor: not-allowed;
}
}
}
}
&__input {
& > input,
& > select,
& > textarea {
border: var(--sbs-box-border);
border-radius: var(--sbs-box-border-radius);
padding: .5rem 1rem;
// min-width: 250px;
width: 100%;
}
}
}
</style>GroupBox.vue
text
<script>
export default {
props: {
label: {
type: String,
required: true
}
}
}
</script>
<template>
<div class="group-box">
<div class="group-box__label">
<span class="group-box__label__text" v-text="label"/>
<div class="group-box__label__buttons">
<slot name="buttons"/>
</div>
</div>
<div class="group-box__container">
<slot/>
</div>
</div>
</template>
<style lang="scss">
.group-box {
display: flex;
flex-direction: column;
justify-content: center;
gap: .2rem;
position: relative;
width: 100%;
&__label {
display: flex;
align-items: center;
gap: 1px;
flex: 1;
font-weight: 700;
&__text {
flex: 1;
white-space: nowrap;
}
&__buttons {
& > * {
border: none;
background-color: transparent;
cursor: pointer;
// transform: $transition;
&:disabled {
opacity: .3;
cursor: not-allowed;
}
}
}
}
&__container {}
}
</style>Popup.vue
text
<script>
export default {
name: 'popup',
props: {
align: {
type: String,
default: 'middle' // top, middle, bottom
}
},
methods: {
onkey (event) { if (event.detail === 27) this.closePopup() },
closePopup () { this.$emit('closePopup') }
},
created () { window.addEventListener('_keydown', this.onkey) },
beforeDestroy () { window.removeEventListener('_keydown', this.onkey) }
}
</script>
<template>
<div class="popup" :class="`popup--align-${align}`" @click.self="closePopup()">
<slot />
</div>
</template>
<style lang="scss" scoped>
.popup {
animation: enter 0.15s linear forwards;
background-color: rgba(0, 0, 0, 0.5);
bottom: 0;
display: none;
left: 0;
height: 100%;
justify-content: center;
opacity: 0;
overflow: auto;
position: fixed;
right: 0;
top: 0;
width: 100%;
z-index: 200;
&--align-top {
align-items: flex-start;
padding-top: 10vh;
}
&--align-middle {
align-items: center;
}
&--align-bottom {
align-items: flex-end;
padding-bottom: 10vh;
}
&[data-show] {
display: flex;
}
}
@keyframes enter {
100% {
opacity: 1;
}
}
/deep/ .close {
color: $gray;
height: 17px;
position: absolute;
right: 10px;
top: 10px;
width: 17px;
z-index: 1;
}
</style>StickyElement.vue
text
<script>
import { getCurrentInstance, onMounted, ref } from 'vue'
export default {
props: {
position: { // top, left, right, bottom
type: String,
required: true
},
tag: {
type: String,
default: 'div'
}
},
setup (props) {
const vm = getCurrentInstance()
const floating = ref(false)
function onMarkerVisible ([entry]) {
floating.value = !entry.isIntersecting
}
onMounted(() => {
const element = vm.proxy.$el
const observer = new IntersectionObserver(onMarkerVisible, {
root: null,
rootMargin: '0px',
threshold: 0
})
const markerElement = document.createElement('div')
const shadowElement = document.createElement('div')
shadowElement.classList.add('sticky-element__shadow')
markerElement.classList.add('sticky-element__marker')
markerElement.classList.add(`sticky-element__marker--position-${props.position}`)
switch (props.position) {
case 'top':
case 'left':
element.insertAdjacentElement('beforebegin', markerElement)
element.insertAdjacentElement('beforeend', shadowElement)
break
case 'right':
case 'bottom':
element.insertAdjacentElement('afterend', markerElement)
element.insertAdjacentElement('afterbegin', shadowElement)
break
}
observer.observe(markerElement)
})
return { floating }
}
}
</script>
<template>
<component :is="tag" class="sticky-element" :class="{ [`sticky-element--position-${position}`]: true, 'sticky-element--floating': floating }">
<slot/>
</component>
</template>
<style lang="scss">
.sticky-element {
--sticky-element-size: 6px;
--sticky-element-color: rgba(0,0,0,.5);
position: sticky;
&--floating &__shadow {
opacity: 1;
}
&--position-top {
top: 0;
}
&--position-top &__shadow {
bottom: 0px;
left: 0px;
height: 1px;
width: 100%;
box-shadow: 0px calc(var(--sticky-element-size) / 2) var(--sticky-element-size) 0px var(--sticky-element-color);
}
&--position-bottom {
bottom: 0;
}
&--position-bottom &__shadow {
box-shadow: 0px calc(-1 * var(--sticky-element-size) / 2) var(--sticky-element-size) 0px var(--sticky-element-color);
}
&--position-left {
left: 0;
}
&--position-left &__shadow {
top: 0px;
right: 0px;
width: 1px;
box-shadow: calc(var(--sticky-element-size) / 2) 0px var(--sticky-element-size) 0px var(--sticky-element-color);
}
&--position-right {
right: 0;
}
&--position-right &__shadow {
top: 0px;
left: 0px;
width: 1px;
box-shadow: calc(-1 * var(--sticky-element-size) / 2) 0px var(--sticky-element-size) 0px var(--sticky-element-color);
}
&__shadow {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--sticky-element-color);
opacity: 0;
transition: all .3s cubic-bezier(.75,0,.125,1);
}
&__marker {
width: 1px;
height: 1px;
z-index: 10;
&--position-top {
position: absolute;
top: 0;
}
&--position-bottom {
position: absolute;
bottom: 0;
}
}
}
</style>TabView.vue
text
<script>
export default {
props: {
tabs: {
type: Array,
required: true
}
},
data: () => ({
activeTab: null,
containerSize: 0
}),
watch: {
activeTab () {
this.updateContainerSize()
}
},
created () {
if (this.tabs.length) {
this.activeTab = this.tabs[0]
}
},
updated () {
this.updateContainerSize()
},
methods: {
updateContainerSize () {
if (this.$refs.container.offsetWidth > this.containerSize) {
if (this.containerSize) {
// this.$refs.container.style.width = `${this.$refs.container.offsetWidth}px`
}
this.containerSize = this.$refs.container.offsetWidth
}
}
}
}
</script>
<template>
<div class="tabview">
<div class="tabview__tabs pitch">
<button
class="tabview__tabs__item"
:class="{ 'tabview__tabs__item--active': tab === activeTab }"
v-for="tab of tabs"
:key="tab.name"
v-text="tab.name"
@click="activeTab = tab"/>
</div>
<div ref="container" class="tabview__container">
<transition name="transition-slide" mode="out-in">
<keep-alive>
<component
:is="activeTab.component"
v-bind="activeTab.props"
v-on="activeTab.on"/>
</keep-alive>
</transition>
</div>
</div>
</template>
<style lang="scss" scoped>
.tabview {
background: $white;
border-radius: 2px;
padding: 12px;
max-width: 90%;
max-height: 90%;
transition: all .3s cubic-bezier(.75,0,.125,1);
display: grid;
gap: 1rem;
align-items: flex-start;
grid-auto-flow: row;
grid-template-areas:
"title"
"body";
&__tabs {
grid-area: title;
display: flex;
gap: 1rem;
margin: 0;
padding: .5rem 1rem 0;
&__item {
border: none;
background-color: transparent;
cursor: pointer;
padding: .7em 1em .5em;
&--active {
font-weight: 700;
background-color: #fff;
}
}
}
&__container {
grid-area: body;
overflow-x: hidden;
transition: all .3s cubic-bezier(.75,0,.125,1);
}
}
</style>