@olenbetong/data-binding

React components that bind to Appframe data objects using context, similar to the Appframe data binding library

Usage no npm install needed!

<script type="module">
  import olenbetongDataBinding from 'https://cdn.skypack.dev/@olenbetong/data-binding';
</script>

README

data-binding

Components with bindings to data objects. These components, combined with the hooks from @olenbetong/react-data-object-connect, JSX can be written with a similar structure to Appframe data binding.

Getting started

Installation

The library is published to NPM. To use, install from the command line

npm i @olenbetong/data-binding

Or use the IIFE build from unpkg.com

<script
  crossorigin
  src="https://unpkg.com/@olenbetong/data-binding@latest/dist/iife/data-binding.min.js"
  type="text/javascript"
></script>

Context

Use the DataObjectProvider to provide data object context to components in the child tree.

This is the equivalent of setting data-object-id on an element in Appframe data binding.

import {
  DataObjectProvider,
  BoundInput
} from "/file/component/modules/esm/data-binding.min.js";

function MyComponent(props) {
  return (
    <DataObjectProvider dataObject={dsMyDataObject}>
      <BoundInput type="text" field="MyField" />
    </DataObjectProvider>
  );
}

Components

BoundInput

Renders an input with a 2-way binding to a field in the data object passed by data object context.

import {
  DataObjectProvider,
  BoundInput
} from "/file/component/modules/esm/data-binding.min.js";

function MyComponent(props) {
  return (
    <DataObjectProvider value={dsMyDataObject}>
      <BoundInput type="text" field="MyField" />
    </DataObjectProvider>
  );
}

BoundSelect

Loads data from a data object with an optional filter, and displays a select element with the data. Doesn't use the data object directly, but uses a data handler. This means the select is independent from the data object state.

There are two ways to set the options from the data object:

  1. Pass a function to the option property. This should take the record as the first argument and return a React node (
  2. Set the valueField to the data object field that represents the value. Optionally set the descriptionField to another field that should be shown in the dropdown.

If the dataObject property isn't set, the select will still be bound to the current data object context, but children have to be set manually. It is also possible to add options manually in addition to data object options.

Other properties:

  • dataObject - Data object used to list the options
  • filter - Filter passed to the data object
  • field - Field to bind the value to
  • nullable - If true, will add an empty option (default: true)

The components in this example will create the same select using the 2 different methods above:

import { DataObjectProvider, BoundSelect } from "@olenbetong/data-binding";

function MyComponentWithOptionFunc() {
  return (
    <BoundSelect
      dataObject={dsSomeDataObject}
      filter="IsAGoodRecord = 1"
      option={record => <option value={record.Value}>{record.Name}</option>}
    />
  );
}

function MyComponentWithFields() {
  return (
    <BoundSelect
      dataObject={dsSomeDataObject}
      filter="IsAGoodRecord = 1"
      valueField="Value"
      descriptionField="Name"
    />
  );
}

function MyComponentWithUnboundOptions() {
  return (
    <BoundSelect field="SomeEnumerableField">
      <option value="value1">Value 1</option>
      <option value="value2">Value 2</option>
      <option value="value3">Value 3</option>
    </BoundSelect>
  );
}

function MyComponentWithCombinedOptions() {
  return (
    <BoundSelect
      field="SomeEnumerableField"
      dataObject={dsSomeDataObject}
      filter="IsAGoodRecord = 1"
      valueField="Value"
      descriptionField="Name"
    >
      <option value="value1">Value 1</option>
      <option value="value2">Value 2</option>
      <option value="value3">Value 3</option>
    </BoundSelect>
  );
}

SaveButton

Calls endEdit on the data object passed by data object context.

Unlike save buttons in Appframe data binding, the button is not disabled if the record isn't dirty. This can be achieved using the useDirty hook from @olenbetong/react-data-object-connect. This also applies to the CancelButton.

import { DataObjectProvider, SaveButton } from '/file/component/modules/esm/data-binding.min.js';

function MyOuterComponent(props) {
  return (
    <DataObjectProvider value={dsMyDataObject}>
      <MySaveButton />
    </DataObjectProvider>
  )
}

function MyComponent(props) {
  const dataObject = useContext(DataObjectProvider);
  const isDirty = useDirty(dataObject);

  return (
    <SaveButton disabled={!isDirty} className='btn btn-secondary'>
      <i className='fa fa-save mr-2'/>
      Save changes
    </SaveButton>
  )

CancelButton

Calls cancelEdit on the data object passed by data object context.

import { DataObjectProvider, CancelButton } from '/file/component/modules/esm/data-binding.min.js';

function MyComponent(props) {
  return (
    <DataObjectProvider value={dsMyDataObject}>
      <CancelButton className='btn btn-secondary'>
        Cancel changes
      </CancelButton>
    </DataObjectProvider>
  )

DeleteButton

Deletes the current row of the data object passed by data object context. Takes a boolean confirm property to prompt the user to confirm the delete. The confirm prompt message can be customized with the prompt property.

The prompt property can also be a function. When the button is clicked, the function will be called with the props passed to the delete button as the first argument. If the function returns true (not other trueish values), the record will be deleted. Alternatively, a promise can be returned. The record will be deleted if the promise resolves with true.

The delete button also accepts an index property to be able to use it outside of the current row. E.g. when listing the records.

import {
  DataObjectProvider,
  DeleteButton
} from "/file/component/modules/esm/data-binding.min.js";

function MyComponent(props) {
  return (
    <DataObjectProvider value={dsMyDataObject}>
      <DeleteButton
        className="btn btn-danger"
        confirm
        prompt="Nooooo! Please dont delete 😿"
      >
        <i className="fa fa-trash mr-2" />
        Delete
      </DeleteButton>
    </DataObjectProvider>
  );
}

function MyListComponent(props) {
  const data = useData(dsMyDataObject);

  return (
    <DataObjectProvider value={dsMyDataObject}>
      {data.map((record, idx) => (
        <DeleteButton
          className="btn btn-danger"
          confirm
          prompt={props =>
            new Promise(resolve => {
              if (checkIfRecordShouldBeDeleted(props.index)) {
                resolve(true);
              } else {
                resolve(false);
              }
            })
          }
          index={idx}
        >
          <i className="fa fa-trash mr-2" />
          Delete
        </DeleteButton>
      ))}
    </DataObjectProvider>
  );
}

Timepicker

Binds a time input to a date field. If the field doesn't have a value when the time is selected, the current date will be used.

import { Timepicker } from "@olenbetong/data-binding";

function MyComponent(props) {
  return (
    <DataObjectProvider value={dsMyDataObject}>
      <Timepicker field="MyDateField" className="form-control" />
    </DataObjectProvider>
  );
}

bind

For custom functionality there is a small HoC available to create a bound component.

If the current value should be assigned to a property other than "value", you can pass a valueProp option to the HoC. If you need to edit the value before it is put into the data object, you may bass a valueConverter function option. The argument passed to the onChange handler will be passed to the valueConverter.

import { bind } from "@olenbetong/data-binding";

function MyComponent(props) {
  return (
    <SomeComponent
      something={props.customValueProperty}
      onChange={props.onChange}
    />
  );
}

function addOneToValue(value) {
  return value + 1;
}

export default bind(MyComponent, {
  valueProp: "customValueProperty",
  valueConverter: addOneToValue
});