Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-carrots-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clack/prompts": patch
---

Replace `onCancel` handler for `group()` by returning `cancel` as results instead. Partial results are no longer wrapped in an object.
89 changes: 43 additions & 46 deletions examples/basic/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as p from '@clack/prompts';
import color from 'picocolors';
import { setTimeout } from 'node:timers/promises';
import color from 'picocolors';

async function main() {
console.clear();
Expand All @@ -9,51 +9,48 @@ async function main() {

p.intro(`${color.bgCyan(color.black(' create-app '))}`);

const project = await p.group(
{
path: () =>
p.text({
message: 'Where should we create your project?',
placeholder: './sparkling-solid',
validate: (value) => {
if (!value) return 'Please enter a path.';
if (value[0] !== '.') return 'Please enter a relative path.';
},
}),
type: ({ results }) =>
p.select({
message: `Pick a project type within "${results.path}"`,
initialValue: 'ts',
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'js', label: 'JavaScript' },
{ value: 'coffee', label: 'CoffeeScript', hint: 'oh no' },
],
}),
tools: () =>
p.multiselect({
message: 'Select additional tools.',
initialValue: ['prettier', 'eslint'],
options: [
{ value: 'prettier', label: 'Prettier', hint: 'recommended' },
{ value: 'eslint', label: 'ESLint', hint: 'recommended' },
{ value: 'stylelint', label: 'Stylelint' },
{ value: 'gh-action', label: 'GitHub Action' },
],
}),
install: () =>
p.confirm({
message: 'Install dependencies?',
initialValue: false,
}),
},
{
onCancel: () => {
p.cancel('Operation cancelled.');
process.exit(0);
},
}
);
const project = await p.group({
path: () =>
p.text({
message: 'Where should we create your project?',
placeholder: './sparkling-solid',
validate: (value) => {
if (!value) return 'Please enter a path.';
if (value[0] !== '.') return 'Please enter a relative path.';
},
}),
type: ({ path }) =>
p.select({
message: `Pick a project type within "${path}"`,
initialValue: 'ts',
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'js', label: 'JavaScript' },
{ value: 'coffee', label: 'CoffeeScript', hint: 'oh no' },
],
}),
tools: () =>
p.multiselect({
message: 'Select additional tools.',
initialValue: ['prettier', 'eslint'],
options: [
{ value: 'prettier', label: 'Prettier', hint: 'recommended' },
{ value: 'eslint', label: 'ESLint', hint: 'recommended' },
{ value: 'stylelint', label: 'Stylelint' },
{ value: 'gh-action', label: 'GitHub Action' },
],
}),
install: () =>
p.confirm({
message: 'Install dependencies?',
initialValue: false,
}),
});

if (p.isCancel(project)) {
p.cancel('Operation cancelled.');
process.exit(0);
}

if (project.install) {
const s = p.spinner();
Expand Down
43 changes: 19 additions & 24 deletions packages/prompts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,34 +125,29 @@ s.stop('Installed via npm');

### Grouping

Grouping prompts together is a great way to keep your code organized. This accepts a JSON object with a name that can be used to reference the group later. The second argument is an optional but has a `onCancel` callback that will be called if the user cancels one of the prompts in the group.
Group prompts together to keep your code organized. Returns results of each prompt accessible by name. If one prompt is cancelled, the entire group is cancelled and `cancel` is returned instead. Each prompt factory function is passed the results of previous prompts.

```js
import * as p from '@clack/prompts';

const group = await p.group(
{
name: () => p.text({ message: 'What is your name?' }),
age: () => p.text({ message: 'What is your age?' }),
color: ({ results }) =>
p.multiselect({
message: `What is your favorite color ${results.name}?`,
options: [
{ value: 'red', label: 'Red' },
{ value: 'green', label: 'Green' },
{ value: 'blue', label: 'Blue' },
],
}),
},
{
// On Cancel callback that wraps the group
// So if the user cancels one of the prompts in the group this function will be called
onCancel: ({ results }) => {
p.cancel('Operation cancelled.');
process.exit(0);
},
}
);
const group = await p.group({
name: () => p.text({ message: 'What is your name?' }),
age: () => p.text({ message: 'What is your age?' }),
color: ({ name }) =>
p.multiselect({
message: `What is your favorite color ${name}?`,
options: [
{ value: 'red', label: 'Red' },
{ value: 'green', label: 'Green' },
{ value: 'blue', label: 'Blue' },
],
}),
});

if (p.isCancel(group)) {
p.cancel('Operation cancelled.');
process.exit(0);
}

console.log(group.name, group.age, group.color);
```
51 changes: 18 additions & 33 deletions packages/prompts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,48 +367,33 @@ function ansiRegex() {
return new RegExp(pattern, 'g');
}

export type PromptGroupAwaitedReturn<T> = {
[P in keyof T]: Exclude<Awaited<T[P]>, symbol>;
type GroupAwaitedReturn<T> = {
[P in keyof T]: Awaited<T[P]>;
};

export interface PromptGroupOptions<T> {
/**
* Control how the group can be canceld
* if one of the prompts is canceld.
*/
onCancel?: (opts: { results: Partial<PromptGroupAwaitedReturn<T>> }) => void;
}
type GroupPromptFactory<T, P extends keyof T = keyof T> = (
results: Partial<GroupAwaitedReturn<T>>
) => Promise<T[P] | symbol>;

export type PromptGroup<T> = {
[P in keyof T]: (opts: { results: Partial<PromptGroupAwaitedReturn<T>> }) => Promise<T[P]>;
export type Group<T> = {
[P in keyof T]: GroupPromptFactory<T, P>;
};

/**
* Define a group of prompts to be displayed
* and return a results of objects within the group
* Define a group of prompts to be displayed in sequence,
* returns results for each prompt in the group or `cancel`.
*/
export const group = async <T>(
prompts: PromptGroup<T>,
opts?: PromptGroupOptions<T>
): Promise<PromptGroupAwaitedReturn<T>> => {
const results = {} as any;
const promptNames = Object.keys(prompts);

for (const name of promptNames) {
const result = await prompts[name as keyof T]({ results }).catch((e) => {
throw e;
});

// Pass the results to the onCancel function
// so the user can decide what to do with the results
// TODO: Switch to callback within core to avoid isCancel Fn
if (typeof opts?.onCancel === 'function' && isCancel(result)) {
results[name] = 'canceled';
opts.onCancel({ results });
continue;
export const group = async <T>(prompts: Group<T>): Promise<GroupAwaitedReturn<T> | symbol> => {
const results = {} as GroupAwaitedReturn<T>;

for (const [key, prompt] of Object.entries<GroupPromptFactory<T>>(prompts)) {
const result = await prompt(results);

if (isCancel(result)) {
return result;
}

results[name] = result;
results[key as keyof T] = result;
}

return results;
Expand Down