ToggleButton Component Demo

A component that renders stateful Bootstrap Toggle Buttons using OverReact’s statically-typed React prop and state API.

Contents

No jQuery Here: The stateful behavior of the ToggleButton / ToggleButtonGroup components demonstrated below is built using nothing but OverReact's UiStatefulComponent.

None of the Bootstrap jQuery "button" plugin pieces are used here.

Checkbox

Set props.toggleType to ToggleBehaviorType.CHECKBOX on a ToggleButtonGroup component with child ToggleButtons for a "multi-select" toggle behavior.

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

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

ReactElement checkboxToggleButtonDemo() =>
  (ToggleButtonGroup()
    ..toggleType = ToggleBehaviorType.CHECKBOX
  )(
    (ToggleButton()
      ..value = '1'
    )('Checkbox 1'),
    (ToggleButton()
      ..value = '2'
      ..defaultChecked = true
    )('Checkbox 2'),
    (ToggleButton()
      ..value = '3'
    )('Checkbox 3')
  );
import 'dart:html';

import 'package:over_react/over_react.dart';

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

UiFactory<ToggleButtonProps> ToggleButton = _$ToggleButton;

mixin ToggleButtonPropsMixin on UiProps {
  /// Whether the `<input>` rendered by the [ToggleButton] should have focus upon mounting.
  ///
  /// _Proxies [DomProps.autoFocus]._
  ///
  /// Default: `false`
  @Accessor(keyNamespace: '')
  bool autoFocus;

  /// Whether the [ToggleButton] is checked by default.
  ///
  /// Setting this without the setting the [checked] prop to will make the
  /// [ToggleButton] _uncontrolled_; it will initially render checked or unchecked
  /// depending on the value of this prop, and then update itself automatically
  /// in response to user input, like a normal HTML input.
  ///
  /// Related: [checked]
  ///
  /// _Proxies [DomProps.defaultChecked]._
  ///
  /// See: <https://facebook.github.io/react/docs/forms.html#uncontrolled-components>.
  @Accessor(keyNamespace: '')
  bool defaultChecked;

  /// Whether the [ToggleButton] is checked.
  ///
  /// Setting this will make the [ToggleButton] _controlled_; it will not update
  /// automatically in response to user input, but instead will always render
  /// checked or unchecked depending on the value of this prop.
  ///
  /// Related: [defaultChecked]
  ///
  /// _Proxies [DomProps.checked]._
  ///
  /// See: <https://facebook.github.io/react/docs/forms.html#controlled-components>.
  @Accessor(keyNamespace: '')
  bool checked;
}

class ToggleButtonProps = UiProps with ButtonProps, ToggleButtonPropsMixin, AbstractInputPropsMixin;

mixin ToggleButtonStateMixin on UiState {
  /// Tracks if the [ToggleButton] is focused. Determines whether to render with the `js-focus` CSS
  /// class.
  ///
  /// Initial: [ToggleButtonProps.autoFocus]
  bool isFocused;

  /// Tracks if the [ToggleButton] input is `checked`. Determines whether to render with the `active` CSS class.
  ///
  /// Initial: [ToggleButtonProps.checked] `??` [ToggleButtonProps.defaultChecked] `?? false`
  bool isChecked;
}

class ToggleButtonState = UiState with ButtonState, ToggleButtonStateMixin, AbstractInputStateMixin;

@Component2(subtypeOf: ButtonComponent)
class ToggleButtonComponent extends ButtonComponent<ToggleButtonProps, ToggleButtonState> {
  // Refs

  /// A reference to the [Dom.input] rendered via [renderInput] within the [ToggleButton].
  InputElement inputRef;

  @override
  get defaultProps => (newProps()
    ..addProps(super.defaultProps)
    ..toggleType = ToggleBehaviorType.CHECKBOX
  );

  @override
  get initialState => (newState()
    ..id = 'toggle_button_' + generateGuid()
    ..isFocused = props.autoFocus
    ..isChecked = props.checked ?? props.defaultChecked ?? false
  );

  @override
  void componentDidMount() {
    _validateProps(props);
  }

  @override
  Map getDerivedStateFromProps(Map nextProps, Map prevState) {
    var tNewProps = typedPropsFactory(nextProps);

    _validateProps(tNewProps);

    if (tNewProps.checked != null && this.props.checked != tNewProps.checked) {
      return newState()..isChecked = tNewProps.checked;
    } else {
      return null;
    }
  }

  @override
  render() {
    return renderButton(
      [
        renderInput(),
        props.children
      ]
    );
  }

  ReactElement renderInput() {
    var builder = Dom.input()
      ..type = props.toggleType.typeName
      ..id = id
      ..name = props.name
      ..tabIndex = props.tabIndex
      ..disabled = props.isDisabled
      ..autoFocus = props.autoFocus
      ..onChange = props.onChange
      ..onClick = props.onClick
      ..style = makeInputNodeInvisible
      ..ref = (ref) { inputRef = ref; }
      ..key = 'input';

    // ********************************************************
    //
    //  React JS 15.0 Workarounds
    //
    //  [1] Starting from React 15.0, the checked/defaultChecked
    //      props should not be set with a cascading setter
    //      because it will recognize the null as a "clear input"
    //      rather than a request to make the input controlled
    //      vs uncontrolled.
    //
    //  [2] React 15.0 introduced a bug that warns when setting
    //      value to null on an input even if that input is of
    //      type radio or checkbox. This comes from treating
    //      setting value as a controlled input even when it
    //      should not.
    //
    //      See: https://github.com/facebook/react/issues/6779
    //
    // ********************************************************

    if (props.defaultChecked != null) builder.defaultChecked = state.isChecked; // [1]
    if (props.checked != null) builder.checked = state.isChecked; // [1]
    if (props.value != null) builder.value = props.value; // [2]

    return builder();
  }

  /// Returns a map of inline CSS styles to be applied to the HTML `<input>` node.
  ///
  /// These styles are a workaround to hide the input in an a11y-friendly manner since
  /// the bootstrap styles we are using for the demo components uses an HTML attribute
  /// CSS selector that we do not want to use since we're demoing how to build a stateful
  /// toggle button with OverReact, not with Bootstrap's jQuery plugin data-api hook.
  ///
  /// In an actual implementation, you would want to add a unique class to the root of this
  /// component, and add these styles in your app / component library stylesheet.
  Map<String, dynamic> get makeInputNodeInvisible => {
    'position': 'absolute',
    'clip': 'rect(0,0,0,0)',
    'pointerEvents': 'none'
  };

  /// Checks the `<input>` element to ensure that [ToggleButtonState.isChecked]
  /// matches the value of the [InputElement.checked] attribute.
  ///
  /// Does not refresh the state if [ToggleButtonProps.checked] is not null
  /// (the component is a "controlled" component).
  void refreshState() {
    if (!_isControlled) setState(newState()..isChecked = inputRef.checked);
  }

  void _validateProps(ToggleButtonProps props) {
    assert(
        (props.toggleType == ToggleBehaviorType.RADIO && props.name != null) ||
        props.toggleType == ToggleBehaviorType.CHECKBOX
    );
  }

  /// Used to check if the `input` element is controlled or not.
  bool get _isControlled => props.checked != null;

  @override
  bool get isActive => state.isChecked;

  @override
  String get type => null;

  @override
  BuilderOnlyUiFactory<DomProps> get buttonDomNodeFactory => Dom.label;

  /// The id to use for a [ToggleButton].
  ///
  /// Attempts to use [AbstractInputPropsMixin.id] _(specified by the consumer)_, falling back to
  /// [AbstractInputStateMixin.id] _(auto-generated)_.
  String get id => props.id ?? state.id;
}
import 'package:over_react/over_react.dart';

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

UiFactory<ToggleButtonGroupProps> ToggleButtonGroup = _$ToggleButtonGroup;

class ToggleButtonGroupProps = UiProps with ButtonGroupProps, AbstractInputPropsMixin;

class ToggleButtonGroupState = UiState with ButtonGroupState, AbstractInputStateMixin;

@Component2(subtypeOf: ButtonGroupComponent)
class ToggleButtonGroupComponent
    extends ButtonGroupComponent<ToggleButtonGroupProps, ToggleButtonGroupState> {
  // Refs

  Map<int, dynamic> _toggleButtonRefs = <int, dynamic>{};

  /// The name to use for all children of a [ToggleButtonGroup].
  ///
  /// Attempts to use [ToggleButtonGroupProps.name] _(specified by the consumer)_, falling back to
  /// [ToggleButtonGroupState.name] _(auto-generated)_.
  String get name => props.name ?? state.name;

  @override
  get defaultProps => (newProps()
    ..addProps(super.defaultProps)
    ..toggleType = ToggleBehaviorType.CHECKBOX
  );

  @override
  get initialState => (newState()
    ..addAll(super.initialState)
    ..name = 'toggle_button_group_' + generateGuid()
  );

  @override
  get consumedProps => propsMeta.forMixins({AbstractInputPropsMixin});

  /// The props that should be added when we clone the given [child] using
  /// [cloneElement] via [renderButton].
  @override
  ToggleButtonProps buttonPropsToAdd(dynamic child, int index) {
    var childProps = childFactory(getProps(child));

    ButtonProps superPropsToAdd = super.buttonPropsToAdd(child, index);

    return childFactory()
      ..addProps(superPropsToAdd)
      ..name = name
      ..toggleType = props.toggleType
      ..onChange = formEventCallbacks.chain(props.onChange, _handleOnChange)
      ..value = childProps.value ?? index
      ..ref = chainRef(child, (ref) { _toggleButtonRefs[index] = ref; });
  }

  @override
  ClassNameBuilder getButtonGroupClasses() {
    return super.getButtonGroupClasses()
      ..add('btn-toggle-group');
  }

  /// The handler for when one of the children of the [ToggleButtonGroup] is changed or unchecked
  void _handleOnChange(SyntheticFormEvent event) {
    _toggleButtonRefs.values.forEach((childComponent) {
      if (childComponent is ToggleButtonComponent) childComponent.refreshState();
    });
  }

  /// The factory expected for each child of [ToggleButtonGroup].
  @override
  UiFactory<ToggleButtonProps> get childFactory => ToggleButton;
}

Radio

Set props.toggleType to ToggleBehaviorType.RADIO on a ToggleButtonGroup component with child ToggleButtons for a "single-select" toggle behavior.

render a submit / reset form button, respectively.

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

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

ReactElement radioToggleButtonDemo() =>
  (ToggleButtonGroup()
    ..toggleType = ToggleBehaviorType.RADIO
  )(
    (ToggleButton()
      ..value = '1'
    )('Radio 1'),
    (ToggleButton()
      ..value = '2'
      ..defaultChecked = true
    )('Radio 2'),
    (ToggleButton()
      ..value = '3'
    )('Radio 3')
  );