Giter VIP home page Giter VIP logo

rearm's Introduction

a collection of React.js abstractions for non-trivial apps

Install

npm install --save rearm

# or
yarn add rearm

Usage

Visit the documentation website.

rearm's People

Contributors

brigand avatar nullhook avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

rearm's Issues

PortalGun: handle multiple Source renders

If multiple <Source /> elements are rendered, then <Dest /> should render all of them, defaulting to the order they're added.

To override the order, you can pass a sort={number} prop to Source, e.g. <Source sort={1} />child</Source>.

The default 'sort' value is 0, and since it's a stable sort, it'll preserve insertion order. This is nice because you can simply specify -1 or 1 and be on either side of the default sorts.

Migrate PortalGun to hooks

The current PortalGun component uses an old api. We need to migrate to hooks and spruce it.

How can we approach this?

Collapse

A collapse component essentially transitions between height: 0 and height: auto, but css transitions don't support that, so it needs to measure the child itself.

react-collapse is great, but I think we want to pass on the spring animations, at least initially.

Basic usage:

import Collapse from 'rearm/lib/Collapse';
<Collapse
  open={boolean}
>
  <Some />
  <Children />
</Collapse>

Another common requirement is transitions on the container element, which requires a class name or inline style that's only applied when the collapse is open. The main time I need this is to set a box-shadow. Since overflow:hidden has to be applied to the collapse container, a box-shadow wouldn't appear until overflow:hidden is removed, so it actually has to be applied to the collapse container itself. A box shadow applied immediately on collapse open isn't good, so it needs to be transitioned.

<Collapse
  open={boolean}
  activeStyle={{ boxShadow: '0 0 3px rgba(0, 0, 0, 0.3)' }}
>
  <X />
</Collapse>

This produces a transition property, defaulting to box-shadow 0.2s ease-in-out here.

In some cases you want the children to always be mounted, just hidden. In react-collapse, the default is to always render them, however I find that almost always I want them unmounted when collapsed. This can be overridden with the alwaysChildren={boolean} prop (needs a better name).

<Collapse
  open={boolean}
  alwaysChildren
>
  <Some />
  <Children />
</Collapse>

Ctx: subscribe prop

Allows passing an array of keys to subscribe to, without filtering the keys received by children. Updates will still propagate to children if any of the keys might have changed.

<Ctx subscribe={['count']}>
  {c => `Count is ${c.count} and other field is ${c.otherField}`}
</Ctx>

DataSource/DataReceive

A data portal can transport data between components. It's similar to redux but fully declarative.

In this example we use the key 'foo', but to make less assumptions, you'd use a random id generated in a recent parent.

class ExampleSource extends React.Component {
  state = { counter: 0 };
  render() {
    return (
      <div>
        <DataSource id="foo" value={this.state.counter} />
        <button onClick={() => this.setState({ counter: this.state.counter + 1 });
      </div>
  }
}

We can use a 'render child' to extract the data.

const ExampleReceiver = () => (
  <DataReceive id="foo">
    {(counter) => <div>Count: {counter}</div>}
  </DataReceive>
);
export default ExampleReceiver;

Or we can use a high order component.

const ExampleReceiver = (props) => <div>Count: {props.counterProp}</div>
export default DataReceive.hoc({ id: 'foo', prop: 'counterProp' })(ExampleReceiver);

ResizeToFit

Say you have a box that's 100px wide. You need to display arbitrary children in the box. If the width of the children are 50px, everything is great. If the width is 120px then you have an issue. Often you can't make the container larger, and wrapping may not be desired. This component tries two strategies:

1. Resize font

Assuming you're using 'em' units, changing the font size can scale down everything in the container. You can use 'px' for things that shouldn't scale. We compute the font size that will make the contents fit in the container.

2. Clipping

We can't have 6px font, that'd be crazy. When there really isn't enough space, we can clip the content to fit the container.

Usage

<ResizeToFit minFontPx={12}>
  <span><img src={src} style={{ height: '1em' }} /> {someText}</span>
</ResizeToFit>

expand Ctx tool

The Ctx tool is super useful when it comes to subscribing to part of the context store.

Would love to re-think on how it can be use with Hooks.

List component

Let's experiment around rendering an array of objects. This would be a different style of array.map

<List 
 items={data}
>
...
</List>

We can also look into adding If/else helpers with the package

<If condition={foo}>
  <div>
    {If.true(() => "yes"}
    {If.false(() => "no"}
  </div>
</If>

Would love to brainstorm here

CtxState

Building on Ctx, this module creates a state manager that holds arbitrary state and passes it down to children via Ctx. By default, it also passes a function to update the state which can be accessed with the Ctx wrapper.

First we provide the state to children. The name passed to makCtxState is purely cosmetic. A random name will be generated for the state key.

import { makeCtxState} from 'rearm/lib/CtxState';

const { ProvideState, GetState } = makCtxState('my-state');

const C = () => {
  <ProvideState initial={{count: 0}}>
    <Display />
    <Incr />
  </ProvideState>
};

Then we can access the state:

const Display = () => (
  <GetState>
    {s => `Count is ${s.count}`}
  </GetState>
);

And update it:

const Incr = () => (
  <GetState>
    {s => <button onClick={() => s.set({ count: s.count + 1 })}>Increment</button>}
  </GetState>
);

Updaters

If you want to pass more refined callbacks to the children instead of exposing the state.set method, you can use updaters.

They're specifically optimized to avoid needless child rerendering. The main cost of this performance is that you can't dynamically add an updater function. The keys need to stay consistent between all renders.

const C = () => {
  <ProvideState initial={{count: 0}} updaters={{ incr: s => ({ count: s.count + 1 })>
    <Display />
    <Incr />
  </ProvideState>
};
const Incr = () => (
  <GetState>
    {s => <button onClick={s.incr}>Increment</button>}
  </GetState>
);

Internally we pass a function named 'incr' that calls ProvideState's props.updaters.count. This allows us to not cause a rerender of all children when ProvideState is rerendered. In the future, we can do some checks for new methods being added, but it's not high priority. You should have a well defined and consistent interface to the state updates.

useAsync

It's very common to fetch data from a component, and the useAsync hook provides a way to declaratively express this request with several subtle details explained later.

A version of this hook can be found here, though it will need some significant changes.

Some examples of usage with the current implementation:

function MatchExample({ userId }) {
  const api = useAsync(() => fetchUser(userId), [userId]);
  
  // Picks the first matching variant
  return api.match({
    failure: (message) => <Error message={message} />,
    success: (user) => <div>User: {user.name}</div>,
    loading: () => <Loading />,
    default: () => null,
  });
}

function AccessorExample({ userId }) {
  const api = useAsync(() => fetchUser(userId), [userId]);
 
  // Multiple can match here. See definition of terms after examples.
  return <div>
    {api.failure().map(error => <Error message={message} />).or_null()}
    {api.value().map(user => <div>User: {user.name}</div>).or_null()}
    {api.loading() && <Loading />}
  </div>
  
}

function OptionalFetch({ userId }) {
  // only fetches if userId is not null
  const api = useAsync(() => Option.of(userId && fetchUser(userId)), [userId]);
 
  // ...
}

Terms:

  • initial: no request is being made (exclusive with success/failure/loading)
  • success: the current state is success (exclusive with initial/failure/loading)
  • failure: the current state is failure (exclusive with initial/success/loading)
  • loading: the current state is loading (exclusive with initial/success/failure)
  • value: at one point there was a success, and this holds the most recent value
  • error: at one point there was a failure, and this holds the most recent error

These allow for more nuanced decisions about what to render, e.g. in AccessorExample if the fetch is successful, then another request is a failure, we might display both the error and the last successful value at once. Then if we make another request and it's successful, the error will disappear because the current state is 'success'. If we used .error instead of .failure, then the error would persist.

OptionalFetch shows that by returning an Option<Promise<T>>, we can decline to make any request at all, leaving us in the 'initial' state, and do so in a fully declarative way.


The dependency on the safe-types package might be too much here, and it uses underscores in method names which many people will strongly oppose. We might want to create a more minimal Option implementation, or go some other route that doesn't rely on an Option type.

We might get rid of the methods like .failure() and require the use of .match, or have something like .failure(error => <Error message={error} />), at a moderate cost in power by losing the Option type, but simplifying the API.

The first argument to useAsync could require a falsy value be returned (like null) to indicate no operation should be performed.


The current code relies on two hooks, and there's quite a bit of conversion between the two APIs. We should try to simplify this in some way.


We can fill in more of the details as we go.

PortalGun: Dest accepts render callback

Dest should accept a render callback which is always called when the Dest is re-rendered, or there's an update at a Source. The main use cases here are transitions, and wrapping the node in another element.

<Dest />

// same as...

<Dest>
  {(nodeOrNull) => nodeOrNull}
</Dest>

For transitions, it would be compatible with react-transition-group, without us doing anything special.

The general wrapper case allows conditionally rendering a wrapper, based on the node existing or not.

<Dest>
  {(nodeOrNull) => nodeOrNull
    ? <Modal>{nodeOrNull}</Modal>
    : null
  }
</Dest>

Component to add !important styles

Applying !important styles is a frequently requested feature in React, so maybe some version of this would make sense in rearm

We already have an initial implementation here

ClickOutside

It's common to hide an element when the user clicks on some other part of the page. Sometimes multiple elements will be waiting for an outside click, and you only want to close the newest one. Sometimes you want to ask the user to confirm they would like to exit out of the view before doing so, either always or e.g. when a form is dirty.

import ClickOutside from 'rearm/lib/ClickOutside';

<ClickOutside
  useQueue
  onClickOutside={() => {
    // this only runs when it's the latest outside click view because we used `useQueue`

    this.closeThing();
  }}
>
  My view
</ClickOutside>

For asking confirmation, you can either do it manually with the above interface, with a conditional setState and return in the onClickOutside callback, or use our abstraction for it via a render callback.

The portalOpts are passed to PortalAt as props. at could be an element or absolute coordinates. If portalOpts are not provided, PortalAt will not be used, and it's up to your renderConfirm result to position itself properly. By default, it'll be rendered directly after "My view".

If confirm is falsy, it behaves like the above example. If it's truthy, when the outside click occurs, renderConfirm will be called to render the confirmation modal (MyModal).

Calling onCancel will dismiss the modal and will not call onClickOutside. onConfirm will dismiss the modal, and will call onClickOutside.

<div ref={(el) => { this.root = el; }>
  <ClickOutside
    confirm={state.dirty}
    portalOpts={{ at: () => this.root }}
    renderConfirm={({ onCancel, onConfirm }) => (
      <MyModal onCancel={onCancel} onConfirm={onConfirm} />
    )}
    onClickOutside={() => {
      this.closeThing();
    }}
  >
    My view
  </ClickOutside>
</div>

Migrate Breakpoint to hooks

We should migrate Breakpoint to hooks:

Rough proposal:

const bp = useBreakpoint(breakpoints, {});

bp.isEq('small');
bp.isLt('small');
bp.isLte('small');
bp.isGte('small');

Do we really need an HOC here or a Render here?
If breakpoints as an option isn't given then we can fallback to defaults.

EDIT: revised as per suggestion below

Breakpoint: Opt-in exact sizes

Sometimes you need the exact size of the viewport in one or more breakpoints. This may only be needed for e.g. your 'small' size, where you do some computed styles.

{ name: 'small', maxWidth: 600, passExact: true }

Then in your component, you can do bp.width() to get the viewport width, and bp.height() to get the viewport height. If passExact is falsy for the current breakpoint, then the width and height methods will throw.

The breakpoint accepts any of these properties:

  • throttle: number milliseconds to wait between updates
  • raf: boolean use requestAnimationFrame to throttle updates
  • idle: number, use requestIdleCallback. The number is the max timeout before an update occurs.

For idle on a viewport breakpoint, it'll generally happen when the user pauses resizing. It's a bit inconsistent. For an element resizing when not based on user interaction, it's highly unstable. I recommend setting this to reasonably low value, like 50 to 250. That cuts down on the inconsistency a bit. Even with a long duration, basic tests show it's called quickly in chrome.

ElementBounds

For more advanced layout, sometimes CSS doesn't offer what you need.

We can get the element size for children with a callback. By default, this gets the size on every render, and calls the onChange callback only when the size has changed. You do have to be careful to not cause infinite render cycles.

const SizeDisplay = (props) => (
  <ElementBounds onChange={({ width, height, top, right, bottom, left }) => {
    console.log(width, height);
  }}>
    <img src="cats.png" />
  </ElementBounds>
);

Sometimes a child resizes on its own. This could be a component that maintains its own state. We can use an element resize listener to call the callback when the child resizes, even if the component rendering ElementBounds doesn't cause it by rendering.

<ElementBounds useResizeListener

It also has a 'render child' variant, which can be useful for rendering things like a DataSource. On the initial render, all properties (width, height, top, etc.) will be 'null'.

<ElementBounds>
  {({ width, height }) => (
    <div>
      <img src="cats.png" />
      <DataSource id="foo" value={{ width, height }} />
    </div>
  )}
</ElementBounds>

AbsoluteLayout

For some views you need an absolutely positioned layout so you can transition elements from one place to another, and preserve elements in the dom despite them visually changing their container element. We won't implement all of css or flexbox; only a few constraint based options.

import { Layout, G } from 'rearm/lib/AbsoluteLayout
const { collapse } = this.state;

<Layout height={!!collapse ? 10 : 20 /* units in em */}>
  <G pos="top-left" id="foo">{!!collapse ? `I choose ` : `Pick a color:`}</G>
  <G pos="center-left" posRef="foo" id="text1" hide={...}>
    <Button onClick={...} plainText={!!collapse}>Red</Button>
  </G>
  <G pos="center-left" posRef={collapse ? 'foo' : 'text1'} id="text2" hide={...}>
    <Button onClick={...} plainText={!!collapse}>Green</Button>
  </G>
  <G pos="center-left" posRef={collapse ? 'foo' : 'text2'} id="text3" hide={...}>
    <Button onClick={...} plainText={!!collapse}>Blue</Button>
  </G>
</Layout>

A bit hard to understand how this will work in the real UI, but:

  • the first G ("foo") is positioned relative to the container since it has no posRef.
  • the next G ("text1") is positioned relative to the first G ("foo")
  • the next G ("text2") is positioned relative to the first G ("text1") except if in the collapsed state where it's positioned relative to "foo"

These elements will transition to their new position, so if "text2" is selected:

  • "text1" is hidden
  • "text3" is hidden
  • "text2" is positioned against the right edge of "foo"
  • "text2" becomes what the button defines as the "plainText" variant, which might also be a transition

The UI feels fluid because the elements don't jump to their new positions.

surpress componentWillReceiveProps warning

After the update #17
The newer version of react warns about deprecated cycle warnings. We need to remove them.

Our Ctx module uses the following:

componentWillReceiveProps(nextProps: CtxProps) {
    this.update(nextProps, this.getParentState());
}

What would be an alternative to this? I'm going to vouch for componentDidUpdate()

DelayedRender

Sometimes you have elements that aren't visible right away. You want to defer rendering these elements, while also desiring that they can be shown instantly when called for.

Real use case: there are 8 icons displayed and a "show more" button. When clicked, an additional 36 icons are displayed. Loading the extra icons on click takes real time and makes the interface seem laggy. Loading them when the always displayed icons are loaded would slow down the page load. This is a compromise that improves the initial performance, and the performance of displaying them when the "show more" is clicked.

DelayedRender can use one or more strategies for deciding when to render.

Timeout

The simplest is a timeout. After X time, the element will render/mount.

<DelayedRender strategy={{ timeout: 500 }}>
  <div style={{ display: state.showStuff ? 'block' : 'none' }}>
    <ExpensiveStuff />
  </div>
</DelayedRender>

Idle

An idle callback can be registered. When the browser says nothing is going on, we render it. We can set a maximum and minimum delay before we render it.

<DelayedRender strategy={{ idle: true, idleMin: 300, idleMax: 1000 }}>
  <div style={{ display: state.showStuff ? 'block' : 'none' }}>
    <ExpensiveStuff />
  </div>
</DelayedRender>

Trigger

Often used in combination with one of the previous, a trigger can cause a load in response to an event. The trigger is its own component.

In this example we use a string literal for the id, but for making less assumptions, you'd use a random id created in a recent parent.

Note that we use mouseover as we want it to load before the button is clicked.

<div>
  <Trigger type="DelayedRender" id="something" events={['mouseover']}>
    <button onClick={showStuff}>show more</button>
  </Trigger>
  <DelayedRender strategy={{ trigger: 'something', idle: true, idleMax: 3000 }}>
    <div style={{ display: state.showStuff ? 'block' : 'none' }}>
      <ExpensiveStuff />
    </div>
  </DelayedRender>
</div>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.