RxJS Filtering and selecting/deselecting list of checkbox - rxjs

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

Related

How to create Strapi component data in a lifecycle

I want to add content to a repeatable component in my beforeUpdate hook. (adding a changed slug to a “previous slugs” list)
in v3, I could just push new data on the component array and it would save.
in v4, it doesn’t work like that. Component data now holds __pivot: and such. I do not know how to add new data to this. I’ve tried adding a component with the entityService first, and adding that result to the array. It seemed to work, but it has strange behavior that the next saves puts in two entries. I feel like there should be an easier way to go about this.
It seems like the way to go about this is to create the pivot manually:
// create an entry for the component
const newRedirect = await strapi.entityService.create('redirects.redirect', {
data: {
from: oldData.slug,
},
});
// add the component to this model entry
data.redirects = [...data.redirects, {
id: newRedirect.id,
__pivot: { field: 'redirects', component_type: 'redirects.redirect' },
}];
But this feels pretty hacky. If I change the components name or the field key, this will break. I'd rather have a Strapi core way of doing this
the way strapi currently handles components is by providing full components array, so in case you want to inject something, you have to read components first and then apply full update, if it makes it clear.
Update
So after few hours of searching, had to do few hours of trail and error, however here is the solution, using knex:
module.exports = {
async beforeUpdate(event) {
// get previous slug
const { slug: previousSlug } = await strapi.db
.query("api::test.test")
.findOne({ where: event.params.where });
// create component
const [component] = await strapi.db
// this name of components table in database
.connection("components_components_previous_slugs")
.insert({ slug: previousSlug })
.returning("id");
// append component to event
event.params.data.previousSlugs = [
...event.params.data.previousSlugs,
{
id: component.id,
// the pivot, you have to copy manually
// 'field' is the name of the components property
// 'component_type' is internal name of component
__pivot: {
field: "previousSlugs",
component_type: "components.previous-slugs",
},
},
];
},
};
So, seems there is no service, or something exposed in strapi to create component for you.
The stuff that also required to be noted, on my first attempt i try to create relation manually in tests_components table, made for me after i added a repeatable component, to content-type, but after an hour more i found out that is WRONG and should not be done, seems strapi does that under the hood and modifying that table actually breaks logic...
so if there is more explanation needed, ping me here...
result:
You can update, create and delete component data that is attached to a record with Query Engine API, which is provided by Strapi.
To modify component data you just need the ID.
const { data } = event.params;
const newData = {
field1: value1,
etc...
};
await strapi.query('componentGroup.component').update({
where: { id: data.myField.id },
data: newData
})
When you have a component field that equals null you need to create that component and point to it.
const tempdata = await strapi.query('componentGroup.component').create(
{ data: newData }
);
data.myField = {
id: tempdata.id,
__pivot: {
field: 'myField',
component_type: 'componentGroup.component'
}
}
Se the Strapi forum for more information.

Storybook problem while migrating argument of type object from addon-knobs to addon-controls

I'm having some trouble migrating one thing from the old addon-knobs to the new controls. Let me explain, maybe it's not such difficult task but I'm blocked at the moment.
I'm using StencilJS to generate Web Components and I have a custom select component that accepts a options prop, this is an array of objects (the options of the select)
So, the story for this component in the previous version of Storybook looks something like this:
export const SelectWithArray = () => {
const selectElement = document.createElement('my-select');
selectElement.name = name;
selectElement.options = object('Options', options);
selectElement.disabled = boolean('Disabled', false);
selectElement.label = text('Label', 'Label');
return selectElement;
};
This works fine, the select component receives the options property correctly as an array of objects.
Now, migrating this to the new Storybook version without addon-knobs, the story is looking like this:
const TemplateWithArray: Story<ISelect> = (args) => {
return `
<my-select
label="${args.label}"
disabled="${args.disabled}"
options="${args.options}"
>
</my-select>
`;
};
export const SelectWithArray: Story<ISelect> = TemplateWithArray.bind({});
SelectWithArray.argTypes = {
options: {
name: 'Options',
control: { type: 'object' },
}
}
SelectWithArray.args = {
options: [
{ text: 'Option 1', value: 1 },
]
}
And with this new method, the component is not able to receive the property as expected.
I believe the problem is that now, the arguments is being set directly on the HTML (which would only be accepting strings) and before it was being set on the JS part, so you could set attributes other than strings.
Is there a way to achieve this? without having to send the arguments as a string.
Thanks a lot!!
One way I've discovered so far is to bind the object after the canvas has loaded via the .play function;
codeFullArgs.play = async () => {
const component = document.getElementsByTagName('your-components-tag')[0];
component.jobData = FullArgs.args.jobData;
}

check store for object before calling api

You know how they say you don't need state management until you know you need it. Well turns out my project needs it. So I need some help wit best practice as I am adding ngxs to an existing angular project.
I have an action called getServiceDetail and my statemodel has a list of objects called DriverListsStopInfoViewModel. each of these objects have a unique ID. The html template of the consuming component uses a selector for the property currentStopDetail, which is a state property that gets set in my action.
GOAL:
in my action I want to check the list of objects in my store to see if an object with the same id exists and return that object, and if it does not exist, call and api to get it.
EXAMPLE:
The following code works, but I would like to hear if this is the right way to do it. do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
export interface SignServiceStateModel {
searchResults: ServiceSearchModel[];
driverStopsDetails: DriverListsStopInfoViewModel[];
driverStopsList: DriverListsStopsViewModel[];
driverStopsMarkers: DriverStopsMarkerViewModel[];
currentStopDetail: DriverListsStopInfoViewModel;
}
const SIGNSERVICE_STATE_TOKEN = new StateToken<SignServiceStateModel>(
'signservice'
);
#State<SignServiceStateModel>({
name: SIGNSERVICE_STATE_TOKEN,
defaults: {
searchResults: [],
driverStopsDetails: [],
driverStopsList: [],
driverStopsMarkers: [],
currentStopDetail: null
},
})
#Injectable()
export class SignServiceState {
constructor(private driverListsService: DriverListsService) {}
#Action(DriverList.GetServiceDetail)
getServiceDetail(
ctx: StateContext<SignServiceStateModel>,
action: DriverList.GetServiceDetail
) {
if (action.serviceId === undefined || action.serviceId <= 0) {
return;
}
// check if record already in list and return
const currentState = ctx.getState();
const existingStopDetail = currentState.driverStopsDetails.find(s => s.SignServiceId === action.serviceId);
if (existingStopDetail !== undefined) {
const currentStopDetail = existingStopDetail;
ctx.patchState({ currentStopDetail });
return currentStopDetail;
}
// else get new record, add it to list and return
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((currentStopDetail) => {
ctx.patchState({ currentStopDetail });
ctx.setState(
patch({
driverStopsDetails: append([currentStopDetail])
})
);
})
);
}
#Selector()
static currentStopDetail(state: SignServiceStateModel) {
return state.currentStopDetail;
}
}
I only included the relevant code from my state class
QUESTION:
is this the best way to check the store for an item and call api if it does not exist?
Thanks in advance
Short answer is yes, what you have done here is a typical way of handling this scenario (in my experience). There's a couple of improvements you could make:
do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
No, you don't return anything from these action handlers, other than possibly an Observable that NGXS will handle (so in your case if there is no matching item found, you return the Observable that fetchs it from the API and patches the state).
Also when you do make the API call, you should only need a single update to the state:
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((result) => {
ctx.setState(
patch({
currentStopDetails: result
driverStopsDetails: append([result]),
})
);
})
);

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

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);

Why session.getSaveBatch() is undefined when child record was added - Ext 5.1.1

Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.

Resources