# Incoming Attributes/Props/Input/whatever We're currently re-evaluating if introducing an `<attrs>` tag with the Tags API was the right decision. This was brought into question while implementing TypeScript support into the language. We discovered that there is no current way to support "Generic" types for the `input` to the template. That is we cannot support the equivalent of the following JSX. ```jsx= function MySelect<T>(props: { items: T[], onChange: (item: T) => void }) { /* ... */ } ``` There is also a need to support typing the `input` to the template when using the Class API. Our plan for typing the `input` in the Class API is to use `export interface Input {...}` which Marko will automatically pick up on, eg: ```jsx= export interface Input { name: string }; <div>${input.name}</div> ``` For the Tags API initially we had thought we would do: ```jsx= <attrs/{ name }: { name: string }/> ``` However there is no way to introduce a generic using the tag variable syntax since it is conceptually the same as a variable declaration and you can't for example do the following: ```jsx= const { name }: <T> { name: string } = {/* ... */} ``` Below we've evaluated what three primary options would be to represent receiving `input` using JavaScript, TypeScript, and TypeScript with Generics in Marko templates. Each includes a list of pros and cons that we've identified. **Additional Context** It's important to know the reason we moved away from a render scoped `input` in the first place in order to evaluate what we should do next. The primary reason we moved to the `<attrs>` tag is that we wanted to encourage destructuring individual `input` properties. Destructuring is important for the current implementation of Marko 6 (and the tags api preview) because the granularity of updates, treeshaking and serialization are tied to identifiers. We're separately exploring [member expression hoisting](#Member-Expression-Hoisting) to automatically optimize this further. --- ## Render scope `input` variable <table> <thead><tr> <th>JS</th><th>TypeScript</th><th>TypeScript w/Generics</th> </tr></thead> <tr><td> ``` <div>${input.name}</div> ``` </td><td> ``` export interface Input { name: string } <div>${input.name}</div> ``` </td><td> ``` export interface Input<T> { name: T } <div>${input.name}</div> ``` </td></tr></table> ### Pros - Already exists in Marko 4 & 5 - Less code when writing in JavaScript (TypeScript gets slightly more verbose to type… but it’s _pretty damn verbose_ already) - Encourages a pattern of exporting the `Input` type. - Doesn't require learning custom syntax immediately (like params or tag variables would) - 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 (but we may want this hoisting regardless?) - Adds nonstandard/special handling of `export interface Input` to pull off the generic and make it available to the rest of the template. - Magic render-scoped variable (one might be fine, but it can get out of hand: Marko 4 & 5 have `input`, `state`, `out`, `component`, `_component` and `widget` - if you count compat layer) - Magic exported `Input` for Typescript. No progression from untyped => typed --- ## `<attrs>` tag with var <table> <thead><tr> <th>JS</th><th>TypeScript</th><th>TypeScript w/Generics</th> </tr></thead> <tr><td> ``` <attrs/{ name }/> <div>${name}</div> ``` </td><td> ``` <attrs/{ name }: { name: string }/> <div>${name}</div> ``` </td><td> ``` <attrs/{ name }: <T>{ name: T }/> <div>${name}</div> ``` </td></tr></table> ### Pros - Already exists in Tags API Preview - Does not require custom syntax (until you get to TypeScript & Generics…) - Encourages destructuring, which without member expression hoisting is important ### Cons - Requires custom syntax when used with Typescript & Generics. - Forces introduction to both tag variables and destructuring to use input. - Is not a normal tag: - Cannot be composed into `<my-attrs>` - Cannot be specified multiple times - Only other tag with these restrictions is `<return>` --- ## Top-level tag `|params|` <table> <thead><tr> <th>JS</th><th>TypeScript</th><th>TypeScript w/Generics</th> </tr></thead> <tr><td> ``` |{ name }| <div>${name}</div> ``` </td><td> ``` |{ name }: { name: string }| <div>${name}</div> ``` </td><td> ``` |<T>{ name }: { name: T }| <div>${name}</div> ``` </td></tr></table> ### Pros - Parity with the `<tag>` tag and tag parameters in general - Conceptually, a template _is_ implicitly wrapped in a `<tag>` that is automatically the default export for the file - Encourages destructuring, which without member expression hoisting is important ### Cons - Forces introduction to both tag parameters and destructuring to use input. - Although tag parameters exist in the language, this could be considered a new syntax - Generics outside the params`<T>|name: T|` becomes ambigous when used at the top-level, so we'd need to move the generic definition inside the pipes`|<T> name: T|`. However in Typescript generics live outside the function params. - `function foo<T>(name: T) { ... }` - `<const/foo<T>(name: T) { ... }/>` - But then... `<some-tag|<T> name: T|>...</>` --- # Member Expression Hoisting Destructuring is important for the current implementation of Marko 6 (and the tags api preview) because right now each `<effect>` runs based on all tag variable identifiers used 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) }/> /* Logs the name only when `input.name` changes */ <const/{ name } = input/> // This is essentially all the <attrs> tag does <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 in the above, the first `effect` causes **all** of `input` to be serialized, while the second one would only serialize `name`. If we were to switch back to a render scoped `input` we would also make it so that MemberExpressions (eg `input.name`, `x?.y`) would act as if they had been destructured if the accessor is a static value. This would mean with the two effects above they would operate identically. This has the benefit that of more performant code in more cases. However it is not without drawbacks, and we're not sure if there are others that need to be considered beyond the ones mentioned here. All of these issues stem from the fact that some code may be executing earlier than it looks like it would. ``` <some-tag/foo/> <effect() { foo.bar = 123; console.log(foo.bar); }/> ``` This does have some drawbacks though and we're trying to determine what all of them might be. The main issue is that we would be evaluating code earlier than it might look like. Eg if `input.name` was somehow a getter and not _pure_ it would be evaluated _once_ before the `effect`, vs now where it's executed actually where you type it. This could also be confusing in cases where the accessor is not static, eg something like the following would actually track the entire `input` object rather than the single property. ```jsx= <effect() { input[dynamicSomeHow] }/> ``` This can technically be considered on it's own merit. ## RANDOM EXAMPLES ```htmlembedded /* loc.marko */ <let/x = null/> <effect() { x = window.location; }/> <return=x/> /* usage */ <loc/location/> <effect() { console.log(location.href) }/> ``` ```htmlembedded= /* loc.marko */ <let/x = null/> <effect() { x = window.location; }/> <return=x/> /* usage */ <loc/location/> <effect() { if (location) { location.href; } }/> ``` ```htmlembedded= /* loc.marko */ <let/x = null/> <effect() { x = window.location; }/> <return=x/> /* usage */ <loc/location/> <const/href=(function() { try { return location.href; } catch (e) { return e })()/> <effect() { if ("MARKO_DEBUG" && href !== location?.href) { throw new Error(`location.href is hoisted out of <effect>, but we received a different value: ${href} vs ${location.href}`) } console.log(href); }/> ``` ```htmlembedded= /* loc.marko */ <let/x = null/> <effect() { x = window.location; }/> <return=x/> /* usage */ <loc/location/> <const/href=hoistedRead(() => location.href)/> <effect() { console.log(href()); }/> ``` ```htmlembedded /* loc.marko */ <return=() => window.location/> /* usage */ <loc/location/> <effect() { console.log(location().href) }/> ``` ``` <return=() => window.location/> ``` <!-- <let/count 0/> <if name[0] === "A"> <await promise> </await> </if> renderBody item, index, all, foo=123, bar="hello" <mytag|item, index, all, { foo, bar }|> --> ```jsx <const/x = (() => { let store = {}; return { update(v) { store = v; }, name() { return store.name; }, age() { return store.age; } } })()/> <effect() { x.update({ name: "Dylan" }); console.log(x.name()); }/> ``` ```jsx <let/x = null/> <div>${x?.stuff}</div> <effect() { x = new Store(); // x; // x.update({ name: "Dylan" }); // console.log(x.name); }/> <button onClick() { x.doThing(); x.stuff; }/> <effect() { if (x !== undefined) { x.doStuff(); } x?.doStuff().a.b.c; console.log(x !== undefined && x.doStuff().a.b.c); doStuff?.(); }/> ``` ```htmlembedded= <let/x = { a: 1 }/> <effect() { x = { a: 2 }; }/> ``` ```htmlembedded <let/count = 1/> <let/y = 1/> <const/store = { data: count }/> <div data-something=y + store.data++/> <div data-something=store.data++/> <if=(store.data > 1)> <effect() { mounted = true; }/> <if=store> $ store.y = 2; </if> ```