本文适合了解到display:"contents"属性的功能,不知道使用场景和用途的人,因为属性比较新又很冷门。
前言(或者叫废话)
!!! 如果不爱听我唠请直接往下翻到正文。
初次见到这个属性是在使用svelte时,index.html模板里,因为冷门和好奇询问AI,说是为了防注入什么的,MDN的描述是也是让人一头雾水,完全想不到有什么用途,直到一个一直困扰我多时的问题,关于shadcn组件库。
MDN:这些元素自身不会产生特定的盒子。它们被伪盒子(pseudo-box)和子盒子取代。
shadcn是一个下载到项目本地的ui组件,不同于过往的npm包组件库(……算了你们应该不爱听,svelte5没什么好用的npm组件库,我是被迫使用的,svelte在国内也很冷门,应该没人感同身受——国内大家更偏爱vue或是react)。
总之我在使用shadcn时遇到一个问题,Dialog组件结构是这样的:
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Are you sure absolutely sure?</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</Dialog.Description>
</Dialog.Header>
dialog body
<Dialog.Footer>
<Button>yes</Button>
<Button>no</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
Content里有Header和Footer,如果把表单放在Dialog里,表单字段在Content,提交按钮在Footer,还有一个表单标题在Header。如果把form元素直接放在Content>form,form会包裹Header和Footer,让Content布局实效。而一般组件会提供插槽让Content样式属性全部转移给form(通过snippet child),而这个Content组件并没有提供child只有children(children相当于vue的插槽slot,svelte4也是slot,svelte5改了)。如果不想破坏布局,给form手写样式,就无法让form包裹Footer的submit按钮,只能通过给form设置id,submit设置form属性和form元素关联(讨厌配置id关联)。聪明的你应该想到可以放在Content外面,可以是可以不过我的代码洁癖不允许,因为Content标签内会随着弹窗展示和消失出现和隐藏,而Content外面会在弹窗组件位置留下元素,所Triiger触发器在Content外,这就会造成弹窗组件没有展示前,组件位置会留下一个空form元素。 在认识display:contents 3个月后的现在,刚刚攻克完使用sveltekit-superforms,创建和编辑复用同一个弹窗表单组件后,突然茅塞顿开……
正文
不善言词,代码说话。vue上手简单国内基数大,用以举例。
场景一:保留功能,样式透传
给元素设置contents属性,只会让盒子模型不生效,但是html标签还在,功能也还在,比如form, 正如前言中提到shadcn弹窗组件和表单的问题。
<!--shadcn-vue -->
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<!-- 重点 -->
<form action="/api/submit" method="POST" style="display: contents">
<DialogHeader>...</DialogHeader>
<Field>
<FiledLabel>...</FieldLabel>
<Input name="..." />
</Field>
<DialogFooter>
<Button type="reset">reset</Button>
<Button type="submit">submit</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
以上,DialogContent布局会忽略form直接作用于Header和Footer,好似form不存在,但是form的功能是完全保留的。
场景二:自身消失子元素节点保留
使用vue时,遇到过这样的需求(原场景忘记了):
<main>
<div role="layout" class="layout">
<button>handle</button>
</div>
</main>
让div消失,button还要,通常想到的vue指令v-show或者v-if:
<main>
<div v-if="isShow" role="layout" class="layout">
<button>handle</button>
</div>
<button v-else>handle</button>
</main>
v-show或者v-if(display:none和设置元素hidden属性)都会让子节点也消失在页面,需要写两遍handle,如果子节点比较复杂还要提取出组件,可能还要涉及组件数据传递,代码洁癖的我难受一批。使用contents的话就容易了:
<main>
<div role="layout" class="layout" :style="{display:isShow ? 'block' : 'contents'}">
<button>handle</button>
</div>
</main>
场景三:tailwind元素选择器透传作用于组件内
框架组件一般都有样式隔离,而tailwind的class里写的选择器可以完成穿透组件的选择器。
// Table.vue
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: {
type: [Boolean, null, String, Object, Array],
required: false,
skipCheck: true,
},
});
</script>
<template>
<div data-slot="table-container" class="relative w-full overflow-x-auto">
<table
data-slot="table"
:class="cn('w-full caption-bottom text-sm', props.class)"
>
<slot />
</table>
</div>
</template>
// App.vue
<!-- 重点 -->
<div class="contents *_data-[slot=table-container]:overflow-y-auto">
<Table/>
</div>
想要给表格做固定thead(就是吸附在表格顶部,设置sticky top-0)tbody滚动,要table外包裹div设置高度,然后overflow-y-auto,这样当table高度超过div高度,向下滚动thead会自动吸附在最近的overflow-auto元素。但如果把shadcn的Table组件直接当table元素使用就发现这不行(为了便于区分Table表示shadcn组件,table表示html元素),打开shadcn的他不了组件查看就是如上代码所示,里面是div>table,所以Table外放div.overflow-y-auto没用,theader吸附会被组件内的div[data-slot="table-container"]捷足先登,如果给Table组件写样式会传到table。这时候可以用div组件设置为display:contents,用这个div写tailwind选择器去控制组件内的元素样式。当然也可以使用框架的透传(vue的:deep,svelte的:global),那样一般写在style里面,用tailwind一般是不想去style的。
场景四
交给你们了,欢迎评论区补充……(有人可能会想到分组,分组用来做逻辑分组,这团是什么那团是什么,分组用contents还不如用注释呢,我就讨厌一堆div)
结语
现在表单验证和提交都用js完成,谁还用难用的原生form那套🤡。主要是我用过svelte已经回不去vue了,svelte的理念就是使用原生功能去实现,需要扎实的原生html css js知识,会原生就会svelte,相比vue和reeact这俩简直是魔法。正如渡一袁老师说的:
世界上最恐怖的事情是什么,就是不知道自己不知道。
vue的魔法让小白可以在原生开发比较薄弱的情况下,忽略实现细节或让事情更简单。
那为什么还去用svelte不用vue?起初看了B站峰华的《2分钟了解 Svelte,与 Vue 类似的前端框架》,可以不用被ref的value恶心,等入门了后才发现最新svelte5已经更新的面目全非……为什么drizzle流行基本占领了node的orm,他的理念不也是会sql就会drizzle吗,少些魔法。反正深入体验svelte后已经会不去vue了。