componentAttrs.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import { type Ref, nextTick, ref } from '@vue/runtime-dom'
  2. import {
  3. createComponent,
  4. createDynamicComponent,
  5. createIf,
  6. createSlot,
  7. defineVaporComponent,
  8. renderEffect,
  9. setClass,
  10. setDynamicProps,
  11. setProp,
  12. setStyle,
  13. template,
  14. withVaporCtx,
  15. } from '../src'
  16. import { makeRender } from './_utils'
  17. import { stringifyStyle } from '@vue/shared'
  18. import { setElementText } from '../src/dom/prop'
  19. const define = makeRender<any>()
  20. // TODO: port more tests from rendererAttrsFallthrough.spec.ts
  21. describe('attribute fallthrough', () => {
  22. it('should allow attrs to fallthrough', async () => {
  23. const t0 = template('<div>', true)
  24. const { component: Child } = define({
  25. props: ['foo'],
  26. setup(props: any) {
  27. const n0 = t0() as Element
  28. renderEffect(() => setElementText(n0, props.foo))
  29. return n0
  30. },
  31. })
  32. const foo = ref(1)
  33. const id = ref('a')
  34. const { host } = define({
  35. setup() {
  36. return createComponent(
  37. Child,
  38. {
  39. foo: () => foo.value,
  40. id: () => id.value,
  41. },
  42. null,
  43. true,
  44. )
  45. },
  46. }).render()
  47. expect(host.innerHTML).toBe('<div id="a">1</div>')
  48. foo.value++
  49. await nextTick()
  50. expect(host.innerHTML).toBe('<div id="a">2</div>')
  51. id.value = 'b'
  52. await nextTick()
  53. expect(host.innerHTML).toBe('<div id="b">2</div>')
  54. })
  55. it('should allow attrs to fallthrough on component with comment at root', async () => {
  56. const t0 = template('<!--comment-->')
  57. const t1 = template('<div>')
  58. const { component: Child } = define({
  59. props: ['foo'],
  60. setup(props: any) {
  61. const n0 = t0()
  62. const n1 = t1()
  63. renderEffect(() => setElementText(n1, props.foo))
  64. return [n0, n1]
  65. },
  66. })
  67. const foo = ref(1)
  68. const id = ref('a')
  69. const { host } = define({
  70. setup() {
  71. return createComponent(
  72. Child,
  73. {
  74. foo: () => foo.value,
  75. id: () => id.value,
  76. },
  77. null,
  78. true,
  79. )
  80. },
  81. }).render()
  82. expect(host.innerHTML).toBe('<!--comment--><div id="a">1</div>')
  83. foo.value++
  84. await nextTick()
  85. expect(host.innerHTML).toBe('<!--comment--><div id="a">2</div>')
  86. id.value = 'b'
  87. await nextTick()
  88. expect(host.innerHTML).toBe('<!--comment--><div id="b">2</div>')
  89. })
  90. it('if block', async () => {
  91. const t0 = template('<div>foo</div>', true)
  92. const t1 = template('<div>bar</div>', true)
  93. const t2 = template('<div>baz</div>', true)
  94. const { component: Child } = define({
  95. setup() {
  96. const n0 = createIf(
  97. () => true,
  98. () => {
  99. const n2 = t0()
  100. return n2
  101. },
  102. () =>
  103. createIf(
  104. () => false,
  105. () => {
  106. const n4 = t1()
  107. return n4
  108. },
  109. () => {
  110. const n7 = t2()
  111. return n7
  112. },
  113. ),
  114. )
  115. return n0
  116. },
  117. })
  118. const id = ref('a')
  119. const { host } = define({
  120. setup() {
  121. return createComponent(
  122. Child,
  123. {
  124. id: () => id.value,
  125. },
  126. null,
  127. true,
  128. )
  129. },
  130. }).render()
  131. expect(host.innerHTML).toBe('<div id="a">foo</div><!--if-->')
  132. })
  133. it('should not allow attrs to fallthrough on component with multiple roots', async () => {
  134. const t0 = template('<span>')
  135. const t1 = template('<div>')
  136. const { component: Child } = define({
  137. props: ['foo'],
  138. setup(props: any) {
  139. const n0 = t0()
  140. const n1 = t1()
  141. renderEffect(() => setElementText(n1, props.foo))
  142. return [n0, n1]
  143. },
  144. })
  145. const foo = ref(1)
  146. const id = ref('a')
  147. const { host } = define({
  148. setup() {
  149. return createComponent(
  150. Child,
  151. {
  152. foo: () => foo.value,
  153. id: () => id.value,
  154. },
  155. null,
  156. true,
  157. )
  158. },
  159. }).render()
  160. expect(host.innerHTML).toBe('<span></span><div>1</div>')
  161. })
  162. it('should not allow attrs to fallthrough on component with single comment root', async () => {
  163. const t0 = template('<!--comment-->')
  164. const { component: Child } = define({
  165. setup() {
  166. const n0 = t0()
  167. return [n0]
  168. },
  169. })
  170. const id = ref('a')
  171. const { host } = define({
  172. setup() {
  173. return createComponent(Child, { id: () => id.value }, null, true)
  174. },
  175. }).render()
  176. expect(host.innerHTML).toBe('<!--comment-->')
  177. })
  178. it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
  179. const t0 = template('<div>', true)
  180. const { component: Child } = define({
  181. props: ['foo'],
  182. inheritAttrs: false,
  183. setup(props: any) {
  184. const n0 = t0() as Element
  185. renderEffect(() => setElementText(n0, props.foo))
  186. return n0
  187. },
  188. })
  189. const foo = ref(1)
  190. const id = ref('a')
  191. const { host } = define({
  192. setup() {
  193. return createComponent(
  194. Child,
  195. {
  196. foo: () => foo.value,
  197. id: () => id.value,
  198. },
  199. null,
  200. true,
  201. )
  202. },
  203. }).render()
  204. expect(host.innerHTML).toBe('<div>1</div>')
  205. foo.value++
  206. await nextTick()
  207. expect(host.innerHTML).toBe('<div>2</div>')
  208. id.value = 'b'
  209. await nextTick()
  210. expect(host.innerHTML).toBe('<div>2</div>')
  211. })
  212. it('should pass through attrs in nested single root components', async () => {
  213. const t0 = template('<div>', true)
  214. const { component: Grandson } = define({
  215. props: ['custom-attr'],
  216. setup(_: any, { attrs }: any) {
  217. const n0 = t0() as Element
  218. renderEffect(() => setElementText(n0, attrs.foo))
  219. return n0
  220. },
  221. })
  222. const { component: Child } = define({
  223. setup() {
  224. const n0 = createComponent(
  225. Grandson,
  226. {
  227. 'custom-attr': () => 'custom-attr',
  228. },
  229. null,
  230. true,
  231. )
  232. return n0
  233. },
  234. })
  235. const foo = ref(1)
  236. const id = ref('a')
  237. const { host } = define({
  238. setup() {
  239. return createComponent(
  240. Child,
  241. {
  242. foo: () => foo.value,
  243. id: () => id.value,
  244. },
  245. null,
  246. true,
  247. )
  248. },
  249. }).render()
  250. expect(host.innerHTML).toBe('<div foo="1" id="a">1</div>')
  251. foo.value++
  252. await nextTick()
  253. expect(host.innerHTML).toBe('<div foo="2" id="a">2</div>')
  254. id.value = 'b'
  255. await nextTick()
  256. expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
  257. })
  258. it('should merge classes', async () => {
  259. const rootClass = ref('root')
  260. const parentClass = ref('parent')
  261. const childClass = ref('child')
  262. const t0 = template('<div>', true /* root */)
  263. const Child = defineVaporComponent({
  264. setup() {
  265. const n = t0() as Element
  266. renderEffect(() => {
  267. // binding on template root generates incremental class setter
  268. setClass(n, childClass.value)
  269. })
  270. return n
  271. },
  272. })
  273. const Parent = defineVaporComponent({
  274. setup() {
  275. return createComponent(
  276. Child,
  277. {
  278. class: () => parentClass.value,
  279. },
  280. null,
  281. true, // pass single root flag
  282. )
  283. },
  284. })
  285. const { host } = define({
  286. setup() {
  287. return createComponent(Parent, {
  288. class: () => rootClass.value,
  289. })
  290. },
  291. }).render()
  292. const list = host.children[0].classList
  293. // assert classes without being order-sensitive
  294. function assertClasses(cls: string[]) {
  295. expect(list.length).toBe(cls.length)
  296. for (const c of cls) {
  297. expect(list.contains(c)).toBe(true)
  298. }
  299. }
  300. assertClasses(['root', 'parent', 'child'])
  301. rootClass.value = 'root1'
  302. await nextTick()
  303. assertClasses(['root1', 'parent', 'child'])
  304. parentClass.value = 'parent1'
  305. await nextTick()
  306. assertClasses(['root1', 'parent1', 'child'])
  307. childClass.value = 'child1'
  308. await nextTick()
  309. assertClasses(['root1', 'parent1', 'child1'])
  310. })
  311. it('should merge styles', async () => {
  312. const rootStyle: Ref<string | Record<string, string>> = ref('color:red')
  313. const parentStyle: Ref<string | null> = ref('font-size:12px')
  314. const childStyle = ref('font-weight:bold')
  315. const t0 = template('<div>', true /* root */)
  316. const Child = defineVaporComponent({
  317. setup() {
  318. const n = t0() as Element
  319. renderEffect(() => {
  320. // binding on template root generates incremental class setter
  321. setStyle(n, childStyle.value)
  322. })
  323. return n
  324. },
  325. })
  326. const Parent = defineVaporComponent({
  327. setup() {
  328. return createComponent(
  329. Child,
  330. {
  331. style: () => parentStyle.value,
  332. },
  333. null,
  334. true, // pass single root flag
  335. )
  336. },
  337. })
  338. const { host } = define({
  339. setup() {
  340. return createComponent(Parent, {
  341. style: () => rootStyle.value,
  342. })
  343. },
  344. }).render()
  345. const el = host.children[0] as HTMLElement
  346. function getCSS() {
  347. return el.style.cssText.replace(/\s+/g, '')
  348. }
  349. function assertStyles() {
  350. const css = getCSS()
  351. expect(css).toContain(stringifyStyle(rootStyle.value))
  352. if (parentStyle.value) {
  353. expect(css).toContain(stringifyStyle(parentStyle.value))
  354. }
  355. expect(css).toContain(stringifyStyle(childStyle.value))
  356. }
  357. assertStyles()
  358. rootStyle.value = { color: 'green' }
  359. await nextTick()
  360. assertStyles()
  361. expect(getCSS()).not.toContain('color:red')
  362. parentStyle.value = null
  363. await nextTick()
  364. assertStyles()
  365. expect(getCSS()).not.toContain('font-size:12px')
  366. childStyle.value = 'font-weight:500'
  367. await nextTick()
  368. assertStyles()
  369. expect(getCSS()).not.toContain('font-size:bold')
  370. })
  371. it('should fallthrough attrs to dynamic component', async () => {
  372. const Comp = defineVaporComponent({
  373. setup() {
  374. const n1 = createDynamicComponent(
  375. () => 'button',
  376. null,
  377. {
  378. default: withVaporCtx(() => {
  379. const n0 = createSlot('default', null)
  380. return n0
  381. }),
  382. },
  383. true,
  384. )
  385. return n1
  386. },
  387. })
  388. const { html } = define({
  389. setup() {
  390. return createComponent(
  391. Comp,
  392. {
  393. class: () => 'foo',
  394. },
  395. null,
  396. true,
  397. )
  398. },
  399. }).render()
  400. expect(html()).toBe(
  401. '<button class="foo"><!--slot--></button><!--dynamic-component-->',
  402. )
  403. })
  404. it('parent value should take priority', async () => {
  405. const parentVal = ref('parent')
  406. const childVal = ref('child')
  407. const t0 = template('<div>', true /* root */)
  408. const Child = defineVaporComponent({
  409. setup() {
  410. const n = t0()
  411. renderEffect(() => {
  412. // prop bindings on template root generates extra `root: true` flag
  413. setProp(n, 'id', childVal.value)
  414. setProp(n, 'aria-x', childVal.value)
  415. setDynamicProps(n, [{ 'aria-y': childVal.value }])
  416. })
  417. return n
  418. },
  419. })
  420. const { host } = define({
  421. setup() {
  422. return createComponent(Child, {
  423. id: () => parentVal.value,
  424. 'aria-x': () => parentVal.value,
  425. 'aria-y': () => parentVal.value,
  426. })
  427. },
  428. }).render()
  429. const el = host.children[0]
  430. expect(el.id).toBe(parentVal.value)
  431. expect(el.getAttribute('aria-x')).toBe(parentVal.value)
  432. expect(el.getAttribute('aria-y')).toBe(parentVal.value)
  433. childVal.value = 'child1'
  434. await nextTick()
  435. expect(el.id).toBe(parentVal.value)
  436. expect(el.getAttribute('aria-x')).toBe(parentVal.value)
  437. expect(el.getAttribute('aria-y')).toBe(parentVal.value)
  438. parentVal.value = 'parent1'
  439. await nextTick()
  440. expect(el.id).toBe(parentVal.value)
  441. expect(el.getAttribute('aria-x')).toBe(parentVal.value)
  442. expect(el.getAttribute('aria-y')).toBe(parentVal.value)
  443. })
  444. it('empty string should not be passed to classList.add', async () => {
  445. const t0 = template('<div>', true /* root */)
  446. const Child = defineVaporComponent({
  447. setup() {
  448. const n = t0() as Element
  449. renderEffect(() => {
  450. setClass(n, {
  451. foo: false,
  452. })
  453. })
  454. return n
  455. },
  456. })
  457. const Parent = defineVaporComponent({
  458. setup() {
  459. return createComponent(
  460. Child,
  461. {
  462. class: () => ({
  463. bar: false,
  464. }),
  465. },
  466. null,
  467. true,
  468. )
  469. },
  470. })
  471. const { host } = define({
  472. setup() {
  473. return createComponent(Parent)
  474. },
  475. }).render()
  476. const el = host.children[0]
  477. expect(el.classList.length).toBe(0)
  478. })
  479. })