uiForwardRef<TProps extends UiProps> function

UiFactory<TProps> uiForwardRef<TProps extends UiProps>(
  1. dynamic functionComponent(
    1. TProps props,
    2. dynamic ref
    ),
  2. dynamic _config
)

Creates a function component capable of forwarding its ref to a component it renders.

Learn more: reactjs.org/docs/forwarding-refs.html.

_config should always be a UiFactoryConfig<TProps> and is only dynamic to avoid an unnecessary cast in the boilerplate.

Example 1: Updating a function component to forward a ref:

This example mirrors the JS example in the link above.

Consider a FancyButton function component that renders the native button DOM element:

mixin FancyButtonProps on UiProps {}

UiFactory<FancyButtonProps> FancyButton = uiFunction(
  (props) {
    return (Dom.button()
      ..className = 'FancyButton'
    )(props.children);
  },
  _$FancyButtonConfig, // ignore: undefined_identifier
);

Normally, you can't set a ref on function components, but if you change uiFunction to uiForwardRef, you'll get a new ref argument that will be populate with any ref set on FancyButton. We This ref can then be "forwarded" to some other component.

mixin FancyButtonProps on UiProps {}

UiFactory<FancyButtonProps> FancyButton = uiForwardRef(
  (props, ref) {
    return (Dom.button()
      ..ref = ref
      ..className = 'FancyButton'
    )(props.children);
  },
  _$FancyButtonConfig, // ignore: undefined_identifier
);

usageExample() {
  // You can now get a ref directly to the DOM button:
  final ref = createRef<ButtonElement>();
  return (FancyButton()..ref = ref)();
}

Example 2: Creating a higher-order component

that forwards its ref to the wrapped component.

/// Wraps a component ([factoryToWrap]) in a new component that logs when rendered.
UiFactory<TProps> withLogging<TProps extends UiProps>(UiFactory<TProps> factoryToWrap) {
  return uiForwardRef(
    (props, ref) {
      useEffect(() => '${factoryToWrap().componentFactory.type} rendered!');

      // ignore statement addresses analyzer bug with this syntax (https://github.com/dart-lang/sdk/issues/42975)
      // ignore: invocation_of_non_function_expression
      return (factoryToWrap()
        ..addAll(props)
        ..ref = ref
      )(props.children);
    },
    factoryToBeWrapped.asForwardRefConfig(
      displayName: 'WithLogging',
    ),
  );
}

UiFactory<FancyButtonProps> FancyButton = ...;

/// This can be used just like FancyButton, and setting a ref on it will work the same!
UiFactory<FancyButtonProps> FancyButtonWithLogging = withLogging(FancyButton);

Example 3: Exposing inner refs in class components

While it's usually easier to pass uiForwardRef's ref argument directly to the desired component, sometimes it's nested within a class component that can't forward refs itself, and it's non-trivial to update that class component to a function component.

In this case, you can pass the ref to the class component as a custom prop, and have the class component forward it to the right location.

Here, we'll make our class component factory private and only expose a version wrapped in uiForwardRef:

UiFactory<FooProps> _Foo = _$_Foo; // ignore: undefined_identifier
mixin FooProps on UiProps {
  // Private since we only use this to pass along the ref provided in
  // uiForwardRef.
  //
  // Consumers will effectively be setting this when they set `ref` on the public factory.
  dynamic _inputRef;
}
class FooComponent extends UiComponent2<FooProps> {
  @override
  render() {
    return Dom.div()(
      (Dom.input()
        ..type = 'text'
        ..ref = props._inputRef
      )()
    );
  }
}

// ---------- Wrapping a Class Component ----------
// Here you have two options:
//   - Option 1: Use the class component's factory as the UiFactoryConfig arg.
//   This needs to be done because the builder recognizes
//   `FooProps` as already consumed (by the class component).
//
//   - Option 2: Create a new props class. This just works around the issue
//   described for Option 1 because it is creating a new props class, but it
//   only needs to mix in the props mixins that the original props class used.
//
// Choosing between the options is likely circumstantial or preferential.
// The advantage to Option 1 is that if the class component has numerous mixins,
// it is much more concise to create the function component config.
// Option 2 has the benefit that it matches the declaration of standard
// function components (which `uiForwardRef` returns). Additionally, Option 2
// illustrates how one could add additional props to the wrapped function component.

//
// Option 1 Example
UiFactory<FooProps> Foo = uiForwardRef((props, ref) {
    return (_Foo()
      ..addProps(props)
      .._inputRef = ref
    )();
  },
  _Foo.asForwardRefConfig(displayName: 'Foo'),
);

//
// Option 2 Example:

// This is not necessary but is just in place to illustrate that more props
// can be specified and consumed.
mixin AnotherPropsMixin on UiProps {
  String anExampleAdditionalProp;
}

class Foo2Props = UiProps with AnotherPropsMixin, FooProps;
UiFactory<Foo2Props> Foo2 = uiForwardRef((props, ref) {
    print(props.anExampleAdditionalProp);

    return (_Foo()
      ..addProps(props)
      .._inputRef = ref
    )();
  },
  _$Foo2Config, // ignore: undefined_identifier
);

Implementation

UiFactory<TProps> uiForwardRef<TProps extends bh.UiProps>(
    dynamic Function(TProps props, dynamic ref) functionComponent, dynamic _config) {
  ArgumentError.checkNotNull(_config, '_config');

  if (_config is! UiFactoryConfig<TProps>) {
    throw ArgumentError('_config should be a UiFactory<TProps>. Make sure you are '
        r'using either the generated factory config (i.e. _$FooConfig) or manually '
        'declaring your config correctly.');
  }

  final config = _config;

  // ignore: invalid_use_of_protected_member
  var propsFactory = config.propsFactory;

  // Get the display name from the inner function if possible so it doesn't become `_uiFunctionWrapper`.
  // If the function is an anonymous function and a display name is not set,
  // this will be an empty string.
  final displayName = config.displayName ?? getFunctionName(functionComponent);

  dynamic _uiFunctionWrapper(JsBackedMap props, dynamic ref) {
    return functionComponent(propsFactory!.jsMap(props), ref);
  }

  // Always pass displayName, even if it's empty,
  // since we don't want forwardRef2 to use _uiFunctionWrapper as the name.
  final factory =
      react_interop.forwardRef2(_uiFunctionWrapper, displayName: displayName);

  if (propsFactory == null) {
    if (TProps != UiProps && TProps != GenericUiProps) {
      throw ArgumentError('config.propsFactory must be provided when using custom props classes');
    }
    propsFactory = PropsFactory.fromUiFactory(([backingMap]) => GenericUiProps(factory, backingMap))
        as PropsFactory<TProps>;
  }

  TProps _uiFactory([Map? backingMap]) {
    TProps builder;
    if (backingMap == null) {
      builder = propsFactory!.jsMap(JsBackedMap());
    } else if (backingMap is JsBackedMap) {
      builder = propsFactory!.jsMap(backingMap);
    } else {
      builder = propsFactory!.map(backingMap);
    }

    return builder..componentFactory = factory;
  }

  registerComponentTypeAlias(factory, _uiFactory);

  return _uiFactory;
}