diff --git a/.changeset/green-camels-hide.md b/.changeset/green-camels-hide.md
new file mode 100644
index 000000000..c33eea8fa
--- /dev/null
+++ b/.changeset/green-camels-hide.md
@@ -0,0 +1,7 @@
+---
+'@astrojs/compiler': minor
+---
+
+- Adds support for dynamic slots inside loops
+- Fixes an issue where successive named slotted elements would cause a runtime error
+- Fixes an issue in which if there was an implicit default slotted element next to named one, the former would get swallowed by the later.
diff --git a/internal/printer/print-to-js.go b/internal/printer/print-to-js.go
index c43b0d8b2..2d6b7bba9 100644
--- a/internal/printer/print-to-js.go
+++ b/internal/printer/print-to-js.go
@@ -69,6 +69,12 @@ type ExtractedStatement struct {
Loc loc.Loc
}
+type NestedSlotChild struct {
+ SlotProp string
+ Children []*Node
+ FirstInGroup bool
+}
+
func printToJs(p *printer, n *Node, cssLen int, opts transform.TransformOptions) PrintResult {
printedMaybeHead := false
render1(p, n, RenderOptions{
@@ -439,6 +445,7 @@ func render1(p *printer, n *Node, opts RenderOptions) {
p.print(`"` + escapeDoubleQuote(a.Val) + `"`)
slotted = true
default:
+ // add ability to use expressions for slot names later
p.handler.AppendError(&loc.ErrorWithRange{
Code: loc.ERROR_UNSUPPORTED_SLOT_ATTRIBUTE,
Text: "slot[name] must be a static string",
@@ -551,7 +558,7 @@ func render1(p *printer, n *Node, opts RenderOptions) {
if !isAllWhiteSpace {
switch true {
case n.CustomElement:
- p.print(`,{`)
+ p.print(`,({`)
p.print(fmt.Sprintf(`"%s": () => `, "default"))
p.printTemplateLiteralOpen()
for c := n.FirstChild; c != nil; c = c.NextSibling {
@@ -565,166 +572,9 @@ func render1(p *printer, n *Node, opts RenderOptions) {
})
}
p.printTemplateLiteralClose()
- p.print(`,}`)
+ p.print(`,})`)
case isComponent:
- p.print(`,`)
- slottedChildren := make(map[string][]*Node)
- conditionalSlottedChildren := make([][]*Node, 0)
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- slotProp := `"default"`
- for _, a := range c.Attr {
- if a.Key == "slot" {
- if a.Type == QuotedAttribute {
- slotProp = fmt.Sprintf(`"%s"`, escapeDoubleQuote(a.Val))
- } else if a.Type == ExpressionAttribute {
- slotProp = fmt.Sprintf(`[%s]`, a.Val)
- } else {
- p.handler.AppendError(&loc.ErrorWithRange{
- Code: loc.ERROR_UNSUPPORTED_SLOT_ATTRIBUTE,
- Text: "slot[name] must be a static string",
- Range: loc.Range{Loc: a.ValLoc, Len: len(a.Val)},
- })
- }
- }
- }
- if c.Expression {
- nestedSlots := make([]string, 0)
- for c1 := c.FirstChild; c1 != nil; c1 = c1.NextSibling {
- for _, a := range c1.Attr {
- if a.Key == "slot" {
- if a.Type == QuotedAttribute {
- nestedSlotProp := fmt.Sprintf(`"%s"`, escapeDoubleQuote(a.Val))
- nestedSlots = append(nestedSlots, nestedSlotProp)
- } else if a.Type == ExpressionAttribute {
- nestedSlotProp := fmt.Sprintf(`[%s]`, a.Val)
- nestedSlots = append(nestedSlots, nestedSlotProp)
- } else {
- panic(`unknown slot attribute type`)
- }
- }
- }
- }
-
- if len(nestedSlots) == 1 {
- slotProp = nestedSlots[0]
- slottedChildren[slotProp] = append(slottedChildren[slotProp], c)
- continue
- } else if len(nestedSlots) > 1 {
- conditionalChildren := make([]*Node, 0)
- child_loop:
- for c1 := c.FirstChild; c1 != nil; c1 = c1.NextSibling {
- for _, a := range c1.Attr {
- if a.Key == "slot" {
- if a.Type == QuotedAttribute {
- nestedSlotProp := fmt.Sprintf(`"%s"`, escapeDoubleQuote(a.Val))
- nestedSlots = append(nestedSlots, nestedSlotProp)
- conditionalChildren = append(conditionalChildren, &Node{Type: TextNode, Data: fmt.Sprintf("{%s: () => ", nestedSlotProp), Loc: make([]loc.Loc, 1)})
- conditionalChildren = append(conditionalChildren, c1)
- conditionalChildren = append(conditionalChildren, &Node{Type: TextNode, Data: "}", Loc: make([]loc.Loc, 1)})
- continue child_loop
- } else if a.Type == ExpressionAttribute {
- nestedSlotProp := fmt.Sprintf(`[%s]`, a.Val)
- nestedSlots = append(nestedSlots, nestedSlotProp)
- conditionalChildren = append(conditionalChildren, &Node{Type: TextNode, Data: fmt.Sprintf("{%s: () => ", nestedSlotProp), Loc: make([]loc.Loc, 1)})
- conditionalChildren = append(conditionalChildren, c1)
- conditionalChildren = append(conditionalChildren, &Node{Type: TextNode, Data: "}", Loc: make([]loc.Loc, 1)})
- continue child_loop
- } else {
- panic(`unknown slot attribute type`)
- }
- }
- }
- conditionalChildren = append(conditionalChildren, c1)
- }
- conditionalSlottedChildren = append(conditionalSlottedChildren, conditionalChildren)
- continue
- }
- }
-
- // Only slot ElementNodes (except expressions containing only comments) or non-empty TextNodes!
- // CommentNode, JSX comments and others should not be slotted
- if expressionOnlyHasComment(c) {
- continue
- }
- if c.Type == ElementNode || c.Type == TextNode && !emptyTextNodeWithoutSiblings(c) {
- slottedChildren[slotProp] = append(slottedChildren[slotProp], c)
- }
- }
- // fix: sort keys for stable output
- slottedKeys := make([]string, 0, len(slottedChildren))
- for k := range slottedChildren {
- slottedKeys = append(slottedKeys, k)
- }
- sort.Strings(slottedKeys)
- if len(conditionalSlottedChildren) > 0 {
- p.print(`$$mergeSlots(`)
- }
- p.print(`{`)
- numberOfSlots := len(slottedKeys)
- if numberOfSlots > 0 {
- childrenLoop:
- for _, slotProp := range slottedKeys {
- children := slottedChildren[slotProp]
-
- // If there are named slots, the default slot cannot be only whitespace
- if numberOfSlots > 1 && slotProp == "\"default\"" {
- // Loop over the children and verify that at least one non-whitespace node exists.
- foundNonWhitespace := false
- for _, child := range children {
- if child.Type != TextNode || strings.TrimSpace(child.Data) != "" {
- foundNonWhitespace = true
- }
- }
- if !foundNonWhitespace {
- continue childrenLoop
- }
- }
-
- // If selected, pass through result object on the Astro side
- if opts.opts.ResultScopedSlot {
- p.print(fmt.Sprintf(`%s: ($$result) => `, slotProp))
- } else {
- p.print(fmt.Sprintf(`%s: () => `, slotProp))
- }
-
- p.printTemplateLiteralOpen()
- for _, child := range children {
- render1(p, child, RenderOptions{
- isRoot: false,
- isExpression: opts.isExpression,
- depth: depth + 1,
- opts: opts.opts,
- cssLen: opts.cssLen,
- printedMaybeHead: opts.printedMaybeHead,
- })
- }
- p.printTemplateLiteralClose()
- p.print(`,`)
- }
- }
- p.print(`}`)
- if len(conditionalSlottedChildren) > 0 {
- for _, children := range conditionalSlottedChildren {
- p.print(",")
- for _, child := range children {
- if child.Type == ElementNode {
- p.printTemplateLiteralOpen()
- }
- render1(p, child, RenderOptions{
- isRoot: false,
- isExpression: opts.isExpression,
- depth: depth + 1,
- opts: opts.opts,
- cssLen: opts.cssLen,
- printedMaybeHead: opts.printedMaybeHead,
- })
- if child.Type == ElementNode {
- p.printTemplateLiteralClose()
- }
- }
- }
- p.print(`)`)
- }
+ handleSlots(p, n, opts, depth)
case isSlot:
p.print(`,`)
p.printTemplateLiteralOpen()
@@ -808,3 +658,300 @@ var voidElements = map[string]bool{
"track": true,
"wbr": true,
}
+
+func handleSlots(p *printer, n *Node, opts RenderOptions, depth int) {
+ p.print(`,`)
+ slottedChildren := make(map[string][]*Node)
+ hasAnyDynamicSlots := false
+ nestedSlotChildren := make([]*NestedSlotChild, 0)
+ numberOfNestedSlots := 0
+
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ slotProp := `"default"`
+ for _, a := range c.Attr {
+ if a.Key == "slot" {
+ if a.Type == QuotedAttribute {
+ slotProp = fmt.Sprintf(`"%s"`, escapeDoubleQuote(a.Val))
+ } else if a.Type == ExpressionAttribute {
+ slotProp = fmt.Sprintf(`[%s]`, a.Val)
+ } else if a.Type == TemplateLiteralAttribute {
+ slotProp = fmt.Sprintf(`[%s%s%s]`, BACKTICK, a.Val, BACKTICK)
+ } else {
+ p.handler.AppendError(&loc.ErrorWithRange{
+ Code: loc.ERROR_UNSUPPORTED_SLOT_ATTRIBUTE,
+ Text: "Unsupported slot attribute type",
+ Range: loc.Range{Loc: a.ValLoc, Len: len(a.Val)},
+ })
+ }
+ }
+ }
+ if c.Expression {
+ nestedSlotsCount := 0
+ var firstNestedSlotProp string
+ for c1 := c.FirstChild; c1 != nil; c1 = c1.NextSibling {
+ var slotProp = ""
+ for _, a := range c1.Attr {
+ if a.Key == "slot" {
+ if a.Type == QuotedAttribute {
+ slotProp = fmt.Sprintf(`"%s"`, escapeDoubleQuote(a.Val))
+ } else if a.Type == ExpressionAttribute {
+ slotProp = fmt.Sprintf(`[%s]`, a.Val)
+ hasAnyDynamicSlots = true
+ } else if a.Type == TemplateLiteralAttribute {
+ slotProp = fmt.Sprintf(`[%s%s%s]`, BACKTICK, a.Val, BACKTICK)
+ hasAnyDynamicSlots = true
+ } else {
+ panic(`unknown slot attribute type`)
+ }
+ }
+ if firstNestedSlotProp == "" && slotProp != "" {
+ firstNestedSlotProp = slotProp
+ }
+ }
+ if firstNestedSlotProp != "" {
+ nestedSlotsCount++
+ }
+ }
+
+ if nestedSlotsCount == 1 && !hasAnyDynamicSlots {
+ slottedChildren[firstNestedSlotProp] = append(slottedChildren[firstNestedSlotProp], c)
+ continue
+ } else if nestedSlotsCount > 1 || hasAnyDynamicSlots {
+ child_loop:
+ for c1 := c.FirstChild; c1 != nil; c1 = c1.NextSibling {
+ foundNamedSlot := false
+ for _, a := range c1.Attr {
+ if a.Key == "slot" {
+ var nestedSlotProp string
+ var nestedSlotEntry *NestedSlotChild
+ if a.Type == QuotedAttribute {
+ nestedSlotProp = fmt.Sprintf(`"%s"`, escapeDoubleQuote(a.Val))
+ hasAnyDynamicSlots = true
+ } else if a.Type == ExpressionAttribute {
+ nestedSlotProp = fmt.Sprintf(`[%s]`, a.Val)
+ hasAnyDynamicSlots = true
+ } else if a.Type == TemplateLiteralAttribute {
+ hasAnyDynamicSlots = true
+ nestedSlotProp = fmt.Sprintf(`[%s%s%s]`, BACKTICK, a.Val, BACKTICK)
+ } else {
+ panic(`unknown slot attribute type`)
+ }
+ foundNamedSlot = true
+ isFirstInGroup := c1 == c.FirstChild
+ nestedSlotEntry = &NestedSlotChild{nestedSlotProp, []*Node{c1}, isFirstInGroup}
+ nestedSlotChildren = append(nestedSlotChildren, nestedSlotEntry)
+ continue child_loop
+ }
+ }
+ isFirstInGroup := c1 == c.FirstChild
+ if !foundNamedSlot && c1.Type == ElementNode {
+ pseudoSlotEntry := &NestedSlotChild{`"default"`, []*Node{c1}, isFirstInGroup}
+ nestedSlotChildren = append(nestedSlotChildren, pseudoSlotEntry)
+ } else {
+ nestedSlotEntry := &NestedSlotChild{`"@@NON_ELEMENT_ENTRY"`, []*Node{c1}, isFirstInGroup}
+ nestedSlotChildren = append(nestedSlotChildren, nestedSlotEntry)
+ }
+ numberOfNestedSlots++
+ }
+ continue
+ }
+ }
+
+ // Only slot ElementNodes (except expressions containing only comments) or non-empty TextNodes!
+ // CommentNode, JSX comments and others should not be slotted
+ if expressionOnlyHasComment(c) {
+ continue
+ }
+ if c.Type == ElementNode || c.Type == TextNode && !emptyTextNodeWithoutSiblings(c) {
+ slottedChildren[slotProp] = append(slottedChildren[slotProp], c)
+ }
+ }
+ // fix: sort keys for stable output
+ slottedKeys := make([]string, 0, len(slottedChildren))
+ for k := range slottedChildren {
+ slottedKeys = append(slottedKeys, k)
+ }
+ sort.Strings(slottedKeys)
+ if numberOfNestedSlots > 0 || hasAnyDynamicSlots {
+ p.print(`$$mergeSlots(`)
+ }
+ p.print(`({`)
+ numberOfSlots := len(slottedKeys)
+ if numberOfSlots > 0 {
+ childrenLoop:
+ for _, slotProp := range slottedKeys {
+ children := slottedChildren[slotProp]
+
+ // If there are named slots, the default slot cannot be only whitespace
+ if numberOfSlots > 1 && slotProp == "\"default\"" {
+ // Loop over the children and verify that at least one non-whitespace node exists.
+ foundNonWhitespace := false
+ for _, child := range children {
+ if child.Type != TextNode || strings.TrimSpace(child.Data) != "" {
+ foundNonWhitespace = true
+ }
+ }
+ if !foundNonWhitespace {
+ continue childrenLoop
+ }
+ }
+
+ // If selected, pass through result object on the Astro side
+ if opts.opts.ResultScopedSlot {
+ p.print(fmt.Sprintf(`%s: ($$result) => `, slotProp))
+ } else {
+ p.print(fmt.Sprintf(`%s: () => `, slotProp))
+ }
+
+ p.printTemplateLiteralOpen()
+ for _, child := range children {
+ render1(p, child, RenderOptions{
+ isRoot: false,
+ isExpression: opts.isExpression,
+ depth: depth + 1,
+ opts: opts.opts,
+ cssLen: opts.cssLen,
+ printedMaybeHead: opts.printedMaybeHead,
+ })
+ }
+ p.printTemplateLiteralClose()
+ p.print(`,`)
+ }
+ }
+ p.print(`})`)
+ // print nested slots
+ if numberOfNestedSlots > 0 || hasAnyDynamicSlots {
+ endSlotIndexes := generateEndSlotIndexes(nestedSlotChildren)
+ mergeDefaultSlotsAndUpdateIndexes(&nestedSlotChildren, endSlotIndexes)
+
+ hasFoundFirstElementNode := false
+ for j, nestedSlot := range nestedSlotChildren {
+ if nestedSlot.FirstInGroup {
+ p.print(`,`)
+ }
+ // whether this is the first element node in the chain
+ // (used to determine which slot render function to use)
+ var isFirstElementInChain bool
+ isLastInChain := endSlotIndexes[j]
+ if nestedSlot.Children[0].Type == ElementNode && !hasFoundFirstElementNode {
+ isFirstElementInChain = true
+ hasFoundFirstElementNode = true
+ }
+ renderNestedSlotChild(p, nestedSlot, isFirstElementInChain, isLastInChain, depth, opts)
+ if isLastInChain {
+ // reset hasFoundFirstElementNode for the next chain
+ hasFoundFirstElementNode = false
+ }
+ }
+ p.print(`)`)
+ }
+}
+
+// Helper function to encapsulate nested slot entry rendering
+func renderNestedSlotChild(p *printer, nestedSlotChild *NestedSlotChild, isFirstElementInChain bool, isLastInChain bool, depth int, opts RenderOptions) {
+ if nestedSlotChild.SlotProp == `"@@NON_ELEMENT_ENTRY"` {
+ for _, child := range nestedSlotChild.Children {
+ p.print(child.Data)
+ }
+ return
+ }
+ slotRenderFunction := getSlotRenderFunction(isFirstElementInChain)
+ slotRenderFunctionNode := &Node{Type: TextNode, Data: fmt.Sprintf(slotRenderFunction, nestedSlotChild.SlotProp), Loc: make([]loc.Loc, 1)}
+ // print the slot render function
+ render1(p, slotRenderFunctionNode, RenderOptions{
+ isRoot: false,
+ isExpression: opts.isExpression,
+ depth: depth + 1,
+ opts: opts.opts,
+ cssLen: opts.cssLen,
+ printedMaybeHead: opts.printedMaybeHead,
+ })
+
+ // print the nested slotted children
+ p.printTemplateLiteralOpen()
+ for _, child := range nestedSlotChild.Children {
+ render1(p, child, RenderOptions{
+ isRoot: false,
+ isExpression: false,
+ depth: depth,
+ opts: opts.opts,
+ cssLen: opts.cssLen,
+ printedMaybeHead: opts.printedMaybeHead,
+ })
+ }
+ p.printTemplateLiteralClose()
+
+ // when we are at the end of the chain, close the slot render function
+ if isLastInChain {
+ p.print(`})`)
+ }
+}
+
+func generateEndSlotIndexes(nestedSlotChildren []*NestedSlotChild) map[int]bool {
+ endSlotIndexes := make(map[int]bool)
+ var latestElementNodeIndex int
+
+ for i, nestedSlot := range nestedSlotChildren {
+ if nestedSlot.Children[0].Type == ElementNode {
+ latestElementNodeIndex = i
+ } else if isNonWhitespaceTextNode(nestedSlot.Children[0]) {
+ endSlotIndexes[latestElementNodeIndex] = true
+ }
+ }
+
+ // Ensure the last element node index is also added to endSlotIndexes
+ if latestElementNodeIndex < len(nestedSlotChildren) {
+ endSlotIndexes[latestElementNodeIndex] = true
+ }
+
+ return endSlotIndexes
+}
+
+func mergeDefaultSlotsAndUpdateIndexes(nestedSlotChildren *[]*NestedSlotChild, endSlotIndexes map[int]bool) {
+ defaultSlot := &NestedSlotChild{SlotProp: `"default"`, Children: []*Node{}}
+ mergedSlotChildren := make([]*NestedSlotChild, 0)
+ numberOfMergedSlotsInSlotChain := 0
+
+ for i, nestedSlot := range *nestedSlotChildren {
+ if isDefaultSlot(nestedSlot) {
+ defaultSlot.Children = append(defaultSlot.Children, nestedSlot.Children...)
+ numberOfMergedSlotsInSlotChain++
+ } else {
+ mergedSlotChildren = append(mergedSlotChildren, nestedSlot)
+ }
+ if shouldMergeDefaultSlot(endSlotIndexes, i, defaultSlot) {
+ resetEndSlotIndexes(endSlotIndexes, i, &numberOfMergedSlotsInSlotChain)
+ mergedSlotChildren = append(mergedSlotChildren, defaultSlot)
+ defaultSlot = &NestedSlotChild{SlotProp: `"default"`, Children: []*Node{}}
+ }
+ }
+ *nestedSlotChildren = mergedSlotChildren
+}
+
+func getSlotRenderFunction(isNewSlotObject bool) string {
+ const FIRST_SLOT_CHILD_FUNCTION = "({%s: () => "
+ const NEXT_SLOT_CHILD_FUNCTION = ", %s: () => "
+
+ if isNewSlotObject {
+ return FIRST_SLOT_CHILD_FUNCTION
+ }
+ return NEXT_SLOT_CHILD_FUNCTION
+}
+
+func isNonWhitespaceTextNode(n *Node) bool {
+ return n.Type == TextNode && strings.TrimSpace(n.Data) != ""
+}
+
+func isDefaultSlot(slot *NestedSlotChild) bool {
+ return slot.SlotProp == `"default"`
+}
+
+func shouldMergeDefaultSlot(endSlotIndexes map[int]bool, i int, defaultSlot *NestedSlotChild) bool {
+ return endSlotIndexes[i] && len(defaultSlot.Children) > 0
+}
+
+func resetEndSlotIndexes(endSlotIndexes map[int]bool, i int, numberOfMergedSlotsInSlotChain *int) {
+ endSlotIndexes[i] = false
+ endSlotIndexes[i-(*numberOfMergedSlotsInSlotChain)+1] = true
+ (*numberOfMergedSlotsInSlotChain) = 0
+}
diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go
index 36c1629a3..22dcf7ebd 100644
--- a/internal/printer/printer_test.go
+++ b/internal/printer/printer_test.go
@@ -165,14 +165,14 @@ func TestPrinter(t *testing.T) {
name: "ternary component",
source: `{special ? Special
Not special
}`, want: want{ - code: `${special ? $$render` + BACKTICK + `${$$renderComponent($$result,'ChildDiv',ChildDiv,{},{"default": () => $$render` + BACKTICK + `${$$maybeRenderHead($$result)}Special
` + BACKTICK + `,})}` + BACKTICK + ` : $$render` + BACKTICK + `Not special
` + BACKTICK + `}`, + code: `${special ? $$render` + BACKTICK + `${$$renderComponent($$result,'ChildDiv',ChildDiv,{},({"default": () => $$render` + BACKTICK + `${$$maybeRenderHead($$result)}Special
` + BACKTICK + `,}))}` + BACKTICK + ` : $$render` + BACKTICK + `Not special
` + BACKTICK + `}`, }, }, { name: "ternary layout", source: `{toggleError ?Paragraph 2
`, want: want{ - code: `${$$renderComponent($$result,'Component',Component,{},{"default": () => $$render` + BACKTICK + ` + code: `${$$renderComponent($$result,'Component',Component,{},({"default": () => $$render` + BACKTICK + ` ${$$maybeRenderHead($$result)}Paragraph 1
-Paragraph 2
` + BACKTICK + `,})}`, +Paragraph 2
` + BACKTICK + `,}))}`, }, }, { @@ -480,7 +583,7 @@ import type data from "test" }, }, { - name: "nested template literal expression", + name: "nested template literal expression I", source: "", want: want{ code: "${$$maybeRenderHead($$result)}", @@ -490,7 +593,7 @@ import type data from "test" name: "component in expression with its child expression before its child element", source: "{list.map(() => (