Datum Components

module.exports = class CollectionPicker extends Datum

CollectionPicker

This react-datum component provides a selector (picker) based on react-select.

The props.attr on props.model should return and id or other key that is used to find a model in a collection and display a value from the lookup model.

Say for example, you have a model like this:

1
2
3
4
5
var kittenModel = new Backbone.Model({
  name: "Fluffy",
  title: "His Royal Cuteness",
  updatedByUserId: 905793
});

and a collection of users like this:

1
2
3
4
5
6
var usersCollection = new Backbone.Collection([{
  id: 905793
  fullName: "Jane Doe"
},{
  ...
}])

To display the user's full name instead of the id:

1
2
3
4
5
<ReactDatum.CollectionPicker
  attr="updatedByUserId"  
  model={kittenModel}
  collection={usersCollection}
  displayAttr="fullName"/>

In inputMode ='readonly', the CollectionPicker above will display the fullName from a model in the usersCollection with id matching kittenModel.get('updatedByUserId') from usersCollection instance var.

In inputMode='edit', the suggestions would also render the displayAttr because we didn't specify a suggestionAttr prop

Asynchronous suggestions

Use the asyncLoadCallback prop to load suggestions asynchronously from a server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loadData = function(userInput, doneCallback) {
  usersCollection.fetch({
    data: {search: userInput}
    success: function(model. response) {
      # doneCallback accepts error and collection, array of models,
      # or array of {label: "displan name", value: "value"}
      doneCallback(null, usersCollection)
    }
  )
}
<ReactDatum.CollectionPicker
  attr="userId"
  model="myOtherModel"
  collection="usersCollection"
  asyncLoadCallback={loadData}
/>

Custom display

By default the CollectionPicker will display the value of props.model.get(props.displayAttr), or model.toString() (if it exists and no displayAttr specified).

You can provide custom rendering via the the optionComponent, valueComponent and displayComponent props.

The optionComponent and valueComponent are passed throught to react-select. For optionComponent, you are required to handle and pass events back through the props given. Providing a custom option render, is a little longish, but value display is easy. The GravatarOption and GravatarValue components below were taken directly from the react-select CustomComponents example

To provide a custom option (suggestion) display:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const GravatarOption = React.createClass({
    propTypes: {
        className: React.PropTypes.string,
        isDisabled: React.PropTypes.bool,
        isFocused: React.PropTypes.bool,
        isSelected: React.PropTypes.bool,
        onSelect: React.PropTypes.func,
        onFocus: React.PropTypes.func,
        onUnfocus: React.PropTypes.func,
        option: React.PropTypes.object.isRequired,
    },
    handleMouseDown (event) {
        event.preventDefault();
        event.stopPropagation();
        this.props.onSelect(this.props.option, event);
    },
    handleMouseEnter (event) {
        this.props.onFocus(this.props.option, event);
    },
    handleMouseMove (event) {
        if (this.props.focused) return;
        this.props.onFocus(this.props.option, event);
    },
    handleMouseLeave (event) {
        this.props.onUnfocus(this.props.option, event);
    },
    render () {
        let gravatarStyle = {
            borderRadius: 3,
            display: 'inline-block',
            marginRight: 10,
            position: 'relative',
            top: -2,
            verticalAlign: 'middle',
        };
        return (
            <div className={this.props.className}
                onMouseDown={this.handleMouseDown}
                onMouseEnter={this.handleMouseEnter}
                onMouseMove={this.handleMouseMove}
                onMouseLeave={this.handleMouseLeave}
                title={this.props.option.title}>
                <Gravatar email={this.props.option.email} size={GRAVATAR_SIZE} style={gravatarStyle} />
                {this.props.children}
            </div>
        );
    }
});
 
<CollectionPicker
  model={myModel}
  collection={usersCollection}
  optionComponent={GravatarOption}
  placeholder="Select user"
/>

You can also provide a custom value display for the current value displayed when the input doesn't have focus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const GravatarValue = React.createClass({
    propTypes: {
        placeholder: React.PropTypes.string,
        value: React.PropTypes.object
    },
    render () {
        var gravatarStyle = {
            borderRadius: 3,
            display: 'inline-block',
            marginRight: 10,
            position: 'relative',
            top: -2,
            verticalAlign: 'middle',
        };
        return (
            <div className="Select-value" title={this.props.value.title}>
                <span className="Select-value-label">
                    <Gravatar email={this.props.value.email} size={GRAVATAR_SIZE} style={gravatarStyle} />
                    {this.props.children}
                </span>
            </div>
        );
    }
});
 
 
<CollectionPicker
  model={myModel}
  collection={usersCollection}
  valueComponent={GravatarValue}
  placeholder="Select user"
/>

Custom display when CollectionPicker is in inputMode="readonly" is even easier:

TODO: finish me!

CollectionPicker propTypes:

Datum.propTypes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#  TBD: Can also be the string name of a shared collection (see ../sharedCollection.cjsx)
#  TBD: Can also accept an array of [{lable: "option 1", id: 1}, ...]
#  Can also accept collection instance as context var via ReactDatum.Collection component.
#    prop has precendence
collection: React.PropTypes.oneOfType([
  React.PropTypes.instanceOf(Backbone.Collection)
  React.PropTypes.string
  React.PropTypes.array
])
 
#  Attribute value from model in lookup collection to render in inputMode='readonly'.
#  if not specified, model.toString() will be displayed
displayAttr: React.PropTypes.string
 
#  attribute value from model in lookup collection to render in suggestions when
#  in inputMode='edit'.  If not specified, @props.displayAttr is used and if that
#  is not specified, model.toString() is used
optionDisplayAttr: React.PropTypes.string

CollectionPicker defaultProps:

1
optionSaveAttr: 'id'

CollectionPicker contextTypes:

1
2
3
4
5
# see @proptypes.collection above
collection: React.PropTypes.oneOfType([
  React.PropTypes.instanceOf(Backbone.Collection)
  React.PropTypes.string
])
module.exports = class Datum extends React.Component

Datum

This is base class of all display+input components that render presentation of an attribute from a Backbone.Model or Javascript object.

There is one required prop, 'attr' - the model attribute name.

There are two modes of presentation controlled by the inputMode prop. All Datums support inputMode="readonly" - the display presentation of the data; and most support inputMode="edit" - the input or form presentation of the attribute. The default presentation is inputMode="readonly"

The backbone model or javascript object to get and set attributes on is specified in the @props.model or @context.model. Note that @props.model always has precendence.

TODO : More Examples

Display attributes from a Backbone model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render: function(){
 
  user = new UserModel()
  user.fetch()
 
  // you don't need to wait for the fetch to complete because we are using
  // the **Model** contextual data component below which will rerender when
  // the model triggers 'sync'
 
  return (
    <Model model={user}>
      <Datum attr="firstName" label="First Name: "/>
      <Datum attr="firstName" label="Last Name: "/>
      <Datum attr="title"/>
    </Model>
  )
}

Note that for clarity we have provided the Text datum which will also ellipsize the display value for you. You should probably use that instead of using Datum directly like the example above. e.g.

1
<Text attr="firstName" label="FirstName"/>

Validations

Datums support validations. All validation methods should return either true or an error message. All datums should support 'required' validation prop.

To make a datum required, simply add the required prop to the component. Ex:

1
<Text attr="name" required>

Validations are run at time of change and the datum element will get an invalid class for optional styling. An exclamation icon is rendered and has a popup that will show all errors for that input.

ReactBootstrap

If ReactBootstrap is available globally, Datum will use that for the ellipsize and validation error popovers. You can style the popover as you will.

Extending ReactDatum.Datum

Datum, and any of it's descendants, can be extended using either ES6 or Coffeescript inheritance. The API docs below call out the methods that you will most likely need to override or extend for customization.

TODO : link to examples - 1 ea. coffeescript and es6

see ./datum.md

Datum propTypes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# additional css classes (space seperated) to add to wrapper div
className: React.PropTypes.string
 
# can also accept model instance as context var. prop has precendence
model: React.PropTypes.oneOfType([
  React.PropTypes.instanceOf(Backbone.Model)
  React.PropTypes.object
])
 
# the backbone attribute on the model to get and set
attr: React.PropTypes.string.isRequired
 
label: React.PropTypes.string
 
# set ellipsizeAt to false to display whole value. Only effects 'readonly' display
# values displayed in 'edit' mode are never truncated.
ellipsizeAt: React.PropTypes.oneOfType([
  React.PropTypes.number
  React.PropTypes.bool
])
 
placeholder: React.PropTypes.string
 
# TODO : add back support of 'inlineEdit'?
# 'readonly' = render for display;
# 'edit' = render for input;
inputMode: React.PropTypes.oneOf(['readonly', 'edit']) 
 
# set to true to not render a popover on ellipsized values
noPopover: React.PropTypes.bool
 
# set to true to set the model value on change. this defaults to true if inputMode = inlineEdit
setOnChange: React.PropTypes.bool
 
# make this input readonly regardless of context or inputMode prop
readonly: React.PropTypes.bool
 
# make this input required and give it required class and invalid class when invalid
required: React.PropTypes.bool

Datum defaultProps:

1
2
# ellipsizeAt is defaulted to prevent really long strings from breaking layouts
ellipsizeAt: 35

Datum contextTypes:

1
2
3
4
5
6
7
8
9
10
11
12
# can also accept model instance as a prop.  prop has precendence
model: React.PropTypes.oneOfType([
  React.PropTypes.instanceOf(Backbone.Model)
  React.PropTypes.object
])
 
# note that the readonly prop takes precendence
inputMode: React.PropTypes.oneOf(['readonly', 'edit', 'inlineEdit'])
 
# if there is a form context, we will register with it so that it can
# interact with us
form: React.PropTypes.object
module.exports = class Email extends Datum

Email

For rendering and input of email addresses. Can render mailto: links like <a href="mailto:"> in display mode

Validates that email address is a semi valid email based on matching /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/

Email propTypes:

Datum.propTypes
1
2
# If true, display as mailto address link when `inputMode='readonly'`
displayAsLink: React.PropTypes.bool
module.exports = class LazyPhoto extends Datum

LazyPhoto

This is a lazy loading image.

To prevent a page heavily loaded with images preventing other content from loading, a small blank image is downloaded and rendered first and then onLoad the real image src is used and rerender.

On error a notFoundUrl is set as the image src to prevent broken image display.

The model attribute specified in @props.attr should return a fully qualified url. The image is only rendered if it's visible and in view. Otherwise the placeholder image is rendered.

module.exports = class Number extends Datum

Number

For real numbers.

Only allows /^\-?[0-9]*\.?[0-9]*$/ on input

Number propTypes:

Datum.propTypes
1
2
3
4
5
6
7
8
9
10
# 'abbreviate' - Add M and K to numbers greater than 1 million and 1 thousand respectively
# 'money' - display dollar sign and two decimal places zero filled
# 'comma' - add comma separators at thousands
format: React.PropTypes.oneOf(['abbreviate','money','comma'])
 
#validate value is at least this value on change
minValue: React.PropTypes.number
 
#validate value is at most this value on change
maxValue: React.PropTypes.number
module.exports = class Text extends Datum

Text

This react-datum component is for the presentation of text. See also ReactDatum.Datum. The Datum base class does much of the work by default of handling text.

Text Component provides the following additional features:

  • Display values can be ellipsized via ellipsizeAt prop.
  • Values are ellipsized by default to prevent unexpected long strings from breaking your layout
  • Can display HTML via the displayAsHtml prop

A note about using displayAsHtml

It's not advised unless you are sure as to the sanity of the data value you are displaying. The danger is in cross site scripting attacks. Read more here.

Also note that using displayAsHtml and anything other than ellipsizeAt={false} is not advised as you may end up ellipsizing in the middle of an element and leave unclosed tags or, worse, break your entire layout.

For example, given this model:

1
model = new Backbone.model({name="Foo Bar", description="Lorem ipsum <b>dolor sit amet</b>, consectetur adipiscing elit. Duis rhoncus lacinia lectus a volutpat. this could go on forever"});

this will render the description truncated with ellipsis to not exceed 35 total characters:

1
<ReactDatum.Text attr="description">

this will render the description at it's full length:

1
<ReactDatum.Text attr="description" ellipsizeAt={false}>

this will render the description with "dolor sit amet" in bold:

1
<ReactDatum.Text attr="description" displayAsHtml ellipsizeAt={false}>

see ./text.md

Text propTypes:

Datum.propTypes
1
2
3
4
5
6
7
8
9
# set to true if rendering known, safe, html.  see https://facebook.github.io/react/tips/dangerously-set-inner-html.html.
displayAsHtml: React.PropTypes.bool
 
# set ellipsizeAt to false to display whole value. Only effects 'readonly' display
# values displayed in 'edit' mode are never truncated.
ellipsizeAt: React.PropTypes.oneOfType([
  React.PropTypes.number
  React.PropTypes.bool
])

Text defaultProps:

1
2
# ellipsizeAt is defaulted to prevent really long strings from breaking layouts
ellipsizeAt: 35
module.exports = class WholeNumber extends Number

WholeNumber

For whole numbers (no decimal input allowed).

Contextual Components

module.exports = class ClickToEditForm extends Form

ClickToEditForm

This component is an extension of Form that initially presents an "Edit" button that the user can click to switch between display and input.

Datum children are initialized readonly, when the user clicks edit, the 'inputMode' context variable is set to 'edit' and all children are rerendered.

See react-datum Form component for more props. All Form properties are supported by ClickToEditForm

module.exports = class Collection extends ContextualData

Collection

This react-datum component provides datums with a Backbone collection and rerenders descendants on collection changes.

  • provides a collection context to all children
  • rerenders children on collection changes
  • adds SelectableCollection mixin to collection if it doesn't already have it
  • will optionally fetch the collection
  • can accept either a Collection class (which will be instantiated) or a collection instance variable to another collection or Collection component

Any descendant datum or react-datum based components like (ReactDatum.TileGrid)[TODO: add a link do doc page], or the (ReactDatum.SelectedModel)[TODO: add link to doc page] that accept a collection context or prop, can be wrapped in a ReactDatum.Collection component and will respond to changes in the collection.

Simple example:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Rd = ReactDatum;
 
<Rd.Collection collection={myBackboneCollection}>
  <Rd.SelectedModel placeholder="Wait for it...">
    <Rd.Text label="You selected: " attr="name"/>
  </Rd.SelectedModel>
</Rd>
 
var index = 0;
setInterval(function(){
  myBackboneCollection.selectModelByIndex(index);
  index += 1;
}, 5000)

The (example above)[TODO: add link to github.io example] when run, will initially display a placeholder, "Wait for it..." and then every five seconds select a new model. See (SelectedModel)[TODO: add link to doc page] for more information about selections.

Adds selection tracking to collection

In order for the components such as ReactDatum.Tilegrid and ReactDatum.SelectedModel to have a common understanding of what is selected, a small mixin called SelectableCollection is added to the collection, unless the collection has an instance variable hasSelectableCollectionMixin=true.

Once used with the ReactDatum.Collection component, your Backbone collection instance will be given the following methods:

  • getSelectedModels() returns array of currently selected models
  • selectModel(model, selected=true, options={}) selects the model. default options: {silent: false}
  • selectModelById(id, selected=true, options={}) see selectModel()
  • selectModelByIndex(index, selected=true, options={}) see selectModel()
  • selectAll(options={}) default options: {silent: false}
  • selectNone(options{}) default options: {silent: false}

A console warning will be issued if any of the methods above already exist on a collection without this.hasSelectableCollectionMixin==true.

If you use another mixin or service for this, it should be relatively easy to build a bridge. Just add the methods above to your collection and call the existing plugin, mixin or external methods.

Collection propTypes:

1
2
3
4
5
6
7
8
# can only accept collection via prop.  Collection component should
# not interfere with any parent Collection commponent or other that
# would provide a collection context, but it will become the provider
# of collection context for all of it's children
collection: React.PropTypes.oneOfType([
  React.PropTypes.instanceOf(Backbone.Collection)
  React.PropTypes.func
]).isRequired
module.exports = class CollectionStats extends React.Component

CollectionStats

  • provides information about the items in your collection.

    Requires either a 'collection' context or prop. And displays counts of items found, selected and viewing.

CollectionStats propTypes:

1
2
# a Backbone.Collection
collection: React.PropTypes.instanceOf(Backbone.Collection)

CollectionStats defaultProps:

1
itemDisplayName: "item"

CollectionStats contextTypes:

1
collection: React.PropTypes.instanceOf(Backbone.Collection)
module.exports = class ContextualData extends React.Component

ContextualData

This is an abstract base class for contextual data components like ReactDatum.Collection and ReactDatum.Model that provide a single contextual data element.

The ReactDatum.ContextualData base class also provides the listener to model or collection events and rendering of child components on changes.

You shouldn't need to use this class directly.

ContextualData propTypes:

1
2
fetch: React.PropTypes.bool
fetchOptions: React.PropTypes.object
module.exports = class Form extends React.Component

Form

This contextual component provides a form context to all of the datums within.

Adding the ReactDatum.Form component, you can easily convert a group of datums into an editable form with save and cancel buttons. By default, when children of a Form component all datums will render in their input presentation inputMode='edit'.

Screenshot from doc/examples/form/form.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var kittenCard = React.createClass({
  displayName:"KittenCard",
  render: function(){
    return (
      <div className='kitten-card'>
        <ReactDatum.Model model={kittenModel}>
          <h3>Adopt <ReactDatum.Text attr="name"/> Today!</h3>
          <ReactDatum.Form>
            <div><ReactDatum.LazyPhoto attr="imgUrl"/></div>
            <div><ReactDatum.Text attr="name" label="Name" setOnChange/> (<ReactDatum.Text attr="title"/>)</div>
            <label>Say something about <ReactDatum.Text attr="name" readonly/>: </label>
            <div><ReactDatum.Text attr="description" className="wide-input"/></div>
            <div><ReactDatum.Email attr="sponsorEmail" label="Adoption Sponsor" displayLink/></div>
            <label>Leave a Comment!</label>
            <div><ReactDatum.Text attr="comment" className="wide-input"/></div>
          </ReactDatum.Form>
        </ReactDatum.Model>
      </div>
    )
  }
})

The ReactDatum.Form component adds save and cancel buttons. When the user presses save, model.save() is called. All of the attributes were set() when the user entered new values If cancel is clicked, the model and subsequently, the form are reset back to the state of the last model.save().

By wrapping the datums in the ReactDatum.Form tag, they implicitedly recieve inputMode='edit' props that make them all render as inputs. Almost all. Some Datums, like ReactDatum.LazyPhoto, only have a display presentation, no update. If given an inputMode='edit' they will ignore, and continue showing their display (`inputMode='readonly'``) representation.

Form propTypes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# can also accept model instance as context var. prop has precendence
model: @modelOrObject()
 
#  no formMode like zform, but we have to support programatic readonly
#  see also ClickToEditForm component.   readonly should always take precendence
readonly: React.PropTypes.bool
 
# you can style a buttonPossiton: 'top' to float left or right in css.
buttonPosition: React.PropTypes.oneOf(['top', 'bottom', 'none'])
 
# specify className of form element
className: React.PropTypes.string
 
# on save success this method, if specified, will be called with the standard
# Backbone success callback arguments (model, response, options)
# If you don't specify a saveSuccessCallback, a small success message will be rerendered
# below the form button after successfully saving the form
saveSuccessCallback: React.PropTypes.func
 
# on save error this method, if specified, will be called with the standard
# Backbone error callback arguments (model, response, options)
# If you don't specify a saveErrorCallback, an error message will be rerendered
# below the form button after failing to save the form on user request
saveErrorCallback: React.PropTypes.func

Form defaultProps:

1
2
3
readonly: false
buttonPosition: 'bottom'
className: 'zform'

Form contextTypes:

1
2
3
### can also accept model instance as a prop.  prop has precendence ###
model: @modelOrObject()
inputMode: Datum.contextTypes.inputMode
module.exports = class Model extends ContextualData

Model

This react-datum component provides datums with a Backbone model and rerenders descendants on model changes.

Any datum component like ReactDatum.Text, will accept the Backbone model provided by this class as a context variable. Outside of a ReactDatum.Model* component, datums will not respond to model changes.

Model propTypes:

1
2
3
4
model: React.PropTypes.oneOfType([
  React.PropTypes.instanceOf(Backbone.Model)
  React.PropTypes.func
]).isRequired
module.exports = class SelectedModel extends ContextualData

SelectedModel

This react-datum contextual component provides datums with a Backbone model that is selected model in the collection. It also rerenders descendants on collection selection changes.

Any descendant datum that accept a model context or prop, can be wrapped in a ReactDatum.SelectedModel component and will respond to changes in the model and on changes to the selected model in the collection.

Simple example:

1
2
3
4
5
6
7
8
9
10
11
var Rd = ReactDatum;
 
<Rd.SelectedModel collection={myBackboneCollection} placeholder="Wait for it...">
  <Rd.Text label="You selected: " attr="name"/>
</Rd.SelectedModel>
 
var index = 0;
setInterval(function(){
  myBackboneCollection.selectModelByIndex(index);
  index += 1;
}, 5000)

The (example above)[TODO: add link to github.io example] when run, will initially display a placeholder, "Wait for it..." and then every five seconds select the next model in the collection.

The placeholder prop will render in place of any children when there is no selected model in the collection and can be any React renderable.

SelectableCollection mixin

Use of the SelectedModel component will automatically add the ReactDatum.SelectableCollection to the collection instance unless collection.hasSelectableCollectionMixin is true. setModelByIndex(index) in the example above is one of the methods added by that mixin. See (ReactDatum.SelectableCollection)[TODO: add doc link]

SelectedModel contextTypes:

1
collection: React.PropTypes.instanceOf(Backbone.Collection)