Skip to content

A library for building statically-typed React UI components using Dart.

License

Notifications You must be signed in to change notification settings

regenvanwalbeek-wf/over_react

 
 

Repository files navigation

OverReact

Pub Documentation Join the chat at https://gitter.im/over_react/Lobby

Build Status Test Coverage

A library for building statically-typed React UI components using Dart.


Dart 2 Migration Guide

If you have existing over_react code written on Dart 1 and want to upgrade to Dart 2, please read the Dart 2 Migration Guide


Using it in your project

If you are not familiar with React JS

Since OverReact is built atop React JS, we strongly encourage you to gain familiarity with it by reading this React JS tutorial first.

  1. Add the over_react package as a dependency in your pubspec.yaml.

    dependencies:
      over_react: ^2.0.0
  2. Include the native JavaScript react and react_dom libraries in your app’s index.html file, and add an HTML element with a unique identifier where you’ll mount your OverReact UI component(s).

    <html>
      <head>
       <!-- ... -->
      </head>
      <body>
        <div id="react_mount_point">
          // OverReact component render() output will show up here.
        </div>
    
        <script src="packages/react/react.js"></script>
        <script src="packages/react/react_dom.js"></script>
    
        <script type="application/javascript" defer src="your_app_entrypoint.dart.js"></script>
      </body>
    </html>

    Note: When serving your application in production, use packages/react/react_with_react_dom_prod.js file instead of the un-minified react.js / react_dom.js files shown in the example above.

  3. Import the over_react and react_dom libraries into your_app_name.dart, and initialize React within your Dart application. Then build a custom component and mount / render it into the HTML element you created in step 3.

    Be sure to namespace the react_dom.dart import as react_dom to avoid collisions with UiComponent.render when creating custom components.

    import 'dart:html';
    import 'package:over_react/react_dom.dart' as react_dom;
    import 'package:over_react/over_react.dart';
    
    main() {
      // Initialize React within our Dart app
      setClientConfiguration();
    
      // Mount / render your component.
      react_dom.render(Foo()(), querySelector('#react_mount_point'));
    }
  4. Run pub run build_runner serve in the root of your Dart project.

Note: After running a build, you'll have to restart your analysis server in your IDE for the built types to resolve properly. Unfortunately, this is a known limitation in the analysis server at this time. See: dart-lang/sdk#34344

 

Running tests in your project

When running tests on code that uses our builder (or any code that imports over_react), you must run your tests using build_runner.

Warning: Do not run tests via pub run build_runner test in a package while another instance of build_runner (e.g. pub run build_runner serve)is running in that same package. This workflow is unsupported by build_runner

  1. Run tests through build_runner, and specify the platform to be a browser platform. Example:

    $ pub run build_runner test -- -p chrome test/your_test_file.dart
  2. When running tests in over_react, our dart_test.yaml specifies some handy presets for running tests in DDC and dart2js:

    Note: These presets exist only in over_react.

    • To run tests in over_react compiled via DDC, run:
    $ pub run build_runner test -- -P dartdevc
    • To run tests in over_react compiled via dart2js, run:
    $ pub run build_runner test -r -- -P dart2js

   

Anatomy of an OverReact component

If you are not familiar with React JS

Since OverReact is built atop React JS, we strongly encourage you to gain familiarity with it by reading this React JS tutorial first.

The over_react library functions as an additional "layer" atop the Dart react package which handles the underlying JS interop that wraps around React JS.

The library strives to maintain a 1:1 relationship with the React JS component class and API. To do that, an OverReact component is comprised of four core pieces that are each wired up via our builder using an analogous annotation.

  1. UiFactory
  2. UiProps
  3. UiState (optional)
  4. UiComponent

 

UiFactory

UiFactory is a function that returns a new instance of a UiComponent’s UiProps class.

@Factory()
UiFactory<FooProps> Foo = _$Foo;

 

UiProps

UiProps is a Map class that adds statically-typed getters and setters for each React component prop. It can also be invoked as a function, serving as a builder for its analogous component.

@Props()
class _$FooProps extends UiProps {
  // ...
}
  • Note: The builder will make the concrete getters and setters available in a generated class which has the same name as the class annotated with @Props(), but without the _$ prefix (which would be FooProps in the above code). The generated class will also have the same API. So, consumers who wish to extend the functionality of _$FooProps should extend the generated version, FooProps.

 

UiProps as a Map

@Factory()
UiFactory<FooProps> Foo = _$Foo;

@Props()
class _$FooProps extends UiProps {
  String color;
}

@Component()
class FooComponent extends UiComponent<FooProps> {
  // ...
}

void bar() {
  FooProps props = Foo();

  props.color = '#66cc00';

  print(props.color); // #66cc00
  print(props);       // {FooProps.color: #66cc00}
}

/// You can use the factory to create a UiProps instance
/// backed by an existing Map.
void baz() {
  Map existingMap = {'FooProps.color': '#0094ff'};

  FooProps props = Foo(existingMap);

  print(props.color); // #0094ff
}

 

UiProps as a builder

@Factory()
UiFactory<FooProps> Foo = _$Foo;

@Props()
class _$FooProps extends UiProps {
  String color;
}

@Component()
class FooComponent extends UiComponent<FooProps> {
  ReactElement bar() {
    // Create a UiProps instance to serve as a builder
    FooProps builder = Foo();

    // Add props
    builder.id = 'the_best_foo';
    builder.color = '#ee2724';

    // Invoke as a function with the desired children
    // to return a new instance of the component.
    return builder('child1', 'child2');
  }

  /// Even better... do it inline! (a.k.a fluent)
  ReactElement baz() {
    return (Foo()
      ..id = 'the_best_foo'
      ..color = 'red'
    )(
      'child1',
      'child2'
    );
  }
}

See fluent-style component consumption for more examples on builder usage.

 

UiState

UiState is a Map class (just like UiProps) that adds statically-typed getters and setters for each React component state property.

@State()
class _$FooState extends UiState {
  // ...
}

UiState is optional, and won’t be used for every component.

  • Note: The builder will make the concrete getters and setters available in a generated class which has the same name as the class annotated with @State(), but without the _$ prefix (which would be FooState in the above code). The generated class will also have the same API. So, consumers who wish to extend the functionality of _$FooState should use the generated version, FooState.

 

UiComponent

UiComponent is a subclass of react.Component, containing lifecycle methods and rendering logic for components.

@Component()
class FooComponent extends UiComponent<FooProps> {
  // ...
}
  • This component provides statically-typed props via UiProps, as well as utilities for prop forwarding and CSS class merging.
  • The UiStatefulComponent flavor augments UiComponent behavior with statically-typed state via UiState.

 

Accessing and manipulating props / state within UiComponent

  • Within the UiComponent class, props and state are not just Maps. They are instances of UiProps and UiState, which means you don’t need String keys to access them!
  • newProps() and newState() are also exposed to conveniently create empty instances of UiProps and UiState as needed.
  • typedPropsFactory() and typedStateFactory() are also exposed to conveniently create typed props / state objects out of any provided backing map.
@Component()
class FooComponent extends UiStatefulComponent<FooProps, FooState> {
  @override
  getDefaultProps() => (newProps()
    ..color = '#66cc00'
  );

  @override
  getInitialState() => (newState()
    ..isActive = false
  );

  @override
  componentWillUpdate(Map newProps, Map newState) {
    var tNewState = typedStateFactory(newState);
    var tNewProps = typedPropsFactory(newProps);

    var becameActive = !state.isActive && tNewState.isActive;

    // Do something here!
  }

  @override
  render() {
    return (Dom.div()
      ..style = {
        'color': props.color,
        'fontWeight': state.isActive ? 'bold' : 'normal'
      }
    )(
      (Dom.button()..onClick = _handleButtonClick)('Toggle'),
      props.children
    );
  }

  void _handleButtonClick(SyntheticMouseEvent event) {
    _toggleActive();
  }

  void _toggleActive() {
    setState(newState()
      ..isActive = !state.isActive
    );
  }
}

   

Fluent-style component consumption

In OverReact, components are consumed by invoking a UiFactory to return a new UiProps builder, which is then modified and invoked to build a ReactElement.

This is done to make "fluent-style" component consumption possible, so that the OverReact consumer experience is very similar to the React JS / "vanilla" react-dart experience.

To demonstrate the similarities, the example below shows a render method for JS, JSX, react-dart, and over_react that will have the exact same HTML markup result.

  • React JS:

    render() {
      return React.createElement('div', {className: 'container'},
        React.createElement('h1', null, 'Click the button!'),
        React.createElement('button', {
          id: 'main_button',
          onClick: _handleClick
        }, 'Click me')
      );
    }
  • React JS (JSX):

    render() {
      return <div className="container">
        <h1>Click the button!</h1>
        <button
          id="main_button"
          onClick={_handleClick}
        >Click me</button>
      </div>;
    }
  • Vanilla react-dart:

    render() {
      return react.div({'className': 'container'},
        react.h1({}, 'Click the button!'),
        react.button({
          'id': 'main_button',
          'onClick': _handleClick
        }, 'Click me')
      );
    }
  • OverReact:

    render() {
      return (Dom.div()..className = 'container')(
        Dom.h1()('Click the button!'),
        (Dom.button()
          ..id = 'main_button'
          ..onClick = _handleClick
        )('Click me')
      );
    }

    Let’s break down the OverReact fluent-style shown above

    render() {
      // Create a builder for a <div>,
      // add a CSS class name by cascading a typed setter,
      // and invoke the builder with the HTML DOM <h1> and <button> children.
      return (Dom.div()..className = 'container')(
    
        // Create a builder for an <h1> and invoke it with children.
        // No need for wrapping parentheses, since no props are added.
        Dom.h1()('Click the button!'),
    
        // Create a builder for a <button>,
        (Dom.button()
          // add a ubiquitous DOM prop exposed on all components,
          // which Dom.button() forwards to its rendered DOM,
          ..id = 'main_button'
          // add another prop,
          ..onClick = _handleClick
        // and finally invoke the builder with children.
        )('Click me')
      );
    }

   

DOM components and props

All react-dart DOM components (react.div, react.a, etc.) have a corresponding Dom method (Dom.div(), Dom.a(), etc.) in OverReact.

ReactElement renderLink() {
  return (Dom.a()
    ..id = 'home_link'
    ..href = '/home'
  )('Home');
}

ReactElement renderResizeHandle() {
  return (Dom.div()
    ..className = 'resize-handle'
    ..onMouseDown = _startDrag
  )();
}
  • OverReact DOM components return a new DomProps builder, which can be used to render them via our fluent interface as shown in the examples above.
  • DomProps has statically-typed getters and setters for all "ubiquitous" HTML attribute props.
    • The domProps() function is also available to create a new typed Map or a typed view into an existing Map. Useful for manipulating DOM props and adding DOM props to components that don’t forward them directly.

   

Component Formatting

A note on dart_style:

Currently, dart_style (dartfmt) decreases the readability of components built using OverReact's fluent-style. See dart-lang/dart_style#549 for more info.

We're exploring some different ideas to improve automated formatting, but for the time being, we do not recommend using dart_style with OverReact.

However, if you do choose to use dart_style, you can greatly improve its output by using trailing commas in children argument lists:

  • dart_style formatting:
return (Button()
  ..id = 'flip'
  ..skin =
      ButtonSkin.vanilla)((Dom.span()
  ..className = 'flip-container')((Dom.span()..className = 'flipper')(
    (Dom.span()
      ..className =
          'front-side')((Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_RIGHT)()),
    (Dom.span()
      ..className =
          'back-side')((Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_LEFT)()))));
  • dart_style formatting, when trailing commas are used:
return (Button()
  ..id = 'flip'
  ..skin = ButtonSkin.vanilla)(
  (Dom.span()..className = 'flip-container')(
    (Dom.span()..className = 'flipper')(
      (Dom.span()..className = 'front-side')(
        (Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_RIGHT)(),
      ),
      (Dom.span()..className = 'back-side')(
        (Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_LEFT)(),
      ),
    ),
  ),
);

Guidelines

To help ensure your OverReact code is readable and consistent, we've arrived at the following formatting rules.

  • ALWAYS place the closing builder parent on a new line.

    Good:

    (Button()
      ..skin = ButtonSkin.SUCCESS
      ..isDisabled = true
    )('Submit')

    Bad:

    (Button()
      ..skin = ButtonSkin.SUCCESS
      ..isDisabled = true)('Submit')
  • ALWAYS pass component children on a new line with trailing commas and 2 space indentation.

    Good:

    Dom.div()(
      Dom.span()('nested component'),
    )
    Dom.div()(
      Dom.span()('nested component A'),
      Dom.span()('nested component B'),
    )

    Bad:

    // Children are not on a new line; in most cases,
    // this makes it difficult to quickly determine nesting.
    Dom.div()(Dom.span()('nested component'), Dom.span()('nested component'))
    // With nested hierarchies, continuation indents can quickly result
    // in a "pyramid of Doom"
    Dom.div()(
        Dom.ul()(
            Dom.li()(
                Dom.a()('A link!')
            )
        )
    )
    // Omitting trailing commas makes it a pain to rearrange lines
    Dom.div()(
      Dom.span()('nested component A'),
      Dom.span()('nested component B')
    )
    Dom.div()(
      Dom.span()('nested component B') // ugh, need to add a comma here...
      Dom.span()('nested component A'),
    )
  • AVOID passing children within lists; lists should only be used when the number/order of the children are dynamic.

    Good:

    Dom.div()(
      Dom.span()('nested component'),
      Dom.span()('nested component'),
    )
    var children = [
      Dom.div()('List of Items:'),
    ]..addAll(props.items.map(renderItem));
    
    return Dom.div()(children)

    Bad:

    Dom.div()([
      (Dom.span()..key = 'span1')('nested component'),
      (Dom.span()..key = 'span2')('nested component'),
    ])
  • AVOID specifying more than one cascading prop setter on the same line.

    Good:

    (Dom.div()
      ..id = 'my_div'
      ..className = 'my-class'
    )()

    Bad:

    (Dom.div()..id = 'my_div'..className = 'my-class')()

Building custom components

Now that we’ve gone over how to use the over_react package in your project, the anatomy of a component and the DOM components that you get for free from OverReact, you're ready to start building your own custom React UI components.

  1. Start with one of the component boilerplate templates below (Or, use OverReact's code snippets for Intellij and Vs Code).
  1. Fill in your props and rendering/lifecycle logic.

  2. Consume your component with the fluent interface.

  3. Run the app you’ve set up to consume over_react

    $ pub run build_runner serve

    That’s it! Code will be automatically generated on the fly by the builder!

Check out some custom component demos to get a feel for what’s possible!

 

Component Boilerplate Templates

  • Component Boilerplate

    import 'package:over_react/over_react.dart';
    
    @Factory()
    UiFactory<FooProps> Foo = _$Foo;
    
    @Props()
    class _$FooProps extends UiProps {
      // Props go here, declared as fields:
      bool isDisabled;
      Iterable<String> items;
    }
    
    @Component()
    class FooComponent extends UiComponent<FooProps> {
      @override
      Map getDefaultProps() => (newProps()
        // Cascade default props here
        ..isDisabled = false
        ..items = []
      );
    
      @override
      render() {
        // Return the rendered component contents here.
        // The `props` variable is typed; no need for string keys!
      }
    }
  • Stateful Component Boilerplate

    import 'dart:html';
    import 'package:over_react/over_react.dart';
    
    @Factory()
    UiFactory<BarProps> Bar = _$Bar;
    
    @Props()
    class _$BarProps extends UiProps {
      // Props go here, declared as fields:
      bool isDisabled;
      Iterable<String> items;
    }
    
    @State()
    class _$BarState extends UiState {
      // State goes here, declared as fields:
      bool isShown;
    }
    
    @Component()
    class BarComponent extends UiStatefulComponent<BarProps, BarState> {
      @override
      Map getDefaultProps() => (newProps()
        // Cascade default props here
        ..isDisabled = false
        ..items = []
      );
    
      @override
      Map getInitialState() => (newState()
        // Cascade initial state here
        ..isShown = true
      );
    
      @override
      render() {
        // Return the rendered component contents here.
        // The `props` variable is typed; no need for string keys!
      }
    }
  • Flux Component Boilerplate

    import 'dart:html';
    import 'package:over_react/over_react.dart';
    
    @Factory()
    UiFactory<BazProps> Baz = _$Baz;
    
    @Props()
    class _$BazProps extends FluxUiProps<BazActions, BazStore> {
      // Props go here, declared as fields.
      // `actions` and `store` are already defined for you!
    }
    
    @Component()
    class BazComponent extends FluxUiComponent<BazProps> {
      getDefaultProps() => (newProps()
        // Cascade default props here
      );
    
      @override
      render() {
        // Return the rendered component contents here.
        // The `props` variables is typed; no need for string keys!
        // E.g., `props.actions`, `props.store`.
      }
    }
  • Stateful Flux Component Boilerplate

    import 'dart:html';
    import 'package:over_react/over_react.dart';
    
    @Factory()
    UiFactory<BazProps> Baz = _$Baz;
    
    @Props()
    class _$BazProps extends FluxUiProps<BazActions, BazStore> {
      // Props go here, declared as fields.
      // `actions` and `store` are already defined for you!
    }
    
    @State()
    class _$BazState extends UiState {
      // State goes here, declared as fields.
    }
    
    @Component()
    class BazComponent extends FluxUiStatefulComponent<BazProps, BazState> {
      getDefaultProps() => (newProps()
        // Cascade default props here
      );
    
      @override
      Map getInitialState() => (newState()
        // Cascade initial state here
      );
    
      @override
      render() {
        // Return the rendered component contents here.
        // The `props` variables is typed; no need for string keys!
        // E.g., `props.actions`, `props.store`.
      }
    }

 

Component Best Practices

  • ALWAYS write informative comments for your component factories. Include what the component relates to, relies on, or if it extends another component.

    Good:

    /// Use the `DropdownButton` component to render a button
    /// that controls the visibility of a child [DropdownMenu].
    ///
    /// * Related to [Button].
    /// * Extends [DropdownTrigger].
    /// * Similar to [SplitButton].
    ///
    /// See: <https://link-to-any-relevant-documentation>.
    @Factory()
    UiFactory<DropdownButtonProps> DropdownButton = _$DropdownButton;

    Bad:

    /// Component Factory for a dropdown button component.
    @Factory()
    UiFactory<DropdownButtonProps> DropdownButton = _$DropdownButton;

 

  • ALWAYS set a default / initial value for props / state fields, and document that value in a comment.

    Why? Without default prop values for bool fields, they could be null - which is extremely confusing and can lead to a lot of unnecessary null-checking in your business logic.

    Good:

    @Props()
    _$DropdownButtonProps extends UiProps {
      /// Whether the [DropdownButton] appears disabled.
      ///
      /// Default: `false`
      bool isDisabled;
    
      /// Whether the [DropdownButton]'s child [DropdownMenu] is open
      /// when the component is first mounted.
      ///
      /// Determines the initial value of [DropdownButtonState.isOpen].
      ///
      /// Default: `false`
      bool initiallyOpen;
    }
    
    @State()
    _$DropdownButtonState extends UiState {
      /// Whether the [DropdownButton]'s child [DropdownMenu] is open.
      ///
      /// Initial: [DropdownButtonProps.initiallyOpen]
      bool isOpen;
    }
    
    @Component()
    DropdownButtonComponent
        extends UiStatefulComponent<DropdownButtonProps, DropdownButtonState> {
      @override
      Map getDefaultProps() => (newProps()
        ..isDisabled = false
        ..initiallyOpen = false
      );
    
      @override
      Map getInitialState() => (newState()
        ..isOpen = props.initiallyOpen
      );
    }

    Bad:

    @Props()
    _$DropdownButtonProps extends UiProps {
      bool isDisabled;
      bool initiallyOpen;
    }
    
    @State()
    _$DropdownButtonState extends UiState {
      bool isOpen;
    }
    
    @Component()
    DropdownButtonComponent
        extends UiStatefulComponent<DropdownButtonProps, DropdownButtonState> {
      // Confusing stuff is gonna happen in here with
      // bool props that could be null.
    }

 

  • AVOID adding props or state fields that don't have an informative comment.

    Good:

    @Props()
    _$DropdownButtonProps extends UiProps {
      /// Whether the [DropdownButton] appears disabled.
      ///
      /// Default: `false`
      bool isDisabled;
    
      /// Whether the [DropdownButton]'s child [DropdownMenu] is open
      /// when the component is first mounted.
      ///
      /// Determines the initial value of [DropdownButtonState.isOpen].
      ///
      /// Default: `false`
      bool initiallyOpen;
    }
    
    @State()
    _$DropdownButtonState extends UiState {
      /// Whether the [DropdownButton]'s child [DropdownMenu] is open.
      ///
      /// Initial: [DropdownButtonProps.initiallyOpen]
      bool isOpen;
    }

    Bad:

    @Props()
    _$DropdownButtonProps extends UiProps {
      bool isDisabled;
      bool initiallyOpen;
    }
    
    @State()
    _$DropdownButtonState extends UiState {
      bool isOpen;
    }

 

Contributing

Yes please! (Please read our contributor guidelines first)

   

Versioning

The over_react library adheres to Semantic Versioning:

  • Any API changes that are not backwards compatible will bump the major version (and reset the minor / patch).
  • Any new functionality that is added in a backwards-compatible manner will bump the minor version (and reset the patch).
  • Any backwards-compatible bug fixes that are added will bump the patch version.

About

A library for building statically-typed React UI components using Dart.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Dart 99.0%
  • Other 1.0%