Dynamic Forms FormArray angular 2 - forms

I followed the Todd Motto article about dynamic form with angular 2 (https://toddmotto.com/angular-dynamic-components-forms).
Every thing works perfectly.
But I have a project, project for pleasure, where I try to pass formatted array to config, this is the data :
travel = [
{
type: 'input',
label: 'From',
placeholder: 'From',
name: 'from',
},
{
type: 'input',
label: 'To',
placeholder: 'To',
name: 'to'
}
];
travellers = [
{
type: 'input',
label: 'Name',
placeholder: 'name',
name: 'name',
}
]
config = {
travel: [
{
...this.travel
},
{
...this.travel
}
],
travellers: [
{
...this.travellers
}
]
};
Here the call of dynamic-form component :
<dynamic-form [config]="config" (submitted)="formSubmitted($event)"></dynamic-form>
This is the dynamic-form component :
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import { FormGroup, FormArray, FormControl, FormBuilder } from '#angular/forms';
import * as _ from 'lodash';
#Component({
selector: 'builder-form',
templateUrl: './builder-form.component.html',
styleUrls: ['./builder-form.component.css']
})
export class BuilderFormComponent implements OnInit {
#Input()
config: any[] = [];
#Output()
submitted: EventEmitter<any> = new EventEmitter<any>();
form: FormGroup;
objectKeys = Object.keys;
constructor (private fb: FormBuilder) { }
ngOnInit() {
this.form = this.createGroup();
}
createGroup () {
const group = this.fb.group({});
let groupArray = Object.keys(this.config);
let control;
groupArray.forEach((value, i) => {
group.addControl(value, this.fb.array([]));
control = group.controls[value] as FormArray;
_.map(this.config[value], (val, key) => {
// object is the travel { from, to } and the traveller { name }
let object = {}
_.map(val, (v, k) => {
Object.assign(object, {[v.name]: null})
});
control.push(this.fb.group(object, this.fb.control(null)));
});
});
return group;
}
}
And I get this :
FormGroup {
...
controls {
travel: FormArray {
...
controls: [
0: FormGroup {
controls: { form: FormControl, to: FormControl }
},
1: FormGroup {
controls: { form: FormControl, to: FormControl }
}
]
...
},
travellers: FormArray {
...
controls: [
0: FormGroup {
controls: { name: FormControl }
}
]
...
}
}
...
}
That seems to be good.
But I don't know why I can't access to the controls with form.controls.travel.controls or form.controls['travel'].controlsI always get the error : Property 'controls' does not exist on type 'AbstractControl'
In HTML :
<form class="dynamic-form" [formGroup]="form" (ngSubmit)="submitted.emit(form.value)">
<ng-container *ngFor="let array of objectKeys(config)">
<div [formArrayName]="array">
<ng-container *ngFor="let field of config[array]; let i = index" [formGroupName]="i">
<ng-container dynamicField [config]="field" [group]="form.controls[array].controls[i]"></ng-container>
</ng-container>
</div>
</ng-container>
</form>
But that doesn't work...
{{ form.value }} return the correct object :
{
"travel": [
{
"from": null,
"to": null
},
{
"from": null,
"to": null
}
],
"travellers": [
{
"name": null
}
]
}
Do you how to make it works ?
UPDATE
Maybe that the mistake is here :
import { Directive, Input, ComponentFactoryResolver, OnInit, ViewContainerRef } from '#angular/core';
import { FormGroup } from '#angular/forms';
import { FormButtonComponent, FormInputComponent, FormSelectComponent, FormResetComponent, FormDateComponent, FormNumberComponent } from '../components';
const components = {
button: FormButtonComponent,
input: FormInputComponent
};
#Directive({
selector: '[dynamicField]'
})
export class DynamicFieldDirective implements OnInit {
#Input()
config;
#Input()
group: FormGroup;
component;
constructor (
private resolver: ComponentFactoryResolver,
private container: ViewContainerRef
) {}
ngOnInit () {
const component = components[this.config.type];
const factory = this.resolver.resolveComponentFactory<any>(component);
this.component = this.container.createComponent(factory);
this.component.instance.config = this.config;
this.component.instance.group = this.group;
}
}
UPDATE 2
BuilderFormComponent.html:12 ERROR Error: No component factory found for undefined. Did you add it to #NgModule.entryComponents?
at noComponentFactoryError (core.es5.js:3202)
at CodegenComponentFactoryResolver.webpackJsonp.../../../core/#angular/core.es5.js.CodegenComponentFactoryResolver.resolveComponentFactory (core.es5.js:3267)
at BuilderFieldDirective.webpackJsonp.../../../../../src/app/builder-form/directives/builder-field.directive.ts.BuilderFieldDirective.ngOnInit (builder-field.directive.ts:35)
at checkAndUpdateDirectiveInline (core.es5.js:10856)
at checkAndUpdateNodeInline (core.es5.js:12357)
at checkAndUpdateNode (core.es5.js:12296)
at debugCheckAndUpdateNode (core.es5.js:13160)
at debugCheckDirectivesFn (core.es5.js:13101)
at Object.eval [as updateDirectives] (BuilderFormComponent.html:13)
at Object.debugUpdateDirectives [as updateDirectives] (core.es5.js:13086)
View_BuilderFormComponent_2 # BuilderFormComponent.html:12
webpackJsonp.../../../core/#angular/core.es5.js.DebugContext_.logError # core.es5.js:13426
webpackJsonp.../../../core/#angular/core.es5.js.ErrorHandler.handleError # core.es5.js:1080
(anonymous) # core.es5.js:4819
webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke # zone.js:392
webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run # zone.js:142
webpackJsonp.../../../core/#angular/core.es5.js.NgZone.runOutsideAngular # core.es5.js:3844
webpackJsonp.../../../core/#angular/core.es5.js.ApplicationRef_.tick # core.es5.js:4819
webpackJsonp.../../../core/#angular/core.es5.js.ApplicationRef_._loadComponent # core.es5.js:4787
webpackJsonp.../../../core/#angular/core.es5.js.ApplicationRef_.bootstrap # core.es5.js:4775
(anonymous) # core.es5.js:4546
webpackJsonp.../../../core/#angular/core.es5.js.PlatformRef_._moduleDoBootstrap # core.es5.js:4546
(anonymous) # core.es5.js:4508
webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke # zone.js:392
onInvoke # core.es5.js:3890
webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke # zone.js:391
webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run # zone.js:142
(anonymous) # zone.js:844
webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask # zone.js:425
onInvokeTask # core.es5.js:3881
webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask # zone.js:424
webpackJsonp.../../../../zone.js/dist/zone.js.Zone.runTask # zone.js:192
drainMicroTaskQueue # zone.js:602
Promise resolved (async)
scheduleMicroTask # zone.js:585
webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.scheduleTask # zone.js:414
webpackJsonp.../../../../zone.js/dist/zone.js.Zone.scheduleTask # zone.js:236
webpackJsonp.../../../../zone.js/dist/zone.js.Zone.scheduleMicroTask # zone.js:256
scheduleResolveOrReject # zone.js:842
ZoneAwarePromise.then # zone.js:932
webpackJsonp.../../../core/#angular/core.es5.js.PlatformRef_._bootstrapModuleWithZone # core.es5.js:4537
webpackJsonp.../../../core/#angular/core.es5.js.PlatformRef_.bootstrapModule # core.es5.js:4522
../../../../../src/main.ts # main.ts:11
__webpack_require__ # bootstrap 92732b2f740421148d04:54
0 # main.bundle.js:1187
__webpack_require__ # bootstrap 92732b2f740421148d04:54
webpackJsonpCallback # bootstrap 92732b2f740421148d04:25
(anonymous) # main.bundle.js:1
BuilderFormComponent.html:12 ERROR CONTEXT DebugContext_ {view: {…}, nodeIndex: 1, nodeDef: {…}, elDef: {…}, elView: {…}}

#yurzui Ok my bad, I've needed to loop on config in directive ... OMG I lost so much time with it !! Thank you for your help, it helped me to understand the mistake.

Related

RouterConfiguration and Router undefined in aurelia

I am very new to Aurelia and just trying to apply navigation to my project.Though i import aurelia-router still it says RouterConfiguration and Router are undefined in constructor
import {Todo} from './ToDo/todo';
import {RouterConfiguration, Router} from 'aurelia-router';
export class App {
heading = "Todos";
todos: Todo[] = [];
todoDescription = '';
router :any;
list: any[];
constructor(RouterConfiguration: RouterConfiguration, Router: Router) {
this.todos = [];
this.configureRouter(RouterConfiguration, Router);
//console.log("klist", this.list);
}
//config.map() adds route(s) to the router. Although only route, name,
//moduleId, href and nav are shown above there are other properties that can be included in a route.
//The class name for each route is
configureRouter(config: RouterConfiguration, router: Router): void {
this.router = router;
config.title = 'Aurelia';
config.map([
{ route: '', name: 'home', moduleId: 'home/home', nav: true, title: 'Home' },
{ route: 'users', name: 'users', moduleId: './Friends/Friends', nav: true },
//{ route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail' },
//{ route: 'files/*path', name: 'files', moduleId: 'files/index', href: '#files', nav: 0 }
]);
}
addTodo() {
if (this.todoDescription) {
this.todos.push(new Todo(this.todoDescription));
// this.todoDescription = '';
}
}
}
By convention, Aurelia looks in the initial class that loads (App) for the configureRouter() function and executes it. This means, you do not have to inject anything in the constructor.
It looks like you've simply added too much. I think fixing your sample seems to be as easy as removing some stuff, like so:
import { Todo } from './ToDo/todo';
import { RouterConfiguration, Router } from 'aurelia-router';
export class App {
heading = "Todos";
todos: Todo[] = [];
todoDescription = '';
list: any[];
constructor() {
// note: removed routing here entirely (you don't need it)
// also, you've already declared this.todos above, so no need to do it here again
}
configureRouter(config : RouterConfiguration, router : Router): void {
this.router = router;
config.title = 'Aurelia';
config.map([
{ route: '', name: 'home', moduleId: 'home/home', nav: true, title: 'Home' },
{ route: 'users', name: 'users', moduleId: './Friends/Friends', nav: true }
]);
}
addTodo() {
// removed this for brevity
}
}
This should resolve your 'undefined' errors on Router and RouteConfiguration. As an additional note, don't forget to add the <router-view> to your html template as well. Otherwise, you'll get no errors but the views won't show up either:
<template>
<div class="content">
<router-view></router-view>
</div>
</template>
Great documentation on this can be found at the Aurelia Docs - Routing.

Angular2 - access the AbstractControl instance in validation directive

I have to trigger validation from the inside of a validator directive.
Here is the directive I have. It works as expected. However I want it to trigger the validation process when the validator function changes. I.e. when its input variable maxDate changes.
How could I do this ?
If I could access the AbstractControl instance in the constructor I could easily do this. I can't think of a way to do it, however.
import { AbstractControl, FormGroup, ValidatorFn, Validator, NG_VALIDATORS, Validators } from '#angular/forms';
import { Directive, Input, OnChanges, SimpleChanges, EventEmitter } from '#angular/core';
function parseDate(date: string):any {
var pattern = /(\d{2})-(\d{2})-(\d{4})/;
if (date) {
var replaced = date.search(pattern) >= 0;
return replaced ? new Date(date.replace(pattern,'$3-$1-$2')) : null;
}
return date;
}
export function maxDateValidator(maxDateObj): ValidatorFn {
return (control:AbstractControl): {[key: string]: any} => {
const val = control.value;
let date = parseDate(val);
let maxDate = parseDate(maxDateObj.max);
if (date && maxDate && date > maxDate) {
return {
maxDateExceeded: true
};
}
return null;
};
}
...
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
//This is where I want to trigger the validation again.
}
validate(control): {[key: string]: any} {
return this.valFn(control);
}
}
Usage:
<input type="text" [(ngModel)]="deathDateVal">
<input class="form-control"
type="text"
tabindex="1"
[maxDate]="deathDateVal"
name="will_date"
[textMask]="{pipe: datePipe, mask: dateMask, keepCharPositions: true}"
ngModel
#willDate="ngModel">
Here is what I've just come up with:
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
private control:AbstractControl;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
if (this.control) {
this.control.updateValueAndValidity(this.control);
}
}
validate(_control:AbstractControl): {[key: string]: any} {
this.control = _control;
return this.valFn(_control);
}
}
It works. Validate is called on initialization so I just store its parameter.
It is fuckin' ugly but it works.
To get your hands on the abstractControl of the input you can do something like this:
#Directive({
// tslint:disable-next-line:directive-selector
selector: 'input[type=date][maxDate]'
})
export class InputFullWithDirective implements Validator, OnChanges {
constructor(#Self() private control: NgControl) {}
/** the rest is mostly unchanged from the question */
}

How to use a checkbox for a boolean data with ag-grid

I have searched for awhile now and haven't seen any real example of this.
I am using ag-grid-react and I would like for a column that holds a boolean to represent that boolean with a checkbox and update the object in the rowData when changed.
I know there is checkboxSelection and I tried using it like what I have below, but realized while it's a checkbox, it's not linked to the data and is merely for selecting a cell.
var columnDefs = [
{ headerName: 'Refunded', field: 'refunded', checkboxSelection: true,}
]
So is there a way to do what I am looking for with ag-grid and ag-grid-react?
You should use the cellRenderer property
const columnDefs = [{ headerName: 'Refunded',
field: 'refunded',
editable:true,
cellRenderer: params => {
return `<input type='checkbox' ${params.value ? 'checked' : ''} />`;
}
}];
I was stuck in the same problem , this is the best I could come up with but I wasn't able to bind the value to this checkbox.
I set the cell property editable to true , now if you want to change the actual value you have to double click the cell and type true or false.
but this is as far as I went and I decided to help you , I know it doesn't 100% solve it all but at least it solved the data presentation part.
incase you found out how please share your answers with us.
What about this? It's on Angular and not on React, but you could get the point:
{
headerName: 'Enabled',
field: 'enabled',
cellRendererFramework: CheckboxCellComponent
},
And here is the checkboxCellComponent:
#Component({
selector: 'checkbox-cell',
template: `<input type="checkbox" [checked]="params.value" (change)="onChange($event)">`,
styleUrls: ['./checkbox-cell.component.css']
})
export class CheckboxCellComponent implements AfterViewInit, ICellRendererAngularComp {
#ViewChild('.checkbox') checkbox: ElementRef;
public params: ICellRendererParams;
constructor() { }
agInit(params: ICellRendererParams): void {
this.params = params;
}
public onChange(event) {
this.params.data[this.params.colDef.field] = event.currentTarget.checked;
}
}
Let me know
We can use cellRenderer to show checkbox in grid, which will work when you want to edit the field also. Grid will not update the checkbox box value in the gridoption - rowdata directly till you do not update node with respective field in node object which can be access by params object.
params.node.data.fieldName = params.value;
here fieldName is field of the row.
{
headerName: "display name",
field: "fieldName",
cellRenderer: function(params) {
var input = document.createElement('input');
input.type="checkbox";
input.checked=params.value;
input.addEventListener('click', function (event) {
params.value=!params.value;
params.node.data.fieldName = params.value;
});
return input;
}
}
Here's how to create an agGrid cell renderer in Angular to bind one of your columns to a checkbox.
This answer is heavily based on the excellent answer from user2010955's answer above, but with a bit more explanation, and brought up-to-date with the latest versions of agGrid & Angular (I was receiving an error using his code, before adding the following changes).
And yes, I know this question was about the React version of agGrid, but I'm sure I won't be the only Angular developer who stumbles on this StackOverflow webpage out of desperation, trying to find an Angular solution to this problem.
(Btw, I can't believe it's 2020, and agGrid for Angular doesn't come with a checkbox renderer included by default. Seriously ?!!)
First, you need to define a renderer component:
import { Component } from '#angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
#Component({
selector: 'checkbox-cell',
template: `<input type="checkbox" [checked]="params.value" (change)="onChange($event)">`
})
export class CheckboxCellRenderer implements ICellRendererAngularComp {
public params: ICellRendererParams;
constructor() { }
agInit(params: ICellRendererParams): void {
this.params = params;
}
public onChange(event) {
this.params.data[this.params.colDef.field] = event.currentTarget.checked;
}
refresh(params: ICellRendererParams): boolean {
return true;
}
}
Next, you need to tell your #NgModule about it:
import { CheckboxCellRenderer } from './cellRenderers/CheckboxCellRenderer';
. . .
#NgModule({
declarations: [
AppComponent,
CheckboxCellRenderer
],
imports: [
BrowserModule,
AgGridModule.withComponents([CheckboxCellRenderer])
],
providers: [],
bootstrap: [AppComponent]
})
In your Component which is displaying the agGrid, you need to import your renderer:
import { CheckboxCellRenderer } from './cellRenderers/CheckboxCellRenderer';
Let's define a new columns for our grid, some of which will use this new renderer:
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
#ViewChild('exampleGrid', {static: false}) agGrid: AgGridAngular;
columnDefs = [
{ headerName: 'Last name', field: 'lastName', editable: true },
{ headerName: 'First name', field: 'firstName', editable: true },
{ headerName: 'Subscribed', field: 'subscribed', cellRenderer: 'checkboxCellRenderer' },
{ headerName: 'Is overweight', field: 'overweight', cellRenderer: 'checkboxCellRenderer' }
];
frameworkComponents = {
checkboxCellRenderer: CheckboxCellRenderer
}
}
And now, when you're creating your agGrid, you need to tell it about the home-made framework components which you're using:
<ag-grid-angular #exampleGrid
style="height: 400px;"
class="ag-theme-material"
[rowData]="rowData"
[columnDefs]="columnDefs"
[frameworkComponents]="frameworkComponents" >
</ag-grid-angular>
Phew!
Yeah... it took me a while to work out how to make all the pieces fit together. agGrid's own website really should've included an example like this...
The code below helps address the issue. The downside is that the normal events in gridOptions will not fired (onCellEditingStarted, onCellEditingStopped,onCellValueChanged etc).
var columnDefs = [...
{headerName: "Label", field: "field",editable: true,
cellRenderer: 'booleanCellRenderer',
cellEditor:'booleanEditor'
}
];
//register the components
$scope.gridOptions = {...
components:{
booleanCellRenderer:BooleanCellRenderer,
booleanEditor:BooleanEditor
}
};
function BooleanCellRenderer() {
}
BooleanCellRenderer.prototype.init = function (params) {
this.eGui = document.createElement('span');
if (params.value !== "" || params.value !== undefined || params.value !== null) {
var checkedStatus = params.value ? "checked":"";
var input = document.createElement('input');
input.type="checkbox";
input.checked=params.value;
input.addEventListener('click', function (event) {
params.value=!params.value;
//checked input value has changed, perform your update here
console.log("addEventListener params.value: "+ params.value);
});
this.eGui.innerHTML = '';
this.eGui.appendChild( input );
}
};
BooleanCellRenderer.prototype.getGui = function () {
return this.eGui;
};
function BooleanEditor() {
}
BooleanEditor.prototype.init = function (params) {
this.container = document.createElement('div');
this.value=params.value;
params.stopEditing();
};
BooleanEditor.prototype.getGui = function () {
return this.container;
};
BooleanEditor.prototype.afterGuiAttached = function () {
};
BooleanEditor.prototype.getValue = function () {
return this.value;
};
BooleanEditor.prototype.destroy = function () {
};
BooleanEditor.prototype.isPopup = function () {
return true;
};
React specific solution
When using React (16.x) functional component with React Hooks make it easy to write your cellRenderer. Here is the function equivalent of what pnunezcalzado had posted earlier.
React component for the cell Renderer
function AgGridCheckbox (props) {
const boolValue = props.value && props.value.toString() === 'true';
const [isChecked, setIsChecked] = useState(boolValue);
const onChanged = () => {
props.setValue(!isChecked);
setIsChecked(!isChecked);
};
return (
<div>
<input type="checkbox" checked={isChecked} onChange={onChanged} />
</div>
);
}
Using it in your grid
Adjust your column def (ColDef) to use this cell renderer.
{
headerName: 'My Boolean Field',
field: 'BOOLFIELD',
cellRendererFramework: AgGridCheckbox,
editable: true,
},
Frameworks - React/Angular/Vue.js
You can easily integrate cell renderers with any JavaScript framework you're using to render ag-Grid, by creating your cell renderers as native framework components.
See this implemented in React in the code segment below:
export default class extends Component {
constructor(props) {
super(props);
this.checkedHandler = this.checkedHandler.bind(this);
}
checkedHandler() {
let checked = event.target.checked;
let colId = this.props.column.colId;
this.props.node.setDataValue(colId, checked);
}
render() {
return (
<input
type="checkbox"
onClick={this.checkedHandler}
checked={this.props.value}
/>
)
}
}
Note: There are no required lifecycle methods when creating cell renderers as framework components.
After creating the renderer, we register it to ag-Grid in gridOptions.frameworkComponents and define it on the desired columns:
// ./index.jsx
this.frameworkComponents = {
checkboxRenderer: CheckboxCellRenderer,
};
this.state = {
columnDefs: [
// ...
{
headerName: 'Registered - Checkbox',
field: 'registered',
cellRenderer: 'checkboxRenderer',
},
// ...
]
// ....
Please see below live samples implemented in the most popular JavaScript frameworks (React, Angular, Vue.js):
React demo.
Angular demo.
Note: When using Angular it is also necessary to pass custom renderers to the #NgModule decorator to allow for dependency injection.
Vue.js demo.
Vanilla JavaScript
You can also implement the checkbox renderer using JavaScript.
In this case, the checkbox renderer is constructed using a JavaScript Class. An input element is created in the ag-Grid init lifecycle method (required) and it's checked attribute is set to the underlying boolean value of the cell it will be rendered in. A click event listener is added to the checkbox which updates this underlying cell value whenever the input is checked/unchecked.
The created DOM element is returned in the getGui (required) lifecycle hook. We have also done some cleanup in the destroy optional lifecycle hook, where we remove the click listener.
function CheckboxRenderer() {}
CheckboxRenderer.prototype.init = function(params) {
this.params = params;
this.eGui = document.createElement('input');
this.eGui.type = 'checkbox';
this.eGui.checked = params.value;
this.checkedHandler = this.checkedHandler.bind(this);
this.eGui.addEventListener('click', this.checkedHandler);
}
CheckboxRenderer.prototype.checkedHandler = function(e) {
let checked = e.target.checked;
let colId = this.params.column.colId;
this.params.node.setDataValue(colId, checked);
}
CheckboxRenderer.prototype.getGui = function(params) {
return this.eGui;
}
CheckboxRenderer.prototype.destroy = function(params) {
this.eGui.removeEventListener('click', this.checkedHandler);
}
After creating our renderer we simply register it to ag-Grid in our gridOptions.components object:
gridOptions.components = {
checkboxRenderer: CheckboxRenderer
}
And define the renderer on the desired column:
gridOptions.columnDefs = [
// ...
{
headerName: 'Registered - Checkbox',
field: 'registered',
cellRenderer: 'checkboxRenderer',
},
// ...
Please see this implemented in the demo below:
Vanilla JavaScript.
Read the full blog post on our website or check out our documentation for a great variety of scenarios you can implement with ag-Grid.
Ahmed Gadir | Developer # ag-Grid
Here is a react hooks version, set columnDef.cellEditorFramework to this component.
import React, {useEffect, forwardRef, useImperativeHandle, useRef, useState} from "react";
export default forwardRef((props, ref) => {
const [value, setValue] = useState();
if (value !== ! props.value) {
setValue(!props.value);
}
const inputRef = useRef();
useImperativeHandle(ref, () => {
return {
getValue: () => {
return value;
}
};
});
const onChange= e => {
setValue(!value);
}
return (<div style={{paddingLeft: "15px"}}><input type="checkbox" ref={inputRef} defaultChecked={value} onChange={onChange} /></div>);
})
I also have the following cell renderer which is nice
cellRenderer: params => {
return `<i class="fa fa-${params.value?"check-":""}square-o" aria-hidden="true"></i>`;
},
In the columnDefs, add a checkbox column. Don't need set the cell property editable to true
columnDefs: [
{ headerName: '', field: 'checkbox', cellRendererFramework: CheckboxRenderer, width:30},
...]
The CheckboxRenderer
export class CheckboxRenderer extends React.Component{
constructor(props) {
super(props);
this.state={
value:props.value
};
this.handleCheckboxChange=this.handleCheckboxChange.bind(this);
}
handleCheckboxChange(event) {
this.props.data.checkbox=!this.props.data.checkbox;
this.setState({value: this.props.data.checkbox});
}
render() {
return (
<Checkbox
checked={this.state.value}
onChange={this.handleCheckboxChange}></Checkbox>);
}
}
The array of the string values doesn't work for me, but array of [true,false] is working.
{
headerName: 'Refunded',
field: 'refunded',
cellEditor: 'popupSelect',
cellEditorParams: {
cellRenderer: RefundedCellRenderer,
values: [true, false]
}
},
function RefundedCellRenderer(params) {
return params.value;
}
You can use boolean (true or false)
I try this and it's work :
var columnDefs = [
{
headerName: 'Refunded',
field: 'refunded',
editable: true,
cellEditor: 'booleanEditor',
cellRenderer: booleanCellRenderer
},
];
Function checkbox editor
function getBooleanEditor() {
// function to act as a class
function BooleanEditor() {}
// gets called once before the renderer is used
BooleanEditor.prototype.init = function(params) {
// create the cell
var value = params.value;
this.eInput = document.createElement('input');
this.eInput.type = 'checkbox';
this.eInput.checked = value;
this.eInput.value = value;
};
// gets called once when grid ready to insert the element
BooleanEditor.prototype.getGui = function() {
return this.eInput;
};
// focus and select can be done after the gui is attached
BooleanEditor.prototype.afterGuiAttached = function() {
this.eInput.focus();
this.eInput.select();
};
// returns the new value after editing
BooleanEditor.prototype.getValue = function() {
return this.eInput.checked;
};
// any cleanup we need to be done here
BooleanEditor.prototype.destroy = function() {
// but this example is simple, no cleanup, we could
// even leave this method out as it's optional
};
// if true, then this editor will appear in a popup
BooleanEditor.prototype.isPopup = function() {
// and we could leave this method out also, false is the default
return false;
};
return BooleanEditor;
}
And then booleanCellRenderer function
function booleanCellRenderer(params) {
var value = params.value ? 'checked' : 'unchecked';
return '<input disabled type="checkbox" '+ value +'/>';
}
Let the grid know which columns and what data to use
var gridOptions = {
columnDefs: columnDefs,
pagination: true,
defaultColDef: {
filter: true,
resizable: true,
},
onGridReady: function(params) {
params.api.sizeColumnsToFit();
},
onCellValueChanged: function(event) {
if (event.newValue != event.oldValue) {
// do stuff
// such hit your API update
event.data.refunded = event.newValue; // Update value of field refunded
}
},
components:{
booleanCellRenderer: booleanCellRenderer,
booleanEditor: getBooleanEditor()
}
};
Setup the grid after the page has finished loading
document.addEventListener('DOMContentLoaded', function() {
var gridDiv = document.querySelector('#myGrid');
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(gridDiv, gridOptions);
fetch('$urlGetData').then(function(response) {
return response.json();
}).then(function(data) {
gridOptions.api.setRowData(data);
})
});
Even though it's an old question, I developed a solution that may be interesting.
You can create a cell renderer component for the checkbox then apply it to the columns that must render a checkbox based on a boolean value.
Check the example below:
/*
CheckboxCellRenderer.js
Author: Bruno Carvalho da Costa (brunoccst)
*/
/*
* Function to work as a constructor.
*/
function CheckboxCellRenderer() {}
/**
* Initializes the cell renderer.
* #param {any} params Parameters from AG Grid.
*/
CheckboxCellRenderer.prototype.init = function(params) {
// Create the cell.
this.eGui = document.createElement('span');
this.eGui.classList.add("ag-icon");
var node = params.node;
var colId = params.column.colId;
// Set the "editable" property to false so it won't open the default cell editor from AG Grid.
if (params.colDef.editableAux == undefined)
params.colDef.editableAux = params.colDef.editable;
params.colDef.editable = false;
// Configure it accordingly if it is editable.
if (params.colDef.editableAux) {
// Set the type of cursor.
this.eGui.style["cursor"] = "pointer";
// Add the event listener to the checkbox.
function toggle() {
var currentValue = node.data[colId];
node.setDataValue(colId, !currentValue);
// TODO: Delete this log.
console.log(node.data);
}
this.eGui.addEventListener("click", toggle);
}
// Set if the checkbox is checked.
this.refresh(params);
};
/**
* Returns the GUI.
*/
CheckboxCellRenderer.prototype.getGui = function() {
return this.eGui;
};
/**
* Refreshes the element according to the current data.
* #param {any} params Parameters from AG Grid.
*/
CheckboxCellRenderer.prototype.refresh = function(params) {
var checkedClass = "ag-icon-checkbox-checked";
var uncheckedClass = "ag-icon-checkbox-unchecked";
// Add or remove the classes according to the value.
if (params.value) {
this.eGui.classList.remove(uncheckedClass);
this.eGui.classList.add(checkedClass);
} else {
this.eGui.classList.remove(checkedClass);
this.eGui.classList.add(uncheckedClass);
}
// Return true to tell the grid we refreshed successfully
return true;
}
/*
The code below does not belong to the CheckboxCellRenderer.js anymore.
It is the main JS that creates the AG Grid instance and structure.
*/
// specify the columns
var columnDefs = [{
headerName: "Make",
field: "make"
}, {
headerName: "Model",
field: "model"
}, {
headerName: "Price",
field: "price"
}, {
headerName: "In wishlist (editable)",
field: "InWishlist",
cellRenderer: CheckboxCellRenderer
}, {
headerName: "In wishlist (not editable)",
field: "InWishlist",
cellRenderer: CheckboxCellRenderer,
editable: false
}];
// specify the data
var rowData = [{
make: "Toyota",
model: "Celica",
price: 35000,
InWishlist: true
}, {
make: "Toyota 2",
model: "Celica 2",
price: 36000,
InWishlist: false
}];
// let the grid know which columns and what data to use
var gridOptions = {
columnDefs: columnDefs,
defaultColDef: {
editable: true
},
rowData: rowData,
onGridReady: function() {
gridOptions.api.sizeColumnsToFit();
}
};
// wait for the document to be loaded, otherwise
// ag-Grid will not find the div in the document.
document.addEventListener("DOMContentLoaded", function() {
// lookup the container we want the Grid to use
var eGridDiv = document.querySelector('#myGrid');
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
});
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/ag-grid/dist/ag-grid.js"></script>
</head>
<body>
<div id="myGrid" style="height: 115px;" class="ag-fresh"></div>
</body>
</html>
Please note that I needed to disable the editable property before ending the init function or else AG Grid would render the default cell editor for the field, having a weird behavior.
import React, { Component } from 'react';
export class CheckboxRenderer extends Component {
constructor(props) {
super(props);
if (this.props.colDef.field === 'noRestrictions') {
this.state = {
value: true,
disable: false
};
}
else if (this.props.colDef.field === 'doNotBuy') {
this.state = {
value: false,
disable: true
};
}
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
}
handleCheckboxChange(event) {
//this.props.data.checkbox=!this.props.data.checkbox; ={this.state.show}
//this.setState({value: this.props.data.checkbox});
if (this.state.value) { this.setState({ value: false }); }
else { this.setState({ value: true }); }
alert(this.state.value);
//check for the last row and check for the columnname and enable the other columns
}
render() {
return (
<input type= 'checkbox' checked = { this.state.value } disabled = { this.state.disable } onChange = { this.handleCheckboxChange } />
);
}
}
export default CheckboxRenderer;
import React, { Component } from 'react';
import './App.css';
import { AgGridReact } from 'ag-grid-react';
import CheckboxRenderer from './CheckboxRenderer';
import 'ag-grid/dist/styles/ag-grid.css';
import 'ag-grid/dist/styles/ag-theme-balham.css';
class App extends Component {
constructor(props) {
super(props);
let enableOtherFields = false;
this.state = {
columnDefs: [
{ headerName: 'Make', field: 'make' },
{
headerName: 'noRestrictions', field: 'noRestrictions',
cellRendererFramework: CheckboxRenderer,
cellRendererParams: { checkedVal: true, disable: false },
onCellClicked: function (event) {
// event.node.columnApi.columnController.gridColumns[1].colDef.cellRendererParams.checkedVal=!event.node.columnApi.columnController.gridColumns[1].colDef.cellRendererParams.checkedVal;
// event.node.data.checkbox=!event.data.checkbox;
let currentNode = event.node.id;
event.api.forEachNode((node) => {
if (node.id === currentNode) {
node.data.checkbox = !node.data.checkbox;
}
//if(!node.columnApi.columnController.gridColumns[1].colDef.cellRendererParams.checkedVal){ // checkbox is unchecked
if (node.data.checkbox === false && node.data.checkbox !== 'undefined') {
enableOtherFields = true;
} else {
enableOtherFields = false;
}
//alert(node.id);
//alert(event.colDef.cellRendererParams.checkedVal);
}); alert("enable other fields:" + enableOtherFields);
}
},
{
headerName: 'doNotBuy', field: 'doNotBuy',
cellRendererFramework: CheckboxRenderer,
cellRendererParams: { checkedVal: false, disable: true }
},
{ headerName: 'Price', field: 'price', editable: true }
],
rowData: [
{ make: "Toyota", noRestrictions: true, doNotBuy: false, price: 35000 },
{ make: "Ford", noRestrictions: true, doNotBuy: false, price: 32000 },
{ make: "Porsche", noRestrictions: true, doNotBuy: false, price: 72000 }
]
};
}
componentDidMount() {
}
render() {
return (
<div className= "ag-theme-balham" style = {{ height: '200px', width: '800px' }}>
<AgGridReact enableSorting={ true }
enableFilter = { true}
//pagination={true}
columnDefs = { this.state.columnDefs }
rowData = { this.state.rowData } >
</AgGridReact>
</div>
);
}
}
export default App;
Boolean data in present part:
function booleanCellRenderer(params) {
var valueCleaned = params.value;
if (valueCleaned === 'T') {
return '<input type="checkbox" checked/>';
} else if (valueCleaned === 'F') {
return '<input type="checkbox" unchecked/>';
} else if (params.value !== null && params.value !== undefined) {
return params.value.toString();
} else {
return null;
}
}
var gridOptions = {
...
components: {
booleanCellRenderer: booleanCellRenderer,
}
};
gridOptions.api.setColumnDefs(
colDefs.concat(JSON.parse('[{"headerName":"Name","field":"Field",
"cellRenderer": "booleanCellRenderer"}]')));
Here's a solution that worked for me. It's mandatory to respect arrow functions to solve context issues.
Component:
import React from "react";
class AgGridCheckbox extends React.Component {
state = {isChecked: false};
componentDidMount() {
let boolValue = this.props.value.toString() === "true";
this.setState({isChecked: boolValue});
}
onChanged = () => {
const checked = !this.state.isChecked;
this.setState({isChecked: checked});
this.props.setValue(checked);
};
render() {
return (
<div>
<input type={"checkbox"} checked={this.state.isChecked} onChange={this.onChanged}/>
</div>
);
}
}
export default AgGridCheckbox;
Column definition object inside columnDefs array:
{
headerName: "yourHeaderName",
field: "yourPropertyNameInsideDataObject",
cellRendererFramework: AgGridCheckbox
}
JSX calling ag-grid:
<div
className="ag-theme-balham"
>
<AgGridReact
defaultColDef={defaultColDefs}
columnDefs={columnDefs}
rowData={data}
/>
</div>
I found a good online example for this feature:
https://stackblitz.com/edit/ag-grid-checkbox?embed=1&file=app/ag-grid-checkbox/ag-grid-checkbox.component.html
The background knowledge is based on the cellRendererFramework : https://www.ag-grid.com/javascript-grid-components/
gridOptions = {
onSelectionChanged: (event: any) => {
let rowData = [];
event.api.getSelectedNodes().forEach(node => {
rowDate = [...rowData, node.data];
});
console.log(rowData);
}
}
You can keep a checkbox on display and edit as following:
headerName: 'header name',
field: 'field',
filter: 'agTextColumnFilter',
cellRenderer: params => this.checkBoxCellEditRenderer(params),
And then create an renderer:
checkBoxCellEditRenderer(params) {
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = params.value;
input.addEventListener('click', () => {
params.value = !params.value;
params.node.data[params.coldDef.field] = params.value;
// you can add here code
});
return input;
}
This is an old question but there is a new answer available if you are using AdapTable in conjunction with AG Grid.
Simply define the column as a Checkbox Column and AdapTable will do it all for you - create the checkbox, check it if the cell value is true, and fire an event each time it is checked:
See: https://demo.adaptabletools.com/formatcolumn/aggridcheckboxcolumndemo
So in the end I somewhat got what I wanted, but in a slightly different way, I used popupSelect and cellEditorParams with values: ['true', 'false']. Of course I don't have an actual check box like I wanted, but it behaves good enough for what I need
{
headerName: 'Refunded',
field: 'refunded',
cellEditor: 'popupSelect',
cellEditorParams: {
cellRenderer: RefundedCellRenderer,
values: ['true', 'false']
}
},
function RefundedCellRenderer(params) {
return params.value;
}

How to perform async validation using reactive/model-driven forms in Angular 2

I have an email input and I want to create a validator to check, through an API, if the entered email it's already in the database.
So, I have:
A validator directive
import { Directive, forwardRef } from '#angular/core';
import { Http } from '#angular/http';
import { NG_ASYNC_VALIDATORS, FormControl } from '#angular/forms';
export function validateExistentEmailFactory(http: Http) {
return (c: FormControl) => {
return new Promise((resolve, reject) => {
let observable: any = http.get('/api/check?email=' + c.value).map((response) => {
return response.json().account_exists;
});
observable.subscribe(exist => {
if (exist) {
resolve({ existentEmail: true });
} else {
resolve(null);
}
});
});
};
}
#Directive({
selector: '[validateExistentEmail][ngModel],[validateExistentEmail][formControl]',
providers: [
Http,
{ provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => ExistentEmailValidator), multi: true },
],
})
export class ExistentEmailValidator {
private validator: Function;
constructor(
private http: Http
) {
this.validator = validateExistentEmailFactory(http);
}
public validate(c: FormControl) {
return this.validator(c);
}
}
A component
import { Component } from '#angular/core';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
import { ExistentEmailValidator } from '../../directives/existent-email-validator';
#Component({
selector: 'user-account',
template: require<string>('./user-account.component.html'),
})
export class UserAccountComponent {
private registrationForm: FormGroup;
private registrationFormBuilder: FormBuilder;
private existentEmailValidator: ExistentEmailValidator;
constructor(
registrationFormBuilder: FormBuilder,
existentEmailValidator: ExistentEmailValidator
) {
this.registrationFormBuilder = registrationFormBuilder;
this.existentEmailValidator = existentEmailValidator;
this.initRegistrationForm();
}
private initRegistrationForm() {
this.registrationForm = this.registrationFormBuilder.group({
email: ['', [this.existentEmailValidator]],
});
}
}
And a template
<form novalidate [formGroup]="registrationForm">
<input type="text" [formControl]="registrationForm.controls.email" name="registration_email" />
</form>
A've made other validator this way (without the async part) and works well. I think te problem it's related with the promise. I'm pretty sure the code inside observable.subscribe it's running fine.
What am I missing?
I'm using angular v2.1
Pretty sure your problem is this line:
...
email: ['', [this.existentEmailValidator]],
...
You're passing your async validator to the synchronous validators array, I think the way it should be is this:
...
email: ['', [], [this.existentEmailValidator]],
...
It would probably be more obvious if you'd use the new FormGroup(...) syntax instead of FormBuilder.

how to access super component class variable into sub component Class?

How i access super component class variable into sub component in Angular2?
super Component Article.ts
#Component({
selector: 'article'
})
#View({
templateUrl: './components/article/article.html?v=<%= VERSION %>',
styleUrls : ['./components/article/article.css'],
directives: [CORE_DIRECTIVES, AmCard, NgFor]
})
export class Article{
articleArr : Array;
constructor() {
this.articleArr = new Array();
}
articleSubmit(articleSubject, articleName, articleUrl)
{
this.articleArr.push({title: articleSubject.value, user : articleName.value, url : articleUrl.value});
}
}
super Component article.html
<div *ng-for="#item of articleArr">
<am-card card-title="{{item.title}}" card-link="{{item.url}}" card-author="{{item.user}}"></am-card>
</div>
sub component amcard.ts
#Component({
selector: 'am-card',
properties : ['cardTitle', 'cardLink', 'cardAuthor']
})
#View({
templateUrl: './components/card/card.html?v=<%= VERSION %>',
styleUrls : ['./components/card/card.css'],
directives: [CORE_DIRECTIVES]
})
export class AmCard {
constructor() {
}
}
sub Component amcard.html
<div class="card">
...
</div>
So my question is how to access articleArr of Article Class in AmCard class ?
advanced
Thanks for helping me.
You can inject a parent component into a child using angular2 Dependency Injection. Use #Inject parameter decorator and forwardRef to do it (forwardRef allows us to refer to Article which wasn't yet defined). So your AmCard component will look like (see this plunker):
#Component({
selector: 'am-card',
template: `
<span>{{ articleLength }} - {{ cardTitle }}<span>
`
})
export class AmCard {
#Input() cardTitle: string;
#Input() cardLink: string;
#Input() cardAuthor: string;
constructor(#Inject(forwardRef(() => Article)) article: Article) {
// here you have the parent element - `article`
// you can do whatever you want with it
this.articleLength = article.articleArr.length;
setTimeout(() => {
article.articleSubmit({ value: Math.random() }, {}, {});
}, 1000)
}
}
But, IMHO, it's a bad pattern. If possible, it's much better to use output property (event binding) to pass message to a parent component and in a parent component handle that message. In your case it would look like (see this plunker):
#Component({ /* ... component config */})
class AmCard {
// ... input properties
#Output() callSubmit = new EventEmitter();
constructor() {
setTimeout(() => {
// send message to a parent component (Article)
this.callSubmit.next({ value: Math.random() });
}, 1000)
}
}
#Component({
// ... component config
template: `
<h3>Article array:</h3>
<div *ng-for="#item of articleArr">
<am-card
[card-title]="item.title"
[card-link]="item.url"
[card-author]="item.user"
`/* handle message from AmCard component */+`
(call-submit)=" articleSubmit($event, {}, {}) "
></am-card>
</div>
`
})
class Article{
// ... properties and constructor
articleSubmit(aa, an, au) {
this.articleArr.push({ title: as.value, user: an.value, url: au.value });
}
}