Skip to content

Instantly share code, notes, and snippets.

@L422Y
Created October 29, 2023 04:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save L422Y/75d8adc30dc441742ecdb45f1bd9c4ad to your computer and use it in GitHub Desktop.
Save L422Y/75d8adc30dc441742ecdb45f1bd9c4ad to your computer and use it in GitHub Desktop.
ResponsivePanes: Vue 3 Component which takes a "components" property for using different parent and child components based on viewport width
import type { Component, Ref } from "vue"
import { defineComponent, h, onBeforeUnmount, onMounted, ref } from "vue"
/**
* ResponsivePanes is a component that renders its children in a responsive manner.
*
* @example
* <ResponsivePanes :components="[
* { parent: BAccordion, child: BAccordionItem }, // Mobile/Small (default)
* { parent: BTabs, child: BTab, minWidth: 768 }, // Tablet/Medium
* { parent: "section", child: "article", minWidth: 1152 } // Desktop/Large
* ]">
*
*/
/**
* A ComponentPair is a pair of components that will be used to render the
* children of the ResponsivePanes component. The "parent" component in the pair
* will be used as the container component, and the "child" component will be
* used to render each child of the ResponsivePanes component.
*
* The "minWidth" property is optional, and if provided, the pair will be used
* when the viewport width is greater than or equal to the specified value.
*
* The "className" property is optional, and if provided, the class will be
* added to the container component.
*
*/
export type ComponentPair = {
parent: Component | string;
child: Component | string;
minWidth?: number; // optional minWidth for breakpoints
className?: string; // Add this line
};
export const ResponsivePanes = defineComponent({
props: {
components: {
type: Array as PropType<ComponentPair[]>,
required: true,
default: () => [
{
parent: "section",
child: "article",
},
],
},
},
setup(props) {
const sortedComponents = [...props.components].sort((a, b) => (b.minWidth || 0) - (a.minWidth || 0))
const containerComponent: Ref<Component | string> = ref(props.components[0].parent)
const childComponent: Ref<Component | string> = ref(props.components[0].child)
const resizeHandler = () => {
const width = window.innerWidth
for (const conf of sortedComponents) {
if (!conf.minWidth || width >= conf.minWidth) {
containerComponent.value = conf.parent
childComponent.value = conf.child
return // We've found the right configuration; exit the loop
}
}
}
const mediaQueryLists: MediaQueryList[] = []
onMounted(() => {
if (window.matchMedia) {
for (const conf of props.components.slice(1)) {
if (conf.minWidth) {
const queryList = window.matchMedia(`(min-width: ${conf.minWidth}px)`)
mediaQueryLists.push(queryList)
queryList.addEventListener("change", (e) => {
if (e.matches) {
containerComponent.value = conf.parent
childComponent.value = conf.child
} else {
resizeHandler() // Revert to suitable configuration
}
})
if (queryList.matches) {
containerComponent.value = conf.parent
childComponent.value = conf.child
}
}
}
} else {
window.addEventListener("resize", resizeHandler)
}
resizeHandler() // Initial setup
})
onBeforeUnmount(() => {
if (window.hasOwnProperty("matchMedia")) {
for (const queryList of mediaQueryLists) {
queryList.removeEventListener("change", resizeHandler)
}
} else {
window.removeEventListener("resize", resizeHandler)
}
})
return {
containerComponent,
childComponent,
}
},
render() {
const children = this.$slots.default ? this.$slots.default() : []
return h(
this.containerComponent,
{},
children.map((child, index) => {
return h(
this.childComponent,
{
key: index,
...child.props || {},
},
child.children || []
)
})
)
},
})
export default ResponsivePanes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment