GrResponseErrorBanner

  • core
  • feedback
  • API · 13 props

A universal generic banner for server or network response errors: it takes a raw error/response, runs it through a chain of parsers (HTTP statuses, Laravel/JSON:API/RFC 7807 validation, file/network/abort) and renders a title, main message, an optional list of per-field details and retry/dismiss actions — without knowing anything about a specific feature (file upload, forms, transactions).

import { GrResponseErrorBanner } from '@feugene/granularity/components/GrResponseErrorBanner'

Overview

When to take it

  • the request failed — the network, a 500, a 422 with field errors: the type of the error is determined by itself and changes the heading;
  • there are several errors by field — they are printed as a list with labels instead of one general phrase;
  • the request can be retried — canRetry gives a button next to the message;
  • the HTTP status is needed — a badge with the code helps support without getting in the way of an ordinary user.

When to take something else

NeedTake
The message is not about a server answerGrAlert
A short notification about a resultGrToaster
The error of a single form fieldGrFormField
An error inside a dialogGrConfirmDialog / GrPromptDialog
There is no data, but that is not an errorGrEmptyState

Variants every tone and view, rendered by the real component

tone

  • primary
  • neutral
  • success
  • warning
  • danger
  • info
  • slate
  • azure

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • A 422 from the API, field by field

    useResponseError takes the Response itself: the chain of parsers recognises the Laravel shape, the kind becomes «validation» with a warning tone, and each field gets its own line. field-labels turns API keys into words — «customer.vat_id» becomes «VAT number».

    Invoice INV-0418 · customer details
    Applies from the next invoice
    Invoices already sent keep the details they were issued with.
  • A report that did not load

    A failed fetch is classified as «network»: a danger tone and, with can-retry, a retry button — repeating may well work. retry hands the error back, and the page runs the same request again.

    Revenue by plan

    The report is built on request.

  • A refusal in RFC 7807 format

    A application/problem+json answer is parsed by its own preset: title and detail become the message. A 403 is a «client» error — repeating it changes nothing — so the banner offers no retry, only the reason and the status badge for support.

    Members · Northwind Traders

    Removing a member signs them out on every device. Invoices they created stay.

    MOMina OrtizOwner · [email protected]
    JWJonas WeberAdmin · [email protected]

API generated from the package · cannot drift

Props

PropTypedefaultDescription
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedA rigid override of the tone. It beats `toneByKind`.
textsPartial<ResponseErrorTexts> | undefined{}A partial override of the texts. It is merged with `DEFAULT_RESPONSE_ERROR_TEXTS`.
toneByKind
Partial<Record<ResponseErrorKind, "primary" | "neutral" | "sPartial<Record<ResponseErrorKind, "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure">> | undefined
{}The mapping of `kind` → `tone`. It is merged with `DEFAULT_TONE_BY_KIND`.
showDetailsboolean | undefinedtrueWhether to show the `details`/`fieldErrors` block under the main message.
showFieldLabelsboolean | undefinedtrueWhether to prefix `field:` before the per-field messages.
fieldLabelsRecord<string, string> | undefined{}Human-readable labels of the fields (`{ file: 'File' }`).
dedupeDetailsboolean | undefinedtrueRemove duplicate messages between `message` and `details`.
canRetryboolean | undefinedfalseWhether to show the "Retry" button.
canDismissboolean | undefinedtrueWhether to show the "Dismiss" button.
autoHideKindsResponseErrorKind[] | undefinedDEFAULT_AUTO_HIDE_KINDSWhich `kind`s not to render at all (the banner hides).
showStatusboolean | undefinedtrueWhether to show the HTTP status badge.
testIdPrefixstring | undefined"response-error"The `data-testid` prefix.
errorrequiredResponseErrorInfo | null—A ready structure of the error. If `null`, the banner is not rendered.

Events

EventSignatureDescription
retry(event: "retry", error: ResponseErrorInfo): void—
dismiss(event: "dismiss"): void—

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { UseResponseErrorOptions, UseResponseErrorReturn, ResponseErrorParserPresets } from '@feugene/granularity/components/GrResponseErrorBanner'

Filedist/types/components/GrResponseErrorBanner/useResponseError.d.ts

UseResponseErrorOptionstype
type UseResponseErrorOptions = {
    parsers?: () => ResponseErrorParser[] | undefined;
UseResponseErrorReturntype
type UseResponseErrorReturn = {
    currentError: Ref<ResponseErrorInfo | null>;
    isVisible: ComputedRef<boolean>;
    lastRaw: Ref<unknown>;
    setRaw: (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo | null>;
useResponseErrorfunction
function useResponseError(options?: UseResponseErrorOptions): UseResponseErrorReturn;

Filedist/types/components/GrResponseErrorBanner/parsers/createResponseErrorClassifier.d.ts

coreResponseErrorParsersconst
const coreResponseErrorParsers: ResponseErrorParser[];
ResponseErrorParserPresetstype
type ResponseErrorParserPresets = {
    core: ResponseErrorParser[];
    all: ResponseErrorParser[];
    laravel: ResponseErrorParser;
    problemDetails: ResponseErrorParser;
    jsonApi: ResponseErrorParser;
    fileValidation: ResponseErrorParser;
};
extendDefaultParsersfunction
function extendDefaultParsers(extra: ResponseErrorParser[]): ResponseErrorParser[];
CreateResponseErrorClassifierOptionstype
type CreateResponseErrorClassifierOptions = {
    parsers?: ResponseErrorParser[];
    texts?: Partial<ResponseErrorTexts>;
    messageKey?: string;
};
createResponseErrorClassifierfunction
function createResponseErrorClassifier(options?: CreateResponseErrorClassifierOptions): (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo>;
export declare function resolveResponseErrorTitle(kind: ResponseErrorKind, texts: ResponseErrorTexts): string;

Filedist/types/components/GrResponseErrorBanner/parsers/normalizeError.d.ts

normalizeErrorfunction
function normalizeError(raw: unknown): Promise<NormalizedRawError>;

Filedist/types/components/GrResponseErrorBanner/responseError.types.d.ts

ResponseErrorKindtype
type ResponseErrorKind = 'network' | 'aborted' | 'validation' | 'client' | 'server' | 'unknown';
ResponseErrorTonetype
type ResponseErrorTone = GrTone;
ResponseErrorFieldErrortype
type ResponseErrorFieldError = {
    field: string;
    messages: string[];
};
ResponseErrorInfotype
type ResponseErrorInfo = {
    kind: ResponseErrorKind;
    status?: number;
    message: string;
    details?: string[];
    fieldErrors?: ResponseErrorFieldError[];
    raw: unknown;
    meta?: Record<string, unknown>;
    isFallbackMessage?: boolean;
};
NormalizedRawErrortype
type NormalizedRawError = {
    raw: unknown;
    status?: number;
    body?: unknown;
    headers?: Record<string, string>;
    isAbort?: boolean;
    isNetwork?: boolean;
};
ResponseErrorContexttype
type ResponseErrorContext = NormalizedRawError & {
    meta: Record<string, unknown>;
    texts: ResponseErrorTexts;
};
ResponseErrorParserResulttype
type ResponseErrorParserResult = {
    kind?: ResponseErrorKind;
    status?: number;
    message?: string;
    details?: string[];
    fieldErrors?: ResponseErrorFieldError[];
    stop?: boolean;
};
ResponseErrorParsertype
type ResponseErrorParser = (ctx: ResponseErrorContext) => ResponseErrorParserResult | null | undefined;
export type ResponseErrorTexts = {
ResponseErrorTextstype
type ResponseErrorTexts = {
    networkTitle: string;
    networkMessage: string;
    abortedTitle: string;
    abortedMessage: string;
    validationTitle: string;
    validationMessage: string;
    clientTitle: string;
    clientMessage: string;
    serverTitle: string;
    serverMessage: string;
    unknownTitle: string;
    unknownMessage: string;
    statusLabel: string;
    retryLabel: string;
    dismissLabel: string;
};

Filedist/types/components/GrResponseErrorBanner/GrFormErrorBanner.vue.d.ts

GrFormErrorBannerPropsinterface
interface GrFormErrorBannerProps {
    error: ResponseErrorInfo | null;
    fieldLabels?: Record<string, string>;
    texts?: Partial<ResponseErrorTexts>;
    tone?: ResponseErrorTone;
    toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
    canRetry?: boolean;
    canDismiss?: boolean;
    autoHideKinds?: ResponseErrorKind[];
    testIdPrefix?: string;
}
GrFormErrorBannerEmitsinterface
interface GrFormErrorBannerEmits {
    (e: 'retry', error: ResponseErrorInfo): void;
    (e: 'dismiss'): void;
}

Filedist/types/components/GrResponseErrorBanner/GrResponseErrorBanner.vue.d.ts

GrResponseErrorBannerPropsinterface
interface GrResponseErrorBannerProps {
    error: ResponseErrorInfo | null;
    texts?: Partial<ResponseErrorTexts>;
    tone?: ResponseErrorTone;
    toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
    showDetails?: boolean;
    showFieldLabels?: boolean;
    fieldLabels?: Record<string, string>;
    dedupeDetails?: boolean;
    canRetry?: boolean;
    canDismiss?: boolean;
    autoHideKinds?: ResponseErrorKind[];
    showStatus?: boolean;
    testIdPrefix?: string;
}
GrResponseErrorBannerEmitsinterface
interface GrResponseErrorBannerEmits {
    (e: 'retry', error: ResponseErrorInfo): void;
    (e: 'dismiss'): void;
}

Filedist/types/components/GrResponseErrorBanner/GrUploadErrorBanner.vue.d.ts

GrUploadErrorBannerPropsinterface
interface GrUploadErrorBannerProps {
    error: ResponseErrorInfo | null;
    files?: File[];
    texts?: Partial<ResponseErrorTexts>;
    tone?: ResponseErrorTone;
    toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
    canRetry?: boolean;
    canDismiss?: boolean;
    autoHideKinds?: ResponseErrorKind[];
    testIdPrefix?: string;
}
GrUploadErrorBannerEmitsinterface
interface GrUploadErrorBannerEmits {
    (e: 'retry', payload: {
        error: ResponseErrorInfo;
        files: File[];
    }): void;
    (e: 'dismiss'): void;
}

Usage from the library prose

Three layers
  1. normalizeError brings axios / a fetch Response / an XMLHttpRequest / a bare Error / a string to a common shape: the status, the parsed body, the headers, the signs of a cancellation and of a network error. The body of a Response is read from a clone, so the consumer can read it themselves;
  2. the chain of parsers — from the normalised context to a ResponseErrorInfo (kind, message, details, fieldErrors). The order matters, and a parser may stop the chain (stop);
  3. the banner — the display: the tone by kind, the texts from the locale, the deduplication of the details.
The chain of parsers

The default is the universal core (coreResponseErrorParsers): abort, network, HTTP status, plain message. It works at the transport level and makes no assumptions about the format of the body, so it does not fire falsely on “almost Laravel” answers.

Server-specific parsers are connected deliberately:

ts
const { setRaw } = useResponseError({
  parsers: () => extendDefaultParsers([myParser]),
})

// or through the builder in the options of the dialogs and of the banner
errorParsers: presets => [...presets.core, presets.laravel]

The ready presets: laravel, problemDetails (RFC 7807), jsonApi, fileValidation (the client-side checking of files from GrFileUpload).

The texts, i18n and the fallback flag

The texts come from the locale (gr.responseError.*), and the texts prop goes on top. The label of the status badge uses the same mechanism: statusLabel with a {status} insertion.

When none of the parsers produced a message, it is substituted by the classifier and marked isFallbackMessage: true. The banner replaces only such a message with a translation. Recognising a fallback by comparing strings, which used to be done here, threw away the server’s answer if it happened to match the default word for word (and a server may well return "Network error.").

Hence the contract of a parser: message is filled only with what was found in the answer. A general text by kind is the job of the classifier, and a parser does not have to substitute it; otherwise the message will arrive without the flag and it will no longer be possible to translate it. Parsers that know only the type of the error (httpStatus, abort, network) do not fill the message at all.

The message of a transport error (axios’s Request failed with status 500) does not count as a message from the server: it is taken only if there was no answer at all. Otherwise the user would see a technical phrase in English instead of a translated text.

The tone and accessibility

kind → the default tone: validation/client is warning, network/server/unknown is danger, aborted is info. It is overridden pointwise (toneByKind) or as a whole (tone).

The role is inherited from GrAlert: warning and danger are announced as role="alert" (they interrupt the speech of a screen reader), the rest as role="status" (they wait for a pause). The banner deliberately has no setting of its own for that — GrAlert has one.

autoHideKinds hides the banner entirely for the listed kinds: the typical case is swallowing aborted quietly.

The presets

GrFormErrorBanner and GrUploadErrorBanner are thin wrappers with preconfigurations: forms need no retry and do need field labels, while an upload needs a retry and the context of the files (its preset gives that into retry as { error, files } — the base banner gives away the error itself).

They add no markup of their own and live in the folder of the base component deliberately: a separate unit of granularity would give nothing — neither CSS of its own nor a safelist of its own — and would cost two extra entries in the build. They are imported from the root of the package.