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

[Select] Create-able Select components #3306

Closed
wants to merge 16 commits into from

Conversation

shuyangli
Copy link
Contributor

@shuyangli shuyangli commented Jan 21, 2019

Fixes #1710

  • Includes tests
  • Update documentation

Changes proposed in this pull request:

  • Select components, via IListItemsProps, now takes two additional props:
    • createNewItemFromQuery?: (query: string) => T which converts a query to a new item; if this prop is present, the select component is considered "create-able";
    • createNewItemRenderer?: (query: string, handleClick: React.MouseEventHandler<HTMLElement>) => JSX.Element | undefined which could be used to render a custom MenuItem at the end of the list, that represents "create a new element from query".
  • If these props are provided, when the component has a non-empty input, a "create new item" entry is rendered at the end of the filtered item list using the given createNewItemRenderer, and the user can activate it to create a new item with the current query.
  • If the component is create-able, when a query returns no results, pressing "enter" key will create a new item with the current query.

@shuyangli shuyangli mentioned this pull request Jan 21, 2019
2 tasks
@shuyangli
Copy link
Contributor Author

shuyangli commented Jan 22, 2019

FLUPs:

  • integrating this with arrow keys
  • code sample in select-component.md that demonstrates using both new props
  • in selectComponentSuite.tsx tests, for renders create item if filtering returns empty list, use the real item list

@shuyangli
Copy link
Contributor Author

@giladgray can you give some feedback on the API before I start with tests and documentation?

@adidahiya adidahiya self-assigned this Feb 15, 2019
* This is invoked when user interaction causes a new item to be created, either by pressing the `enter` key or
* by clicking on the "Create Item" option. It transforms a query string into an item type.
*/
createItemFromQuery?: (query: string) => T;
Copy link
Contributor

Choose a reason for hiding this comment

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

API mostly looks good but can we be a little more explicit and say createNewItemFromQuery / createNewItemRenderer ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack

*/
createItemRenderer?: (
query: string,
active: boolean,
Copy link
Contributor

Choose a reason for hiding this comment

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

when is this active param useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is for when the "create" item should be highlighted; for example, if you interact with the dropdown with the up/down key, when your selection is on the create item, active would be true, and the item should be rendered accordingly

Copy link
Contributor

Choose a reason for hiding this comment

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

what else could you select in this case? there would be no other results to select, right? so it should be automatically "active"? I think the param is probably required for full customization of rendering, I'm not rejecting it, but maybe you could post a gif of that interaction?

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 create item is actually always rendered as long as there's input, so the "active" flag is useful:

active-create

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And to the point of "full customization of rendering" - the consumers provide code for rendering the "create" item just like they provide code for rendering other items, so to some extent this rendering is always custom

Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be isActive, or is active the norm (like disabled)?

@shuyangli
Copy link
Contributor Author

@adidahiya cleaned up tests and docs, this is ready for another look/merging

@blueprint-bot
Copy link

Merge branch 'develop' into sl/1710-create-able-multiselect

Previews: documentation | landing | table

@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@shuyangli shuyangli changed the title Create-able Select components [Select] Create-able Select components Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
@palantir palantir deleted a comment from blueprint-bot Feb 20, 2019
Copy link
Contributor

@cmslewis cmslewis left a comment

Choose a reason for hiding this comment

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

@shuyangli Great stuff - I want this desperately! 🎉 Left some thoughts.

  • Also, can we add an "Allow creating new items" toggle to the Suggest example?

@@ -56,6 +59,8 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
// explicit undefined (not null) for default behavior (show full list)
undefined
);
const maybeCreateNewItemFromQuery = this.state.allowCreate ? createFilm : undefined;
const maybeCreateNewItemRenderer = this.state.allowCreate ? this.renderCreateFilmOption : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit; Unpack this on L50 since you're accessing it twice.

const { allowCreate, films, hasInitialContent, tagMinimal, popoverMinimal, ...flags } = this.state;

@@ -72,6 +77,8 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
tagRenderer={this.renderTag}
tagInputProps={{ tagProps: getTagProps, onRemove: this.handleTagRemove, rightElement: clearButton }}
selectedItems={this.state.films}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemRenderer={maybeCreateNewItemRenderer}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Alphabetize props.

@@ -55,12 +58,16 @@ export class SelectExample extends React.PureComponent<IExampleProps, ISelectExa
) : (
undefined
);
const maybeCreateNewItemFromQuery = this.state.allowCreate ? createFilm : undefined;
const maybeCreateNewItemRenderer = this.state.allowCreate ? this.renderCreateFilmOption : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same.

}

export function getActiveItem<T>(activeItem: IQueryListActiveItem<T> | null | undefined): T | null {
if (activeItem && activeItem.type === QueryListActiveItemType.ITEM) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefer explicit boolean expressions for clarity and to generally avoid possibly unintended ignoring of falsey values (like "", []). activeItem != null.

Also, I'd just use a ternary here.

return activeItem != null && activeItem.type === QueryListActiveItemType.ITEM
    ? activeItem.item
    : null;

@@ -28,7 +28,7 @@ export interface IListItemsProps<T> extends IProps {
* uncontrolled (managed by the component's state). Use `onActiveItemChange`
* to listen for updates.
*/
activeItem?: T | null;
activeItem?: IQueryListActiveItem<T> | null;
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 a breaking change, no? Also, I don't love this API. Is there a way for us to just accept T | null, or T | IQueryListCreateItem<T> | null, or something?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, this is a break and we can't merge as-is. I don't see a way to make this non-breaking if we try to adjust the existing prop types (we can make the addition to activeItem non-breaking but not onActiveItemChange)... so we'll have to add new optional props for this behavior. we could consider refactoring them to make the API more streamlined in the next major version, but for now I propose:

isCreateNewItemActive: boolean;

onCreateNewItemActiveChange: () => null;

users will have to reconcile for themselves what happens between onActiveItemChange and onCreateNewItemActiveChange

private getActiveElement() {
if (this.itemsParentRef != null) {
return this.itemsParentRef.children.item(this.getActiveIndex()) as HTMLElement;
if (this.state.activeItem && this.state.activeItem.type === QueryListActiveItemType.CREATE) {
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Unpack
  • Explicit boolean expression

private getNextActiveItem(direction: number, startIndex = this.getActiveIndex()): T | null {
private getNextActiveItem(direction: number, startIndex = this.getActiveIndex()): IQueryListActiveItem<T> | null {
if (this.isCreateItemRendered()) {
const reachedCreate =
Copy link
Contributor

Choose a reason for hiding this comment

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

Improve this boolean variable name. Maybe willSelectCreateItem?

@@ -341,6 +398,10 @@ export class QueryList<T> extends React.Component<IQueryListProps<T>, IQueryList
}
Utils.safeInvoke(this.props.onActiveItemChange, activeItem);
}

private isCreateItemRendered(): boolean {
return !!(this.props.createNewItemFromQuery && this.props.createNewItemRenderer && this.state.query !== "");
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Use explicit boolean expressions for existence checks.
  • Implement validateProps to assert (in dev mode at least) that if one of these is provided, both must be provided. Otherwise the create item will just mysteriously never appear.
    • Add unit tests verifying the above.

text={`Create "${query}"`}
active={active}
onClick={handleClick}
shouldDismissPopover={false}
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well alphabetize these IMO.

itemPredicate={Films.itemPredicate}
itemRenderer={Films.itemRenderer}
noResults={<MenuItem disabled={true} text="No results." />}
createNewItemFromQuery={createFilm}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same.

@@ -28,7 +28,7 @@ export interface IListItemsProps<T> extends IProps {
* uncontrolled (managed by the component's state). Use `onActiveItemChange`
* to listen for updates.
*/
activeItem?: T | null;
activeItem?: IQueryListActiveItem<T> | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, this is a break and we can't merge as-is. I don't see a way to make this non-breaking if we try to adjust the existing prop types (we can make the addition to activeItem non-breaking but not onActiveItemChange)... so we'll have to add new optional props for this behavior. we could consider refactoring them to make the API more streamlined in the next major version, but for now I propose:

isCreateNewItemActive: boolean;

onCreateNewItemActiveChange: () => null;

users will have to reconcile for themselves what happens between onActiveItemChange and onCreateNewItemActiveChange

@cmslewis
Copy link
Contributor

@adidahiya @shuyangli See my second attempt in this separate PR: #3381. It avoids breaking the API, and it also avoids bloating the props interface with any other fields.

@shuyangli
Copy link
Contributor Author

Closing in favor of #3381 for backcompat-preserving API

@shuyangli shuyangli closed this Feb 28, 2019
@shuyangli shuyangli deleted the sl/1710-create-able-multiselect branch February 28, 2019 00:48
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.

4 participants