Elm integration for the Ag Grid data grid library.
For available configurations of the grid, the columns, and the usage of the grid reference to the documentation of GridConfig
, ColumnSettings
, and grid
in the AgGrid
module.
An example can be found in the examples/
folder.
Elm component: elm install mercurymedia/elm-ag-grid
It's also required to make the webcomponent from the package available to your project. This can be done by installing the package from NPM.
npm i @mercurymedia/elm-ag-grid
And then importing it into the Javascript pipeline.
import ElmAgGrid from "@mercurymedia/elm-ag-grid";
import { Elm } from "./src/Main.elm";
new ElmAgGrid();
Elm.Main.init({ node: document.getElementById("app") });
For most features of AG Grid it is necessary to install modules and register them:
import { ModuleRegistry } from '@ag-grid-community/core'
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'
ModuleRegistry.registerModules([
ClientSideRowModelModule
]);
AG Grid will print warnings into the browser console if modules are missing. For a full list of modules, see https://www.ag-grid.com/javascript-data-grid/modules/.
The latest Elm package version always works well with the latest NPM package version. Otherwise, please keep the NPM version in the accepted range for the used version of the Elm package to reduce the possibility of errors.
Elm Version | Npm Version |
---|---|
1.0.0 | 1.0.0 <= v < 1.1.0 |
2.0.0 - 3.0.0 | 1.1.0 <= v < 1.3.0 |
3.1.0 - 4.0.0 | 1.3.0 |
5.0.0 - 6.0.0 | 2.1.0 |
7.0.0 - 7.0.1 | 3.0.0 |
8.0.0 - 9.1.1 | 3.1.0 |
10.0.0 - 11.0.0 | 3.3.0 |
12.0.0 | 3.3.1 |
13.0.0 | 3.3.2 |
14.0.0 - 18.0.0 | 3.4.0 - 3.4.2 |
19.0.0 - 22.0.0 | 3.5.0 |
23.0.0 - 23.1.0 | 3.6.0 |
24.0.0 - 27.0.2 | 3.7.0 - 4.0.2 |
28.0.0 - * | 3.7.0 - 4.1.1 |
The elm-ag-grid
package uses Ag Grid Enterprise features. To enable them install the @ag-grid-enterprise/core
package and activate it by setting the license key. See the official Ag Grid documentation for further details.
import { LicenseManager } from '@ag-grid-enterprise/core'
LicenseManager.setLicenseKey("YOUR LICENSE KEY");
To use a theme, add the theme class name to the config
argument for your grid. For example when using the "balham" theme:
viewGrid : Model -> Html Msg
viewGrid model =
let
gridConfig =
AgGrid.defaultGridConfig
|> (\config -> { config | themeClasses = Just "ag-theme-balham ag-basic" })
in
AgGrid.grid gridConfig [] [] []
You just need to make sure that the classes are available to you. For example, you can import pre-built bundles from the ag-grid-community
package.
import "ag-grid-community/styles/ag-theme-balham.css";
To support browsers that don't have built-in support for the Web Components API, you can install a Web Component Polyfill (for example: @webcomponents/custom-elements) to simulate the missing browser capabilities. This just needs to be loaded before the Ag-Grid-Webcomponent.
import "@webcomponents/custom-elements";
import "@mercurymedia/elm-ag-grid";
A custom view for the cell (such as a button or link) can be defined with a separate Elm application which gets rendered into the cell. The application must therefore be defined as Browser.element
.
FYI, there are two components in the example application used that you might find very useful.
To reference the component application and use it for the table, you must register the component to the package. This can be done by configuring the ElmAgGrid class when instantiating and providing an object with your components via the apps
config. The key is the name used in your main application to refer to the component and the value is the object containing the init
function to initialize the Elm application.
You can either just use the usual Elm object for this:
import { Elm } from "./src/Components/Link.elm";
new ElmAgGrid({
apps: {
linkRenderer: Elm.Components.Link,
},
});
Or define your own object with an init
function if you want to use ports for communication between the applications.
Note: In this case, make sure to return the initialized application to be able to access the ports from the cellRenderer.
import { Elm } from "./src/Components/Button.elm";
new ElmAgGrid({
apps: {
buttonRenderer: {
init: function ({ node, flags }) {
let component = Elm.Components.Button.init({
node: node,
flags: flags,
});
component.ports.onButtonClick.subscribe(function (id) {
// This `app` is the main application
if (app) app.ports.buttonClicked.send(id);
});
return component;
},
},
},
});
The function is called with an object containing the keys node
and flags
. The flags are containing the usual AgGrid cellRendererParams and the defined componentParams
from your AppRenderer
definition.
To use a component for cell rendering, you can use the AppRenderer { componentName : String, componentParams : Maybe Json.Encode.Value } (dataType -> String)
renderer in your column definition.
{ field = "details"
, renderer = AppRenderer { componentName = "linkRenderer", componentParams = Nothing } .url
, headerName = "Details"
, settings = { defaultSettings | editable = Expression.Const False }
}
The componentName
is the reference to the registered component and the componentParams
can be defined to share information from the main application with the component application. The componentParams
do not depend on the row data and will always be the same for all rows.
The (dataType -> String)
part makes it possible to provide a cell value that can be used in your component application if needed.
It's also possible to encode complex data into a string that can then be decoded in the component application, if you want to share more information.
To respond to component updates while the component is already initialized, you can implement the componentRefresh
port in your component application, which will get the similar flags
value as when the application is initialized.
port componentRefresh : (Flags -> msg) -> Sub msg
While the default editor derives from the defined renderer
, it can also be overridden. Either by using another existing ag-grid editor (e.g. "agLargeTextCellEditor") or by using another Elm app to render the editor.
In order to use an Elm app, the component must be registered similar to the custom view renderers. After that, the component can be used as a customCellEditor
in your application.
{ field = "info"
, renderer = StringRenderer (.infos >> String.join ", ")
, headerName = "Info"
, settings = { defaultSettings | editable = Expression.Const True, customCellEditor = AgGrid.AppEditor { componentName = "editor", componentParams = Nothing } }
}
To notify ag-grid of the changed value for the cell, you need to set up a currentValue
port in your cell editor component. This needs to be called every time the cell value is changed.
port currentValue : String -> Cmd msg
Normally this can be any type - the renderer just has to be able to interpret it. So it's even possible to work with JSON in the cells as long as the editor understands it and the renderer can interpret it. In combination with a custom cell renderer, this could lead to fancy data representations.
The current column state is sent whenever a column changes.
Callbacks listened for column state updates:
onSortChanged
onColumnMoved
onGridColumnsChanged
onColumnResized
The changes can be retrieved and evaluated via the onColumnStateChanged
event. This returns the event that led to the change and a list of column states.
The column state list can be used to restore the column state in the table (e.g. when loaded from local storage).
type Msg = ColumnStateChanged { event : { type_ : String }, states : List ColumnState }
AgGrid.grid { gridConfig | columnState = model.columnState } [ onColumnStateChanged ColumnStateChanged ] [] []
Similar can be done for the filter state using the onFilterStateChanged
event.
type Msg = FilterStateChanged { event : { type_ : String }, states : Dict String FilterState }
AgGrid.grid { gridConfig | filterState = model.filterState } [ onFilterStateChanged FilterStateChanged ] [] []
The package allows defining valueGetter
, valueFormatter
, valueParser
, valueSetter
, and filterValueFormatter
as expressions for formatting cell values.
These make it possible to change the type of the cell value the table works with (e.g. strings parsed as numbers to allow aggregations - valueGetter), to format values displayed to the user in the table/filter (e.g. with thousand separators and currency symbol - valueFormatter/filterValueFormatter), and to parse user input before applying the value (e.g. parsing a formatted currency value back into a floating-point number string - valueParser).
For a detailed explanation of what the expressions can do, refer to the AgGrid documentation.
The package provides preconfigured value formatting for currencies, decimals, and percentages. Those are available as a Renderer
type and already configure a certain valueFormatter
, valueGetter
, filterValueFormatter
, and cellEditor
. If required, the formatting can be overwriten by setting a valueFormatter
/filterValueFormatter
yourself on the ColumnDef
. Also, these predefined expressions can be applied individually to other Renderer
types if necessary. These are available in the AgGrid.ValueFormat
module.
- CurrencyRenderer to format floating-point strings into localized currency strings (e.g. '1000.15' --> '1.000,15 €')
- DecimalRenderer to format floats into localized strings (e.g. 1000.8 --> '1.000,8')
- PercentRenderer to format floats into localized percent strings (e.g. 0.22 --> '22%')
By default, each column will use a filter
and a filterValueGetter
depending on the renderer. To override it, you can change the filter
and filterValueGetter
on the ColumnSettings
.
Requires AgGrid Enterprise
Custom Detail views can be defined similar to the custom cell views by defining the component on the GridConfig
.
{ gridConfig | detailRenderer = Just { componentName = "detailRenderer", componentParams = Nothing, rowHeight = Nothing } }
As with the custom cell views, the componentName
refers to the component and the componentParams
can be used to pass information from the main application to the detail application (e.g. auth tokens). The AgGrid default detail height can be overridden by specifying a new fixed rowHeight
value, which applies equally to all detail views.
To see the actual MasterDetail view the GroupRenderer
can be used on a column to group the row.
{ renderer = AgGrid.GroupRenderer (.id >> String.fromInt), ... }
Requires AgGrid Enterprise
Aggregation for a column can be defined by setting the aggFunc
setting on the ColumnSettings
. This may require that the cell's value be specified in the correct data type.
Not all aggregation functions may work with string values. Using a valueGetter
expression can help with this.
{ settings = { defaultSettings | aggFunc = AvgAggregation, valueGetter = Just "Number(data.cost)" }}
Similar to Register Component, custom aggregation functions can be defined by passing the custom configuration during the instantiation to the ElmAgGrid class. The key is the name of the aggregation function and the value is the implementation accepting the ag-grid params and returning the aggregated value.
new ElmAgGrid({
aggregations: {
foo: function (params) {
return "bar";
},
},
});
This aggregation can be referenced in Elm by using the CustomAggregation
type when defining a aggFunc
.
{ ..., settings = { defaultSettings | aggFunc = CustomAggregation "foo" }}
Requires AgGrid Enterprise
Context menu actions can be defined in Elm in the gridConfig
by using the AgGrid.ContextMenu
module. The predefined context menu actions are covered within that module.
gridConfig =
{ defaultGridConfig
| contextMenu =
Just
[ AgGrid.ContextMenu.autoSizeAllContextAction ]
}
To create a custom context action we make use of events to handle the action in elm.
gridConfig =
{ defaultGridConfig
| contextMenu =
Just
[ AgGrid.ContextMenu.contextAction
{ defaultActionAttributes
| name = "Increase counter"
, actionName = Just "incrementCounter"
}
]
}
-- The String contains the clicked action name
type Msg =
ContextMenuAction (Result DecodeError Data, String)
view =
AgGrid.grid gridConfig
[ AgGrid.onContextMenu dataDecoder ContextMenuAction ]
columns
data
The disabled
attribute uses the AgGrid.Expression
module, to serialize expressions in a save way. This makes sure that we prevent XSS atacks.
import AgGrid.Expression as Expression
...
AgGrid.ContextMenu.contextAction
{ defaultActionAttributes
| name = "Increase counter"
, action = Just "incrementCounter"
, disabled = Expression.Expr (Expression.lte (Expression.value "id") (Expression.int 10))
}
To run the examples in the browser clone the repo and run:
$ npm start
Open you browser at localhost:1234