How to create custom floating filter component in ag-grid that uses "inRange" filter type - filter

I'm trying to build a custom filter component that takes a range from a text input control (e.g. '3-5') to filter the data. To do so I have modified the example given in the ag-grid documentation (see code below).
When changing the type in onFloatingFilterChanged() to 'equals', 'greaterThan', 'lessThan' etc. everything works fine. But with type 'inRange' no filtering is performed.
Working example can be found on Plunkr: https://plnkr.co/edit/oHWFIaHgWIDXP0P5
import { Component } from '#angular/core';
import {
IFloatingFilter,
IFloatingFilterParams,
NumberFilter,
NumberFilterModel,
} from '#ag-grid-community/all-modules';
import { AgFrameworkComponent } from '#ag-grid-community/angular';
export interface RangeFloatingFilterParams extends IFloatingFilterParams {
value: number;
}
#Component({
template: `
<input
type="text"
[(ngModel)]="currentValue"
(ngModelChange)="valueChanged()"
style="width: 70px;"
/>
`,
})
export class RangeFloatingFilter
implements IFloatingFilter, AgFrameworkComponent<RangeFloatingFilterParams> {
private params: RangeFloatingFilterParams;
public currentValue: string;
agInit(params: RangeFloatingFilterParams): void {
this.params = params;
this.currentValue = '';
}
valueChanged() {
let valueToUse = this.currentValue === 0 ? null : this.currentValue;
this.params.parentFilterInstance(function(instance) {
(<NumberFilter>instance).onFloatingFilterChanged(
'inRange',
valueToUse
);
});
}
onParentModelChanged(parentModel: NumberFilterModel): void {
if (!parentModel) {
this.currentValue = 0;
} else {
// note that the filter could be anything here, but our purposes we're assuming a greater than filter only,
// so just read off the value and use that
this.currentValue = parentModel.filter;
}
}
}

Faced the same issue with custom floating datepicker. I used setModelIntoUi method instead of onFloatingFilterChanged:
instance.setModelIntoUi({
type: 'inRange',
dateFrom: moment(value.min).format('YYYY-MM-DD'), // you have to use exactly this date format in order for it to work
dateTo: moment(value.max).format('YYYY-MM-DD'),
});
And in your case with numbers it'll be:
instance.setModelIntoUi({
type: 'inRange',
filter: value.min,
filterTo: value.max,
});
UPD: Added this line
instance.onUiChanged(true);
after the setModelIntoUi method, because of the bug: filter model wasn't updating on second use.

The code inside instance.onFloatingFilterChanged() only sets the first from value.
Use these lines below to get the correct result, as it is the only way to get inRange working.
instance.setTypeFromFloatingFilter('inRange');
instance.eValueFrom1.setValue(this._input1.value);
instance.eValueTo1.setValue(this._input2.value);
instance.onUiChanged(true);

Related

RxJS Filtering and selecting/deselecting list of checkbox

I have a list of checkboxes, and I need to be able to select them ( check them) and I should also be able to filter them and check them while they are filtered. I am able to select the item, filter the item, but as soon as I filter them and then check any value it unchecks the previously selected value. I know the reason why it unchecks because every time user checks/unchecks the value I start with the original checkbox value set. So when I check/uncheck a value in filtered set, it starts again with default set where checkbox value is set to false.
Any suggestions on how to make it work? That is works seamlessly with filtering and checking/unchecking the values.
It looks like a really simple issue but i am stuck with it from past few days. Replicated the issue here. Please take a look. Any suggestions are appreciated .
https://stackblitz.com/edit/angular-wmyxtd
variables = [
{
"label": "Phone",
"value": "phone",
checked: false
},
{
"label": "Machine Id",
"value": "machine_id",
checked: false
},
{
"label": "Address",
"value": "address",
checked: false
},
{
"label": "Store",
"value": "store",
checked: false
},
{
"label": "Email",
"value": "email",
checked: false
},
{
"label": "Name",
"value": "name",
checked: false
},
{
"label": "Credit Card",
"value": "credit_Card",
checked: false
},
{
"label": "Bank Account",
"value": "bank_account",
checked: false
}
]
variables$ = of(this.variables).pipe(shareReplay(1),delay(1000));
filteredFlexibleVariables$ = combineLatest(this.variables$, this.filterBy$).pipe(
map(([variables, filterBy]) => {
return variables.filter((item) => item.value.indexOf(filterBy) !== -1);
})
);
toggleStateSelectedVariables$ = combineLatest(
this.variables$,
this.toggleFlexibleVariableBy$
).pipe(
map(([availableVariables, clickedVariable]) => {
const clickedVariableValues = clickedVariable.map((item) => item.value);
if (clickedVariable.length) {
// If condition just to save availableVariables iteration for no reason if there is no clicked variables
return availableVariables.map((variable) => {
const checked = clickedVariableValues.indexOf(variable.value) !== -1;
return {
...variable,
checked
};
});
}
return availableVariables;
}),
);
flexibleVariables$ = merge(
this.filteredFlexibleVariables$,
this.toggleStateSelectedVariables$
);
I'd take a different approach. As you're trying to edit a group of values, this makes me think that using a form would be a good idea.
I'm one of the authors of ngx-sub-form and I'll explain the approach I would take using this library to (hopefully?) simplify the workflow and encapsulate the edition of the data then just be warned whenever the form value changes.
Before we start, here's a stackblitz you can play with to see my solution:
https://stackblitz.com/edit/angular-nhnmcm
First, I'd like to point out that the filter should not be tight to the data themselves. It should just show or hide data in the view based on the filter. For that, we can use a pipe:
filter-item.pipe.ts
#Pipe({
name: "filterItem"
})
export class FilterItemPipe implements PipeTransform {
public transform(item: Item, searchFilter: string): Item {
return item.value.includes(searchFilter);
}
}
And before I forget, here's what an "Item" looks like:
export interface Item {
label: string;
value: string;
checked: boolean;
}
Now, within our main component we don't want to be aware of how the data are being displayed nor visualized. The only thing which matters is:
Where do we get our data from?
When do we update them?
app.component.ts
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
public items$: Observable<Item[]> = this.itemsService.items$;
public search: string = "";
constructor(private itemsService: ItemsService) {}
public itemsUpdated(items) {
this.itemsService.updateItems(items);
}
}
Then, from the view of the main component, we should have 2 components:
One to tell us what's the value of the filter
One to display the list of items and tell us when it's been updated
app.component.html
<app-items-filter (search)="search = $event"></app-items-filter>
<app-items-form
[items]="items$ | async"
(itemsUpdated)="itemsUpdated($event)"
[searchFilter]="search"
></app-items-form>
So far, it doesn't look too bad, does it?
But how should we build the app-items-form which seems to be holding all the magic?
items-form.component.ts
interface ItemsForm {
items: Item[];
}
#Component({
selector: "app-items-form",
templateUrl: "./items-form.component.html",
styleUrls: ["./items-form.component.css"]
})
export class ItemsFormComponent extends NgxAutomaticRootFormComponent<
Item[],
ItemsForm
> {
#DataInput()
#Input("items")
public dataInput: Item | null | undefined;
#Output("itemsUpdated")
public dataOutput: EventEmitter<Item> = new EventEmitter();
#Input()
searchFilter: string;
protected getFormControls(): Controls<ItemsForm> {
return {
items: new FormArray([])
};
}
protected transformToFormGroup(obj: Item[]): ItemsForm {
return {
items: obj
};
}
public getDefaultValues(): Partial<ItemsForm> {
return {
items: []
};
}
protected transformFromFormGroup(formValue: ItemsForm): Item[] | null {
return formValue.items;
}
}
It may look like a lot is going on there if you haven't discovered ngx-sub-form yet. So let me walk you through it.
We use a top level class called NgxAutomaticRootFormComponent. That class will ask us to implement the form which is going to hold our data and will automagically bind the form to our input and give us the updated value as an output every time the form is being updated.
It might be slightly easier but as it's an array we need to wrap the array into an object as ngx-sub-form only wants to deal with a FormGroup internally. But that formGroup can contain a FormArray (which is what we just did there).
Now, let's take a look to the view:
items-form.component.html
<ng-container [formGroup]="formGroup">
<ng-container [formArrayName]="formControlNames.items">
<app-item-ctrl [hidden]="!(itemCtrl.value | filterItem:searchFilter)"
*ngFor="let itemCtrl of formGroupControls.items.controls" [formControl]="itemCtrl">
</app-item-ctrl>
</ng-container>
</ng-container>
Few important things to notice:
- ngx-sub-form gives us access to a formGroup property that we can use directly without having to declare it ourselves
- as we're holding a list of values, we use the formArrayName directive to work with the FormArray
- we use the hidden directive provided natively by Angular to hide the controls which are not included by our filter
- last, we delegate the display/edit of the item to a sub component called app-item-ctrl
This is the "magic" part of ngx-sub-form. It's really easy to break down a form into sub form and also at the top level abstract how it's been displayed/edited.
Final part, let's take a look into the sub component:
item-ctrl.component.ts
#Component({
selector: "app-item-ctrl",
templateUrl: "./item-ctrl.component.html",
styleUrls: ["./item-ctrl.component.css"],
providers: subformComponentProviders(ItemCtrlComponent)
})
export class ItemCtrlComponent extends NgxSubFormComponent<Item> {
protected getFormControls(): Controls<Item> {
return {
label: new FormControl(),
value: new FormControl(),
checked: new FormControl()
};
}
}
Things to notice:
providers: subformComponentProviders(ItemCtrlComponent): behind the scenes, ngx-sub-form is a simple wrapper to deal with a ControlValueAccessor in an easier way and with a lot less boilerplate. Hence, we need to register the DI tokens required by a ControlValueAccessor through that simple function where you just pass the class of the current component
getFormControls this is the method where we have to pass an object that'll be used to create our internal formGroup
And the corresponding view:
item-ctrl.component.html
<div [formGroup]="formGroup">
{{ formGroupValues.label }}
<input type="checkbox" [formControl]="formGroupControls.checked" />
</div>
Conclusion:
When you need to edit data, I'd recommend using a form. Whether you use ngx-sub-form or decide to use forms directly is up to you.
If I'm explaining how to use ngx-sub-form though, it's because we've invested quite a lot of time at work to come up with that library as we do manage a lot of forms and we needed to abstract the complexity somehow. It has greatly simplified our workflow and I believe the solution above is quite verbose (by the number of components used) but really simple too as we haven't had to deal with streams for ex. (Even though I do love streams but I don't think that in that case it's the best approach).
Hope it helps and here's the final stackblitz I've made: https://stackblitz.com/edit/angular-nhnmcm
Wow.. so after a lot of brainstorming and frustrating moments I was able to solve this. I was thinking it in a different way. What I ended up doing was to create a stream of toggled Values (select/unselect checkboxes) and then filter on those toggled values.
So in summary -
Stream 1 - Merged stream of original set of variables & clicked variable. Use scan operator to get the updated stream at any point
Stream 2 - Consume the above stream and apply filter on the above stream.
toggleStateSelectedVariables$ = merge(this.variables$, this.toggleFlexibleVariableBy$).pipe(
throttleTime(100),
scan((availableVariables: NzCheckBoxOptionInterface[], clickedVariableValue: NzCheckBoxOptionInterface) => {
return availableVariables.map((variable) => {
// If its clicked, toggle it. If its selected, make it unselected. If unselected, select it
const checked = variable.value === clickedVariableValue.value ? !variable.checked : variable.checked;
return {
...variable,
checked
};
});
}),
);
flexibleVariables$ = combineLatest(this.toggleStateSelectedVariables$, this.filterBy$).pipe(
map(([variables, filterBy]) => variables.filter((item) => item.value.indexOf(filterBy) !== -1);
));
Implementation available here - https://stackblitz.com/edit/angular-ymft6o
Hope this helps someone. If anyone has any better solutions, please chime in
Happy Learning,
Vatsal

Is there a way to let Apollo Client globally insert empty strings during loading?

I'm using Apollo Client to receive the GraphQL data for my application. Over time, I see a pattern emerging where for every value I'm querying, I have to include a conditional statement to handle the moment where my data is still loading.
Assume a query looks like this:
query TestQuery($userId: Int!) {
getUser(id: $userId) {
name
}
}
Then, in every place where I want to display the user name, I have to write something like:
{ !this.props.data.loading && this.props.data.getUser.name }
or
{ this.props.data.getUser && this.props.data.getUser.name }
I don't want to display "Loading..." or a rotating spinner in any of these places. Is there a way to avoid this conditional statement by globally replacing all this.props.data.x.y.z values with null or an empty String during loading?
If so, how? Would this be considered an antipattern or bad practice?
If not, which of the above two forms is preferred?
Thanks.
How about this approach?
class GraphqlComponent extends React.Component {
renderError(){
// ...
}
renderLoading(){
// ...
}
renderLoaded(){
}
render(){
const { loading, error } = this.props;
if(error){
return renderError();
}
if(loading){
return renderLoading();
}
return renderLoaded();
}
}
class MyComponent extends GraphqlComponent{
renderLoaded(){
// your logic goes here
}
}

Angular 2, custom validation messages with parameters

I'm just starting with JS/Typescript and Angular 2 and I'm struggling with the following.
export function MinImageDimensionsValidator(minWidth: number, minHeight: number): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
// it's an image control where a user uploads an image.
// the whole image related code has been removed for better readability.
//Just assume that 'actualWidth' holds the actual width of the image
if(actualWidth < minWidth) {
return { valid: false };
}
return null;
};
}
this is just a very basic example of a validator factory.
All the examples I found just wrote the validation messages/errors directly in the template (I'm using template forms)
Is it possible to "tie" the validation messages to the validator itself and use parameters with it?
like:
'Min width has to be 100. you supplied ' + actualWidth
this would be returned from the validator itself.
or is there another way (apart from storing everything in variables somewhere) ?
Yes, you can return any object from the validator. In your case it could be something like
return { minImageDimensions: { min: minWidth, value: actualWidth } }
When displaying field validation errors, you can do this:
<input #myField="ngModel" [(ngModel)]="...">
<span *ngIf="myField.errors.minImageDimensions">
Min width has to be {{ myField.errors.minImageDimensions.min }}.
You supplied {{ myField.errors.minImageDimensions.value }}.
</span>
Or even better use some localization and messages with parameters. You can make a component that will take a field object and display all kinds of error messages you use in your application according to the myField.errors object.
ValidatorFn should return a {[k:string]:any}, so it's as easy as this :
export function MinImageDimensionsValidator(minWidth: number, minHeight: number): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
if (actualWidth < minWidth) {
return {
myValidator: `Min width has to be ${minWidth}. you supplied ${actualWidth}`
};
}
return null;
};
}
then you can access this error like myFormControl.errors.myValidator.

Can I make a TextInput required?

I have a lot of code like
if (myTextInput.text != "") {
handleEvent();
}
Does TextInput have some property I can use to automatically check for an empty string? Similar to how if I set it like :
<s:TextInput id="myInput" enter="myInputHandler()" restrict="A-Za-Z0-9"/>
then myInputHandler() only gets called if the text is alphanumeric. I'd like to add an additional restriction that length is greater than 0.
I know about the validators, but I still have to call them manually.
To make a TextInput component "required", you can, for example, create your own text input component and use a property to indicate if the control is required or not, and some event listeners like for FocusEvent.FOCUS_OUT event to force your user to enter something in that input.
For that, take this example :
package myComponent
{
import flash.events.FocusEvent;
import spark.components.TextInput;
public dynamic class MyTextInput extends TextInput
{
private var _required:Boolean = false;
public function MyTextInput()
{
super();
this.addEventListener(FocusEvent.FOCUS_OUT, on_KeyDown);
}
public function set required(required:Boolean): void {
this._required = required;
}
public function get required(): Boolean {
return this._required;
}
private function on_KeyDown(e:FocusEvent): void {
if(this.text == '' && this._required){
this.setFocus();
}
}
}
}
Of course this is just an example, you can use any behavior you want when your user left the input empty ...
Then to use that new component :
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
<!-- don't forget to include the namespace definition for your custom component -->
xmlns:MyComponent="myComponent.*">
<MyComponent:MyTextInput required="true" restrict="A-Za-z0-9"/>
</s:Application>
For more about creating your own components, take a look here.
Hope that can help.
Try :
if ( String(myTextInput.text).length > 0 )
{
handleEvent();
}
If that's all the code you need (no extra commands) then just do as one-line :
if ( String(myTextInput.text).length > 0 ) { handleEvent(); }
Maybe, this is not the kind of solution but, you can put the TextField into a FormItem, which has the "required" field

Angular2 Sorting Pipe with Object Array

How to make a sorting pipe in angular2 with an array of objects
Original Problem:
I have a TODOs list, (Todo[ ]) and I want to sort it every time I make some changes. I want that the completed todo are displayed at the bottom of the list. The Todo object has a property named .completed that stores a boolean value, it will tell us if the todo is completed or not.
Creating the Pipe:
In Angular2 the "OrderBy" pipe does not exist. So we have to build it:
import { Pipe, PipeTransform } from "angular2/core";
//Todo is the interface for our todo object
import {Todo} from './todo';
#Pipe({
name: "sort",
//set to false so it will always update, read below the code.
pure: false
})
export class TodosSortPipe implements PipeTransform {
transform(array: Todo[], args: any): Todo[] {
//watch the console to see how many times you pipe is called
console.log("calling pipe");
/* javascript is async, so could be that the pipe is called before
that the todos list is created, in this case we do nothing
returning the array as it is */
if (isBlank(array)) return null;
array.sort((a, b) => {
if (a.completed < b.completed) {
return -1;
//.completed because we want to sort the list by completed property
} else if (a.completed > b.completed) {
return 1;
} else {
return 0;
}
});
return array;
}
}
If you didn't understand the sort method check MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
The Pipe is done, let's move to the Component.
Creating the Component:
The AppComponent class, creates an array of Todo, called Todos, getting the objects from a mock with a service.
import {Component, OnInit} from 'angular2/core';
import {Todo} from './todo';
import {TodoService} from './todo.service';
import {TodosSortPipe} from './sort-pipe.component'
#Component({
//name of the html element
selector: 'my-app',
//view of the selector, " ` " is alt + 9
templateUrl: "./app/todo-list.component.html",
providers: [TodoService],
pipes: [ TodosSortPipe ]
})
export class AppComponent implements OnInit{
public todos: Todo[];
public edited = false;
public changes = 0;
//creating an istance of todoService
constructor(private _todoService: TodoService) { };
//getting the todos list from the service
getTodos() {
this._todoService.getTodos().then(todos => this.todos = todos);
}
(...)
editTodo(todo: Todo): void {
//slice is very important, read below the code
this.todos = this.todos.slice();
this.saveTodos();
}
}
Template implementation
This is the pipe calling:
<li *ngFor="#todo of todos | sort; #i=index">
(...)
</li>
Demo:
For the full example with all the code: https://plnkr.co/edit/VICRMVNhqdqK9V4rJZYm?p=preview
Watch it on github: https://github.com/AndreaMiotto/Angular2-TodoApp
Pipe Unpure
Pipes only change by default when your Pipe input parameters change and not when your data changes.
Setting Pure to false, you will make it "unpure" so you pipe will always update.
Perhaps the value todos is null at the beginning because it's loaded asynchronously using HTTP.
To prevent from such use case you could add this in your pipe:
#Pipe({
name: "sort"
})
export class TodosSortPipe implements PipeTransform {
transform(array: Todo[], args: any): Todo[] {
if (array == null) {
return null;
}
(...)
}
}
Then the value todos will be received and the transform method of the pipe will be called again with this non null value...
Moreover it seems that your <li> tag isn't ended. You must have valid HTML into component templates. I don't know if it's the complete code or a truncated one...
Hope it helps you,
Thierry
I've created an OrderBy pipe that supports both single and multi-dimensional arrays. It also supports being able to sort on multiple columns of the multi-dimensional array.
<li *ngFor="#todo of todos | orderBy : ['completed']; #i=index">
{{i}}) {{todo.name}} - {{todo.completed}}
</li>
This pipe does allow for adding more items to the array after rendering the page, and still sort the arrays with the new items dynamically.
I have a write up on the process here.
And here's a working demo: http://fuelinteractive.github.io/fuel-ui/#/pipe/orderby and https://plnkr.co/edit/DHLVc0?p=info
You need to change your html template so that your pipe can accommodate your async code.
Change this line:
<li *ngFor="#todo of todos | sort; #i=index">
(...)
</li>
To this:
<li *ngFor="#todo of todos | async | sort; #i=index">
(...)
</li>
I copied your Pipe code into this Plunker:
https://plnkr.co/edit/RBpZilgxIyRDLwktNZN1?p=preview

Resources