# `input` to `<attrs>` and back again **TL;DR** Upon further reflection, the team is leaning towards keeping the `input` variable as the way tags receive attribute data passed to them (like in Marko 4–5), instead of the `<attrs>` tag introduced in the TagsAPI Preview. <table> <thead><tr> <th><code>input</code></th><th><code>&lt;attrs></code></th> </tr></thead> <tr><td> ``` <div>${input.name}</div> ``` </td><td width="100%"> ``` <attrs/{ name }/> <div>${name}</div> ``` </td></tr></table> _However_, this has implications for some finer details of Marko 6. **We'd like your feedback.** Read on for more info. ## Why `<attrs>`? The `<attrs>` tag was introduced for reasons that proved to be less compelling than we originally thought: Language consistency and TypeScript support. ### Language consistency In Marko 5, `input` is "magically" available in template scope instead of received like parameters. _However,_ we didn't want to wrap the whole template in a tag to use Tag Parameters, so we went with Tag Variables instead. This led to inconsistencies with the `<tag>` tag, among others. <table> <thead><tr> <th>What we wanted to convey</th><th>What we ended up with</th> </tr></thead> <tr><td> ``` <template|{ name }|> <div>${name}</div> </template> ``` </td><td> ``` <attrs/{ name }/> <div>${name}</div> ``` </td></tr></table> Additionally, `<attrs>` doesn't follow the same rules as other tags: - Can't be composed into a reimplementation of itself (e.g. `<my-attrs>`) - Can't be specified multiple times - **Note:** `<return>` also has these restrictions ### Typescript support A place to declare the input data would also be a place for it to be typed. <table> <thead><tr> <th>TypeScript</th><th>TypeScript w/Generics</th> </tr></thead> <tr><td> ``` <attrs/{ name }: { name: string }/> <div>${name}</div> ``` </td><td> ``` <attrs/{ name }: <T>{ name: T }/> <div>${name}</div> ``` </td></tr></table> _However,_ since `<attrs>` uses Tag Variables, and Typescript does not allow defining Generics for variables, we'd need to introduce a custom syntax for this to be supported (`<T>{ name: T }` in the example above is not valid TypeScript). ### Encourage destructuring Marko 6 currently uses identifiers (in the official JavaScript sense) as the granules of reactivity. This affects code execution, data serialization, and bundling. In our current model, having a singular `input` variable would force _all_ reactivity to bind to _all_ input data, rather than depending on individual destructured properties. _However,_ we're exploring the idea of extending this reactivity to member expressions with [member expression hoisting](#Member-Expression-Hoisting). This was initially planned for a later version of Marko, but if we add it now many of the uses of `input` will be destructured automatically. ## Revisiting `input` **JS** ``` <div>${input.name}</div> ``` **TS** ``` export interface Input { name: string } <div>${input.name}</div> ``` **TS w/ Generics** ``` export interface Input<T> { name: T } <div>${input.name}</div> ``` ### Pros - Already exists in Marko 4 & 5 - Less code when writing in JavaScript - declaration/destructuring isn't required - Encourages a pattern of exporting the `Input` type. - Doesn't require custom Typescript syntax - Allows a more gradual introduction to Marko's syntax - doesn't require learning Tag Variables/Parameters immediately - Aligns with `<tag>` - conceptually the whole template is wrapped in `<tag|input|>...</tag>` - cut & paste refactoring friendly ### Cons - Basically requires [member expression hoisting](#Member-Expression-Hoisting) or templates would default to a deopt-state and increase both bundle-size and serialized-data-size - In order to support all use-cases for Typescript Generics, we require non-standard scoping of Generic definitions. - given `export interface Input<T>`, `T` would be available to use as a type anywhere in the template (not only within the type definition for `Input`) ## Member Expression Hoisting If we were to switch back to a render scoped `input` we would also make it so that certain MemberExpressions (eg `input.name`, `x.y`) would act as if they had been destructured by the Marko compiler. Currently, destructuring is important in Marko 6 (and the tags api preview) because each `<effect>` runs based on the tag variable identifiers present in its function body. Here's an example of how things currently work: ```jsx <attrs/input/> /* Logs the name every time _any_ input changes */ <effect() { console.log(input.name) }/> ``` ```jsx <attrs/input/> <const/{ name } = input/> /* Logs the name only when `input.name` changes */ <effect() { console.log(name) }/> ``` Beyond just determining when `<effect>` tags execute, the current resumable model for Marko 6 automatically serializes variables from tags when they are used within event handlers or effects. This means - the first example causes **all** of `input` to be serialized - the second example would only serialize `name`. It also has implications on treeshaking. However, with member expression hoisting the Marko compiler would conceptually re-write the first example to act like the second. This means: - the two effects would operate identically - in the first example, `input.name` would be evaluated during render ### Implementation Member expressions would be hoisted if: - they used the `.` notation (not `[]`), and - the root identifier is a Marko Variable (`input` or originates from a Tag Variable/Parameter). The following examples **would** be hoisted: ```jsx <button onClick() { input.onClick?.(e); // input.onClick is hoisted }/> ``` ```jsx <effect() { console.log(input.name); }/> ``` The following examples would **not** be hoisted: ```jsx <effect() { console.log(input["name"]); }/> ``` ```jsx <div/el/> <effect() { console.log(el().innerHTML); }/> ``` ```jsx <const/items = [1, 2, 3]/> <effect() { console.log(items[0]); }/> ``` ```jsx <some-tag/x/> <effect() { x?.doStuff(); }/> ``` ```jsx <effect() { input[dynamicSomeHow] }/> ``` ```jsx <const/foo = { x: { y } }/> <effect() { const x = foo.x; // foo.x *is* hoisted console.log(x.y); // x.y (foo.x.y) is *not* hoisted }/> ``` ### Concerns #### Early evaluation & caching Evalutation of an expression at an earlier time does mean that certain things you might expect to work would not. - mutation between hoisted read and location of original read - the object itself could be mutated - or, it could be a getter returning an externally mutated value - side-effectful getter that now executes during the render phase - and is cached, so won't re-execute at the same time it would have However, these shouldn't be problematic in theory, since we would only hoist member expressions on identifiers originating from tag variables and we deep freeze objects returned through tag variables. #### Teaching 1. This has the benefit that even more so now, the code you write will be optimized, but it also complicates the rules of what gets bundled/serialized/executed. 2. How do we error in a predicatable way when the assumptions in ["Early evaluation & caching"](#Early-evaluation-amp-caching) are violated? #### Others? If you have additional concerns, or specific examples to think through, please share them.