Skip to content

Commit

Permalink
[Select] Create-able Select components (#3381)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmslewis authored and adidahiya committed Feb 28, 2019
1 parent b3c5c1f commit 3d6f13e
Show file tree
Hide file tree
Showing 13 changed files with 692 additions and 55 deletions.
66 changes: 66 additions & 0 deletions packages/docs-app/src/examples/select-examples/films.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ export const renderFilm: ItemRenderer<IFilm> = (film, { handleClick, modifiers,
);
};

export const renderCreateFilmOption = (
query: string,
active: boolean,
handleClick: React.MouseEventHandler<HTMLElement>,
) => (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
onClick={handleClick}
shouldDismissPopover={false}
/>
);

export const filterFilm: ItemPredicate<IFilm> = (query, film) => {
return `${film.rank}. ${film.title.toLowerCase()} ${film.year}`.indexOf(query.toLowerCase()) >= 0;
};
Expand Down Expand Up @@ -182,3 +196,55 @@ export const filmSelectProps = {
itemRenderer: renderFilm,
items: TOP_100_FILMS,
};

export function createFilm(title: string): IFilm {
return {
rank: 100 + Math.floor(Math.random() * 100 + 1),
title,
year: new Date().getFullYear(),
};
}

export function areFilmsEqual(filmA: IFilm, filmB: IFilm) {
// Compare only the titles (ignoring case) just for simplicity.
return filmA.title.toLowerCase() === filmB.title.toLowerCase();
}

export function arrayContainsFilm(films: IFilm[], filmToFind: IFilm): boolean {
return films.some((film: IFilm) => film.title === filmToFind.title);
}

export function addFilmToArray(films: IFilm[], filmToAdd: IFilm) {
return [...films, filmToAdd];
}

export function deleteFilmFromArray(films: IFilm[], filmToDelete: IFilm) {
return films.filter(film => film !== filmToDelete);
}

export function maybeAddCreatedFilmToArrays(
items: IFilm[],
createdItems: IFilm[],
film: IFilm,
): { createdItems: IFilm[]; items: IFilm[] } {
const isNewlyCreatedItem = !arrayContainsFilm(items, film);
return {
createdItems: isNewlyCreatedItem ? addFilmToArray(createdItems, film) : createdItems,
// Add a created film to `items` so that the film can be deselected.
items: isNewlyCreatedItem ? addFilmToArray(items, film) : items,
};
}

export function maybeDeleteCreatedFilmFromArrays(
items: IFilm[],
createdItems: IFilm[],
film: IFilm,
): { createdItems: IFilm[]; items: IFilm[] } {
const wasItemCreatedByUser = arrayContainsFilm(createdItems, film);

// Delete the item if the user manually created it.
return {
createdItems: wasItemCreatedByUser ? deleteFilmFromArray(createdItems, film) : createdItems,
items: wasItemCreatedByUser ? deleteFilmFromArray(items, film) : items,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,29 @@ import * as React from "react";
import { Button, H5, Intent, ITagProps, MenuItem, Switch } from "@blueprintjs/core";
import { Example, IExampleProps } from "@blueprintjs/docs-theme";
import { ItemRenderer, MultiSelect } from "@blueprintjs/select";
import { filmSelectProps, IFilm, TOP_100_FILMS } from "./films";
import {
areFilmsEqual,
arrayContainsFilm,
createFilm,
filmSelectProps,
IFilm,
maybeAddCreatedFilmToArrays,
maybeDeleteCreatedFilmFromArrays,
renderCreateFilmOption,
TOP_100_FILMS,
} from "./films";

const FilmMultiSelect = MultiSelect.ofType<IFilm>();

const INTENTS = [Intent.NONE, Intent.PRIMARY, Intent.SUCCESS, Intent.DANGER, Intent.WARNING];

export interface IMultiSelectExampleState {
allowCreate: boolean;
createdItems: IFilm[];
films: IFilm[];
hasInitialContent: boolean;
intent: boolean;
items: IFilm[];
openOnKeyDown: boolean;
popoverMinimal: boolean;
resetOnSelect: boolean;
Expand All @@ -27,15 +40,19 @@ export interface IMultiSelectExampleState {

export class MultiSelectExample extends React.PureComponent<IExampleProps, IMultiSelectExampleState> {
public state: IMultiSelectExampleState = {
allowCreate: false,
createdItems: [],
films: [],
hasInitialContent: false,
intent: false,
items: filmSelectProps.items,
openOnKeyDown: false,
popoverMinimal: true,
resetOnSelect: true,
tagMinimal: false,
};

private handleAllowCreateChange = this.handleSwitchChange("allowCreate");
private handleKeyDownChange = this.handleSwitchChange("openOnKeyDown");
private handleResetChange = this.handleSwitchChange("resetOnSelect");
private handlePopoverMinimalChange = this.handleSwitchChange("popoverMinimal");
Expand All @@ -44,7 +61,7 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
private handleInitialContentChange = this.handleSwitchChange("hasInitialContent");

public render() {
const { films, hasInitialContent, tagMinimal, popoverMinimal, ...flags } = this.state;
const { allowCreate, films, hasInitialContent, tagMinimal, popoverMinimal, ...flags } = this.state;
const getTagProps = (_value: string, index: number): ITagProps => ({
intent: this.state.intent ? INTENTS[index % INTENTS.length] : Intent.NONE,
minimal: tagMinimal,
Expand All @@ -56,6 +73,8 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
// explicit undefined (not null) for default behavior (show full list)
undefined
);
const maybeCreateNewItemFromQuery = allowCreate ? createFilm : undefined;
const maybeCreateNewItemRenderer = allowCreate ? renderCreateFilmOption : null;

const clearButton = films.length > 0 ? <Button icon="cross" minimal={true} onClick={this.handleClear} /> : null;

Expand All @@ -64,8 +83,14 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
<FilmMultiSelect
{...filmSelectProps}
{...flags}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemRenderer={maybeCreateNewItemRenderer}
initialContent={initialContent}
itemRenderer={this.renderFilm}
itemsEqual={areFilmsEqual}
// we may customize the default filmSelectProps.items by
// adding newly created items to the list, so pass our own
items={this.state.items}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={this.handleFilmSelect}
popoverProps={{ minimal: popoverMinimal }}
Expand Down Expand Up @@ -96,6 +121,11 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
checked={this.state.hasInitialContent}
onChange={this.handleInitialContentChange}
/>
<Switch
label="Allow creating new films"
checked={this.state.allowCreate}
onChange={this.handleAllowCreateChange}
/>
<H5>Tag props</H5>
<Switch
label="Minimal tag style"
Expand Down Expand Up @@ -150,11 +180,40 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
}

private selectFilm(film: IFilm) {
this.setState({ films: [...this.state.films, film] });
const { films } = this.state;

const { createdItems: nextCreatedItems, items: nextItems } = maybeAddCreatedFilmToArrays(
this.state.items,
this.state.createdItems,
film,
);

this.setState({
createdItems: nextCreatedItems,
// Avoid re-creating an item that is already selected (the "Create
// Item" option will be shown even if it matches an already selected
// item).
films: !arrayContainsFilm(films, film) ? [...films, film] : films,
items: nextItems,
});
}

private deselectFilm(index: number) {
this.setState({ films: this.state.films.filter((_film, i) => i !== index) });
const { films } = this.state;

const film = films[index];
const { createdItems: nextCreatedItems, items: nextItems } = maybeDeleteCreatedFilmFromArrays(
this.state.items,
this.state.createdItems,
film,
);

// Delete the item if the user manually created it.
this.setState({
createdItems: nextCreatedItems,
films: films.filter((_film, i) => i !== index),
items: nextItems,
});
}

private handleFilmSelect = (film: IFilm) => {
Expand Down
34 changes: 26 additions & 8 deletions packages/docs-app/src/examples/select-examples/omnibarExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,25 @@ import {
} from "@blueprintjs/core";
import { Example, handleBooleanChange, IExampleProps } from "@blueprintjs/docs-theme";
import { Omnibar } from "@blueprintjs/select";
import { filmSelectProps, IFilm } from "./films";
import { areFilmsEqual, createFilm, filmSelectProps, IFilm, renderCreateFilmOption } from "./films";

const FilmOmnibar = Omnibar.ofType<IFilm>();

export interface IOmnibarExampleState {
allowCreate: boolean;
isOpen: boolean;
resetOnSelect: boolean;
}

@HotkeysTarget
export class OmnibarExample extends React.PureComponent<IExampleProps, IOmnibarExampleState> {
public state: IOmnibarExampleState = {
allowCreate: false,
isOpen: false,
resetOnSelect: true,
};

private handleAllowCreateChange = handleBooleanChange(allowCreate => this.setState({ allowCreate }));
private handleResetChange = handleBooleanChange(resetOnSelect => this.setState({ resetOnSelect }));

private toaster: Toaster;
Expand All @@ -59,15 +62,13 @@ export class OmnibarExample extends React.PureComponent<IExampleProps, IOmnibarE
}

public render() {
const options = (
<>
<H5>Props</H5>
<Switch label="Reset on select" checked={this.state.resetOnSelect} onChange={this.handleResetChange} />
</>
);
const { allowCreate } = this.state;

const maybeCreateNewItemFromQuery = allowCreate ? createFilm : undefined;
const maybeCreateNewItemRenderer = allowCreate ? renderCreateFilmOption : null;

return (
<Example options={options} {...this.props}>
<Example options={this.renderOptions()} {...this.props}>
<span>
<Button text="Click to show Omnibar" onClick={this.handleClick} />
{" or press "}
Expand All @@ -77,6 +78,9 @@ export class OmnibarExample extends React.PureComponent<IExampleProps, IOmnibarE
<FilmOmnibar
{...filmSelectProps}
{...this.state}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemRenderer={maybeCreateNewItemRenderer}
itemsEqual={areFilmsEqual}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={this.handleItemSelect}
onClose={this.handleClose}
Expand All @@ -86,6 +90,20 @@ export class OmnibarExample extends React.PureComponent<IExampleProps, IOmnibarE
);
}

protected renderOptions() {
return (
<>
<H5>Props</H5>
<Switch label="Reset on select" checked={this.state.resetOnSelect} onChange={this.handleResetChange} />
<Switch
label="Allow creating new films"
checked={this.state.allowCreate}
onChange={this.handleAllowCreateChange}
/>
</>
);
}

private handleClick = (_event: React.MouseEvent<HTMLElement>) => {
this.setState({ isOpen: true });
};
Expand Down
Loading

1 comment on commit 3d6f13e

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

[Select] Create-able Select components (#3381)

Previews: documentation | landing | table

Please sign in to comment.