-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Filter state two-way binding, custom equals #1
base: datagrid-state-input
Are you sure you want to change the base?
Conversation
/** | ||
* Compare objects by reference | ||
*/ | ||
public equals(objectToCompare: ColorFilter): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This problematic and shows the main difficulty with this approach. The filter object that is passed in the input state must be the same as the registered one, otherwise a new filter is added. You therefore cannot pass a differently configured filter in the input state, because the only the way to do this would be to modify the existing filter, which already changes the state.
For this to work, filters must be serializable or have a state input and output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the string filter the equals is ok to check prop name? For color filters may also need an identifier prop name, to determine which filter uses it, and with that we could implement the equals. Or am I wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The string filters also needs to compare the value and, if the value is not the same, you must set the value in the string filter to activate or deactivate it. Basically, you need to have getters and setters for the internal state of the filter.
You cannot activate or deactive filters using this approach, because filters have to standardized way to do that. The filter interface is just too small. The comparator interface the same issue. Including these methods/inputs/outputs int the filter and comparator interfaces would a very backwards incompatible change and increase the difficulty for those that do not care about the state input. My approach would be to extend the filter and comparator interfaces as |
So if I get it right, I should create a new interface which is called SerializableFilter and extends ClrDatagridFilterInterface, and I should create a new implementation as well which will implement this new interface, and i think it can extend the string filter implementation class. In this case we wont affect custom filters like color filter. What about the equals logic, is it fine to you to check the property names? |
Yes, but with one difference: Just patch the builtin sting filter implementation to extend the ClrDatagridFilterInterface because just about everything useful there is private anyway. Some pseudo-code for setting the state:
For implementing the method in the filters provider, the
The state of the builtin string filter for example would be
I recommend to have a |
You mean to patch the builtin sting filter implementation to implement the new SerializableFilter interface. |
Yes, of course. So many interfaces confuse me! |
@audax The refactor is committed, One thing is different, I don't set the filterstate directly, I just set it by the value setter, and via the constructor. |
* The full license information can be found in LICENSE in the root directory of this project. | ||
*/ | ||
|
||
export interface FilterStateInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs to be specific for each filter. There are filters that span multiple properties or that do not have a string value. Each filter therefore needs to provide it's own filter interface and the compatibleToState
method needs to accept any
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe, instead of any
, just a very basic interface with only the type which state interface of the individual interfaces then extend. We always lose the type safety anyway when serializing the stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think every filter has a type, a property name and the value itself, so we should not remove any of these just we should go a generic type parameter, which gives the type of the value, and it should be the same type which is given in the type property in string format.
Pseudo code:
FilterStateInterface <T>{
type: string; //if T is Date then this value should be "Date", so we can preserve the type after serialization
property: string;
value: T;
}
What's your opinion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that every filter has a property and the value of a filter is not usually a T
, because T
is the value of the row and not the column.
FilterStateInterface {
readonly type: string
}
is the only thing I would require and then the builtin string would have it's own interface:
StringFilterStateInterface extends FilterStateInterface {
property: string,
value: string
}
You can then cast the state here:
compatibleToState(state: any): boolean {
if nstate.type === 'BuiltinStringFilter') {
return state.property === this.property
}
else {
return false
}
}
This is semantically the same as my code snippet in #1 (comment) , I just unrolled the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's pushed now. I think instead of any we need FilterStateInterface. Please check the last commit.
@@ -102,17 +103,17 @@ export class StateProvider<T> { | |||
if (state.filters) { | |||
const activeFilters = this.filters.getActiveFilters(); | |||
for (const filter of state.filters) { | |||
let filterObject: ClrDatagridFilterInterface<T>; | |||
let filterObject: SerializableFilter<T>; | |||
if (filter.hasOwnProperty('property') && filter.hasOwnProperty('value')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This special case should be removed in favor of the new state interface of filters. The special case don't work properly because it creates new filters objects instead of updating the existing ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New filter creation happens after this "if else" block, in this block we are converting the old filter prop value into the new filterstate object, which is essential, if we omit this block, the functionality will be broken. Can you specify the details of this refactor, because I can't see what exactly do you mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is missed the piece in the code where const existing = activeFilters.findIndex(value => value.compatibleToState(filterObject.filterState));
is called even for the builtin filters.
So the code looks correct, but I would eliminate the special case anyway by modifying the state getter of the state provider to just emit the filterState
for builtin string filters as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I should move the filterstate field to ClrDatagridFilterInterface
from SerializableFilter
? So serializable filter will only contain the compatibleToState
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would break existing filters. But you need to check filters if the filters are SerializableFilter
and if so, use the state getter. So instead of having this special case, you can use the state getter for all serializable filters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't check that, because interfaces exists only at compile time, so there is no solution to check an object runtime whether it implements an interface. However you can do it with abstract classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, or by checking the interface manually. The abstract class would be a nicer solution in that regard, but I don't know which problems this creates in the long term. I guess it would be a good start and then you can see what the clarity maintaners think about that approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checked since getActiveFilters() already returns SerializableFilter
s, we don't need to check the type, special case is eliminated. Check the last commit.
this.filters.add(filterObject); | ||
} | ||
} | ||
activeFilters.forEach(filter => this.filters.remove(filter)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe serializable filters should have an option to just deactivate them instead of removing them. Removing filters also clears all bindings to UI elements, so passing a state without an active string filter and then passing a state with an active string filter probably breaks the whole thing. Did you test this somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's tested on the UI, it works, but still leaks the memory somewhere.
So find all the pitfalls of the state setting stuff, you probably should build an end-to-end test that loads a predetermined state and changes filter values in the UI to be sure that no UI bindings are broken when setting a state that deactivates, modifies or activates filters. My test cases have shown that integration tests are not enough to find these quirks. |
Memory leak is fixed, now it works fine for built in string filter. |
@audax are there any e2e tests (protractor) in the project, is it runned by the pipeline? Or it is enough to make a dedicated page under the datagrid section to demonstrate the usage, with the built in filters, and the custom ones? |
I have no idea, sorry. I am just a user of Clarity poked a bit around in the source. There are some gemini tests, but the datagrid gemini tests are disabled. |
All right, I'll prepare demo site, with different filters, and presorted columns, prepaged grid. Is that enough to accept the pull request? |
and you should update the tests that I made and create new tests for the new methods in the filter provider. |
I'm just wondering, how could we restore the state of a custom filter, because it is a content child of the grid, so somehow we have to deliver the state info to the content child, which should implement the SerializableFilter interface, and restore the values in the component. How should we do this, or we should let the user to handle it locally? |
} | ||
const existing = activeFilters.findIndex(value => value.compatibleToState(filterObject.filterState)); | ||
if (existing !== -1) { | ||
activeFilters.splice(existing, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point we only found a compatible filter, but now we must set the state of that filter with existing.filterState = filter
.
The SerializableFilters should be responsible to restore their state by setting their
|
It seems, it works now, but we have to identify the filters somehow, we identifies string filters by type and property name, but for custom filters like color filter we also have to use type and id as well, because there can be two different colorfilters on the same grid, so I think we should update FilterStateInterface to include some kind of id beside the type field.
|
Now the preset for custom filters works, but I still don't know, how to pass an identifier for the custom filters, because we have to differentiate if multiple instance exists of the same type of the custom filter. Given this filter:
When this filter is registered in the grid, we cannot access input properties, because they are null, because the filter component is not in that lifecycle to access these fields, so we need something which is available in the constructor, can we get
Any idea is welcome. |
This is an annoying issue. Maybe this would work, even though it is a bit convoluted:
All state setting and getting is queued until all custom filters are registered in the lookup table, e.g. in a replay-subject. That is probably the point where we should require custom filters to extend an abstract class were all this is defined. |
@Wallabeng helped me with the solution, which was obvious, but I didn't think about that. He sets the custom filter ids in the binding-state component's onInit, not in the HTML. It works perfectly, but that means the user have to pay attention to set it. |
Date interval filter, Number interval filter, List filter Signed-off-by: Daniel Szegvari <[email protected]>
Ok, but just to clarify: This is not the official clarity project, but just my fork which currently has no changes whatsoever. Also, I decided to not use the a writable state api of the datagrid, but instead to bind all writes to properties myself and write a custom filter component that is not integrated with the datagrid at all. I only use the exported state to set the pagination and sorting options, all external writes to those properties are done my binding to the component itself:
|
this.to = state.to; | ||
} | ||
|
||
setId(id: string): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use a property and not a set-method, since it doesn't do anything anyway. Less code is better and properties are easy.
} | ||
isActive(): boolean { | ||
return this.nbColors > 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is just full of indentation and formatting changes which clutters up the diff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may not understand this, could you please show me an example how you use the custom filters? |
The custom filters are only interacting with the NgRx Store and my DataSource reads the filter option and every other option, like pagination and sorting from the store. The Datagrid just displays the data that is in the store.
|
Minor refactors, Write new tests, Fix failing tests Signed-off-by: Daniel Szegvari <[email protected]>
Hi @audax . Sorry for the delay. I had a couple of minor refactors, and wrote some tests. Please review it, if you have time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that vmware-archive#2846 has been implemented, the two-way binding just extend that interface and integrate in the same way.
package-lock.json
Outdated
@@ -93,7 +93,7 @@ | |||
"dependencies": { | |||
"source-map": { | |||
"version": "0.5.6", | |||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", | |||
"resolved": "http://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why, oh why?
Renderer2, | ||
ViewChild, | ||
ViewContainerRef, | ||
AfterContentInit, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-indenting the whole file is not a good idea. Makes for terrible diffs.
Minor refactors Signed-off-by: Daniel Szegvari <[email protected]>
I'm interested in getting this across the finish line -- let me know if there's anything I can help with. Should it be rebased on top of vmware-archive#2846? |
I guess that would be reasonable to decrease the amount of changes that this PR introduces. |
Here is a working version of the filter two-way binding, it's not ready to merge yet, because there are serious memory leaks (and some coding guideline issues), I justed wanted to ask you to take a look that the approach is fine, and if you have a suggestion to eliminate the memory leak, then please share the idea :).