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

[WIP][CS2] JSX support (Haml-inspired) #4553

Closed
wants to merge 14 commits into from
Closed

Conversation

helixbass
Copy link
Collaborator

This is not merge-ready (and not necessarily thinking that it should be merged, see below): there are some files included (JSX_TODO and jsx_examples/) that wouldn't be merged but may be of interest and there are some remaining features (from JSX_TODO) that I'd like to implement as part of this. But a PR seems like the right place to go into detail about what I've got working and raise some questions. @GeoffreyBooth: will retarget to 2

I've avoided React out of an aesthetic repulsion to JSX but decided to check it out this past week. Doesn't seem at all bad in a lot of ways but if I'm going to use it, I want to be able to use it nicely in Coffeescript (the nicest language). I love Haml's whitespace-indented syntax and to me the natural way to embed markup in Coffeescript is along those lines. Pretty and clean. JSXY. Normal JSX syntax (using Coffeescript instead of Javascript inside expressions) is supported as well.

Like Haml, elements are specified using %h1 syntax, with shorthands for id and class (aka className) attributes, and attributes can be specified using {} object syntax and/or () more HTML-esque syntax:

#page                            # implicit <div>, => <div id='page'>
  .panel.clearfix                # implicit <div>, => <div className='panel clearfix'>
    %section.container( title='Container' id={@id})    # => <section className="container" title='Container' id={this.id}>
      %form{ @onSubmit }         # => <form onSubmit={this.onSubmit}>
        %input{                  # => <input type={'text'} value={value} onChange={this.onChange}></input>
          type: 'text'           # multiline attributes, inside the {} this gets treated like a CS object literal
          value, @onChange
        }
        %input(                  # => <input type='submit' value='Submit'></input>
          type='submit'          # ()-style attributes can also be multiline
          value='Submit' )
                                 # </form></section></div></div>

At the moment you can't compile the above as-is because line-ending comments aren't supported inside JSX elements - that's high on the TODO list but for now you'd have to strip out the comments.

Element content can be inline or indented. In addition to the JSX {...} expression syntax, you can use Haml-style = syntax (inline or from the start of an indented line, with optional indented expression body) for expressions as well. With Coffeescript's everything-is-an-expression approach, this lets you do things like use a whitespace-indented for loop to generate a list of child elements (as opposed to { list.map( ...callback returning element )}, which you can still use (with {} or = syntax) if you prefer). To nest child elements, you can use Haml-style elements or JSX tags. Like JSX, other element content is treated as text content.

%button{ @onClick } Login           # inline text content, => <button onClick={this.onClick}>Login</button>
%button{ @onClick }= @loginText     # inline expression content using `=`, => <button onClick={this.onClick}>{this.loginText}</button>
%button{ @onClick } {@loginText}    # inline expression content using `{...}`
%button{ @onClick } Please {        # inline text content and expression which goes multiline
  @resources
  .getLoginText()
}
%button{ @onClick }                 # indented body with nested text, element, and expression content
  Please
  %b Please
  { 'Please!' }
  = @loginText
<button onClick={@onClick}>        # same idea for JSX-style tags, you aren't then constrained to JSX-style syntax inside them
  %b Please
  = @loginText
</button>

%ul.ingredients
  = for ingredient, i in @props.ingredients  # generates a for-expression that returns a list of elements
    %li{ key: i }= ingredient
# or alternatively, using `.map()` and `{...}`:
%ul.ingredients
  {@props.ingredients.map (ingredient, i) ->
    %li{ key: i } {ingredient}
  }

To do conditional rendering inside an element body, you can again use = syntax, or {...} if you prefer. Currently if you use = syntax you have to indent in such a way that all subsequent expression lines (including else) are indented with respect to the initial (eg = if ...) line.

render: ->
  {isLoggedIn} = @state

  %div
    %Greeting{ isLoggedIn }
    = if isLoggedIn
      %LogoutButton( onClick={@handleLogoutClick} )
     else
      %LoginButton{ onClick: @handleLoginClick }
    # or:
    {if isLoggedIn
      %LogoutButton( onClick={@handleLogoutClick} )
    else
      %LoginButton{ onClick: @handleLoginClick }
    }

The React docs also suggest the use of && for conditional rendering - you can do that, but = if syntax seems just as clean and more readable:

Mailbox = ({unreadMessages: {length}}) ->
  %div
    %h1 Hello!
    = length and
      %h2 You have {length} unread messages.
    # vs:
    = if length
      %h2 You have {length} unread messages

You can also use postfix if/unless/for (ie comprehension) syntax to an extent -- basically if you use JSX tag syntax for these then you're good to go because the end tag is explicit but if you use Haml-style elements then currently it only treats it as postfix if it immediately follows the tag (ie it can't have content/body):

getButton: ->
  return %LogoutButton if @props.loggedIn
  %LoginButton

render: ->
  .container
    = %Item{ item } for item in @props.list
    # or go nuts:
    = <Item item={item}>Children of any {@kind}</Item> for item in @props.list

So hopefully that's a decent overview of the syntax. I didn't give many examples of JSX-style syntax but it should basically mimic JSX just with Coffeescript in expressions instead of JS. One notable exception as also noted by @xixixao in #4551 is object spread syntax.

As far as how this relates to Coffeescript proper, I'll read some of the other threads that seem to be discussing similar questions but I was sort of figuring the options were (1) merge into 2 despite breaking changes (2) try to have some kind of plugin/option for enabling this within Coffeescript proper, though this does seem hard or (3) maintain this separate from Coffeescript proper. Interestingly, no existing tests (against 1.x) are currently failing, but this would be my understanding of the breaking changes:

  • #id syntax would break most comments that don't have a space after the #
  • %div syntax would break some code that doesn't leave a space after the % operator
  • <div> syntax would break some code that doesn't leave a space after the < operator
  • I'm trying to get a little fancy to allow leading .class implicit-div syntax in some places where it shouldn't be ambiguous with chained .prop syntax. So far I don't think I've introduced anything breaking there but am tempted to make a leading .abc after a blank line be treated as a .class element

My sense (ie my own sensibilities and my understanding of Coffeescript philosophy as espoused by @jashkenas) is that incorporating lots of code (including some breaking changes) just to support a syntax that in a few years may be passe is not the cleanest. However I do feel that this syntax (call it Coffeescript-JSX-Haml or if it's a bad idea to actually use the name Haml then Coffeescript-JSXY) is as Coffeescript-y as you're gonna get with adding JSX. So I wonder what the downsides are of maintaining this separately from Coffeescript proper but (hopefully "officially") promoting it as the (or one) way to use Coffeescript in a React project. Then people with existing (Coffeescript) codebases would just change an NPM dependency, fix any possible breaking changes, and start using the extended syntax? Is the hard part keeping it up to date with Coffeescript proper?

I'd like to go through and comment on the code in detail for @GeoffreyBooth / @lydell / whomever might be reviewing it but that seems premature. And I'll save some of the thoughts on how this might relate to the future of Coffeescript for other threads where this seems to be being discussed, but I basically think if you're gonna add support for this, then make it way nicer than using JSX/Javascript. That was my immediate Reaction and I have to imagine there are lots of other coders out there who may want or have to use React but think JSX is (syntactically, at least) kind of gross and would jump at the chance to use it in a way that's more appealing in the same ways that Coffeescript will always be more appealing than Javascript

@GeoffreyBooth GeoffreyBooth changed the title JSX support (Haml-inspired) [WIP][CS2] JSX support (Haml-inspired) May 18, 2017
o 'JsxElement', -> $1
o '{ Expression }', -> $2
o '{ INDENT Expression OUTDENT }', -> $3
]
Copy link
Collaborator

Choose a reason for hiding this comment

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

There’s over 100 lines of new grammar rules here. This would be a significant burden to maintain.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@GeoffreyBooth I hear you, that's part of what made me uneasy about whether this belongs in Coffeescript proper. I took a closer look at @xixixao's approach in #4551 and it's super elegant/lightweight. But it relies on having a known end tag to be able to treat JSX element bodies like specially-interpolated strings. If you want whitespace-indented tags I think you have to go more heavyweight as far as lexing differently while you're inside an element body and then having grammar rules (at least to the extent of representing the indentation structure). I do think there's a demand for the whitespace-indented version of JSX, it's certainly what I wanted (thus the itch-scratching) and I saw your comments wishing for a Jade-like syntax (which is pretty similar to what I've implemented). But I'm inclined to think that @xixixao's version, being less breaking-changes-y, much more lightweight, and just actual JSX syntactically (with interpolated Coffeescript) is what belongs in Coffeescript proper. And then I'm not sure exactly what constitutes maintaining something like this (releasing a separate NPM package? periodically merging in master/2?) but I'd think that I could maintain this syntactically sweeter version separately and try to make it visible for those who might prefer it, what do you think?

@GeoffreyBooth
Copy link
Collaborator

Yeah, I’m inclined to agree. Actually seeing the proposed syntax for both versions, I find myself unexpectedly preferring the </>-based version, because it’s more obvious what code is part of a “template” and what is really code. I guess it makes sense to think of the XML tags as like a template, or interpolated string, as the other PR does. And once you’re thinking of JSX blocks as conceptually equivalent to interpolated strings/template literals, it makes sense to think of < and > as the delimiters like quotation marks.

I think the way forward for the HAML-like version is to follow up on #4540 and implement a plugin architecture that could supply sufficient hooks to achieve what you’re doing here, without the code from this PR becoming part of the compiler. I recognize that this would be a significant challenge and I’m not sure if it’s even possible, but it would be a future-proof way of implementing not just this syntax but any other templating languages or other customizations people want that don’t belong in CoffeeScript proper. If you could create hooks that support this as a plugin, then presumably almost anything people would want to do should be possible as a plugin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants