Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for transition:persist directive #840

Merged
merged 8 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/purple-chairs-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/compiler': minor
---

Support the transition:persist directive
5 changes: 5 additions & 0 deletions cmd/astro-wasm/astro-wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func makeTransformOptions(options js.Value) transform.TransformOptions {
if jsBool(options.Get("experimentalTransitions")) {
experimentalTransitions = true
}
experimentalPersistence := false
if jsBool(options.Get("experimentalPersistence")) {
experimentalPersistence = true
}
transitionsAnimationURL := jsString(options.Get("transitionsAnimationURL"))
if transitionsAnimationURL == "" {
transitionsAnimationURL = "astro/components/viewtransitions.css"
Expand Down Expand Up @@ -136,6 +140,7 @@ func makeTransformOptions(options js.Value) transform.TransformOptions {
ResultScopedSlot: scopedSlot,
ScopedStyleStrategy: scopedStyleStrategy,
ExperimentalTransitions: experimentalTransitions,
ExperimentalPersistence: experimentalPersistence,
TransitionsAnimationURL: transitionsAnimationURL,
}
}
Expand Down
48 changes: 46 additions & 2 deletions internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var RENDER_SLOT = "$$renderSlot"
var MERGE_SLOTS = "$$mergeSlots"
var ADD_ATTRIBUTE = "$$addAttribute"
var RENDER_TRANSITION = "$$renderTransition"
var CREATE_TRANSITION_SCOPE = "$$createTransitionScope"
var SPREAD_ATTRIBUTES = "$$spreadAttributes"
var DEFINE_STYLE_VARS = "$$defineStyleVars"
var DEFINE_SCRIPT_VARS = "$$defineScriptVars"
Expand Down Expand Up @@ -122,6 +123,10 @@ func (p *printer) printInternalImports(importSpecifier string, opts *RenderOptio
p.addNilSourceMapping()
p.print("renderTransition as " + RENDER_TRANSITION + ",\n ")
}
if opts.opts.ExperimentalPersistence {
p.addNilSourceMapping()
p.print("createTransitionScope as " + CREATE_TRANSITION_SCOPE + ",\n ")
}

// Only needed if using fallback `resolvePath` as it calls `$$metadata.resolvePath`
if opts.opts.ResolvePath == nil {
Expand Down Expand Up @@ -276,14 +281,33 @@ func (p *printer) printFuncSuffix(opts transform.TransformOptions, n *astro.Node
p.println(fmt.Sprintf("export default %s;", componentName))
}

var skippedAttributes = map[string]bool{
"define:vars": true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are exactly the same except that the object form doesn't skip define:vars.

"set:text": true,
"set:html": true,
"is:raw": true,
"transition:animate": true,
"transition:name": true,
"transition:persist": true,
}

var skippedAttributesToObject = map[string]bool{
"set:text": true,
"set:html": true,
"is:raw": true,
"transition:animate": true,
"transition:name": true,
"transition:persist": true,
}

func (p *printer) printAttributesToObject(n *astro.Node) {
lastAttributeSkipped := false
p.print("{")
for i, a := range n.Attr {
if i != 0 && !lastAttributeSkipped {
p.print(",")
}
if a.Key == "set:text" || a.Key == "set:html" || a.Key == "is:raw" || a.Key == "transition:animate" || a.Key == "transition:name" {
if _, ok := skippedAttributesToObject[a.Key]; ok {
lastAttributeSkipped = true
continue
}
Expand Down Expand Up @@ -338,7 +362,7 @@ func (p *printer) printAttributesToObject(n *astro.Node) {
}

func (p *printer) printAttribute(attr astro.Attribute, n *astro.Node) {
if attr.Key == "define:vars" || attr.Key == "set:text" || attr.Key == "set:html" || attr.Key == "is:raw" || attr.Key == "transition:animate" || attr.Key == "transition:name" {
if _, ok := skippedAttributes[attr.Key]; ok {
return
}

Expand Down Expand Up @@ -458,6 +482,26 @@ func maybeConvertTransition(n *astro.Node) {
Type: astro.ExpressionAttribute,
})
}
if transform.HasAttr(n, transform.TRANSITION_PERSIST) {
transitionPersistIndex := transform.AttrIndex(n, transform.TRANSITION_PERSIST)
// If there no value, create a transition scope for this element
if n.Attr[transitionPersistIndex].Val != "" {
// Just rename the attribute
n.Attr[transitionPersistIndex].Key = "data-astro-transition-persist"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<div transition:persist="media-player"> we only need to transform this to a data-attribute. No runtime processing is needed.


} else if transform.HasAttr(n, transform.TRANSITION_NAME) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a transition:name on the element, use that as the persist value as well.

<div transition:name="media-player" transition:persist> This just avoids repeating the same thing.

transitionNameAttr := transform.GetAttr(n, transform.TRANSITION_NAME)
n.Attr[transitionPersistIndex].Key = "data-astro-transition-persist"
n.Attr[transitionPersistIndex].Val = transitionNameAttr.Val
n.Attr[transitionPersistIndex].Type = transitionNameAttr.Type
} else {
n.Attr = append(n.Attr, astro.Attribute{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No value provided, so we will assign a value at runtime based on the position of the element within the component and the number of times this component has been used. This is the same algorithm used to auto-name view-transition-name. Subject to change/better algorithm.

<div transition:persist>

Key: "data-astro-transition-persist",
Val: fmt.Sprintf(`%s(%s, "%s")`, CREATE_TRANSITION_SCOPE, RESULT, n.TransitionScope),
Type: astro.ExpressionAttribute,
})
}
}
}

func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.TransformOptions, source []byte) {
Expand Down
88 changes: 61 additions & 27 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,23 @@ var RETURN = fmt.Sprintf("return %s%s", TEMPLATE_TAG, BACKTICK)
var SUFFIX = fmt.Sprintf("%s;", BACKTICK) + `
}, undefined, undefined);
export default $$Component;`
var SUFFIX_EXP_TRANSITIONS = fmt.Sprintf("%s;", BACKTICK) + `
}, undefined, 'self');
export default $$Component;`
var CREATE_ASTRO_CALL = "const $$Astro = $$createAstro('https://astro.build');\nconst Astro = $$Astro;"
var RENDER_HEAD_RESULT = "${$$renderHead($$result)}"

// SPECIAL TEST FIXTURES
var NON_WHITESPACE_CHARS = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[];:'\",.?")

func suffixWithFilename(filename string) string {

func suffixWithFilename(filename string, transitions bool) string {
propagationArg := "undefined"
if transitions {
propagationArg = `'self'`
}
return fmt.Sprintf("%s;", BACKTICK) + fmt.Sprintf(`
}, '%s', undefined);
export default $$Component;`, filename)
}, '%s', %s);
export default $$Component;`, filename, propagationArg)
}

type want struct {
Expand All @@ -69,11 +75,12 @@ type metadata struct {
}

type testcase struct {
name string
source string
only bool
filename string
want want
name string
source string
only bool
transitions bool
filename string
want want
}

type jsonTestcase struct {
Expand Down Expand Up @@ -2792,35 +2799,55 @@ const items = ["Dog", "Cat", "Platipus"];
},
},
{
name: "transition:name with an expression",
source: `<div transition:name={one + '-' + 'two'}></div>`,
filename: "/projects/app/src/pages/page.astro",
name: "transition:name with an expression",
source: `<div transition:name={one + '-' + 'two'}></div>`,
filename: "/projects/app/src/pages/page.astro",
transitions: true,
want: want{
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$renderTransition($$result, "", "", (one + '-' + 'two')), "data-astro-transition-scope")}></div>`,
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$renderTransition($$result, "Y64PBINH", "", (one + '-' + 'two')), "data-astro-transition-scope")}></div>`,
},
},
{
name: "transition:name with an template literal",
source: "<div transition:name=`${one}-two`></div>",
filename: "/projects/app/src/pages/page.astro",
name: "transition:name with an template literal",
source: "<div transition:name=`${one}-two`></div>",
filename: "/projects/app/src/pages/page.astro",
transitions: true,
want: want{
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$renderTransition($$result, "", "", ` + BACKTICK + `${one}-two` + BACKTICK + `), "data-astro-transition-scope")}></div>`,
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$renderTransition($$result, "SSXG7JT5", "", ` + BACKTICK + `${one}-two` + BACKTICK + `), "data-astro-transition-scope")}></div>`,
},
},
{
name: "transition:animate with an expression",
source: "<div transition:animate={slide({duration:15})}></div>",
filename: "/projects/app/src/pages/page.astro",
name: "transition:animate with an expression",
source: "<div transition:animate={slide({duration:15})}></div>",
filename: "/projects/app/src/pages/page.astro",
transitions: true,
want: want{
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$renderTransition($$result, "", (slide({duration:15})), ""), "data-astro-transition-scope")}></div>`,
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$renderTransition($$result, "RIBY4XVZ", (slide({duration:15})), ""), "data-astro-transition-scope")}></div>`,
},
},
{
name: "transition:animate on Component",
source: `<Component class="bar" transition:animate="morph"></Component>`,
filename: "/projects/app/src/pages/page.astro",
name: "transition:animate on Component",
source: `<Component class="bar" transition:animate="morph"></Component>`,
filename: "/projects/app/src/pages/page.astro",
transitions: true,
want: want{
code: `${$$renderComponent($$result,'Component',Component,{"class":"bar","data-astro-transition-scope":($$renderTransition($$result, "X2GW3NPO", "morph", ""))})}`,
},
},
{
name: "transition:persist converted to a data attribute",
source: `<div transition:persist></div>`,
transitions: true,
want: want{
code: `${$$maybeRenderHead($$result)}<div${$$addAttribute($$createTransitionScope($$result, "U5SZQYAD"), "data-astro-transition-persist")}></div>`,
},
},
{
name: "transition:persist uses transition:name if defined",
source: `<div transition:persist transition:name="foo"></div>`,
transitions: true,
want: want{
code: `${$$renderComponent($$result,'Component',Component,{"class":"bar","data-astro-transition-scope":($$renderTransition($$result, "", "morph", ""))})}`,
code: `${$$maybeRenderHead($$result)}<div data-astro-transition-persist="foo"${$$addAttribute($$renderTransition($$result, "56KLXPQC", "", "foo"), "data-astro-transition-scope")}></div>`,
},
},
}
Expand All @@ -2847,7 +2874,12 @@ const items = ["Dog", "Cat", "Platipus"];

hash := astro.HashString(code)
transform.ExtractStyles(doc)
transform.Transform(doc, transform.TransformOptions{Scope: hash}, h) // note: we want to test Transform in context here, but more advanced cases could be tested separately
transformOptions := transform.TransformOptions{
Scope: hash,
ExperimentalTransitions: true,
ExperimentalPersistence: true,
}
transform.Transform(doc, transformOptions, h) // note: we want to test Transform in context here, but more advanced cases could be tested separately
result := PrintToJS(code, doc, 0, transform.TransformOptions{
Scope: "XXXX",
InternalURL: "http://localhost:3000/",
Expand Down Expand Up @@ -2964,8 +2996,10 @@ const items = ["Dog", "Cat", "Platipus"];

if len(tt.filename) > 0 {
escapedFilename := strings.ReplaceAll(tt.filename, "'", "\\'")
toMatch += suffixWithFilename(escapedFilename)
toMatch += suffixWithFilename(escapedFilename, tt.transitions)
toMatch = strings.Replace(toMatch, "$$Component", getComponentName(tt.filename), -1)
} else if tt.transitions {
toMatch += SUFFIX_EXP_TRANSITIONS
} else {
toMatch += SUFFIX
}
Expand Down
14 changes: 12 additions & 2 deletions internal/transform/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

const TRANSITION_ANIMATE = "transition:animate"
const TRANSITION_NAME = "transition:name"
const TRANSITION_PERSIST = "transition:persist"

type TransformOptions struct {
Scope string
Expand All @@ -27,6 +28,7 @@ type TransformOptions struct {
Compact bool
ResultScopedSlot bool
ExperimentalTransitions bool
ExperimentalPersistence bool
TransitionsAnimationURL string
ResolvePath func(string) string
PreprocessStyle interface{}
Expand All @@ -44,9 +46,9 @@ func Transform(doc *astro.Node, opts TransformOptions, h *handler.Handler) *astr
if shouldScope {
ScopeElement(n, opts)
}
if opts.ExperimentalTransitions && (HasAttr(n, TRANSITION_ANIMATE) || HasAttr(n, TRANSITION_NAME)) {
if opts.ExperimentalTransitions && (HasAttr(n, TRANSITION_ANIMATE) || HasAttr(n, TRANSITION_NAME) || HasAttr(n, TRANSITION_PERSIST)) {
doc.Transition = true
n.TransitionScope = astro.HashString(fmt.Sprintf("%s-%v", opts.Scope, i))
getOrCreateTransitionScope(n, &opts, i)
}
if len(definedVars) > 0 {
didAdd := AddDefineVars(n, definedVars)
Expand Down Expand Up @@ -550,3 +552,11 @@ func mergeClassList(doc *astro.Node, n *astro.Node, opts *TransformOptions) {
func remove(slice []astro.Attribute, s int) []astro.Attribute {
return append(slice[:s], slice[s+1:]...)
}

func getOrCreateTransitionScope(n *astro.Node, opts *TransformOptions, i int) string {
if n.TransitionScope != "" {
return n.TransitionScope
}
n.TransitionScope = astro.HashString(fmt.Sprintf("%s-%v", opts.Scope, i))
return n.TransitionScope
}
6 changes: 4 additions & 2 deletions internal/transform/transform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func transformScopingFixtures() []struct {
name: "empty (space)",
source: `
<style>

</style>
<div />
`,
Expand Down Expand Up @@ -253,7 +253,9 @@ func TestFullTransform(t *testing.T) {
ExtractStyles(doc)
// Clear doc.Styles to avoid scoping behavior, we're not testing that here
doc.Styles = make([]*astro.Node, 0)
Transform(doc, TransformOptions{}, handler.NewHandler(tt.source, "/test.astro"))
Transform(doc, TransformOptions{
ExperimentalTransitions: true,
}, handler.NewHandler(tt.source, "/test.astro"))
astro.PrintToSource(&b, doc)
got := strings.TrimSpace(b.String())
if tt.want != got {
Expand Down
12 changes: 8 additions & 4 deletions internal/transform/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ func HasInlineDirective(n *astro.Node) bool {
return HasAttr(n, "is:inline")
}

func HasAttr(n *astro.Node, key string) bool {
for _, attr := range n.Attr {
func AttrIndex(n *astro.Node, key string) int {
for i, attr := range n.Attr {
if attr.Key == key {
return true
return i
}
}
return false
return -1
}

func HasAttr(n *astro.Node, key string) bool {
return AttrIndex(n, key) != -1
}

func GetAttr(n *astro.Node, key string) *astro.Attribute {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface TransformOptions {
*/
as?: 'document' | 'fragment';
experimentalTransitions?: boolean;
experimentalPersistence?: boolean;
transitionsAnimationURL?: string;
resolvePath?: (specifier: string) => Promise<string>;
preprocessStyle?: (content: string, attrs: Record<string, string>) => null | Promise<PreprocessorResult | PreprocessorError>;
Expand Down
Loading