Skip to content

Commit

Permalink
QueryList itemRenderer prop & renderItem method (#1877)
Browse files Browse the repository at this point in the history
* QueryList IItemModifiers for data about _how_ to render

* refactor examples, common Film impls

* revert to single object arg syntax cuz it's established and reliable

* rename data => films.tsx

* fix MultiSelectExample

* focused => active

* update Select docs

* add itemRenderer.ts file with new ItemRenderer two-arg signature

* move itemRenderer prop definition to QueryList itself, refactor to use new signature

* renderItem method in IQueryListRendererProps gets modifiers and delegates to itemRenderer prop

makes for much simpler item rendering!

* update Item Renderer API docs

* fix the tests

* IFilm interface, better names, filmSelectProps

* modifiers.filtered => modifiers.matchesPredicate
  • Loading branch information
giladgray authored Jan 25, 2018
1 parent cdd306d commit 59830d5
Show file tree
Hide file tree
Showing 18 changed files with 255 additions and 396 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import { Classes, MenuItem } from "@blueprintjs/core";
import { ItemRenderer } from "@blueprintjs/select";
import * as classNames from "classnames";
import * as React from "react";

export interface IFilm {
/** Title of film. */
title: string;
/** Release year. */
year: number;
/** IMDb ranking. */
rank: number;
}

/** Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top */
export const TOP_100_FILMS = [
export const TOP_100_FILMS: IFilm[] = [
{ title: "The Shawshank Redemption", year: 1994 },
{ title: "The Godfather", year: 1972 },
{ title: "The Godfather: Part II", year: 1974 },
Expand Down Expand Up @@ -108,4 +122,31 @@ export const TOP_100_FILMS = [
{ title: "Monty Python and the Holy Grail", year: 1975 },
].map((m, index) => ({ ...m, rank: index + 1 }));

export type Film = typeof TOP_100_FILMS[0];
export const renderFilm: ItemRenderer<IFilm> = (film, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) {
return null;
}
const classes = classNames({
[Classes.ACTIVE]: modifiers.active,
[Classes.INTENT_PRIMARY]: modifiers.active,
});
return (
<MenuItem
className={classes}
label={film.year.toString()}
key={film.rank}
onClick={handleClick}
text={`${film.rank}. ${film.title}`}
/>
);
};

export function filterFilm(query: string, film: IFilm) {
return `${film.rank}. ${film.title.toLowerCase()} ${film.year}`.indexOf(query.toLowerCase()) >= 0;
}

export const filmSelectProps = {
itemPredicate: filterFilm,
itemRenderer: renderFilm,
items: TOP_100_FILMS,
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import * as React from "react";

import { Classes, Intent, ITagProps, MenuItem, Switch } from "@blueprintjs/core";
import { BaseExample } from "@blueprintjs/docs-theme";
import { ISelectItemRendererProps, MultiSelect } from "@blueprintjs/select";
import { Film, TOP_100_FILMS } from "./data";
import { ItemRenderer, MultiSelect } from "@blueprintjs/select";
import { filmSelectProps, IFilm, TOP_100_FILMS } from "./films";

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

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

export interface IMultiSelectExampleState {
films?: Film[];
films?: IFilm[];
hasInitialContent?: boolean;
intent?: boolean;
openOnKeyDown?: boolean;
Expand Down Expand Up @@ -59,10 +59,9 @@ export class MultiSelectExample extends BaseExample<IMultiSelectExampleState> {

return (
<FilmMultiSelect
{...filmSelectProps}
{...flags}
initialContent={initialContent}
items={TOP_100_FILMS}
itemPredicate={this.filterFilm}
itemRenderer={this.renderFilm}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={this.handleFilmSelect}
Expand Down Expand Up @@ -119,14 +118,16 @@ export class MultiSelectExample extends BaseExample<IMultiSelectExampleState> {
];
}

private renderTag = (film: Film) => {
return film.title;
};
private renderTag = (film: IFilm) => film.title;

private renderFilm = ({ handleClick, isActive, item: film }: ISelectItemRendererProps<Film>) => {
private renderFilm: ItemRenderer<IFilm> = (film, { modifiers, handleClick }) => {
if (!modifiers.matchesPredicate) {
return null;
}
// NOTE: not using Films.itemRenderer here so we can set icons.
const classes = classNames({
[Classes.ACTIVE]: isActive,
[Classes.INTENT_PRIMARY]: isActive,
[Classes.ACTIVE]: modifiers.active,
[Classes.INTENT_PRIMARY]: modifiers.active,
});

return (
Expand All @@ -142,31 +143,27 @@ export class MultiSelectExample extends BaseExample<IMultiSelectExampleState> {
);
};

private filterFilm(query: string, film: Film, index: number) {
return `${index + 1}. ${film.title.toLowerCase()} ${film.year}`.indexOf(query.toLowerCase()) >= 0;
}

private handleTagRemove = (_tag: string, index: number) => {
this.deselectFilm(index);
};

private getSelectedFilmIndex(film: Film) {
private getSelectedFilmIndex(film: IFilm) {
return this.state.films.indexOf(film);
}

private isFilmSelected(film: Film) {
private isFilmSelected(film: IFilm) {
return this.getSelectedFilmIndex(film) !== -1;
}

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

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

private handleFilmSelect = (film: Film) => {
private handleFilmSelect = (film: IFilm) => {
if (!this.isFilmSelected(film)) {
this.selectFilm(film);
} else {
Expand Down
45 changes: 6 additions & 39 deletions packages/docs-app/src/examples/select-examples/omnibarExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,14 @@
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import * as classNames from "classnames";
import * as React from "react";

import {
Button,
Classes,
Hotkey,
Hotkeys,
HotkeysTarget,
MenuItem,
Position,
Switch,
Toaster,
} from "@blueprintjs/core";
import { Button, Hotkey, Hotkeys, HotkeysTarget, MenuItem, Position, Switch, Toaster } from "@blueprintjs/core";
import { BaseExample, handleBooleanChange } from "@blueprintjs/docs-theme";
import { ISelectItemRendererProps, Omnibar } from "@blueprintjs/select";
import { Film, TOP_100_FILMS } from "./data";
import { Omnibar } from "@blueprintjs/select";
import { filmSelectProps, IFilm } from "./films";

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

export interface IOmnibarExampleState {
isOpen: boolean;
Expand Down Expand Up @@ -61,10 +50,8 @@ export class OmnibarExample extends BaseExample<IOmnibarExampleState> {
return (
<div>
<FilmOmnibar
{...filmSelectProps}
{...this.state}
items={TOP_100_FILMS}
itemPredicate={this.filterFilm}
itemRenderer={this.renderFilm}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={this.handleItemSelect}
onClose={this.handleClose}
Expand Down Expand Up @@ -99,31 +86,11 @@ export class OmnibarExample extends BaseExample<IOmnibarExampleState> {
];
}

private renderFilm({ handleClick, isActive, item: film }: ISelectItemRendererProps<Film>) {
const classes = classNames({
[Classes.ACTIVE]: isActive,
[Classes.INTENT_PRIMARY]: isActive,
});
return (
<MenuItem
className={classes}
label={film.year.toString()}
key={film.rank}
onClick={handleClick}
text={`${film.rank}. ${film.title}`}
/>
);
}

private filterFilm(query: string, film: Film, index: number) {
return `${index + 1}. ${film.title.toLowerCase()} ${film.year}`.indexOf(query.toLowerCase()) >= 0;
}

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

private handleItemSelect = (film: Film) => {
private handleItemSelect = (film: IFilm) => {
this.setState({ isOpen: false });

this.toaster.show({
Expand Down
37 changes: 7 additions & 30 deletions packages/docs-app/src/examples/select-examples/selectExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import * as classNames from "classnames";
import * as React from "react";

import { Button, Classes, MenuItem, Switch } from "@blueprintjs/core";
import { Button, MenuItem, Switch } from "@blueprintjs/core";
import { BaseExample } from "@blueprintjs/docs-theme";
import { ISelectItemRendererProps, Select } from "@blueprintjs/select";
import { Film, TOP_100_FILMS } from "./data";
import { Select } from "@blueprintjs/select";
import { filmSelectProps, IFilm, TOP_100_FILMS } from "./films";

const FilmSelect = Select.ofType<Film>();
const FilmSelect = Select.ofType<IFilm>();

export interface ISelectExampleState {
film?: Film;
film?: IFilm;
filterable?: boolean;
hasInitialContent?: boolean;
minimal?: boolean;
Expand Down Expand Up @@ -53,12 +52,10 @@ export class SelectExample extends BaseExample<ISelectExampleState> {

return (
<FilmSelect
{...filmSelectProps}
{...flags}
disabled={disabled}
initialContent={initialContent}
items={TOP_100_FILMS}
itemPredicate={this.filterFilm}
itemRenderer={this.renderFilm}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={this.handleValueChange}
popoverProps={{ minimal }}
Expand Down Expand Up @@ -111,27 +108,7 @@ export class SelectExample extends BaseExample<ISelectExampleState> {
];
}

private renderFilm({ handleClick, isActive, item: film }: ISelectItemRendererProps<Film>) {
const classes = classNames({
[Classes.ACTIVE]: isActive,
[Classes.INTENT_PRIMARY]: isActive,
});
return (
<MenuItem
className={classes}
label={film.year.toString()}
key={film.rank}
onClick={handleClick}
text={`${film.rank}. ${film.title}`}
/>
);
}

private filterFilm(query: string, film: Film, index: number) {
return `${index + 1}. ${film.title.toLowerCase()} ${film.year}`.indexOf(query.toLowerCase()) >= 0;
}

private handleValueChange = (film: Film) => this.setState({ film });
private handleValueChange = (film: IFilm) => this.setState({ film });

private handleSwitchChange(prop: keyof ISelectExampleState) {
return (event: React.FormEvent<HTMLInputElement>) => {
Expand Down
39 changes: 7 additions & 32 deletions packages/docs-app/src/examples/select-examples/suggestExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import * as classNames from "classnames";
import * as React from "react";

import { Classes, MenuItem, Switch } from "@blueprintjs/core";
import { BaseExample } from "@blueprintjs/docs-theme";
import { ISelectItemRendererProps, Suggest } from "@blueprintjs/select";
import { Film, TOP_100_FILMS } from "./data";
import { Suggest } from "@blueprintjs/select";
import { filmSelectProps, IFilm, TOP_100_FILMS } from "./films";

const FilmSuggest = Suggest.ofType<Film>();
const FilmSuggest = Suggest.ofType<IFilm>();

export interface ISuggestExampleState {
closeOnSelect?: boolean;
film?: Film;
film?: IFilm;
minimal?: boolean;
openOnKeyDown?: boolean;
}
Expand All @@ -37,11 +36,9 @@ export class SuggestExample extends BaseExample<ISuggestExampleState> {
const { film, minimal, ...flags } = this.state;
return (
<FilmSuggest
{...filmSelectProps}
{...flags}
inputValueRenderer={this.renderInputValue}
items={TOP_100_FILMS}
itemPredicate={this.filterFilm}
itemRenderer={this.renderFilm}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={this.handleValueChange}
popoverProps={{ popoverClassName: minimal ? Classes.MINIMAL : "" }}
Expand Down Expand Up @@ -74,31 +71,9 @@ export class SuggestExample extends BaseExample<ISuggestExampleState> {
];
}

private renderFilm({ handleClick, isActive, item: film }: ISelectItemRendererProps<Film>) {
const classes = classNames({
[Classes.ACTIVE]: isActive,
[Classes.INTENT_PRIMARY]: isActive,
});
return (
<MenuItem
className={classes}
label={film.year.toString()}
key={film.rank}
onClick={handleClick}
text={`${film.rank}. ${film.title}`}
/>
);
}

private renderInputValue = (film: Film) => {
return film.title;
};

private filterFilm(query: string, film: Film, index: number) {
return `${index + 1}. ${film.title.toLowerCase()} ${film.year}`.indexOf(query.toLowerCase()) >= 0;
}
private renderInputValue = (film: IFilm) => film.title;

private handleValueChange = (film: Film) => this.setState({ film });
private handleValueChange = (film: IFilm) => this.setState({ film });

private handleSwitchChange(prop: keyof ISuggestExampleState) {
return (event: React.FormEvent<HTMLInputElement>) => {
Expand Down
1 change: 1 addition & 0 deletions packages/select/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

export * from "./omnibar/omnibar";
export * from "./query-list/itemRenderer";
export * from "./query-list/queryList";
export * from "./select/multiSelect";
export * from "./select/select";
Expand Down
Loading

1 comment on commit 59830d5

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

QueryList itemRenderer prop & renderItem method (#1877)

Preview: documentation | landing | table

Please sign in to comment.