diff --git a/ivette/src/dome/renderer/layout/forms.tsx b/ivette/src/dome/renderer/layout/forms.tsx index 621ff83af522ab9ae1d13ac623ed0642753ecd44..12cc162f1f588fda7bd846e7b161af5f93d51943 100644 --- a/ivette/src/dome/renderer/layout/forms.tsx +++ b/ivette/src/dome/renderer/layout/forms.tsx @@ -126,6 +126,7 @@ export type ResetCallback = () => void; */ export class BufferController { private readonly evt = new Events(); + private errors = 0; /** Notify all reset listener events. */ reset(): void { this.evt.emit('reset'); } @@ -139,18 +140,33 @@ export class BufferController { /** There are active listeners for Commit event. */ hasCommit(): boolean { return this.evt.listenerCount('commit') > 0; } + /** Get the number of errors */ + getErrors(): number { return this.errors; } + /** @internal */ onReset(fn: ResetCallback): void { this.evt.addListener('reset', fn); } /** @internal */ - offReset(fn: ResetCallback): void { this.evt.addListener('reset', fn); } + offReset(fn: ResetCallback): void { this.evt.removeListener('reset', fn); } /** @internal */ onCommit(fn: ResetCallback): void { this.evt.addListener('commit', fn); } /** @internal */ - offCommit(fn: ResetCallback): void { this.evt.addListener('commit', fn); } + offCommit(fn: ResetCallback): void { this.evt.removeListener('commit', fn); } + + /** @internal */ + addError(): void { this.errors++; } + + /** @internal */ + removeError(): void { this.errors--; } +} +export type Equal<A> = (a:A, b:A) => boolean; + +function compare<A>(equal: Equal<A> | undefined, a: A, b: A): boolean +{ + return equal ? equal(a, b) : a === b; } /** @@ -162,7 +178,8 @@ export class BufferController { - on Reset event, the buffered state is restored to the input value. - - on Commit event, the buffered state is sent to the input callback. + - on Commit event, + the buffered state is sent to the input callback or restored. The returned field state reflects the internal buffer state. Its local reset value is either the input reset value or the current input value. @@ -170,39 +187,55 @@ export class BufferController { */ export function useBuffer<A>( remote : BufferController, - state: FieldState<A> + state: FieldState<A>, + equal?: Equal<A>, ): FieldState<A> { const { value, error, reset, onChanged } = state; const [ modified, setModified ] = React.useState(false); const [ buffer, setBuffer ] = React.useState<A>(value); const [ berror, setBerror ] = React.useState<FieldError>(error); - const staged = modified && !berror; + + const valid = !isValid(berror); + const rollback = reset ?? value; + + // --- Error Count + React.useEffect(() => { + if (valid) return; + remote.addError(); + return () => remote.removeError(); + }, [remote, valid]); // --- Reset React.useEffect(() => { if (modified) { const doReset = (): void => { setModified(false); - setBuffer(value); - setBerror(error); + setBuffer(rollback); + setBerror(undefined); }; remote.onReset(doReset); return () => remote.offReset(doReset); } else return; - }, [remote, modified, value, error]); + }, [remote, modified, rollback]); // --- Commit React.useEffect(() => { - if (staged) { + if(modified) { const doCommit = (): void => { - setModified(false); - onChanged(buffer, undefined, false); + if (valid) { + setModified(false); + onChanged(buffer, undefined, false); + } else { + setModified(false); + setBuffer(rollback); + setBerror(undefined); + } }; remote.onCommit(doCommit); return () => remote.offCommit(doCommit); } else return; - }, [remote, staged, buffer, onChanged]); + }, [remote, modified, valid, buffer, rollback, onChanged]); // --- Callback const onLocalChange = React.useCallback( @@ -210,9 +243,9 @@ export function useBuffer<A>( setModified(!isReset); setBuffer(newValue); setBerror(newError); - if (isReset && newValue !== value) + if (isReset && !compare(equal, newValue, value)) onChanged(newValue, newError, isReset); - }, [value, onChanged]); + }, [equal, value, onChanged]); return { value: modified ? buffer : value, diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 13644db3a403696352168de1b8cc81d216b26b53..f7e4f52f1aa74f27084b091525efea40a2dc1751 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -36,6 +36,7 @@ import { Order } from 'dome/data/compare'; import { GlobalState, useGlobalState } from 'dome/data/states'; import { Client, useModel } from 'dome/table/models'; import { CompactModel } from 'dome/table/arrays'; +import { FieldState, FieldError, isValid } from 'dome/layout/forms'; import * as Ast from 'frama-c/kernel/api/ast'; import * as Server from './server'; @@ -307,6 +308,32 @@ export function useSyncValue<A>(value: Value<A>): A | undefined { return v; } +/** Synchronize FieldState and server state only if there is no error. */ +export function useServerField<A>( + state: State<A>, + defaultValue: A, +): FieldState<A> { + const [value, setState] = useSyncState(state); + const stateValue = value !== undefined ? value : defaultValue; + const [local, setLocal] = React.useState(stateValue); + const [error, setError] = React.useState<FieldError>(undefined); + + const update = React.useCallback((newValue: A, newError: FieldError) => { + setLocal(newValue); + setError(newError); + if (isValid(newError)) { + setState(newValue); + } + }, [setState]); + + return { + error, + value: isValid(error) ? stateValue : local, + reset: isValid(error) ? undefined : stateValue, + onChanged: update + }; +} + // -------------------------------------------------------------------------- // --- Synchronized Arrays // --------------------------------------------------------------------------