# Adding Custom Types When you map a field between a REST API response and ng-admin, you give it a type. This type determines how the data is displayed and edited. It is very easy to customize existing ng-admin types and add new ones. ## Understanding Types A ng-admin *type* has two components: - a Field class, used to configure the field - a FieldView class, used for rendering Let's see that through an example: the `number` type. When you define a field with `nga.field()`, the second argument is the field type ('string' by default). `nga.field()` is a factory method returning a specialized instance of [the `Field` class](https://github.com/marmelab/admin-config/blob/master/lib/Field/Field.js). For instance: ```js product.listView().fields([ nga.field('price', 'number') .format('$0.00') ]); ``` The call to `nga.field('price', 'number')` translates to `new NumberField('price')`. The [`NumberField` class source](https://github.com/marmelab/admin-config/blob/master/lib/Field/NumberField.js) is: ```js import Field from "./Field"; class NumberField extends Field { constructor(name) { super(name); this._type = "number"; this._format = undefined; } /** * Specify format pattern for number to string conversion. */ format(value) { if (!arguments.length) return this._format; this._format = value; return this; } } export default NumberField; ``` Yep, this is ES6 syntax, but you get the idea. The sole purpose of a `Field` instance is to store configuration - not data (ng-admin stores data in another object, called the `DataStore`). Here, the `NumberField` can store a rendering format in addition to normal `Field` capabilities. Once it has fetched data from REST endpoints, ng-admin displays it on screen. What will it be: an image, a text input, an elaborate date widget? This depends on the type, of course, but also on the view. The rendering rules for each type are contained in `FieldView` objects. For instance, the [`NumberFieldView` source](https://github.com/marmelab/ng-admin/blob/master/src/javascripts/ng-admin/Crud/fieldView/NumberFieldView.js) is: ```js module.exports = { // displayed in listView and showView getReadWidget: () => '', // displayed in listView and showView when isDetailLink is true getLinkWidget: () => '' + module.exports.getReadWidget() + '', // displayed in the filter form in the listView getFilterWidget: () => '', // displayed in editionView and creationView getWriteWidget: () => '' }; ``` What this means is that, to render a `NumberField` in a `listView`, ng-admin will use the related `ReadWidget`, which is: ``` ``` The `` is a directive defined by ng-admin, but what it essentially does is the following: ``` {{ value() | numeraljs:field().format() }} ``` So a field of type 'number' renders in a `listView` as a formatted number string (e.g. `$45.99`). One thing that may sound curious is that the configuration logic (`Field` subclasses) is defined in the `admin-config` module, while the rendering logic (`FieldView` subclasses) is defined in the `ng-admin` module. Don't let it confuse you. This is just because the configuration logic can be reused by another renderer not based on Angular.js (e.g. [react-admin](https://github.com/marmelab/react-admin)). One last thing to understand is that ng-admin uses the field type *name* (e.g. 'number') to relate a Field subclass with a FieldView subclass. For type 'number', the [registered Field subclass is `NumberField`](https://github.com/marmelab/admin-config/blob/master/lib/Factory.js#L116), and [the registered FieldView subclass is `NumberFieldView`](https://github.com/marmelab/ng-admin/blob/master/src/javascripts/ng-admin/Crud/config/factories.js#L11). You'll see shortly how to register your own classes. ## Writing a Custom Field Class Let's write an `AmountField` to manage not only numbers, but amounts. An amount, in addition to a number, has a currency. Create the following `AmountType.js` class in your project tree: ```js import NumberField from 'admin-config/lib/Field/NumberField'; class AmountField extends NumberField { constructor(name) { super(name); this._type = 'amount'; this._currency = '$'; } currency(currency) { if (!arguments.length) return this._currency; this._currency = currency; return this; } } export default AmountField; ``` Compared to a `NumberField`, this adds a `.currency()` method, which is both a getter and a setter. The `AmountField` code is ES6, so you'll need to transpile it to JavaScript to make it executable by a web browser. The solution depends on your build tool ; here is the configuration for [Webpack](http://webpack.github.io/) and [babel](https://babeljs.io/). ```js // in webpack.config.js module.exports = { // ... module: { loaders: [ { test: /\.js/, loaders: ['babel'], exclude: /node_modules/ }, ] } }; ``` The `AmountField` class depends on the `NumberField` class from the `admin-config` module. You'll have to install it. ```sh npm install admin-config --save-dev ``` This module is also written in ES6, so update the `exclude` pattern in `webpack.config.js` to let Webpack transpile all "*.js" file, except the ones under `node_modules`, but including `node_modules/admin-config`: ```js // in webpack.config.js module.exports = { // ... module: { loaders: [ { test: /\.js/, loaders: ['babel'], exclude: /node_modules\/(?!admin-config)/ } ] } }; ``` You need to *register* this new field type in your application: ```js myApp.config(['NgAdminConfigurationProvider', function(nga) { nga.registerFieldType('amount', require('path/to/AmountField')) }]); ``` Now you can use the new type in your admin configuration: ```js product.listView().fields([ nga.field('price', 'amount') .format('0.00') .currency('$') ]); ``` ## Defining The Rendering Logic For a Type An `amount` field will render as text with the currency in read context, and as an input with a lateral addon in write context. Create the following `AmountFieldView` to define the rendering logic: ```js export default { getReadWidget: () => '{{ field.currency() }}', getLinkWidget: () => '' + module.exports.getReadWidget() + '', getFilterWidget: () => '', getWriteWidget: () => '
{{ field.currency() }}
' }; ``` You also need to *register* this new field view type in your application for ng-admin to find it. Beware that it's a different configuration provider this time: ```js myApp.config(['FieldViewConfigurationProvider', function(fvp) { fvp.registerFieldView('amount', require('path/to/AmountFieldView')) }]); ``` ## Using Custom Directives Optionally, you can write a custom directive, and use that directive in one of the widget definitions. ```js function amountStringDirective() { return { restrict: 'E', scope: { value: '&', field: '&' }, template: '{{ field().currency() }}{{ value() | numeraljs:field().format() }}' }; } export default amountStringDirective; ``` ```js myApp.directive('amountString', require('path/to/amountStringDirective.js')); ``` ```js // in AmountFieldView.js export default { getReadWidget: () => '', // ... }; ``` ## Fetching data Custom directives can fetch data, too. For instance, the `` directive can fetch the conversion rate to another currency, and display the amount in more than one currency. To fetch a remote API, use either the `Restangular` service, or `$http` if you prefer. ```js function amountStringDirective($http) { return { restrict: 'E', scope: { value: '&', field: '&' }, link: function(scope) { scope.fromCurrency = field().currency(); scope.conversionRate = 1; scope.format = field().format(); $http.get(`http://myconverter.example.com?fromCurrency=${scope.fromCurrency}&toCurrency=USD`) .then(response => { scope.conversionRate = response.value; }); }, template: `{{ fromCurrency }} {{ value() | numeraljs:format }} ( USD {{ value() * conversionRate | numeraljs:format }})` }; } amountStringDirective.$inject = ['$http']; export default amountStringDirective; ``` ## Fetching Data Before Rendering Fetching data from the `link()` function of a directive has drawbacks: - it triggers redraws of the page when the remote response arrives - in a `listView`, a custom directive will make a lot of queries (one per entry) The alternative is to prepare the data before the page starts rendering, to store it in the `datastore`, and to pass the `datastore` to the custom directive. Fortunately, every view has a `prepare()` method, which expects a function as parameter. This function is executed before the view is rendered. It's the ideal place to group fetches to a remote API, and store the results in the `datastore`. The `prepare()` function uses the Angular Dependency Injection system, so you can require any service defined previously in the controller logic - like the current DataStore instance (already filled with all the entries required by the view). ```js product.listView().prepare(['$http', 'datastore', 'view', function($http, datastore, view) { const fromCurrency = view.getField('price').currency(); return $http.get(`http://myconverter.example.com?fromCurrency=${fromCurrency}&toCurrency=USD`) .then(response => { datastore.addEntry('conversionRate', response.value) }); }]) ``` The directives doesn't need `$http` anymore, but can use the `datastore` instead: ```js function amountStringDirective($http) { return { restrict: 'E', scope: { value: '&', field: '&', datastore: '&' }, link: function(scope) { scope.fromCurrency = field().currency(); scope.conversionRate = 1; scope.format = field().format(); scope.convertedValue = datastore.getFirstEntry('conversionRate') * scope.value(); }, template: `{{ fromCurrency }} {{ value() | numeraljs:format }} ( USD {{ convertedValue | numeraljs:format }})` }; } export default amountStringDirective; ``` The `datastore` just needs to be declared in the `fieldView`: ```js // in AmountFieldView.js export default { getReadWidget: () => '', // ... }; ``` ## Overriding Existing Types Just like you can add new types, it's very easy to override existing types. For instance, if you want all `number` fields to use a directive of yours called ``, create a `CustomNumberFieldView` script as follows: ```js module.exports = { getReadWidget: () => '', // ... }; ``` Then, register this field view for the `number` type: ```js myApp.config(['FieldViewConfigurationProvider', function(fvp) { fvp.registerFieldView('number', require('path/to/CustomNumberFieldView')) }]); ``` ## Conclusion Once you get passed the installation step, defining new types is straightforward. It's also a very powerful way to extend ng-admin to your custom needs. Use it as much as possible!