diff --git a/docs/data.js b/docs/data.js index 6410bb695c..c52a318d4c 100644 --- a/docs/data.js +++ b/docs/data.js @@ -109,3 +109,396 @@ export const groupedOptions = [ options: flavourOptions, }, ]; + +export const carOptions = [ + { label: '4Runner', value: '4Runner' }, + { label: 'Accent', value: 'Accent' }, + { label: 'Acclaim', value: 'Acclaim' }, + { label: 'Accord', value: 'Accord' }, + { label: 'Achieva', value: 'Achieva' }, + { label: 'Aerio', value: 'Aerio' }, + { label: 'Aerostar', value: 'Aerostar' }, + { label: 'Alero', value: 'Alero' }, + { label: 'Allante', value: 'Allante' }, + { label: 'Alliance', value: 'Alliance' }, + { label: 'Altima', value: 'Altima' }, + { label: 'Amigo', value: 'Amigo' }, + { label: 'Aries', value: 'Aries' }, + { label: 'Arnage', value: 'Arnage' }, + { label: 'Arrow', value: 'Arrow' }, + { label: 'Ascender', value: 'Ascender' }, + { label: 'Aspen', value: 'Aspen' }, + { label: 'Aspire', value: 'Aspire' }, + { label: 'Astro', value: 'Astro' }, + { label: 'Aurora', value: 'Aurora' }, + { label: 'Avalanche', value: 'Avalanche' }, + { label: 'Avenger', value: 'Avenger' }, + { label: 'Aviator', value: 'Aviator' }, + { label: 'Axiom', value: 'Axiom' }, + { label: 'Axxess', value: 'Axxess' }, + { label: 'Aztek', value: 'Aztek' }, + { label: 'Azure', value: 'Azure' }, + { label: 'Baja', value: 'Baja' }, + { label: 'Barchetta', value: 'Barchetta' }, + { label: 'Beetle', value: 'Beetle' }, + { label: 'Beretta', value: 'Beretta' }, + { label: 'Blackwood', value: 'Blackwood' }, + { label: 'Blazer', value: 'Blazer' }, + { label: 'Bonneville', value: 'Bonneville' }, + { label: 'Boxter', value: 'Boxter' }, + { label: 'Brat', value: 'Brat' }, + { label: 'Brava', value: 'Brava' }, + { label: 'Bravada', value: 'Bravada' }, + { label: 'Breeze', value: 'Breeze' }, + { label: 'Bronco', value: 'Bronco' }, + { label: 'Brooklands', value: 'Brooklands' }, + { label: 'Brougham', value: 'Brougham' }, + { label: 'Caballero', value: 'Caballero' }, + { label: 'Cabrio', value: 'Cabrio' }, + { label: 'Cabriolet', value: 'Cabriolet' }, + { label: 'Calais', value: 'Calais' }, + { label: 'Camargue', value: 'Camargue' }, + { label: 'Camry', value: 'Camry' }, + { label: 'Capri', value: 'Capri' }, + { label: 'Caprice', value: 'Caprice' }, + { label: 'Caravan', value: 'Caravan' }, + { label: 'Caravelle', value: 'Caravelle' }, + { label: 'Catera', value: 'Catera' }, + { label: 'Cavalier', value: 'Cavalier' }, + { label: 'Cayenne', value: 'Cayenne' }, + { label: 'Celebrity', value: 'Celebrity' }, + { label: 'Celica', value: 'Celica' }, + { label: 'Century', value: 'Century' }, + { label: 'Challenger', value: 'Challenger' }, + { label: 'Champ', value: 'Champ' }, + { label: 'Charade', value: 'Charade' }, + { label: 'Charger', value: 'Charger' }, + { label: 'Cherokee', value: 'Cherokee' }, + { label: 'Chevelle', value: 'Chevelle' }, + { label: 'Chevette', value: 'Chevette' }, + { label: 'Cheyenne', value: 'Cheyenne' }, + { label: 'Ciera', value: 'Ciera' }, + { label: 'Cimarron', value: 'Cimarron' }, + { label: 'Cirrus', value: 'Cirrus' }, + { label: 'Citation', value: 'Citation' }, + { label: 'Civic', value: 'Civic' }, + { label: 'Club Wagon', value: 'Club Wagon' }, + { label: 'Colorado', value: 'Colorado' }, + { label: 'Colt', value: 'Colt' }, + { label: 'Comanche', value: 'Comanche' }, + { label: 'Concord', value: 'Concord' }, + { label: 'Concorde', value: 'Concorde' }, + { label: 'Conquest', value: 'Conquest' }, + { label: 'Continental', value: 'Continental' }, + { label: 'Contour', value: 'Contour' }, + { label: 'Cooper', value: 'Cooper' }, + { label: 'Cordia', value: 'Cordia' }, + { label: 'Cordoba', value: 'Cordoba' }, + { label: 'Corniche', value: 'Corniche' }, + { label: 'Corolla', value: 'Corolla' }, + { label: 'Corrado', value: 'Corrado' }, + { label: 'Corsica', value: 'Corsica' }, + { label: 'Corvette', value: 'Corvette' }, + { label: 'Cougar', value: 'Cougar' }, + { label: 'Countach', value: 'Countach' }, + { label: 'Courier', value: 'Courier' }, + { label: 'Cressida', value: 'Cressida' }, + { label: 'Crown Victoria', value: 'Crown Victoria' }, + { label: 'Cutlass', value: 'Cutlass' }, + { label: 'Dakota', value: 'Dakota' }, + { label: 'Dart', value: 'Dart' }, + { label: 'Dasher', value: 'Dasher' }, + { label: 'Daytona', value: 'Daytona' }, + { label: 'Defender', value: 'Defender' }, + { label: 'del Sol', value: 'del Sol' }, + { label: 'DeVille', value: 'DeVille' }, + { label: 'Diablo', value: 'Diablo' }, + { label: 'Diamante', value: 'Diamante' }, + { label: 'Dino', value: 'Dino' }, + { label: 'Diplomat', value: 'Diplomat' }, + { label: 'Discovery', value: 'Discovery' }, + { label: 'Durango', value: 'Durango' }, + { label: 'Duster', value: 'Duster' }, + { label: 'Dynasty', value: 'Dynasty' }, + { label: 'Eagle', value: 'Eagle' }, + { label: 'Echo', value: 'Echo' }, + { label: 'Eclipse', value: 'Eclipse' }, + { label: 'Econoline', value: 'Econoline' }, + { label: 'Eight', value: 'Eight' }, + { label: 'Eighty-Eight', value: 'Eighty-Eight' }, + { label: 'Elantra', value: 'Elantra' }, + { label: 'ElCamino', value: 'ElCamino' }, + { label: 'Eldorado', value: 'Eldorado' }, + { label: 'Electra', value: 'Electra' }, + { label: 'Element', value: 'Element' }, + { label: 'Encore', value: 'Encore' }, + { label: 'Envoy', value: 'Envoy' }, + { label: 'Equinox', value: 'Equinox' }, + { label: 'Escalade', value: 'Escalade' }, + { label: 'Escape', value: 'Escape' }, + { label: 'Escort', value: 'Escort' }, + { label: 'Esperante', value: 'Esperante' }, + { label: 'Esprit', value: 'Esprit' }, + { label: 'Estate', value: 'Estate' }, + { label: 'Esteem', value: 'Esteem' }, + { label: 'Eurovan', value: 'Eurovan' }, + { label: 'Excel', value: 'Excel' }, + { label: 'Excursion', value: 'Excursion' }, + { label: 'Expedition', value: 'Expedition' }, + { label: 'Explorer', value: 'Explorer' }, + { label: 'Expo', value: 'Expo' }, + { label: 'Express', value: 'Express' }, + { label: 'Fairmont', value: 'Fairmont' }, + { label: 'Festiva', value: 'Festiva' }, + { label: 'Fiero', value: 'Fiero' }, + { label: 'Fifth Avenue', value: 'Fifth Avenue' }, + { label: 'Firebird', value: 'Firebird' }, + { label: 'Firenza', value: 'Firenza' }, + { label: 'Fleetwood', value: 'Fleetwood' }, + { label: 'Focus', value: 'Focus' }, + { label: 'Forester', value: 'Forester' }, + { label: 'Fox', value: 'Fox' }, + { label: 'Freelander', value: 'Freelander' }, + { label: 'Frontier', value: 'Frontier' }, + { label: 'Fuego', value: 'Fuego' }, + { label: 'Galant', value: 'Galant' }, + { label: 'Golf', value: 'Golf' }, + { label: 'Graduate', value: 'Graduate' }, + { label: 'Gran Fury', value: 'Gran Fury' }, + { label: 'Grand Am', value: 'Grand Am' }, + { label: 'Grand Prix', value: 'Grand Prix' }, + { label: 'Grand Voyager', value: 'Grand Voyager' }, + { label: 'Gremlin', value: 'Gremlin' }, + { label: 'Grenada', value: 'Grenada' }, + { label: 'Highlander', value: 'Highlander' }, + { label: 'Hombre', value: 'Hombre' }, + { label: 'Horizon', value: 'Horizon' }, + { label: 'Hornet', value: 'Hornet' }, + { label: 'Hummer', value: 'Hummer' }, + { label: 'Impala', value: 'Impala' }, + { label: 'Imperial', value: 'Imperial' }, + { label: 'Impreza', value: 'Impreza' }, + { label: 'Impulse', value: 'Impulse' }, + { label: 'Insight', value: 'Insight' }, + { label: 'Integra', value: 'Integra' }, + { label: 'Intrepid', value: 'Intrepid' }, + { label: 'Intrigue', value: 'Intrigue' }, + { label: 'Javelin', value: 'Javelin' }, + { label: 'Jetta', value: 'Jetta' }, + { label: 'Jimmy', value: 'Jimmy' }, + { label: 'Justy', value: 'Justy' }, + { label: 'Kodiak', value: 'Kodiak' }, + { label: 'Lagonda', value: 'Lagonda' }, + { label: 'Lancer', value: 'Lancer' }, + { label: 'Land Cruiser', value: 'Land Cruiser' }, + { label: 'Lanos', value: 'Lanos' }, + { label: 'Laser', value: 'Laser' }, + { label: 'LeBaron', value: 'LeBaron' }, + { label: 'Legacy', value: 'Legacy' }, + { label: 'Leganza', value: 'Leganza' }, + { label: 'Legend', value: 'Legend' }, + { label: 'LeMans', value: 'LeMans' }, + { label: 'LeSabre', value: 'LeSabre' }, + { label: 'Liberte', value: 'Liberte' }, + { label: 'Liberty', value: 'Liberty' }, + { label: 'Loyale', value: 'Loyale' }, + { label: 'Lumina', value: 'Lumina' }, + { label: 'Luv', value: 'Luv' }, + { label: 'Lynx', value: 'Lynx' }, + { label: 'Malibu', value: 'Malibu' }, + { label: 'Mangusta', value: 'Mangusta' }, + { label: 'Marquis', value: 'Marquis' }, + { label: 'Matador', value: 'Matador' }, + { label: 'Matrix', value: 'Matrix' }, + { label: 'Maxima', value: 'Maxima' }, + { label: 'Medallion', value: 'Medallion' }, + { label: 'Metro', value: 'Metro' }, + { label: 'Miata', value: 'Miata' }, + { label: 'Midget', value: 'Midget' }, + { label: 'Milano', value: 'Milano' }, + { label: 'Millenia', value: 'Millenia' }, + { label: 'Mirada', value: 'Mirada' }, + { label: 'Mirage', value: 'Mirage' }, + { label: 'Modena', value: 'Modena' }, + { label: 'Monaco', value: 'Monaco' }, + { label: 'Mondial', value: 'Mondial' }, + { label: 'Montana', value: 'Montana' }, + { label: 'Monte Carlo', value: 'Monte Carlo' }, + { label: 'Montero', value: 'Montero' }, + { label: 'Mountaineer', value: 'Mountaineer' }, + { label: 'Mulsanne', value: 'Mulsanne' }, + { label: 'Murano', value: 'Murano' }, + { label: 'Murcielago', value: 'Murcielago' }, + { label: 'Mustang', value: 'Mustang' }, + { label: 'Mystique', value: 'Mystique' }, + { label: 'Navajo', value: 'Navajo' }, + { label: 'Navigator', value: 'Navigator' }, + { label: 'Neon', value: 'Neon' }, + { label: 'New Yorker', value: 'New Yorker' }, + { label: 'Newport', value: 'Newport' }, + { label: 'Ninety-Eight', value: 'Ninety-Eight' }, + { label: 'Nova', value: 'Nova' }, + { label: 'Nubira', value: 'Nubira' }, + { label: 'Oasis', value: 'Oasis' }, + { label: 'Odyssey', value: 'Odyssey' }, + { label: 'Omega', value: 'Omega' }, + { label: 'Omni', value: 'Omni' }, + { label: 'Optima', value: 'Optima' }, + { label: 'Outback', value: 'Outback' }, + { label: 'Outlander', value: 'Outlander' }, + { label: 'Pacer', value: 'Pacer' }, + { label: 'Pacifica', value: 'Pacifica' }, + { label: 'Parisienne', value: 'Parisienne' }, + { label: 'Park Avenue', value: 'Park Avenue' }, + { label: 'Park Ward', value: 'Park Ward' }, + { label: 'Paseo', value: 'Paseo' }, + { label: 'Passat', value: 'Passat' }, + { label: 'Passport', value: 'Passport' }, + { label: 'Pathfinder', value: 'Pathfinder' }, + { label: 'Phaeton', value: 'Phaeton' }, + { label: 'Phantom', value: 'Phantom' }, + { label: 'Phoenix', value: 'Phoenix' }, + { label: 'Pilot', value: 'Pilot' }, + { label: 'Pininfarina', value: 'Pininfarina' }, + { label: 'Precis', value: 'Precis' }, + { label: 'Prelude', value: 'Prelude' }, + { label: 'Premier', value: 'Premier' }, + { label: 'Previa', value: 'Previa' }, + { label: 'Prizm', value: 'Prizm' }, + { label: 'Probe', value: 'Probe' }, + { label: 'Protégé', value: 'Protégé' }, + { label: 'Prowler', value: 'Prowler' }, + { label: 'Pruis', value: 'Pruis' }, + { label: 'PT Cruiser', value: 'PT Cruiser' }, + { label: 'Pulsar', value: 'Pulsar' }, + { label: 'Quadrifoglio', value: 'Quadrifoglio' }, + { label: 'Quantum', value: 'Quantum' }, + { label: 'Quattro', value: 'Quattro' }, + { label: 'Quest', value: 'Quest' }, + { label: 'Rabbit', value: 'Rabbit' }, + { label: 'Raider', value: 'Raider' }, + { label: 'Rally', value: 'Rally' }, + { label: 'Ram', value: 'Ram' }, + { label: 'Ramcharger', value: 'Ramcharger' }, + { label: 'Rampage', value: 'Rampage' }, + { label: 'Range Rover', value: 'Range Rover' }, + { label: 'Ranger', value: 'Ranger' }, + { label: 'Reatta', value: 'Reatta' }, + { label: 'Rebel', value: 'Rebel' }, + { label: 'Regal', value: 'Regal' }, + { label: 'Regency', value: 'Regency' }, + { label: 'Reliant', value: 'Reliant' }, + { label: 'Rendezvous', value: 'Rendezvous' }, + { label: 'Rio', value: 'Rio' }, + { label: 'Riviera', value: 'Riviera' }, + { label: 'Roadmaster', value: 'Roadmaster' }, + { label: 'Roadster', value: 'Roadster' }, + { label: 'Rocky', value: 'Rocky' }, + { label: 'Rodeo', value: 'Rodeo' }, + { label: 'Sable', value: 'Sable' }, + { label: 'Safari', value: 'Safari' }, + { label: 'Safari', value: 'Safari' }, + { label: 'Samuri', value: 'Samuri' }, + { label: 'Santa Fe', value: 'Santa Fe' }, + { label: 'Sapporo', value: 'Sapporo' }, + { label: 'Savana', value: 'Savana' }, + { label: 'Scamp', value: 'Scamp' }, + { label: 'Scirocco', value: 'Scirocco' }, + { label: 'Scorpio', value: 'Scorpio' }, + { label: 'Scrambler', value: 'Scrambler' }, + { label: 'Sebring', value: 'Sebring' }, + { label: 'Sedona', value: 'Sedona' }, + { label: 'Sentra', value: 'Sentra' }, + { label: 'Sephia', value: 'Sephia' }, + { label: 'Sequoia', value: 'Sequoia' }, + { label: 'Seville', value: 'Seville' }, + { label: 'Shadow', value: 'Shadow' }, + { label: 'Sidekick', value: 'Sidekick' }, + { label: 'Sienna', value: 'Sienna' }, + { label: 'Sierra', value: 'Sierra' }, + { label: 'Sigma', value: 'Sigma' }, + { label: 'Silhouette', value: 'Silhouette' }, + { label: 'Silver Dawn', value: 'Silver Dawn' }, + { label: 'Silver Seraph', value: 'Silver Seraph' }, + { label: 'Silver Shadow', value: 'Silver Shadow' }, + { label: 'Silver Spirit', value: 'Silver Spirit' }, + { label: 'Silver Spur', value: 'Silver Spur' }, + { label: 'Silver Wraith', value: 'Silver Wraith' }, + { label: 'Silverado', value: 'Silverado' }, + { label: 'Skyhawk', value: 'Skyhawk' }, + { label: 'Skylark', value: 'Skylark' }, + { label: 'Solara', value: 'Solara' }, + { label: 'Somerset', value: 'Somerset' }, + { label: 'Sonata', value: 'Sonata' }, + { label: 'Sonoma', value: 'Sonoma' }, + { label: 'Sorento', value: 'Sorento' }, + { label: 'Spectra', value: 'Spectra' }, + { label: 'Spectrum', value: 'Spectrum' }, + { label: 'Spider', value: 'Spider' }, + { label: 'Spirit', value: 'Spirit' }, + { label: 'Sportage', value: 'Sportage' }, + { label: 'Sportvan', value: 'Sportvan' }, + { label: 'Sprint', value: 'Sprint' }, + { label: 'Spyder', value: 'Spyder' }, + { label: 'St. Regis', value: 'St. Regis' }, + { label: 'Stanza', value: 'Stanza' }, + { label: 'Starion', value: 'Starion' }, + { label: 'Starlet', value: 'Starlet' }, + { label: 'Stealth', value: 'Stealth' }, + { label: 'Storm', value: 'Storm' }, + { label: 'Strada', value: 'Strada' }, + { label: 'Stratus', value: 'Stratus' }, + { label: 'Stylus', value: 'Stylus' }, + { label: 'Suburban', value: 'Suburban' }, + { label: 'Suburban', value: 'Suburban' }, + { label: 'Summit', value: 'Summit' }, + { label: 'Sunbird', value: 'Sunbird' }, + { label: 'Sundance', value: 'Sundance' }, + { label: 'Sunfire', value: 'Sunfire' }, + { label: 'Supra', value: 'Supra' }, + { label: 'Swift', value: 'Swift' }, + { label: 'Tacoma', value: 'Tacoma' }, + { label: 'Tahoe', value: 'Tahoe' }, + { label: 'Talon', value: 'Talon' }, + { label: 'Taurus', value: 'Taurus' }, + { label: 'Tempo', value: 'Tempo' }, + { label: 'Tercel', value: 'Tercel' }, + { label: 'Testarossa', value: 'Testarossa' }, + { label: 'Thunderbird', value: 'Thunderbird' }, + { label: 'Tiburon', value: 'Tiburon' }, + { label: 'Titan', value: 'Titan' }, + { label: 'Topaz', value: 'Topaz' }, + { label: 'Toronado', value: 'Toronado' }, + { label: 'Touareg', value: 'Touareg' }, + { label: 'Town & Country', value: 'Town & Country' }, + { label: 'Town Car', value: 'Town Car' }, + { label: 'tracker', value: 'tracker' }, + { label: 'TrailBlazer', value: 'TrailBlazer' }, + { label: 'Trans Sport', value: 'Trans Sport' }, + { label: 'Tredia', value: 'Tredia' }, + { label: 'Tribute', value: 'Tribute' }, + { label: 'Trooper', value: 'Trooper' }, + { label: 'Tundra', value: 'Tundra' }, + { label: 'Turbo', value: 'Turbo' }, + { label: 'Turismo', value: 'Turismo' }, + { label: 'Vanagon', value: 'Vanagon' }, + { label: 'Vandura', value: 'Vandura' }, + { label: 'Vanquish', value: 'Vanquish' }, + { label: 'Vantage', value: 'Vantage' }, + { label: 'VehiCROSS', value: 'VehiCROSS' }, + { label: 'Venture', value: 'Venture' }, + { label: 'Vibe', value: 'Vibe' }, + { label: 'Vigor', value: 'Vigor' }, + { label: 'Villager', value: 'Villager' }, + { label: 'Viper', value: 'Viper' }, + { label: 'Virage', value: 'Virage' }, + { label: 'Vision', value: 'Vision' }, + { label: 'Vitara', value: 'Vitara' }, + { label: 'Voyager', value: 'Voyager' }, + { label: 'Wagoneer', value: 'Wagoneer' }, + { label: 'Windstar', value: 'Windstar' }, + { label: 'Wrangler', value: 'Wrangler' }, + { label: 'Xterra', value: 'Xterra' }, + { label: 'Yukon', value: 'Yukon' }, + { label: 'Zephyr', value: 'Zephyr' }, +]; diff --git a/docs/examples/AsyncPagination.js b/docs/examples/AsyncPagination.js new file mode 100644 index 0000000000..ad85f0e6e6 --- /dev/null +++ b/docs/examples/AsyncPagination.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; + +import AsyncSelect from '../../src/Async'; +import { carOptions } from '../data'; + +type State = { + inputValue: string, +}; + +const filterCars = (inputValue: string, page: number, itemsPerPage: number) => + (console.log(inputValue), + carOptions.filter(i => + i.label.toLowerCase().includes(inputValue.toLowerCase()) + ).slice((page-1) * itemsPerPage, page * itemsPerPage)); + +const loadOptions = (inputValue, page, callback) => { + setTimeout(() => { + callback(filterCars(inputValue, page, 10)); + }, 1000); +}; + +export default class WithPagination extends Component<*, State> { + state = { inputValue: '' }; + handleInputChange = (newValue: string) => { + const inputValue = newValue.replace(/\W/g, ''); + this.setState({ inputValue }); + return inputValue; + }; + render() { + return ( +
+
inputValue: "{this.state.inputValue}"
+ +
+ ); + } +} diff --git a/docs/examples/index.js b/docs/examples/index.js index e36969af13..0ea0b8d7f5 100644 --- a/docs/examples/index.js +++ b/docs/examples/index.js @@ -3,6 +3,7 @@ export { default as ControlledMenu } from './ControlledMenu'; export { default as AnimatedMulti } from './AnimatedMulti'; export { default as AsyncCallbacks } from './AsyncCallbacks'; export { default as AsyncCreatable } from './AsyncCreatable'; +export { default as AsyncPagination } from './AsyncPagination'; export { default as AsyncPromises } from './AsyncPromises'; export { default as BasicGrouped } from './BasicGrouped'; export { default as BasicMulti } from './BasicMulti'; diff --git a/docs/pages/async/index.js b/docs/pages/async/index.js index 1e95313da1..627615465c 100644 --- a/docs/pages/async/index.js +++ b/docs/pages/async/index.js @@ -7,6 +7,7 @@ import md from '../../markdown/renderer'; import { AsyncCallbacks, AsyncMulti, + AsyncPagination, AsyncPromises, } from '../../examples'; @@ -55,6 +56,16 @@ export default function Async() { )} + ${( + + + + )} + ${( void) => Promise<*> | void, + loadOptions: ((string, (OptionsType) => void) => Promise<*> | void) & + ((string, number, (OptionsType) => void) => Promise<*> | void), /* If cacheOptions is truthy, then the loaded data will be cached. The cache will remain until `cacheOptions` changes value. */ cacheOptions: any, + /* Indicate that loadOptions should be called with page + and to also trigger it when the user scrolls to the bottom of the options. */ + pagination: boolean, }; export type Props = SelectProps & AsyncProps; @@ -29,6 +33,7 @@ type State = { defaultOptions?: OptionsType, inputValue: string, isLoading: boolean, + page: number, loadedInputValue?: string, loadedOptions: OptionsType, passEmptyOptions: boolean, @@ -40,7 +45,13 @@ export const makeAsyncSelect = (SelectComponent: ComponentType<*>) => select: ElementRef<*>; lastRequest: {}; mounted: boolean = false; - optionsCache: { [string]: OptionsType } = {}; + optionsCache: { + [string]: { + options: OptionsType, + page: number, + hasReachedLastPage: boolean, + }, + } = {}; constructor(props: Props) { super(); this.state = { @@ -49,6 +60,7 @@ export const makeAsyncSelect = (SelectComponent: ComponentType<*>) => : undefined, inputValue: '', isLoading: props.defaultOptions === true ? true : false, + page: 0, loadedOptions: [], passEmptyOptions: false, }; @@ -57,11 +69,7 @@ export const makeAsyncSelect = (SelectComponent: ComponentType<*>) => this.mounted = true; const { defaultOptions } = this.props; if (defaultOptions === true) { - this.loadOptions('', options => { - if (!this.mounted) return; - const isLoading = !!this.lastRequest; - this.setState({ defaultOptions: options || [], isLoading }); - }); + this.optionsFromCacheOrLoad(''); } } componentWillReceiveProps(nextProps: Props) { @@ -79,67 +87,92 @@ export const makeAsyncSelect = (SelectComponent: ComponentType<*>) => blur() { this.select.blur(); } - loadOptions(inputValue: string, callback: (?Array<*>) => void) { - const { loadOptions } = this.props; + loadOptions( + inputValue: string, + page: number, + callback: (?Array<*>) => void + ) { + const { loadOptions, pagination } = this.props; if (!loadOptions) return callback(); - const loader = loadOptions(inputValue, callback); + const loader = pagination + ? loadOptions(inputValue, page, callback) + : loadOptions(inputValue, callback); if (loader && typeof loader.then === 'function') { loader.then(callback, () => callback()); } } - handleInputChange = (newValue: string, actionMeta: InputActionMeta) => { - const { cacheOptions, onInputChange } = this.props; - // TODO - const inputValue = handleInputChange(newValue, actionMeta, onInputChange); - if (!inputValue) { - delete this.lastRequest; - this.setState({ - inputValue: '', - loadedInputValue: '', - loadedOptions: [], - isLoading: false, - passEmptyOptions: false, - }); - return; - } - if (cacheOptions && this.optionsCache[inputValue]) { + optionsFromCacheOrLoad(inputValue: string, page: number = 1) { + const { cacheOptions, pagination } = this.props; + const cache = this.optionsCache[inputValue]; + if (cacheOptions && cache && cache.options) { this.setState({ inputValue, loadedInputValue: inputValue, - loadedOptions: this.optionsCache[inputValue], + loadedOptions: cache.options, isLoading: false, + page: cache.page, passEmptyOptions: false, }); - } else { - const request = (this.lastRequest = {}); - this.setState( - { - inputValue, - isLoading: true, - passEmptyOptions: !this.state.loadedInputValue, - }, - () => { - this.loadOptions(inputValue, options => { - if (!this.mounted) return; - if (options) { - this.optionsCache[inputValue] = options; + if ( + !pagination || + (pagination && (cache.page >= page || cache.hasReachedLastPage)) + ) { + return; + } + } + const request = (this.lastRequest = {}); + this.setState( + { + inputValue, + isLoading: true, + passEmptyOptions: !this.state.loadedInputValue, + }, + () => { + this.loadOptions(inputValue, page, options => { + if (!this.mounted) return; + if (options) { + const hasReachedLastPage = pagination && options.length === 0; + if (page > 1) { + options = this.state.loadedOptions.concat(options); } - if (request !== this.lastRequest) return; - delete this.lastRequest; - this.setState({ - isLoading: false, - loadedInputValue: inputValue, - loadedOptions: options || [], - passEmptyOptions: false, - }); + this.optionsCache[inputValue] = { + options, + hasReachedLastPage, + page, + }; + } + if (request !== this.lastRequest) return; + delete this.lastRequest; + this.setState({ + isLoading: false, + page, + loadedInputValue: inputValue, + loadedOptions: options || [], + passEmptyOptions: false, + defaultOptions: + page === 1 && !inputValue + ? options || this.state.defaultOptions + : [], }); - } - ); - } + }); + } + ); + } + handleInputChange = (newValue: string, actionMeta: InputActionMeta) => { + const inputValue = handleInputChange( + newValue, + actionMeta, + this.props.onInputChange + ); + this.optionsFromCacheOrLoad(inputValue); return inputValue; }; + handleMenuScrollToBottom = () => { + if (!this.props.pagination || this.state.isLoading) return; + this.optionsFromCacheOrLoad(this.state.inputValue, this.state.page + 1); + }; render() { - const { loadOptions, ...props } = this.props; + const { loadOptions, pagination, ...props } = this.props; const { defaultOptions, inputValue, @@ -147,10 +180,14 @@ export const makeAsyncSelect = (SelectComponent: ComponentType<*>) => loadedInputValue, loadedOptions, passEmptyOptions, + page, } = this.state; - const options = passEmptyOptions - ? [] - : inputValue && loadedInputValue ? loadedOptions : defaultOptions || []; + const options = + !pagination && passEmptyOptions + ? [] + : (inputValue && loadedInputValue) || page > 1 + ? loadedOptions + : defaultOptions || []; return ( // $FlowFixMe ) => filterOption={null} isLoading={isLoading} onInputChange={this.handleInputChange} + onMenuScrollToBottom={this.handleMenuScrollToBottom} /> ); } diff --git a/src/__tests__/Async.test.js b/src/__tests__/Async.test.js index 7bb5b24cd5..b78c03d159 100644 --- a/src/__tests__/Async.test.js +++ b/src/__tests__/Async.test.js @@ -109,6 +109,73 @@ test.skip('to not call loadOptions again for same value when cacheOptions is tru expect(loadOptionsSpy).toHaveBeenCalledTimes(2); }); +test('to call loadOptions with page when pagination is true', () => { + const loadOptionsSpy = jest.fn((inputValue, page, callback) => callback(OPTIONS)); + mount(); + + expect(loadOptionsSpy).toHaveBeenCalledTimes(1); + expect(loadOptionsSpy.mock.calls[0].length).toBe(3); + expect(loadOptionsSpy.mock.calls[0][1]).toBe(1); +}); + +test('to call loadOptions with incremented page when pagination is true and the user scrolls to the bottom of the options', () => { + const loadOptionsSpy = jest.fn((inputValue, page, callback) => callback(OPTIONS)); + const asyncSelectWrapper = mount( + + ); + const instanceOne = asyncSelectWrapper.instance(); + + instanceOne.handleMenuScrollToBottom(); + + expect(loadOptionsSpy).toHaveBeenCalledTimes(2); + expect(loadOptionsSpy.mock.calls[0][1]).toBe(1); + expect(loadOptionsSpy.mock.calls[1][1]).toBe(2); +}); + +test('to call loadOptions with page 1 when pagination is true and the user changes input to a new, non-cached, value', () => { + const loadOptionsSpy = jest.fn((inputValue, page, callback) => callback(OPTIONS)); + const asyncSelectWrapper = mount( + + ); + const inputValueWrapper = asyncSelectWrapper.find( + 'div.react-select__input input' + ); + const instanceOne = asyncSelectWrapper.instance(); + + instanceOne.handleMenuScrollToBottom(); + asyncSelectWrapper.setProps({ inputValue: 'a' }); + inputValueWrapper.simulate('change', { currentTarget: { value: 'a' } }); + + expect(loadOptionsSpy).toHaveBeenCalledTimes(3); + expect(loadOptionsSpy.mock.calls[0][1]).toBe(1); + expect(loadOptionsSpy.mock.calls[1][1]).toBe(2); + expect(loadOptionsSpy.mock.calls[2][1]).toBe(1); +}); + +test('to to load all of the cached pages from cache when pagination is true', () => { + const loadOptionsSpy = jest.fn((inputValue, page, callback) => callback(OPTIONS)); + const asyncSelectWrapper = mount( + + ); + const inputValueWrapper = asyncSelectWrapper.find( + 'div.react-select__input input' + ); + const instanceOne = asyncSelectWrapper.instance(); + + instanceOne.handleMenuScrollToBottom(); + asyncSelectWrapper.setProps({ inputValue: 'a' }); + inputValueWrapper.simulate('change', { currentTarget: { value: 'a' } }); + + asyncSelectWrapper.setProps({ inputValue: '' }); + inputValueWrapper.simulate('change', { currentTarget: { value: '' } }); + + expect(loadOptionsSpy).toHaveBeenCalledTimes(3); + expect(loadOptionsSpy.mock.calls[0][1]).toBe(1); + expect(loadOptionsSpy.mock.calls[1][1]).toBe(2); + expect(loadOptionsSpy.mock.calls[2][1]).toBe(1); + expect(asyncSelectWrapper.find(Option).length).toBe(OPTIONS.length * 2); +}); + test('to create new cache for each instance', () => { const asyncSelectWrapper = mount(); const instanceOne = asyncSelectWrapper.instance(); diff --git a/src/__tests__/__snapshots__/Async.test.js.snap b/src/__tests__/__snapshots__/Async.test.js.snap index b7543586a3..7dc56709a6 100644 --- a/src/__tests__/__snapshots__/Async.test.js.snap +++ b/src/__tests__/__snapshots__/Async.test.js.snap @@ -14,6 +14,7 @@ exports[`defaults - snapshot 1`] = ` filterOption={null} isLoading={false} onInputChange={[Function]} + onMenuScrollToBottom={[Function]} options={Array []} >