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

Add support for JSX fragment syntax #19249

Merged
merged 11 commits into from
Oct 31, 2017

Conversation

uniqueiniquity
Copy link
Contributor

Fixes #19094, which in turn reflects facebook/jsx#93.

This PR adds support for the <>...</> syntax for fragments in JSX. Under preserve emit, the syntax is maintained, and under react emit, fragment blocks are transformed into React.createElement(React.Fragment, null, ...) (and are thus affected by --jsxFactory). Due to similarities with the JSXElement syntax, the implementation shares much of the same code where appropriate.

// which is a type of the parameter of the signature we are trying out.
// If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName
const attributesType = getContextualType(jsxAttributes);
if (jsxAttributes) {
Copy link
Member

Choose a reason for hiding this comment

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

Just do an early return of undefined here if jsxAttributes is undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed.

@@ -14731,14 +14741,19 @@ namespace ts {
}
}

function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) {
checkGrammarJsxElement(node);
function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) {
Copy link
Member

Choose a reason for hiding this comment

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

No need to rename this - a fragment is an element (IMHO?)

Copy link
Member

Choose a reason for hiding this comment

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

I take it back

else if (opening.kind === SyntaxKind.JsxOpeningFragment) {
const node = <JsxFragment>createNode(SyntaxKind.JsxFragment, opening.pos);
node.openingFragment = opening;

Copy link
Member

Choose a reason for hiding this comment

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

Nit extra newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

@@ -4040,7 +4058,13 @@ namespace ts {
else if (token() === SyntaxKind.EndOfFileToken) {
// If we hit EOF, issue the error at the tag that lacks the closing element
// rather than at the end of the file (which is useless)
parseErrorAtPosition(openingTagName.pos, openingTagName.end - openingTagName.pos, Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, getTextOfNodeFromSourceText(sourceText, openingTagName));
if (isJsxOpeningElement(openingTag)) {
Copy link
Member

Choose a reason for hiding this comment

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

Reading this whole thing it seems like it'd be clearer if we wrote e.g. if (isJsxFragment(openingTag)) instead of isJsxOpeningElement since the former is more the thing we're special-casing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Restructured.

@@ -4179,6 +4209,23 @@ namespace ts {
return finishNode(node);
}

function parseJsxClosingFragment(inExpressionContext: boolean): JsxClosingFragment {
Copy link
Member

Choose a reason for hiding this comment

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

Reading the spec, I don't see how we'd legally get into the inExpressionContext = false case since you can't write <foo><><div /></></foo>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The spec changed, now you can.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -11,6 +11,11 @@ namespace ts {
return token >= SyntaxKind.Identifier;
}

/* @internal */
export function tokenIsIdentifierOrKeywordOrGreaterThan(token: SyntaxKind): boolean {
return token === SyntaxKind.GreaterThanToken || token >= SyntaxKind.Identifier;
Copy link
Member

@RyanCavanaugh RyanCavanaugh Oct 17, 2017

Choose a reason for hiding this comment

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

Call tokenIsIdentifierOrKeyword in the right arm in case that logic changes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@@ -1618,7 +1621,7 @@ namespace ts {
closingElement: JsxClosingElement;
}

/// Either the opening tag in a <Tag>...</Tag> pair, or the lone <Tag /> in a self-closing form
/// Either the opening tag in a <Tag>...</Tag> pair, the opening tag in a <>...</> pair, or the lone <Tag /> in a self-closing form
Copy link
Member

Choose a reason for hiding this comment

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

This comment doesn't match our usage/terminology of isJsxOpeningLikeElement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

}

// OK
let k1 = <Comp a={10} b="hi"><></><Button /><AnotherButton /></Comp>;
Copy link
Member

Choose a reason for hiding this comment

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

This should be a parse error

Copy link
Member

Choose a reason for hiding this comment

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

Or perhaps we should parse it leniently but issue a grammar error in checking

Copy link
Member

Choose a reason for hiding this comment

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

(disregard)

@uniqueiniquity
Copy link
Contributor Author

@mhegazy could you take a look at this?

@uniqueiniquity
Copy link
Contributor Author

Editor actions work as expected in VS and VS Code (i.e., go to def, find all references, etc are basically no-ops or return no results). I will be updating the syntax highlighter accordingly.

// If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName
const attributesType = getContextualType(jsxAttributes);
if (!jsxAttributes) {
return anyType; // don't check children of a fragment
Copy link
Member

Choose a reason for hiding this comment

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

It's better to return undefined than anyType here because it will cause the calling code to bail out earlier

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed.

@uniqueiniquity
Copy link
Contributor Author

Tracking syntax highlighter changes for VS Code at microsoft/TypeScript-TmLanguage#534.

const tagName = createPropertyAccess(
createReactNamespace(reactNamespace, parentElement),
"Fragment"
);
Copy link

@Jessidhia Jessidhia Oct 19, 2017

Choose a reason for hiding this comment

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

Should this perhaps be customizable, similar to how --jsxFactory is? e.g. --jsxFragmentComponent?

Arguably not useful for anything but React and React clones, though; as other less similar frameworks would want to do other special processing on fragments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with your second point here; to take the example of Mithril, the framework supports fragments, but with a completely different approach than React will (i.e. m.fragment(attrs, children)) so it's not as convenient as simply replacing the call to createElement with --jsxFactory. It seems to make sense in this case to just preserve the JSX syntax as is for non-React libs and have each define their own transpilation depending on their implementation of fragments.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Kovensky do you know what babel will do here? would it support another version of the paragma for the React.createElement?

Copy link
Contributor

Choose a reason for hiding this comment

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

checked with @hzoo over slack and does not seem that babel have plans for this in the immediate term.
I wounder if we should just make it an error to use Fragment + --jsx React + --jsxNamespace

Copy link
Contributor

Choose a reason for hiding this comment

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

@uniqueiniquity can you file an issue for adding the error.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I think I see now. I'm confusing jsxFactory and reactNamespace

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So are you on board with issuing the error?

Copy link
Member

Choose a reason for hiding this comment

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

Probably, but feel free not to block this PR on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the PR to issue an error.

Copy link

Choose a reason for hiding this comment

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

}

// OK
let k4 = <SingleChildComp a={10} b="hi"><><Button /><AnotherButton /></></SingleChildComp>;

Choose a reason for hiding this comment

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

I'd kinda prefer for these to be a different type like, say, JSX.Fragment, but having one would not be useful anyway without nominal typing.

Copy link
Member

Choose a reason for hiding this comment

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

We'd like to make JSX.Element generic in the future - possibly looking it up and instantiating it based on the return type of the factory function; that would handle the Fragment case somewhat elegantly.

Copy link

@SMotaal SMotaal Nov 14, 2017

Choose a reason for hiding this comment

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

Generic JSX.Element<T> would be great. Outside of the React bubble, TypeScript-oriented developers like myself often wonder why the disconnect.

"category": "Error",
"code": 17014
},
"Expected corresponding JSX fragment closing tag.": {
Copy link
Member

Choose a reason for hiding this comment

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

Expected corresponding closing tag for JSX fragment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.


if (isJsxOpeningElement(node)) {
emitJsxTagName(node.tagName);
writeIfAny(node.attributes.properties, " ");
Copy link
Member

Choose a reason for hiding this comment

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

You should move this into the below if check and replace the writeIfAny with a write

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved.

@DanielRosenwasser
Copy link
Member

Looks reasonable apart from the nits I requested changes on. 👍

@@ -13467,9 +13467,15 @@ namespace ts {

function getContextualTypeForJsxExpression(node: JsxExpression): Type {
// JSX expression can appear in two position : JSX Element's children or JSX attribute
const jsxAttributes = isJsxAttributeLike(node.parent) ?
const jsxAttributes: JsxAttributes = isJsxAttributeLike(node.parent) ?
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need this type annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

This was referenced Feb 2, 2018
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
10 participants