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

JSX children as code #561

Closed
texastoland opened this issue Jul 2, 2023 · 13 comments · Fixed by #1386
Closed

JSX children as code #561

texastoland opened this issue Jul 2, 2023 · 13 comments · Fixed by #1386
Labels
proposal Proposal or discussion about a significant language feature

Comments

@texastoland
Copy link

texastoland commented Jul 2, 2023

I ran into a confusing DX issue experimenting with JSX in the playground.

Working example:

const Component = (items: Item[]) =>
  <ul>
    <For each=items()>
      (item) =>
        <li>{
          if item.type === "link"
            <a href=item.url>some link title: {item.title}
          else
            <p>{item.content}
        }

Despite being a React instructor (but also CoffeeScript in the past) I intuitively expected Civet to work without braces.

Broken example:

const Component = (items: Item[]) =>
  <ul>
    for item of items
      <li>
        if item.type === "link"
          <a href=item.url>some link title: {item.title}
        else
          <p>{item.content}

I think code would be a better default for all JSX nodes not only https://civet.dev/cheatsheet#function-children as in:

Proposed example:

const Component = (items: Item[]) =>
  <ul>
    for item of items
      <li>
        if item.type === "link"
          <a href=item.url> "some link title: " item.title
        else
          <p> item.content

Alternatively signal code children with a marker like do:

const Component = (items: Item[]) =>
  <ul> do
    for item of items
      <li> do
        if item.type === "link"
          <a href=item.url>some link title: {item.title}
        else
          <p>{item.content}
@edemaine
Copy link
Collaborator

edemaine commented Jul 6, 2023

Yes, we've discussed something similar in the #jsx channel on Discord. Thanks for putting this proposal together though!

One exception with your proposed example: I don't think "some link title: " item.title should work. (This looks like a potential function call...?) More intuitive to me would be "some link title: " + item.title or `some link title: ${item.title}`.

Multiple Children Code Blocks

But I think the bigger issue is how to have multiple code children. Digging up old posts, here's an example I put together with a proposal for treating each top-level line as its own braced block:

<div>
  user := getUser()
  if user?
    {name} = user
    <h1> `Welcome ${name}!`
  <h2>Posts</h2>
  for post of posts
    <div .post> post.jsx()
↓↓↓
let user
<div>
  {user = getUser(), void 0}
  {user != null ?
    {name} = user,
    <h1> {`Welcome ${user.name}!`} </h1>
  : void 0
  }
  <h2>Posts</h2>
  {posts.map(post =>
    <div class="post"> {post.jsx()}
  }
</div>

Basically the rule is "wrap each non-JSX child of JSX with braces". If you wanted multiple lines to correspond to a single braced child, you could wrap in do. The tricky work here is hoisting the let user out of the user := getUser() code, which should maybe be the second thing to do. But even that should be doable; we do similar things with if user := getUser() today.

Same-Line Children

Another issue is whether we want same-line children to behave differently from different-line children. For example, you could imagine <h1>Hello produces a string when Hello is on the same line as <h1>. Or it could be a flag...

Automatic Fragments with Strings

Another proposal I see is to automatically wrap strings and tags in a fragment, like so:

<span>
  if message.deleted
    " "
    <span .label.label-danger>Deleted
↓↓↓
<span>
  {message.deleted ?
    <>
      {" "}
      <span .label.label-danger>Deleted</span>
    </>
  : void 0}
</span>

Or maybe " " and <span .label.label-danger>Deleted need to be on the same line? But then it's harder to add a trailing space.

Let us know what you think about these further ideas/extensions!

@texastoland
Copy link
Author

texastoland commented Jul 7, 2023

My original XY problem is I want to map (or for 🤯)/if/switch without pairing (or remembering) braces (curlies or round). Any solution that accomplishes that would be a substantial DX improvement over JSX 🍾

@texastoland
Copy link
Author

I reread your feedback 🤓

I don't think "some link title: " item.title should work.

Agreed 👍🏼

Basically the rule is "wrap each non-JSX child of JSX with braces".

Your example makes sense!

The tricky work here is hoisting ...

Example (I didn't follow)?

Let us know what you think about these further ideas/extensions!

Treating raw strings like Hello as identifiers and quoted strings as … well strings solves ambiguities/spacing. It's how ReScript currently works based on multiple prior JSX proposals (including 1 by Seb).

<div>
  user := getUser()

  if user?
    {name} = user
    <h1> `Welcome ${name}!`

  <h2> "Posts"

  for post of posts
    <div .post> post.jsx()

For context the h2 in ReScript would almost be:

<h2> "Posts" </h2>

Except it must be wrapped to satisfy types which requires braces again:

<h2> {"Posts"->React.string} </h2>

@edemaine edemaine added the proposal Proposal or discussion about a significant language feature label Jul 15, 2023
@edemaine
Copy link
Collaborator

edemaine commented Nov 11, 2023

I have a new related proposal, which is to use > to enter an indented code block — essentially a new way to go from XML mode to Civet mode, like { but that doesn't need a closing }. This is similar to the proposal "Alternatively signal code children with a marker like do" in the root post, but with > in place of do. The motivations for > are:

  • > is invalid in JSX text, so this isn't ambiguous. (The same reason we can automatically wrap arrow functions in braces.)
  • > is like the second half of an arrow, which denotes code.
  • > is like Markdown's quoting syntax (which quotes until the next dedent or blank line).
  • > intuitively means "do this thing to the right". FWIW, I actually came up with it first when thinking what a natural character. It's used in a lot of prompts etc.

Examples:

Component := (items: Item[]) =>
  <ul>>  // reminds me of Pug's trailing dot
    for item of items
      <li>>
        if item.type === "link"
          <a href=item.url>some link title: >item.title
        else
          <p>>item.content
<div>
  > if props.warning
    <div .warning>
      >props.warning
  > if loggedIn()
    <Inbox>
    <Logout>
  > else  // special case to allow an else at same indentation level
    <Login>
↓↓↓
<div>
  {props.warning ?
    <div class="warning">
      {props.warning}
    </div> : void 0
  }
  {loggedIn() ?
    <>
      <Inbox/>
      <Logout/>
    </>
  : <Login/>
 } 
</div>
<label>
  Comment
  >if count > 1
    's'
  :
↓↓↓
<label>
  Comment
  {count > 1 ? 's' : void 0}
  :
</label>

We could allow > for property values too. This looks a little weird, because > also means "end the tag", but it's not ambiguous after an =:

<Show when=>
  data = getData()
  data.has data.stuff
fallback=>
  <div>Nothing to see here.
>
  Welcome!
↓↓↓
<Show when={
  data = getData(),
  data.has(data.stuff)
} fallback={
  <div>Nothing to see here.</div>
}>
  Welcome!
</Show>

I think I would personally prefer explicit blocks like this, as opposed to implicitly treating all children as code, because it makes it easy to be explicit about when you want multiple code blocks (as above) vs. when you want one code block that requires multiple lines:

>do
  data := getData() |> formatData
  data.sort()
  for item of data
    <li>>item.title

The other advantage is that it's fully backward compatible, so we can do it without a flag like "civet jsxCode". That said, I think there's room for both approaches; I'm just more clear on what the > feature looks like.

In "civet jsxCode" mode, there's a question of whether the children block should be treated as one code block or as multiple. Above I proposed that each top-level statement become its own block, which is usually what I want, but I admit this isn't particularly intuitive. (You could always wrap in do to combine statements.) Alternatively, Daniel seems to be leaning toward a single code block, which could return an array if you want to return multiple results. (This works for React but not fine-grained systems like Solid.) If we go this way, I think a nice add-on would be for top-level bulleted arrays (#803) to automatically be treated like multiple code blocks:

<div>
  . if props.warning
    <div .warning>
      . 'Warning: '
      . props.warning
  . if loggedIn()
      <Inbox>
      <Logout>
    else
      <Login>
↓↓↓
<div>
  {props.warning ?
    <div class="warning">
      {'Warning: '}
      {props.warning}
    </div> : void 0
  }
  {loggedIn() ?
    <>
      <Inbox/>
      <Logout/>
    </>
  : <Login/>
 } 
</div>

@texastoland
Copy link
Author

My 2¢ with a grain of salt because I'm not actively using Civet 🙏🏼

I like the concept of entering curly mode via some marker. But >> and >= look confusing to me. I think as programmers we've formed an intuition they're always balanced like quotes or brackets. I'm sure there's a reason do wouldn't work here but couldn't dig it up in the thread?

Alternatively (brainstorming inspired by Pug/Vue hybrid) := might be rare enough to not conflict with user code:

<div>:=
  if props.warning <div .warning>:= props.warning
  if loggedIn()
    <Inbox>
    <Logout>
  else <Login>
<Show
  when:=
    data = getData()
    data.has data.stuff
  fallback:= <div> Nothing to see here.
> Welcome!

@edemaine
Copy link
Collaborator

when:= looks quite nice for blocks of code assigned to attributes. On the other hand, I find := a little weird for children within tags. I'm also not sure it would work well for multiple code-block children; I think your first example would have to be:

<div>
  := if props.warning then <div .warning>:= props.warning
  := if loggedIn()
    <Inbox>
    <Logout>
  else <Login>

(otherwise the code block would be treated as a single code block with one return value, whereas you need multiple here)

Note that := has a specific meaning in Civet, which is const definition. I'm not sure it's the right connotation here, but maybe we can come up with some different notation that everyone would enjoy...


Unrelated, I had another idea for "civet jsxCode" mode (where children are code by default), which is to use --- separators between code blocks. Thus:

<div>
  if props.warning
    <div .warning>
      'Warning: '
      ---
      props.warning
  ---
  if loggedIn()
    <Inbox>
    <Logout>
  else
    <Login>

Currently --- has no meaning in code, and it's somewhat natural given YAML/Markdown separators. (I used one earlier on in this message!) Conversely, --- does take a lot of space. It also might be easy to forget one (but > and other notations have the same issue).

@edemaine
Copy link
Collaborator

By the way, back to an unambiguous backward-compatible notation, an option other than > would be &, which in JSX can only be used in special ways (character codes, in particular with a semicolon within the same word). Does this look any better?

<Show when=&
  data = getData()
  data.has data.stuff
fallback=&
  <div>Nothing to see here.
>
  Welcome!
<div>
  & if props.warning
    <div .warning>
      &props.warning
  & if loggedIn()
    <Inbox>
    <Logout>
  else
    <Login>

I like that it's a different character from <> of tags.
The obvious connection is to Civet's & shorthand function blocks. On the one hand, these are code blocks, so it seems like a reasonable similarity. On the other hand, this use of & behaves very differently; & currently refers to the function parameter, which it isn't doing here.

Some other ideas for other symbols:

  • &: (backward compatible too, but not sure it's very natural i.e. it's ugly)
  • : (Pythonic, not backward compatible)
  • = (Excel vibes! not backward compatible)
  • . or (as above mentioned, naturally extends bullet notation Bulleted arrays #803, also like the opposite of Pug's . notation; . is not backward compatible)

I'm wondering whether we could have two options for breaking backward compatibility:

  • One option "civet jsxBullets" to enable notation like say . to enter code mode, at the cost of . not being writable in JSX text without quoting (probably &period; is nicest; otherwise, {'.'} or .'.').
  • Another option "civet jsxCode" to make children code by default, so you use strings for text content, and multiple code children can be specified via multiple bullets (.) and possibly --- separators.

@texastoland
Copy link
Author

texastoland commented Nov 28, 2023

I think do is most compelling to me. --- takes second though. A lot of your examples add a character to the beginning of each line. My intuition would be that a character after a tag would apply to the entire nested scope. Re. backwards compatibility you aren't 1.0 yet right? Not sure if you could provide a code mod for yourself and other users. Anyway keep up the interesting work!

@mixty1
Copy link

mixty1 commented Dec 6, 2023

I haven’t fully delved into the problems of this issue, but I think that it is still necessary to somehow make the following syntax possible:

<div>
  if items
    <h1> "List of items:"
    <ul> for item in items
      <li> <span> item
  else
    <span> "No items found"

or as the author of the issue suggested:

const Component = (items: Item[]) =>
  <ul>
     for item of items
       <li>
         if item.type === "link"
           <a href=item.url>some link title: {item.title}
         else
           <p>{item.content}

Here's how in Imba language.
Of course, it’s not jsx, but this syntax is much nicer than prefixes of different characters (. , & = := ; -) in each line of an expression.

Is it very difficult to implement this?

<div>
  user := getUser()
  if user?
    {name} = user
    <h1> `Welcome ${name}!`
  <h2>Posts</h2>
  for post of posts
    <div .post> post.jsx()
↓↓↓
let user
<div>
  {user = getUser(), void 0}
  {user != null ?
    {name} = user,
    <h1> {`Welcome ${user.name}!`} </h1>
  : void 0
  }
  <h2>Posts</h2>
  {posts.map(post =>
    <div class="post"> {post.jsx()}
  }
</div>

@edemaine
Copy link
Collaborator

edemaine commented Dec 6, 2023

If I understand correctly, this is the same as my original proposal. Yes, I think this should be an option, via a directive of "civet jsxCode" or "civet jsxCodeMulti" or something (to distinguish between wanting one code block child vs. multiple code block children).

@mixty1
Copy link

mixty1 commented Dec 6, 2023

Yeah, even if it is an additional option like jsxCode, it will be cool.
My main desire is to avoid of any characters(. , & := ; etc.) at the beginning of each line.
like this:

<div>
  & if props.warning
    <div .warning>
      &props.warning
  & if loggedIn()
    <Inbox>
    <Logout>
  else
    <Login>

or this:

<div>
  if props.warning
    <div .warning>
      'Warning: '
      ---
      props.warning
  ---
  if loggedIn()
    <Inbox>
    <Logout>
  else
    <Login>
<div>
  . if props.warning
    <div .warning>
      . 'Warning: '
      . props.warning
  . if loggedIn()
      <Inbox>
      <Logout>
    else
      <Login>

Anyway, thanks for your work. I really like civet 🙂

@texastoland
Copy link
Author

texastoland commented Dec 6, 2023

Here's how in Imba language

FWIW Imba is another Coffee fork I quite enjoy for toy projects that probably influenced my expectations how Civet would behave (as well as ReScript mentioned in the description).

@cie
Copy link

cie commented Jan 31, 2024

Hi all! An idea..

const Component = (items: Item[]) =>
  <ul>
    {for item of items}
      <li>
        {if item.type === "link"}
          <a href=item.url>some link title: {item.title}
        {else}
          <p>{item.content}

A new syntactic construct is introduced: JSX {} with indented children. It treats the children as if it was a <></> fragment indented into the contents of {}. But if an indented {} contains a continuation of a statement (else, case/when, patterns in switch, while after do), it is treated as part of that statement.

Other examples:

<ul>
  {if !items.length}
    No items.
  {else items.map (item) =>}
    <li>{item.text}

<ResizeObserver>
  {({width, height}) =>}
    {width}x{height}
    
<span>
  {&.toLowerCase()}
    {name}

<div>
  {switch value}
    {{type: "user"}}
      {value.name}
    {else}
      Invalid type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal Proposal or discussion about a significant language feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants