uiFunction<TProps extends UiProps> function

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

Declares a function component and returns a factory that can be used to render it.

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

Example:

UiFactory<FooProps> Foo = uiFunction(
  (props) {
    // Set default props using null-aware operators.
    final isDisabled = props.isDisabled ?? false;
    final items = props.items ?? [];

    // Return the rendered component contents here.
    return Fragment()(
      Dom.div()(items),
      (Dom.button()..disabled = isDisabled)('Click me!'),
    );
  },
  // The generated props config will match the factory name.
  _$FooConfig, // ignore: undefined_identifier
);

// Multiple function components can be declared with the same props.
UiFactory<FooProps> AnotherFoo = uiFunction(
  (props) {
      return (Foo()
        ..items = props.items
        ..isDisabled = true
      )();
  },
  _$AnotherFooConfig, // ignore: undefined_identifier
);

mixin FooProps on UiProps {
  bool isDisabled;
  Iterable<String> items;
}

OR Optionally pass in an existing PropsFactory in place of a props _config.

UiFactory<FooProps> Bar = uiFunction(
  (props) {
    return (Dom.button()..disabled = props.isDisabled)('Click me!');
  },
  UiFactoryConfig(
    propsFactory: PropsFactory.fromUiFactory(Foo),
    displayName: 'Bar',
  ),
);

OR Don't set propsFactory when using UiProps, as a generic one will be created for the component in uiFunction.

UiFactory<UiProps> Foo = uiFunction(
  (props) {
    return Dom.div()('prop id: ${props.id}');
  },
  UiFactoryConfig(
    displayName: 'Foo',
  ),
);

Learn more: reactjs.org/docs/components-and-props.html#function-and-class-components.

Implementation

// TODO: right now only top level factory declarations will generate props configs.
UiFactory<TProps> uiFunction<TProps extends UiProps>(
  dynamic Function(TProps props) 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;

  var propsFactory = config.propsFactory;

  // Get the display name from the inner function if possible so it doesn't become `_uiFunctionWrapper`
  final displayName = config.displayName ?? getFunctionName(functionComponent);

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

  final factory = react.registerFunctionComponent(
    _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>;
  }
  // Work around propsFactory not getting promoted to non-nullable in _uiFactory: https://github.com/dart-lang/language/issues/1536
  final nonNullablePropsFactory = propsFactory;

  TProps _uiFactory([Map? backingMap]) {
    TProps builder;
    if (backingMap == null) {
      // propsFactory should get promoted to non-nullable here, but it does not some reason propsF
      builder = nonNullablePropsFactory.jsMap(JsBackedMap());
    } else if (backingMap is JsBackedMap) {
      builder = nonNullablePropsFactory.jsMap(backingMap);
    } else {
      builder = nonNullablePropsFactory.map(backingMap);
    }

    return builder..componentFactory = factory;
  }

  registerComponentTypeAlias(factory, _uiFactory);

  return _uiFactory;
}