Zum Hauptinhalt springen

Migration von internen Jutro-Einstiegspunkten

Einige Anwendungen können unbeabsichtigt vom internen Einstiegspunkt @jutro/components/jutro-editor importieren. Dieser Pfad ist nicht Teil der öffentlichen API-Oberfläche von Jutro und wurde nie von Jutro-Abwärtskompatibilitätsgarantien abgedeckt. Er wurde in Jutro 10.13 entfernt.

Wenn Ihr Build bei einem der folgenden Importe aus @jutro/components/jutro-editor mit Fehlern fehlschlägt, wird in dieser Anleitung beschrieben, wie Sie Ihre Anwendung migrieren:

  • FormContainer
  • useFormData
  • useFormValidation
  • useFormContext
  • formField
  • overrideProps
  • getUniqueValueEditorInput
  • copyUniqueValue
  • ModalNextAlert
  • ModalNextConfirmation
  • ModalNextCustom
  • InlineNotification

Auswählen einer Migrationsoption​

Die richtige Vorgehensweise hängt davon ab, wie FormContainer und die zugehörigen Hooks in Ihrer Anwendung verwendet werden. Es gibt drei Muster mit jeweils unterschiedlichen empfohlenen Korrekturen:

MusterWie man es erkenntEmpfohlene Lösung
Passiver WrapperuseFormData und useFormValidation werden aufgerufen, aber ihre Ausgaben werden nur an FormContainer übergeben – nichts anderes in der Komponente liest sie.Löschen Sie <FormContainer> und die Hooks .
Zustand des aktiven FormularsHook-Ausgaben geben das tatsächliche Verhalten vor: Einreichung, Validierungsgating oder Filterung.Verschieben Sie den Status in einen seitenspezifischen Hook mit domänenspezifischen Settern.
Kontext-KonsumentenEine untergeordnete Komponente importiert useFormContext, um Formulardaten direkt aus dem Kontext zu lesen.Entfernen Sie die Kontextabhängigkeit und übergeben Sie Werte als explizite Eigenschaften.

Wenn Sie die Migration nicht vor dem Upgrade auf Jutro 10.13 abschließen können, finden Sie weitere Informationen unter Temporäre Alternative: Implementierungen lokal kopieren.

Option 1: Entfernen des passiven Wrappers​

Verwenden Sie diese Option, wenn FormContainer ein struktureller Wrapper ohne aktive Rolle ist: useFormData und useFormValidation werden aufgerufen, aber ihre Ausgaben fließen nur in FormContainer-Eigenschaften ein, und nichts anderes in der Komponente liest sie aus.

In diesem Fall kann das FormContainer-Element zusammen mit den nicht verwendeten Hooks gelöscht werden. Die gerenderte Ausgabe und das Verhalten der Komponente sind ohne sie identisch.

Gehen Sie folgendermaßen vor, um den Code zu refaktorieren:

  1. Entfernen Sie das <FormContainer>-Element aus JSX und verschieben Sie seine untergeordneten Elemente eine Ebene nach oben.
  2. Entfernen Sie die Hook-Aufrufe useFormData und useFormValidation sowie alle lokalen Variablen und Funktionen, die nur dazu dienten, FormContainer-Eigenschaften zu füllen (z. B. callbackMap, submitted sowie alle Inline-Reset- oder Save-Handler).
  3. Entfernen Sie den Import aus @jutro/components/jutro-editor.

Beispiel für das Entfernen des passiven Wrappers​

Das folgende Beispiel zeigt eine Seitenkomponente, bei der <FormContainer> den zugehörigen Inhalt umschließt und useFormData und useFormValidation aufgerufen werden, deren Ausgaben jedoch nur in die FormContainer-Eigenschaften einfließen und nicht darüber hinaus.

Vorher​

Die Komponente ruft useFormData und useFormValidation auf und umschließt ihre untergeordneten Elemente in <FormContainer>, aber keine der beiden Hook-Ausgaben wird außerhalb von der FormContainer-Eigenschaften gelesen. Zusätzliche lokale Status- und Handler-Funktionen sind nur vorhanden, um die API von FormContainer zu erfüllen.

import React, { useState } from 'react';
import {
FormContainer,
useFormData,
useFormValidation,
} from '@jutro/components/jutro-editor';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';

interface PoliciesProps {
className?: string;
overrides: { [key: string]: Record<string, unknown> };
}

export const Policies: React.FC<PoliciesProps> = (props) => {
const [formData, setFormValue, resetFormData] = useFormData({});
const [isFormValid, handleFormValidation] = useFormValidation();
const [submitted, setSubmitted] = useState(false);

const resetForm = () => {
setSubmitted(false);
resetFormData({});
};
const saveForm = () => {
if (!isFormValid) setSubmitted(true);
};
const callbackMap = { resetForm, saveForm };

return (
<FormContainer
data={formData}
onDataChange={setFormValue}
callbackMap={callbackMap}
onValidationChange={handleFormValidation}
showErrors={submitted}>
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, props.className)}>
<PolicyTable className={classNames('instance', styles.policyTable)} />
</div>
</div>
</FormContainer>
);
};

Nachher​

Die Hooks, der zugehörige Zustand und <FormContainer> werden vollständig entfernt. Die gerenderte Ausgabe und das Verhalten der Komponente sind identisch.

import React from 'react';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';

interface PoliciesProps {
className?: string;
}

export const Policies: React.FC<PoliciesProps> = ({ className }) => (
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, className)}>
<PolicyTable className={classNames('instance', styles.policyTable)} />
</div>
</div>
);

Option 2: Ersetzen durch einen seitenspezifischen Hook​

Nutzen Sie diese Option, wenn die Ausgaben des Hooks echte Komponentenlogik steuern – zum Beispiel, ob ein Formular abgesendet werden kann, wann ein Datenabruf ausgeführt wird oder das Anwenden eines Filters basierend auf Benutzereingaben.

Die Lösung besteht darin, den Formularstatus in einen eigenen Hook mit domänenspezifischen Settern und Handlern zu verschieben und FormContainer aus JSX zu entfernen. Untergeordnete Komponenten erhalten Werte und Handler als einfache Eigenschaften.

Gehen Sie folgendermaßen vor, um den Code zu refaktorieren:

  1. Erstellen Sie eine dedizierte Hook-Datei, z. B. usePolicies.ts.
  2. Ersetzen Sie useFormData im Hook durch einzelne useState Aufrufe – einen für jeden nachverfolgten Wert. Geben Sie jedem Setter einen Namen, der seinen Domänenzweck widerspiegelt, anstatt die generische (value, path)-Signatur zu verwenden.
  3. Ersetzen Sie useFormValidation durch einen einfachen Gültigkeitsausdruck, der zum Hook gehört. Der Hook definiert, was „gültig“ für diese bestimmte Seite bedeutet.
  4. Verschieben Sie die Action- und Reset-Logik in benannte Handler-Funktionen innerhalb des Hooks, die deren Domänenzweck widerspiegeln. callbackMap entfernen.
  5. Entfernen Sie <FormContainer> aus dem JSX.
  6. Verwenden Sie den Hook in der Seitenkomponente und übergeben Sie Werte und Handler direkt als Eigenschaften an untergeordnete Komponenten.
  7. Entfernen Sie den Import aus @jutro/components/jutro-editor.

Beispiel für einen seitenspezifischen Hook​

Das folgende Beispiel zeigt eine Seitenkomponente, in der useFormData und useFormValidation das tatsächliche Verhalten steuern – in diesem Fall die Validierung eines Policennummernfelds und dessen Anwendung als Filter. Die Vorher-Version verwaltet den gesamten Zustand inline direkt mit FormContainer. Die Nachher-Version verschiebt diesen Status in einen dedizierten usePolicies-Hook und entfernt FormContainer vollständig, wobei untergeordnete Komponenten Werte und Handler als einfache Eigenschaften erhalten.

Vorher​

Die Seitenkomponente verwaltet alle Formularstatus inline. useFormData verfolgt den Feldwert, useFormValidation verfolgt die Gültigkeit und FormContainer fungiert als Datenanbieter. Die Submit- und Reset-Logik befindet sich direkt im Hauptteil der Komponente.

Policies.tsx
import React, { useState } from 'react';
import {
FormContainer,
useFormData,
useFormValidation,
} from '@jutro/components/jutro-editor';
import { TextInput, Button } from '@jutro/components';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';

interface PoliciesProps {
className?: string;
overrides: { [key: string]: Record<string, unknown> };
}

export const Policies: React.FC<PoliciesProps> = (props: PoliciesProps) => {
const [formData, setFormValue, resetFormData] = useFormData({
policyNumber: '',
});
const [isFormValid, handleFormValidation] = useFormValidation();
const [submitted, setSubmitted] = useState(false);
const [appliedFilter, setAppliedFilter] = useState('');

const resetForm = () => {
setSubmitted(false);
resetFormData({ policyNumber: '' });
setAppliedFilter('');
};

const saveForm = () => {
if (!isFormValid) {
setSubmitted(true);
return;
}
setAppliedFilter(formData.policyNumber as string);
};

return (
<FormContainer
data={formData}
onDataChange={setFormValue}
callbackMap={{ resetForm, saveForm }}
onValidationChange={handleFormValidation}
showErrors={submitted}>
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, props.className)}>
<TextInput
path="policyNumber"
label="Policy number"
value={formData.policyNumber as string}
onValueChange={setFormValue}
onValidationChange={handleFormValidation}
showErrors={submitted}
required
/>
<Button onClick={saveForm}>Apply</Button>
<Button
onClick={resetForm}
type="tertiary">
Reset
</Button>
<PolicyTable
filter={appliedFilter}
className={classNames('instance', styles.policyTable)}
/>
</div>
</div>
</FormContainer>
);
};

Nachher​

Die Seitenkomponente ist jetzt nur noch für die Benutzeroberfläche bestimmt. Sie verwendet den Hook und übergibt Werte und Handler als Eigenschaften. Es gibt keinen Formularzustand oder FormContainer im JSX.

Policies.tsx
import React from 'react';
import { TextInput, Button } from '@jutro/components';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
import { usePolicies } from './usePolicies';

interface PoliciesProps {
className?: string;
}

export const Policies: React.FC<PoliciesProps> = ({ className }) => {
const {
policyNumber,
setPolicyNumber,
appliedFilter,
showErrors,
applyFilter,
resetFilter,
} = usePolicies();

return (
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, className)}>
<TextInput
path="policyNumber"
label="Policy number"
value={policyNumber}
onValueChange={setPolicyNumber}
showErrors={showErrors}
required
/>
<Button onClick={applyFilter}>Apply</Button>
<Button
onClick={resetFilter}
type="tertiary">
Reset
</Button>
<PolicyTable
filter={appliedFilter}
className={classNames('instance', styles.policyTable)}
/>
</div>
</div>
);
};

Der Hook besitzt die gesamte Statusverwaltung und Logik. Jedes Feld verfügt über einen eigenen useState-Aufruf mit einem benannten Setter. Validity (Gültigkeit) ist ein einfacher Ausdruck. Submit und Reset sind benannte Funktionen (applyFilter und resetFilter), die von der Seitenkomponente direkt aufgerufen werden.

usePolicies.ts
import { useCallback, useState } from 'react';

export const usePolicies = () => {
const [policyNumber, setPolicyNumberState] = useState('');
const [appliedFilter, setAppliedFilter] = useState('');
const [showErrors, setShowErrors] = useState(false);

const isValid = policyNumber.trim().length > 0;

// path is unused but kept to match the (value, path) onValueChange signature
const setPolicyNumber = useCallback((value: string, _path: string) => {
setPolicyNumberState(value);
}, []);

const applyFilter = useCallback(() => {
if (!isValid) {
setShowErrors(true);
return;
}
setAppliedFilter(policyNumber);
}, [isValid, policyNumber]);

const resetFilter = useCallback(() => {
setShowErrors(false);
setPolicyNumberState('');
setAppliedFilter('');
}, []);

return {
policyNumber,
setPolicyNumber,
appliedFilter,
showErrors,
applyFilter,
resetFilter,
};
};

Option 3: Impliziten Kontext durch explizite Eigenschaften ersetzen​

Verwenden Sie diese Option, wenn eine untergeordnete Komponente useFormContext importiert, um Daten von einem übergeordneten FormContainer zu lesen.

Die Lösung besteht darin, die Kontextabhängigkeit aus dem untergeordneten Element zu entfernen und die erforderlichen Werte stattdessen als explizite Eigenschaften aus dem übergeordneten Element zu übergeben. Dadurch werden die Abhängigkeiten des untergeordneten Elements in seinem Eigenschaftentyp sichtbar und ein übergeordneter FormContainer ist nicht mehr erforderlich.

Gehen Sie folgendermaßen vor, um den Code zu refaktorieren:

  1. Fügen Sie in der untergeordneten Komponente die erforderlichen Werte als explizite Eigenschaften in der Schnittstelle der Komponente hinzu.
  2. Entfernen Sie den useFormContext-Aufruf und ersetzen Sie alle im Kontext gelesenen Werte durch die neuen Eigenschaften.
  3. Entfernen Sie den Import aus @jutro/components/jutro-editor in der untergeordneten Komponente.
  4. Entfernen Sie in der übergeordneten Komponente alle useFormData, useFormValidation und die zugehörigen lokalen Status- oder Handler-Variablen, die nur vorhanden waren, um FormContainer zu beliefern. Verwalten Sie den erforderlichen Status direkt mit useState und übergeben Sie die Werte als explizite Eigenschaften.
  5. Entfernen Sie <FormContainer> aus dem übergeordneten Element.

Beispiel für das Ersetzen von Kontext durch explizite Eigenschaften​

Das folgende Beispiel zeigt eine untergeordnete Komponente, die mithilfe von useFormContext einen Filterwert aus dem Formularkontext liest. Nach der Migration deklariert das untergeordnete Element den Wert als explizite Eigenschaft, und das übergeordnete Element verwaltet den Zustand direkt, wodurch die Abhängigkeit von FormContainer entfernt wird.

Vorher​

Die untergeordnete Komponente liest data.policyNumber aus FormContext mit useFormContext. Dadurch entsteht eine implizite Abhängigkeit von einem übergeordneten FormContainer – das untergeordnete Element kann nicht unabhängig verwendet oder getestet werden.

PolicyTable.tsx
import React from 'react';
import { useFormContext } from '@jutro/components/jutro-editor';
interface PolicyTableProps {
className?: string;
}
export const PolicyTable: React.FC<PolicyTableProps> = ({ className }) => {
const { data } = useFormContext();
const filter = (data?.policyNumber as string) ?? '';
// ...fetch / filter rows by `filter`, render the table
return <table className={className}>{/* rows for `filter` */}</table>;
};

Nachher​

Das untergeordnete Objekt deklariert nun den erforderlichen Wert als explizite Eigenschaft. Die Kontextabhängigkeit ist verschwunden und die Komponente benötigt keine übergeordnete FormContainer-Komponente mehr.

PolicyTable.tsx
import React from 'react';
interface PolicyTableProps {
className?: string;
filter?: string;
}
export const PolicyTable: React.FC<PolicyTableProps> = ({
className,
filter = '',
}) => {
// ...fetch / filter rows by `filter`, render the table
return <table className={className}>{/* rows for `filter` */}</table>;
};

Das übergeordnete Element verwaltet den Zustand direkt mit useState und gibt ihn als Eigenschaft weiter. In JSX gibt es keinen FormContainer.

Policies.tsx
import React, { useState } from 'react';
import classNames from 'classnames';
import styles from './Policies.module.scss';
import { PolicyTable } from '../../components/PolicyTable/PolicyTable';
interface PoliciesProps {
className?: string;
}
export const Policies: React.FC<PoliciesProps> = ({ className }) => {
const [filter] = useState('');
return (
<div className="page_wrapper">
<div className={classNames('tokens', styles.root, className)}>
<PolicyTable
filter={filter}
className={classNames('instance', styles.policyTable)}
/>
</div>
</div>
);
};

Temporäre Alternative: Implementierungen lokal kopieren​

Wenn Sie die vollständige Migration nicht vor dem Upgrade auf Jutro 10.13 abschließen können, können Sie die betroffenen Implementierungen als lokale Dateien in Ihre Anwendung kopieren und nur die Importpfade aktualisieren. Die Komponentenlogik und JSX bleiben unverändert.

Note: Dies ist nur eine vorübergehende Maßnahme. Dadurch bleibt Ihre Anwendung in einem veralteten Muster, und eine vollständige Migration mit einer der vorherigen Optionen muss folgen.

Gehen Sie folgendermaßen vor, um die temporäre Alternative anzuwenden:

  1. Erstellen Sie die folgenden drei Dateien in Ihrer Anwendung, zum Beispiel unter src/utils/form/:

src/utils/form/FormContext.tsx
import React, { createContext, useContext, useMemo } from 'react';

export type FormContextValue = {
data?: Record<string, unknown>;
onDataChange?: (value: unknown, path: string) => void;
onValidationChange?: (isFieldValid: boolean, fieldPath: string) => void;
callbackMap?: Record<string, (...args: unknown[]) => unknown>;
showErrors?: boolean;
readOnly?: boolean;
disabled?: boolean;
displayOnly?: boolean;
};

const FormContext = createContext<FormContextValue | undefined>(undefined);

export const useFormContext = (): FormContextValue => {
const ctx = useContext(FormContext);
if (!ctx) {
throw new Error('Please check that your page is wrapped in FormContainer');
}
return ctx;
};

export type FormContainerProps = FormContextValue & {
children: React.ReactNode;
};

export const FormContainer: React.FC<FormContainerProps> = ({
children,
data,
onDataChange,
onValidationChange,
callbackMap,
showErrors,
readOnly,
disabled,
displayOnly,
}) => {
const value = useMemo(
() => ({
data,
onDataChange,
onValidationChange,
callbackMap,
showErrors,
readOnly,
disabled,
displayOnly,
}),
[
data,
onDataChange,
onValidationChange,
callbackMap,
showErrors,
readOnly,
disabled,
displayOnly,
]
);
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};

src/utils/form/useFormData.ts
import { useCallback, useRef, useState } from 'react';
import { cloneDeep, set } from 'lodash';

type FormValue = unknown;
type FormData = Record<string, FormValue>;
type SetFormValue = (value: FormValue, path: string) => void;
type ResetFormData = (next?: FormData | (() => FormData)) => void;

export const useFormData = (
initial: FormData | (() => FormData) = {}
): [FormData, SetFormValue, ResetFormData, FormData | undefined] => {
const previous = useRef<FormData | undefined>(undefined);

const [formData, setFormData] = useState<FormData>(() => {
const seed =
typeof initial === 'function' ? (initial as () => FormData)() : initial;
previous.current = seed ? cloneDeep(seed) : {};
return seed;
});

const resetFormData: ResetFormData = useCallback((next) => {
const seed =
typeof next === 'function' ? (next as () => FormData)() : next ?? {};
setFormData(() => {
previous.current = cloneDeep(seed);
return seed;
});
}, []);

const setFormValue: SetFormValue = useCallback((value, path) => {
setFormData((prev) => {
const nextState = { ...prev };
const v = value as {
code?: unknown;
name?: { defaultMessage?: unknown };
} | null;
if (
v &&
typeof v === 'object' &&
v.code !== undefined &&
v.name?.defaultMessage !== undefined
) {
set(nextState, path, { code: v.code, name: v.name.defaultMessage });
} else {
set(nextState, path, value);
}
return nextState;
});
}, []);

return [formData, setFormValue, resetFormData, previous.current];
};

src/utils/form/useFormValidation.ts
import { useCallback, useState } from 'react';

type ValidationChangeFn = (isFieldValid: boolean, fieldPath: string) => void;

export const useFormValidation = (): [boolean, ValidationChangeFn] => {
const [invalidPaths, setInvalidPaths] = useState<string[]>([]);

const handleFieldValidationChange: ValidationChangeFn = useCallback(
(isFieldValid, fieldPath) => {
setInvalidPaths((prev) => {
const idx = prev.indexOf(fieldPath);
if (isFieldValid && idx !== -1) {
const next = [...prev];
next.splice(idx, 1);
return next;
}
if (!isFieldValid && idx === -1) {
return [...prev, fieldPath];
}
return prev;
});
},
[]
);

return [invalidPaths.length === 0, handleFieldValidationChange];
};
  1. Aktualisieren Sie nach dem Erstellen der Dateien nur die import-Anweisungen in jeder betroffenen Datei:
// Before
import {
FormContainer,
useFormData,
useFormValidation,
} from '@jutro/components/jutro-editor';

// After
import { FormContainer } from 'src/utils/form/FormContext';
import { useFormData } from 'src/utils/form/useFormData';
import { useFormValidation } from 'src/utils/form/useFormValidation';