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

Several JSX fixes (addresses #73) #123

Merged
merged 5 commits into from Jan 13, 2017
Merged

Conversation

rattrayalex
Copy link
Collaborator

I wasn't sure where to add the new test cases, so I just threw them as separate files in the prettier dir. Happy to move/rename etc as desired.

@@ -0,0 +1 @@
<BaseForm url="/auth/google" method="GET" colour="blue" size="large" submitLabel="Sign in with Google"/>
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you want to move these under a new tests/jsx directory?

Copy link
Member

Choose a reason for hiding this comment

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

The tests situation is a little weird, but I'm hesitant to touch much outside of the prettier directory because all of those tests are Flow's test suite. In the future we should be able to remove our version and copy in the latest Flow tests.

@rattrayalex rattrayalex mentioned this pull request Jan 11, 2017
11 tasks
colour=\"blue\"
size=\"large\"
submitLabel=\"Sign in with Google\"
>
Copy link
Contributor

Choose a reason for hiding this comment

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

Most conventions I've seen is to put the > in the same line as the last attribute. This is likely going to be a bit trickier to implement but I think that it's important.

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Jan 11, 2017 via email

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Jan 11, 2017 via email

@vjeux
Copy link
Contributor

vjeux commented Jan 11, 2017

Yeah, probably warrants some discussions. I didn't know that Airbnb recommended this.

On the React docs we use the one I suggested and across our codebase at Facebook as well.
https://facebook.github.io/react/docs/optimizing-performance.html#examples

@gaearon
Copy link

gaearon commented Jan 11, 2017

TBH I don't understand why we do this at Facebook. Putting it on the new line means less friction when adding/removing attributes, and is more diff-friendly. Also easier to read when there is a bunch of attributes and children.

@rattrayalex
Copy link
Collaborator Author

Airbnb doesn't offer any justification, nor is there any in the eslint rule docs.

Would it be easy to find out why FB does this? Would it be possible to change FB's internal guide (so that prettier could be adopted within FB even with this change)?

@jlongster
Copy link
Member

I agree, see this example:

      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>

You don't really see where the opening tag ends. It looks like Count is an attribute. However, if Facebook is interested in using this, I want to make sure we talk about this more.

@vjeux
Copy link
Contributor

vjeux commented Jan 11, 2017

I don't think that there's a good logical argument for > on the last line. This dates back to XHP days.

Now, this is a pretty visible change and there's already going to be a lot of friction for integrating prettier inside of the Facebook codebase so I would love to avoid a big debate on this and if we could get either use this form or get an option for it at the very least.

@jlongster
Copy link
Member

It'd be an easy option to make, but need to decide if we should have that option or not. If that's the only thing that would stop Facebook from adopting this it's tempting to make it an option, but I also want to be fair to other users who want other options.

@vjeux What do you think about putting > on a new line by default, with the caveat that you will talk with your coworkers and see if this is the only thing stopping them from using it and we can follow-up with possibly making it an option?

@vjeux
Copy link
Contributor

vjeux commented Jan 11, 2017

Let's do that. There is a bunch more blockers until we can actually use it. When that list is down to something where we can actually test it, we'll see how people react.

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Jan 11, 2017 via email

@rattrayalex
Copy link
Collaborator Author

I added a second commit to this PR fixing parens. Let me know if I should split that out to a separate PR.

submitLabel=\"Sign in with Google\"
/>;

long_open = <BaseForm
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious, since you add () around arrow functions, should you also do it here for consistency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Possibly...

In this case, I'm only using variables so that one file can contain multiple JSX statements, which otherwise can't be siblings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That would seem to conform to AirBnB's guide so sure why not!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm... I should probably refactor this (and any others like the corresponding code in ReturnStatement) within JSXElement itself... might be tricky...

@vjeux
Copy link
Contributor

vjeux commented Jan 11, 2017

I really like your approach of going through the Airbnb style-guide as a reference when in doubt :)

@rattrayalex
Copy link
Collaborator Author

Also added a fix for #73 (comment) , though using atrr={"\""} instead of attr="&#34;", as recommended in the react docs.

@rattrayalex rattrayalex changed the title Break JSXOpeningElement between attributes (fixes #15) Several JSX fixes (addresses #73) Jan 12, 2017
@jlongster
Copy link
Member

@rattrayalex This will conflict with #126, any chance you could remove the commits that do the escaping? I'm not sure I want to force the {"\""} but rather just keep the original code. (I'm not sure if you will convert attr="&#34;" to atrr={"\""} automatically, haven't looked too closely at this code)

Copy link
Member

@jlongster jlongster left a comment

Choose a reason for hiding this comment

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

See above

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Jan 12, 2017

Yep can do!

indent(options.tabWidth, concat([ line, path.call(print, "body") ])),
]));
}
return conditionalGroup(groups);
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer avoiding conditionalGroup in this special case which makes this code path a little clearer:

const body = path.call(print, "body");
const collapsed = concat([ concat(parts), " ", body ]);

if (n.body.type === "JSXElement") {
  return group(collapsed);
}

return conditionalGroup([
  collapsed,
  groups.push(concat([
    concat(parts),
    indent(options.tabWidth, concat([ line, body ])),
  ]));
])

Also note that I lifted path.call(print, "body") out which is very important. I wrote that code so it's my fault, but when using conditionalGroup you should never print the same node twice because it makes the intermediate representation explode in size (we want to share sub-trees).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

awesome, will do!

hah, I'd originally wrote it with body separated as you have now, and changed it back. Good to hear of the tradeoffs

")",
]));

return conditionalGroup(groups);
Copy link
Member

Choose a reason for hiding this comment

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

I don't think you need conditionalGroup here. Use conditionalGroup only as a last resort :) (I plan on writing more documentation about all this)

conditionalGroup is only needed when you really need manual control over how things are displayed when they don't fit. The majority of use cases fits into these 2 categories though:

  1. You want to break something across lines and indent if it doesn't fit (this assumes nothing inside of it has "hard" lines, meaning all contents try to fit on the same line)
  2. You want to break something across lines if any of its children are broken across lines (they have "hard" lines)

group solves #1 easily: use indent and line and the printer, when the group doesn't fit, will output newlines where line was used and it will be indented an extra level.

I made multilineGroup for #2 because it's a very common use case. It will see if any of its contents have hard lines, and if they do, the group will break. Hard lines are lines that emit a newline no matter what. The reason we don't have to worry about normal breaking is because this group is always larger than it's contents -- if anything inside of it naturally breaks, this group will already have broken as well.

There are some rare cases where you want to let something inside of you break, but not break the group, and control indentation rules in each specific case. That's what conditionalGroup is for, but we should use it rarely as it has a performance cost.

There's one last character in this cast: ifBreak. This is a very simple construct that does what it says: if the group break, include this content. You can say ifBreak(",") and it will only include the comma if the group has broken. The solves some very simple use cases.

Will all that said, I think this should work:

multilineGroup(concat([
  ifBreak("("),
  indent(options.tabWidth, concat([ softline, elem ])),
  softline,
  ifBreak(")"),
]))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's extremely helpful! Also sounds like a great cleanup. Thanks!


const render5 = ({ styles }) => <div>Keep it on one line.</div>;

const notJSX = (aaaaaaaaaaaaaaaaa, bbbbbbbbbbb) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

This is glorious

@@ -0,0 +1 @@
blah
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove ;)

Copy link
Member

Choose a reason for hiding this comment

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

I think this is the only thing left to do, right? Once you remove this, is this ready to be merged? Also check the test failures (slow to respond right now, will focus more later today)

@jlongster
Copy link
Member

@rattrayalex When you rebase locally, you should be able to just git push -f ?

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Jan 12, 2017

Sorry, I hadn't run jest -u after the most recent change, and found another edge case that refactored code didn't handle as well... the underlying issue was a separate problem, which I am now fixing, and hope to add to this PR if that's ok.

(children need to be broken up if the open element is multiline)

@jlongster
Copy link
Member

Sure, feel free to add any small fixes to this PR. I wanted to do another pass at JSX support and improve many things at once, and this is basically that! Thanks.

@rattrayalex
Copy link
Collaborator Author

Awesome! have a few more small fixes in the pipeline, should be done by EOD or tomorrow.

const firstChild = children[0];
const lastChild = util.getLast(children);
const beginBreak = firstChild && firstChild.type === "line";
const endBreak = lastChild && lastChild.type === "line";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jlongster my beginBreak / endBreak logic here felt a bit awkward – I'm adding a softline unless there's already a line. Is there a cleaner way to do that?

Copy link
Member

Choose a reason for hiding this comment

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

@rattrayalex Sorry I didn't review this until now.

I'm not entirely sure of the logic you're going for here, but is util.newlineExistsBefore helpful? If you're trying to emulate the breaking behavior in the original source, that and util.newlineExistsAfter allows you to peek into the original source and see if a node is on a newline or not. Here's how you use it:

util.newlineExistsBefore(text, util.locStart(node))

What this does is it scans backward until it hits either a newline or a non-whitespace character. If it hits a newline, it reports true, otherwise it's false.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's definitely helpful to know about, though in this case I think the lines could be inserted by prettier, not just from the original source. So I think it wouldn't be adequate in this case.

But it would be very helpful when deciding whether {' '} needs to be inserted, if that's a route we go.

attr4
>
ddd d dd d d dddd dddd <strong>hello</strong>
</div><strong>hello</strong>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fwiw, I'm trying to fix this now (bump siblings to own line)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I couldn't figure out a clean solution to this, so leaving for now.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can do that. We would be adding significant whitespace, which changes the program. That's a limitation because of how JSX works; we aren't as free to move things around as other things.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As it turns out, adding newlines between JSX nodes doesn't add whitespace between the nodes to the compiled HTML. So that would be a safe change to make in this case.

Copy link
Member

Choose a reason for hiding this comment

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

But it would have to be a newline & a bunch of spaces for indentation. How could that not add whitespace?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

JSX isn't HTML, the elements compile to function calls:

React.createElement('div', {}, [
  React.createElement('span', {}, ['hello world']),
  React.createElement('strong', {}, ['I am a sibling'])
])

^^ there's no whitespace between the <span /> and the <strong /> above. That would render to:

<div><span>hello world</span><strong>I am a sibling</strong></div>

const render7 = () => (
<div>
<span />
<span>Break each elem onto its own line.</span>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm pretty iffy on this. There don't seem to be any eslint rules about preventing multiple JSX elements on one line...

I think I'll cut this, even if it means having this on the same line:

</div><span>should really be next line</span>

though if anyone can figure out a solution to that case, I'm all ears

@rattrayalex
Copy link
Collaborator Author

Hmm, the other challenges I had hoped to tackle look a bit too challenging for my current knowledge of the tools at hand. (Actually, I suspect they may not be possible with the current tools at hand).

Specifically:

  • inserting {' '} only on artificial line breaks, and otherwise a literal , doesn't seem possible. ifBreak("{' '}") will add the element because of breaks higher up the tree (not just immediately before/after the element) and enclosing jsx children in a group or multilineGroup breaks the parent jsx element multilineGroup.
  • as mentioned in General JSX Improvements #73 (comment) , dedenting the JSXExpressionContainer's closing }, possibly along with other punctuation, isn't something I was able to figure out how to do.
  • as mentioned above, breaking </div><strong>hello</strong> after </div> is beyond me for now.

Would appreciate guidance, though for now my preference would be to merge this PR. Perhaps more skilled hands than mine could take up the remainder of this work thereafter.

@jlongster
Copy link
Member

Thanks a lot for working on this. It's definitely fine to do other things as followup. I'll look at this soon.

@vjeux
Copy link
Contributor

vjeux commented Jan 13, 2017

FYI, we recently rewrote the JSX documentation to explain how whitespace works:

JSX removes whitespace at the beginning and ending of a line. It also removes blank lines. New lines adjacent to tags are removed; new lines that occur in the middle of string literals are condensed into a single space. So these all render to the same thing:

<div>Hello World</div>

<div>
  Hello World
</div>

<div>
  Hello
  World
</div>

<div>

  Hello World
</div>

https://facebook.github.io/react/docs/jsx-in-depth.html#children-in-jsx

@jlongster
Copy link
Member

@vjeux @rattrayalex oh, shows how much I know about JSX 😆 I'm just now starting to use it, not that I can consistently format it.

I'm not sure we want to be aggressive about bumping tags to newlines. JSX seems like something that the programmer lays out specifically, so we should be more lenient about respecting JSX. There was someone else talking about not doing this specifically (can't remember where) so that if you had a big paragraph you could have <strong> tags inside it and it would work fine.

@jlongster
Copy link
Member

@rattrayalex Can you look at my one comment and see if those utility functions are what you're after?

Once we get that comment resolved, I will merge this. Thanks so much for all the hard work!

@jlongster
Copy link
Member

Alright this has been sitting for too long. I'm eager to get it in and make a release, so I'll go ahead and merge it. Thanks for the excellent work @rattrayalex. This will make prettier usuable for JSX folks!

I'm going to follow-up with the comments I made and possibly make some PRs myself. But if everything mostly checks out, I'll push a new release.

@jlongster jlongster merged commit 47102bc into prettier:master Jan 13, 2017
@lock lock bot added the locked-due-to-inactivity Please open a new issue and fill out the template instead of commenting. label Jan 21, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Jan 21, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
locked-due-to-inactivity Please open a new issue and fill out the template instead of commenting.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants