vSlot.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import {
  2. ElementNode,
  3. ObjectExpression,
  4. createObjectExpression,
  5. NodeTypes,
  6. createObjectProperty,
  7. createSimpleExpression,
  8. createFunctionExpression,
  9. DirectiveNode,
  10. ElementTypes,
  11. ExpressionNode,
  12. Property,
  13. TemplateChildNode,
  14. SourceLocation,
  15. createConditionalExpression,
  16. ConditionalExpression,
  17. JSChildNode,
  18. SimpleExpressionNode,
  19. FunctionExpression,
  20. CallExpression,
  21. createCallExpression,
  22. createArrayExpression
  23. } from '../ast'
  24. import { TransformContext, NodeTransform } from '../transform'
  25. import { createCompilerError, ErrorCodes } from '../errors'
  26. import { findDir, isTemplateNode, assert, isVSlot } from '../utils'
  27. import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
  28. import { parseForExpression, createForLoopParams } from './vFor'
  29. const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
  30. p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
  31. const defaultFallback = createSimpleExpression(`undefined`, false)
  32. // A NodeTransform that:
  33. // 1. Tracks scope identifiers for scoped slots so that they don't get prefixed
  34. // by transformExpression. This is only applied in non-browser builds with
  35. // { prefixIdentifiers: true }.
  36. // 2. Track v-slot depths so that we know a slot is inside another slot.
  37. // Note the exit callback is executed before buildSlots() on the same node,
  38. // so only nested slots see positive numbers.
  39. export const trackSlotScopes: NodeTransform = (node, context) => {
  40. if (
  41. node.type === NodeTypes.ELEMENT &&
  42. (node.tagType === ElementTypes.COMPONENT ||
  43. node.tagType === ElementTypes.TEMPLATE)
  44. ) {
  45. // We are only checking non-empty v-slot here
  46. // since we only care about slots that introduce scope variables.
  47. const vSlot = findDir(node, 'slot')
  48. if (vSlot) {
  49. const slotProps = vSlot.exp
  50. if (!__BROWSER__ && context.prefixIdentifiers) {
  51. slotProps && context.addIdentifiers(slotProps)
  52. }
  53. context.scopes.vSlot++
  54. return () => {
  55. if (!__BROWSER__ && context.prefixIdentifiers) {
  56. slotProps && context.removeIdentifiers(slotProps)
  57. }
  58. context.scopes.vSlot--
  59. }
  60. }
  61. }
  62. }
  63. // A NodeTransform that tracks scope identifiers for scoped slots with v-for.
  64. // This transform is only applied in non-browser builds with { prefixIdentifiers: true }
  65. export const trackVForSlotScopes: NodeTransform = (node, context) => {
  66. let vFor
  67. if (
  68. isTemplateNode(node) &&
  69. node.props.some(isVSlot) &&
  70. (vFor = findDir(node, 'for'))
  71. ) {
  72. const result = (vFor.parseResult = parseForExpression(
  73. vFor.exp as SimpleExpressionNode,
  74. context
  75. ))
  76. if (result) {
  77. const { value, key, index } = result
  78. const { addIdentifiers, removeIdentifiers } = context
  79. value && addIdentifiers(value)
  80. key && addIdentifiers(key)
  81. index && addIdentifiers(index)
  82. return () => {
  83. value && removeIdentifiers(value)
  84. key && removeIdentifiers(key)
  85. index && removeIdentifiers(index)
  86. }
  87. }
  88. }
  89. }
  90. // Instead of being a DirectiveTransform, v-slot processing is called during
  91. // transformElement to build the slots object for a component.
  92. export function buildSlots(
  93. node: ElementNode,
  94. context: TransformContext
  95. ): {
  96. slots: ObjectExpression | CallExpression
  97. hasDynamicSlots: boolean
  98. } {
  99. const { children, loc } = node
  100. const slotsProperties: Property[] = []
  101. const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
  102. // If the slot is inside a v-for or another v-slot, force it to be dynamic
  103. // since it likely uses a scope variable.
  104. // TODO: This can be further optimized to only make it dynamic when the slot
  105. // actually uses the scope variables.
  106. let hasDynamicSlots = context.scopes.vSlot > 1 || context.scopes.vFor > 0
  107. // 1. Check for default slot with slotProps on component itself.
  108. // <Comp v-slot="{ prop }"/>
  109. const explicitDefaultSlot = findDir(node, 'slot', true)
  110. if (explicitDefaultSlot) {
  111. const { arg, exp, loc } = explicitDefaultSlot
  112. if (arg) {
  113. context.onError(
  114. createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
  115. )
  116. }
  117. slotsProperties.push(buildDefaultSlot(exp, children, loc))
  118. }
  119. // 2. Iterate through children and check for template slots
  120. // <template v-slot:foo="{ prop }">
  121. let hasTemplateSlots = false
  122. let extraneousChild: TemplateChildNode | undefined = undefined
  123. const seenSlotNames = new Set<string>()
  124. for (let i = 0; i < children.length; i++) {
  125. const slotElement = children[i]
  126. let slotDir
  127. if (
  128. !isTemplateNode(slotElement) ||
  129. !(slotDir = findDir(slotElement, 'slot', true))
  130. ) {
  131. // not a <template v-slot>, skip.
  132. if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
  133. extraneousChild = slotElement
  134. }
  135. continue
  136. }
  137. if (explicitDefaultSlot) {
  138. // already has on-component default slot - this is incorrect usage.
  139. context.onError(
  140. createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
  141. )
  142. break
  143. }
  144. hasTemplateSlots = true
  145. const { children: slotChildren, loc: slotLoc } = slotElement
  146. const {
  147. arg: slotName = createSimpleExpression(`default`, true),
  148. exp: slotProps,
  149. loc: dirLoc
  150. } = slotDir
  151. // check if name is dynamic.
  152. let staticSlotName: string | undefined
  153. if (isStaticExp(slotName)) {
  154. staticSlotName = slotName ? slotName.content : `default`
  155. } else {
  156. hasDynamicSlots = true
  157. }
  158. const slotFunction = createFunctionExpression(
  159. slotProps,
  160. slotChildren,
  161. false,
  162. slotChildren.length ? slotChildren[0].loc : slotLoc
  163. )
  164. // check if this slot is conditional (v-if/v-for)
  165. let vIf: DirectiveNode | undefined
  166. let vElse: DirectiveNode | undefined
  167. let vFor: DirectiveNode | undefined
  168. if ((vIf = findDir(slotElement, 'if'))) {
  169. hasDynamicSlots = true
  170. dynamicSlots.push(
  171. createConditionalExpression(
  172. vIf.exp!,
  173. buildDynamicSlot(slotName, slotFunction),
  174. defaultFallback
  175. )
  176. )
  177. } else if (
  178. (vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */))
  179. ) {
  180. // find adjacent v-if
  181. let j = i
  182. let prev
  183. while (j--) {
  184. prev = children[j]
  185. if (prev.type !== NodeTypes.COMMENT) {
  186. break
  187. }
  188. }
  189. if (prev && isTemplateNode(prev) && findDir(prev, 'if')) {
  190. // remove node
  191. children.splice(i, 1)
  192. i--
  193. __DEV__ && assert(dynamicSlots.length > 0)
  194. // attach this slot to previous conditional
  195. let conditional = dynamicSlots[
  196. dynamicSlots.length - 1
  197. ] as ConditionalExpression
  198. while (
  199. conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
  200. ) {
  201. conditional = conditional.alternate
  202. }
  203. conditional.alternate = vElse.exp
  204. ? createConditionalExpression(
  205. vElse.exp,
  206. buildDynamicSlot(slotName, slotFunction),
  207. defaultFallback
  208. )
  209. : buildDynamicSlot(slotName, slotFunction)
  210. } else {
  211. context.onError(
  212. createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
  213. )
  214. }
  215. } else if ((vFor = findDir(slotElement, 'for'))) {
  216. hasDynamicSlots = true
  217. const parseResult =
  218. vFor.parseResult ||
  219. parseForExpression(vFor.exp as SimpleExpressionNode, context)
  220. if (parseResult) {
  221. // Render the dynamic slots as an array and add it to the createSlot()
  222. // args. The runtime knows how to handle it appropriately.
  223. dynamicSlots.push(
  224. createCallExpression(context.helper(RENDER_LIST), [
  225. parseResult.source,
  226. createFunctionExpression(
  227. createForLoopParams(parseResult),
  228. buildDynamicSlot(slotName, slotFunction),
  229. true
  230. )
  231. ])
  232. )
  233. } else {
  234. context.onError(
  235. createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, vFor.loc)
  236. )
  237. }
  238. } else {
  239. // check duplicate static names
  240. if (staticSlotName) {
  241. if (seenSlotNames.has(staticSlotName)) {
  242. context.onError(
  243. createCompilerError(
  244. ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES,
  245. dirLoc
  246. )
  247. )
  248. continue
  249. }
  250. seenSlotNames.add(staticSlotName)
  251. }
  252. slotsProperties.push(createObjectProperty(slotName, slotFunction))
  253. }
  254. }
  255. if (hasTemplateSlots && extraneousChild) {
  256. context.onError(
  257. createCompilerError(
  258. ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
  259. extraneousChild.loc
  260. )
  261. )
  262. }
  263. if (!explicitDefaultSlot && !hasTemplateSlots) {
  264. // implicit default slot.
  265. slotsProperties.push(buildDefaultSlot(undefined, children, loc))
  266. }
  267. let slots: ObjectExpression | CallExpression = createObjectExpression(
  268. slotsProperties.concat(
  269. createObjectProperty(`_compiled`, createSimpleExpression(`true`, false))
  270. ),
  271. loc
  272. )
  273. if (dynamicSlots.length) {
  274. slots = createCallExpression(context.helper(CREATE_SLOTS), [
  275. slots,
  276. createArrayExpression(dynamicSlots)
  277. ])
  278. }
  279. return {
  280. slots,
  281. hasDynamicSlots
  282. }
  283. }
  284. function buildDefaultSlot(
  285. slotProps: ExpressionNode | undefined,
  286. children: TemplateChildNode[],
  287. loc: SourceLocation
  288. ): Property {
  289. return createObjectProperty(
  290. `default`,
  291. createFunctionExpression(
  292. slotProps,
  293. children,
  294. false,
  295. children.length ? children[0].loc : loc
  296. )
  297. )
  298. }
  299. function buildDynamicSlot(
  300. name: ExpressionNode,
  301. fn: FunctionExpression
  302. ): ObjectExpression {
  303. return createObjectExpression([
  304. createObjectProperty(`name`, name),
  305. createObjectProperty(`fn`, fn)
  306. ])
  307. }