ListGroup Component Demo

A component that renders a Bootstrap List Group element using OverReact’s statically-typed React prop API.

Contents

Basic example

The most basic ListGroup component is simply list ListGroupItem components.

The components encapsulate the complexity of the underlying HTML, ensuring that all the default CSS classes and other HTML attributes are present, leaving you with minimal markup.

Rendering OverReact Demo . . .
import 'package:over_react/over_react.dart';

import '../../demo_components.dart';

ReactElement listGroupBasicDemo() =>
  ListGroup()(
    ListGroupItem()('Dapibus ac facilisis in'),
    ListGroupItem()('Cras sit amet nibh libero'),
    ListGroupItem()('Porta ac consectetur ac'),
    ListGroupItem()('Vestibulum at eros')
  );
import 'package:over_react/over_react.dart';

import '../demo_components.dart';
part 'list_group.over_react.g.dart';

UiFactory<ListGroupProps> ListGroup = _$ListGroup;

mixin ListGroupProps on UiProps {
  /// The HTML element type for the [ListGroup], specifying its
  /// DOM representation when rendered.
  ///
  /// Default: [ListGroupElementType.DIV]
  ListGroupElementType elementType;
}

class ListGroupComponent extends UiComponent2<ListGroupProps> {
  @override
  get defaultProps => (newProps()..elementType = ListGroupElementType.DIV);

  @override
  render() {
    var classes = forwardingClassNameBuilder()
      ..add('list-group');

    return (props.elementType.componentBuilderFactory()
      ..modifyProps(addUnconsumedDomProps)
      ..className = classes.toClassName()
    )(props.children);
  }
}

class ListGroupElementType {
  final BuilderOnlyUiFactory<DomProps> componentBuilderFactory;
  ListGroupElementType._internal(this.componentBuilderFactory);

  /// A [Dom.ul] (HTML `<ul>` element)
  static final ListGroupElementType UL  = ListGroupElementType._internal(Dom.ul);

  /// A [Dom.div] (HTML `<div>` element)
  static final ListGroupElementType DIV = ListGroupElementType._internal(Dom.div);
}
import 'package:over_react/over_react.dart';

import '../demo_components.dart';
part 'list_group_item.over_react.g.dart';

UiFactory<ListGroupItemProps> ListGroupItem = _$ListGroupItem;

mixin ListGroupItemProps on UiProps {
  /// The HTML element type for the [ListGroupItem], specifying its DOM
  /// representation when rendered.
  ///
  /// Will only be used if [href] and [onClick] are both `null`.
  ///
  /// Default: [ListGroupItemElementType.SPAN]
  ListGroupItemElementType elementType;

  /// Optional header text to display within the [ListGroupItem] above
  /// the value of [children].
  ///
  /// See: <https://getbootstrap.com/docs/4.4/components/list-group/#custom-content>.
  dynamic header;

  /// The size of the [header] text you desire.
  ///
  /// Default: [ListGroupItemHeaderElementSize.H5]
  ListGroupItemHeaderElementSize headerSize;

  /// Additional props to be added to the [header] element _(if specified)_.
  Map headerProps;

  /// The skin / "context" for the [ListGroupItem].
  ///
  /// See: <https://getbootstrap.com/docs/4.4/components/list-group/#contextual-classes>.
  ///
  /// Default: [ListGroupItemSkin.DEFAULT]
  ListGroupItemSkin skin;

  /// Whether the [ListGroupItem] should appear "active".
  ///
  /// See: <https://getbootstrap.com/docs/4.4/components/list-group/#anchors-and-buttons>
  ///
  /// Default: false
  bool isActive;

  /// Whether the [ListGroupItem] is disabled.
  ///
  /// See: <https://getbootstrap.com/docs/4.4/components/list-group/#disabled-items>
  ///
  /// Default: false
  @Accessor(key: 'disabled', keyNamespace: '')
  bool isDisabled;

  /// The HTML `href` attribute value for the [ListGroupItem].
  ///
  /// If set, the item will render via [Dom.a].
  ///
  /// _Proxies [DomProps.href]_
  @Accessor(keyNamespace: '')
  String href;

  /// The HTML `target` attribute value for the [ListGroupItem].
  ///
  /// If set, the item will render via [Dom.a].
  ///
  /// _Proxies [DomProps.target]_
  @Accessor(keyNamespace: '')
  String target;

  /// The HTML `type` attribute value for the [ListGroupItem] when
  /// rendered via [Dom.button].
  ///
  /// This will only be applied if [onClick] is also set.
  ///
  /// _Proxies [DomProps.type]_
  ///
  /// Default: [ButtonType.BUTTON]
  ButtonType type;
}

class ListGroupItemComponent extends UiComponent2<ListGroupItemProps> {
  @override
  get defaultProps => (newProps()
    ..elementType = ListGroupItemElementType.SPAN
    ..skin = ListGroupItemSkin.DEFAULT
    ..isActive = false
    ..isDisabled = false
    ..type = ButtonType.BUTTON
    ..headerSize = ListGroupItemHeaderElementSize.H5
  );

  @override
  render() {
    var children = props.children;

    if (props.header != null) {
      children = [
        renderItemHeader(),
        (Dom.p()
          ..className = 'list-group-item-text'
          ..key = 'item-text'
        )(props.children)
      ];
    }

    BuilderOnlyUiFactory<DomProps> factory = _getItemDomNodeFactory();

    return (factory()
      ..modifyProps(addUnconsumedDomProps)
      ..className = _getItemClasses().toClassName()
      ..href = props.href
      ..target = props.target
      ..type = _isActionItem && !_isAnchorLink ? props.type.typeName : null
      ..disabled = _useDisabledAttr ? props.isDisabled : null
      .aria.disabled = !_useDisabledAttr ? props.isDisabled : null
    )(children);
  }

  ReactElement renderItemHeader() {
    if (props.header == null) return null;

    var headerClasses = ClassNameBuilder.fromProps(props.headerProps)
      ..add('list-group-item-heading');

    return (props.headerSize.componentBuilderFactory()
      ..addProps(props.headerProps)
      ..className = headerClasses.toClassName()
      ..key = 'item-header'
    )(props.header);
  }

  BuilderOnlyUiFactory<DomProps> _getItemDomNodeFactory() {
    BuilderOnlyUiFactory<DomProps> factory;

    if (props.href != null) {
      factory = Dom.a;
    } else if (props.onClick != null) {
      factory = Dom.button;
    } else {
      factory = props.elementType.componentBuilderFactory;
    }

    return factory;
  }

  ClassNameBuilder _getItemClasses() {
    return forwardingClassNameBuilder()
      ..add('list-group-item')
      ..add('list-group-item-action', _isActionItem)
      ..add('active', props.isActive)
      ..add('disabled', props.isDisabled)
      ..add(props.skin.className);
  }

  bool get _useDisabledAttr => _getItemDomNodeFactory() == Dom.button;

  bool get _isActionItem => (props.href ?? props.onClick) != null;

  bool get _isAnchorLink => props.href != null;
}

class ListGroupItemSkin extends ClassNameConstant {
  const ListGroupItemSkin._(String name, String className) : super(name, className);

  /// [className] value: null
  static const ListGroupItemSkin DEFAULT =
      ListGroupItemSkin._('DEFAULT', null);

  /// [className] value: 'list-group-item-danger'
  static const ListGroupItemSkin DANGER =
      ListGroupItemSkin._('DANGER', 'list-group-item-danger');

  /// [className] value: 'list-group-item-success'
  static const ListGroupItemSkin SUCCESS =
      ListGroupItemSkin._('SUCCESS', 'list-group-item-success');

  /// [className] value: 'list-group-item-warning'
  static const ListGroupItemSkin WARNING =
      ListGroupItemSkin._('WARNING', 'list-group-item-warning');

  /// [className] value: 'list-group-item-info'
  static const ListGroupItemSkin INFO =
      ListGroupItemSkin._('INFO', 'list-group-item-info');
}

class ListGroupItemElementType {
  final BuilderOnlyUiFactory<DomProps> componentBuilderFactory;
  ListGroupItemElementType._internal(this.componentBuilderFactory);

  /// A [Dom.li] (HTML `<li>` element)
  ///
  /// Will only be used if [ListGroupItemProps.href] and
  /// [ListGroupItemProps.onClick] are both `null`.
  ///
  /// Only use this when the parent [ListGroup] has
  /// [ListGroupProps.elementType] set to [ListGroupElementType.UL].
  static final ListGroupItemElementType LI =
      ListGroupItemElementType._internal(Dom.li);

  /// A [Dom.span] (HTML `<span>` element)
  ///
  /// Will only be used if [ListGroupItemProps.href] and
  /// [ListGroupItemProps.onClick] are both `null`.
  static final ListGroupItemElementType SPAN =
      ListGroupItemElementType._internal(Dom.span);
}

class ListGroupItemHeaderElementSize {
  final BuilderOnlyUiFactory<DomProps> componentBuilderFactory;
  ListGroupItemHeaderElementSize._internal(this.componentBuilderFactory);

  /// A [Dom.h1] (HTML `<h1>` element)
  static final ListGroupItemHeaderElementSize H1 =
      ListGroupItemHeaderElementSize._internal(Dom.h1);

  /// A [Dom.h2] (HTML `<h2>` element)
  static final ListGroupItemHeaderElementSize H2 =
      ListGroupItemHeaderElementSize._internal(Dom.h2);

  /// A [Dom.h3] (HTML `<h3>` element)
  static final ListGroupItemHeaderElementSize H3 =
      ListGroupItemHeaderElementSize._internal(Dom.h3);

  /// A [Dom.h4] (HTML `<h4>` element)
  static final ListGroupItemHeaderElementSize H4 =
      ListGroupItemHeaderElementSize._internal(Dom.h4);

  /// A [Dom.h5] (HTML `<h5>` element)
  static final ListGroupItemHeaderElementSize H5 =
      ListGroupItemHeaderElementSize._internal(Dom.h5);

  /// A [Dom.h6] (HTML `<h6>` element)
  static final ListGroupItemHeaderElementSize H6 =
      ListGroupItemHeaderElementSize._internal(Dom.h6);
}

Tags

Nest a Tag component within any `ListGroupItem` to show unread counts, activity, etc.

Rendering OverReact Demo . . .
import 'package:over_react/over_react.dart';

import '../../demo_components.dart';

ReactElement listGroupTagsDemo() =>
  ListGroup()(
    ListGroupItem()(
      (Tag()
        ..className = 'float-xs-right'
        ..isPill = true
      )(14),
      'Cras justo odio'
    ),
    ListGroupItem()(
      (Tag()
        ..className = 'float-xs-right'
        ..isPill = true
      )(2),
      'Dapibus ac facilisis in'
    ),
    ListGroupItem()(
      (Tag()
        ..className = 'float-xs-right'
        ..isPill = true
      )(1),
      'Morbi leo risus'
    )
  );

Anchors and buttons

Set props.onClick to render an HTML <button> element, or props.href to render an HTML <a> element with hover, disabled, and active states.

Set props.isDisabled to disable an item, and props.isActive to make an item appear active.

Rendering OverReact Demo . . .
import 'package:over_react/over_react.dart';

import '../../demo_components.dart';

ReactElement listGroupAnchorsAndButtonsDemo() =>
  ListGroup()(
    (ListGroupItem()
      ..isActive = true
      ..href = '#'
    )('Cras justo odio'),
    (ListGroupItem()
      ..onClick = (_) {}
    )('Dapibus ac facilisis in'),
    (ListGroupItem()
      ..onClick = (_) {}
    )('Morbi leo risus'),
    (ListGroupItem()
      ..onClick = (_) {}
    )('Porta ac consectetur ac'),
    (ListGroupItem()
      ..isDisabled = true
      ..onClick = (_) {}
    )('Vestibulum at eros')
  );

Contextual skins

Set props.skin to style a ListGroupItem using contextual colors.

Rendering OverReact Demo . . .
import 'package:over_react/over_react.dart';

import '../../demo_components.dart';

ReactElement listGroupContextualSkinDemo() =>
  ListGroup()(
    (ListGroupItem()
      ..onClick = (_) {}
      ..skin = ListGroupItemSkin.SUCCESS
    )('Dapibus ac facilisis in'),
    (ListGroupItem()
      ..onClick = (_) {}
      ..skin = ListGroupItemSkin.INFO
    )('Cras sit amet nibh libero'),
    (ListGroupItem()
      ..onClick = (_) {}
      ..skin = ListGroupItemSkin.WARNING
    )('Porta ac consectetur ac'),
    (ListGroupItem()
      ..onClick = (_) {}
      ..skin = ListGroupItemSkin.DANGER
    )('Vestibulum at eros')
  );

Headers

Set props.header to render a ListGroupItem that has a distinct header and body content region.

Optionally, set props.headerSize to modify the size of the header itself.

Rendering OverReact Demo . . .
import 'package:over_react/over_react.dart';

import '../../demo_components.dart';

ReactElement listGroupHeaderDemo() =>
  ListGroup()(
    (ListGroupItem()
      ..header = 'List group item heading'
      ..onClick = (_) {}
      ..isActive = true
    )(
      'Donec id elit non mi porta gravida at eget metus. '
      'Maecenas sed diam eget risus varius blandit.'
    ),
    (ListGroupItem()
      ..header = 'List group item heading'
      ..headerSize = ListGroupItemHeaderElementSize.H4
      ..onClick = (_) {}
    )(
      'Donec id elit non mi porta gravida at eget metus. '
      'Maecenas sed diam eget risus varius blandit.'
    ),
    (ListGroupItem()
      ..header = 'List group item heading'
      ..headerSize = ListGroupItemHeaderElementSize.H3
      ..onClick = (_) {}
    )(
      'Donec id elit non mi porta gravida at eget metus. '
      'Maecenas sed diam eget risus varius blandit.'
    )
  );