Skip to content
This repository has been archived by the owner on Jul 28, 2020. It is now read-only.
/ electrum Public archive

Electrum simplifies framework-agnostic declaration of React components.

License

Notifications You must be signed in to change notification settings

epsitec-sa/electrum

Repository files navigation

Electrum

NPM version Build Status Build status

Electrum simplifies framework-agnostic declaration of React components. It is used internally by Epsitec SA to bridge the gap with its Xcraft toolchain and with its Lydia framework.

Where does the name Electrum come from?

Electrum is an alloy of gold and silver used to produce ancient Lydian coinage.

The first metal coins ever made, were of Electrum and date back to the end of the 7th century, beginning of the 6th century BC.

Exported sub-components

In order to reduce dependency hell, electrum exports its own dependencies directly:

  • React, ReactDOM and ReactDOMServer from react, react-dom and react-dom/server
  • radium from radium.
  • captureMouseEvents from electrum-events.
  • FieldStates from electrum-field.
  • Store and State from electrum-state.
  • Styles, Theme and ColorManipulator from electrum-theme.
  • Trace from electrum-trace.

THIS IS WORK IN PROGRESS

The implementation of electrum is being modified radically.
Please wait until version has stabilized.

Linking components with their state

Let's say we want to display an article which contains the content and information about the author. The article data might be represented like this:

{ "article":
  { "content":
    { "title": "About typography"
    , "text": "Lorem ipsum..."
    , "date": "2015-12-02" }
  , "author":
    { "name": "John"
    , "mail": "[email protected]" } } }

This can be loaded into a store instance. The "article" node will be passed as state to the <Article> component:

// In this example, the article is the root component
const state = store.select ('article');
return <Article state={state}/>;

The <Article> can be implemented as a stateless function component:

import E from 'electrum';
function Article (props) {
  return (
    <div>
      <Content {...E.link (props, 'content')} />
      <Author {...E.link (props, 'author')} />
    </div>
  );
}

Reading state

Components will very often need to read values from the store. To make life easier for the developer, electrum provides a read() method, which takes the props of the component and an optional key of the value to read:

import E from 'electrum';
function Content (props) {
  return (
    <div>
      <h1>{E.read (props)}</h1>
      <p>{E.read (props, 'details')}</p>
    </div>
  );
}

Managing styles with Radium

We decided to use radium as the way to go to inject styles into components. By using the E instance provided by import Electrum from 'electrum'), components are automatically configured to use radium when wrapped like this:

import Electrum from 'electrum';
import _Button from './Button.component.js';
import _Button$styles from './Button.styles.js';
export const Button = Electrum.wrap ('Button', _Button, {styles: _Button$styles});

See electrum-theme for an explanation of how style functions should be defined. Style functions can have following signatures:

  • () => ... → a parameterless style function.
  • (theme) => ... → a style function, based on the theme.
  • (theme, props) => ... → a style function, based on the theme and on the component properties. The component should implement a getter named styleProps which returns a hash with the meaningful properties.

Multiple styles definitions can be exported as a hash of style functions.

Component with a single style function

A component linked with a style definition consisting of a single style function will expose following method and property:

  • styles → a styles object which can be set on DOM element style properties; the styles object is compatible with Radium. It exposes a with(s1, s2, ...) function which can be used to obtain an updated styles object into which additional styles have been merged.
  • mergeStyles(s1, s2, ...) → a hash containing the merged styles.

Component with a multiple style functions

A component linked with a style definition consisting of a multiple style functions will expose following methods:

  • getStyles(key) → a styles object for the specified style definiton, which can be set on DOM element style properties; the styles object is compatible with Radium. It exposes a with(s1, s2, ...) function which can be used to obtain an updated styles object into which additional styles have been merged.
  • mergeStyles(key, s1, s2, ...) → a hash containing the merged styles for the specified style definition.

Note that the signature of the methods are different from a component with a single style function.

Wrapping and automatic component extensions

Electrum.wrap() returns a new component class, which will be treated as a pure component by React:

  • shouldComponentUpdate(nextProps, nextState) → pure component.
    The test is based on a shallow comparison of the properties and of the state (if any).

It injects some additional functionality:

  • link(id) → shorthand for Electrum.link(this.props, id).
  • link(id, overrides) → shorthand for Electrum.link(this.props, id, overrides).
  • read() → shorthand for this.read('value').
  • read(key) → returns named property if it exists on this.props, otherwise calls Electrum.read(this.props, key) and reads the value from the state.
  • theme → shorthand for this.props.theme.
  • styles → resolves the styles based on rules implemented by Styles.

The component is also extended by Radium which will flatten styles arrays injected in child components, and handle the state required to handle browser states such as :hover.

One more trick Electrum.wrap() does is that it ensures that event handler methods (e.g. onChange or handleFoo) get properly bound to the component instance. Therefore, event handlers can be passed to React in a natural way:

render () {
  return <div onClick={this.onClick}>Click me</div>;
}

whereas normally, you would have to write this:

render () {
  return <div onClick={this.onClick.bind (this)}>Click me</div>;
}

or do the binding manually in the constructor:

constructor () {
  super ();
  this.onClick = this.onClick.bind (this);
}
render () {
  return <div onClick={this.onClick}>Click me</div>;
}

Electrum's autobinding looks for methods starting with on or handle and using camel case (such as onClick); other methods won't be automatically bound to this.

See also the explanation on autobinding on the React blog.

Sending events to the bus

Electrum can use a bus to dispatch messages/commands and notify changes. The bus interface looks like this:

{
  dispatch (props, message, payload) {}
  notify (props, source, value, ...states) {}
}

Bus configuration

A bus can be attached with Electrum.useBus(bus).

Event forwarding

The default Electrum instance is configured to use electrum-events, which injects various event handlers into the wrapped components:

  • onChange → fires notification of type change
  • onKeyDown, onKeyUp, onKeyPress → fire notifications of type key-down, key-up, key-press
  • onFocus → fires notification of type focus
  • onSelect → fires notification of type select

Note: if the component provides its own event handlers, they will be called by the injected methods.

Events will automatically be sent to the bus, if one has been configured (see Electrum.use). The EventHandlers class in electrum-events is in charge of the event forwarding. It will provide the value and the states associated with the underlying component, usually by reading the DOM:

  • source{type, event} where type is the event name
  • valueevent.target.value
  • states{begin:0, end:10} for text fields

Custom value or states

When the defaults are not meaningful (e.g. for a checkbox, where the value does not exist per se), the component can provide the value (method getValue()) or the states (method getStates()):

class MyCheckbox extends React.Component {
  render () {
    return <input type='checkbox' /* ... */ />;
  }
  getValue (target) {
    // The value will be 'on' or 'off', depending on the checked state
    // of the target DOM node:
    return target.checked ? 'on' : 'off';
  }
}

Automating component wrapping

The easiest way to get all components of a module wrapped is to use the electrum-require-components module.

See electrum-require-components.

Install electrum-require-components

npm install electrum-require-components --save-dev

Configure your package

Edit package.json to add a script that can be invoked with npm run regen in order to regenerate the source file all.js which includes, wraps and exports all components.

"scripts": {
  ...
  "regen": "electrum-require-components --wrap ./src components .component.js all.js"
}

Export all wrapped components

To export all components found in your module, use:

export * from './all.js';

Tracing

Electrum includes basic tracing functionality, which might come in handy when live debugging wrapped components.

shouldComponentUpdate()

Whenever React calls a wrapped component's shouldComponentUpdate(), Electrum will call the corresponding logging function:

import E from 'electrum';
E.configureLog ('shouldComponentUpdate', (component, nextProps, nextState, result) => { /* ... */ });

The arguments are:

  • component → component instance.
  • nextProps → next properties, as provided to shouldComponentUpdate.
  • nextState → next state, as provided to shouldComponentUpdate.
  • result → result of the call to shouldComponentUpdate, where true means that the component should be rendered.

States and fingerprints

Components may need to represent their internal state as a collection of simple state objects:

const fieldSelection = { from: 12, to: 17 };     // 'from,to'
const listSelection  = { first: 5, active: 8 };  // 'active,first'

These state objects have fingerprints which are based on their sorted property names ('from,to', 'active,first'). It does not include the optional id property.

FieldStates class

The FieldStates class maintains an internal array of state objects. It is implemented in electrum-field and made available by electrum as a convenience.

Query the field states

  • FieldStates.fingerprint (state) → the fingerprint of a state object.
  • find (fingerprint) → the first state object which matches the specified fingerprint, or undefined if none can be found.
  • get () → an immutable array of immutable state objects.

Update the field states

The instances are immutable. All methods which modify the internal array of state objects will return a new instance (or the unchanged instance if the update was a no-op). The original instance is never modified.

  • add (state) → a new instance where the internal array of states has been updated by adding or replacing a state; matching is done based on the state's fingerprint.
  • add (state1, state2, ...) → same as add() called multiple times.
  • remove (fingerprint) → a new instance where the internal array of states has been updated by removing the first state matching the specified fingerprint.

Actions

Electrum prescribes how actions represent their specific state and provides the Action class to inspect it:

  • Action.isEnabled (state)true if the state is enabled.
  • Action.isDisabled (state)true if the state is disabled.

About

Electrum simplifies framework-agnostic declaration of React components.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published