Selecting a Row in React Griddle, and changing tr background color - css

I'm just wondering if someone has already been able to change the color of a row, in React Griddle, by clicking on it (just once).
I'm experimenting things with JQuery, and even with Griddle Metadata, but it may be done in a cleaner way ?
Edit : I'm using React 15, Griddle inside MantraJS/Meteor, getting the data in my react Component using a Mantra container.
I can get the data by using onClick event, but not able to switch the background color in the onClick event, or playing with Metadatas.
Thanks !
EDIT : I use another view to display the content of the table, so for now I don't need to change the background of my tables cells, but if I found a solution I'll complete this post

You can use react-griddle props rowMetadata and onRowClick to do this:
class ComponentWithGriddle extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedRowId: 0,
};
}
onRowClick(row) {
this.setState({ selectedRowId: row.props.data.id });
}
render() {
const rowMetadata = {
bodyCssClassName: rowData => (rowData.id === this.state.selectedRowId ? 'selected' : ''),
};
return (
<Griddle
...
rowMetadata={rowMetadata}
onRowClick={this.onRowClick.bind(this)}
/>
);
}
}
Now this adds a selected class to the selected <tr> elements, so you can use custom styles to add colors or whatever styles you want to apply to the selected row.
Note that a more convenient API for selecting rows has been called for in the Griddle Github issues.

For whatever reason, I couldn't get Waiski's answer to work for me at all. I'm assuming that something must have changed in Griddle over the past two years. It looks like the current prevailing advice on the Web is to "implement row selection as a plugin", but I couldn't find any examples of that either. After a long hard look at the code for the Position plugin’s TableEnhancer on GitHub and a bunch of trial and error I eventually managed to cobble together the following row selection plugin for Griddle in TypeScript:
import * as React from "react";
import * as Redux from "redux";
import Griddle, { connect, GriddlePlugin, components } from "griddle-react";
export type RowId = string | number;
export type RowClickHandler = (event: React.MouseEvent<Element>, rowId: RowId) => void;
export type RowIdGetter<TData> = (rowData: TData) => RowId;
export interface IRowEnhancerProps {
rowClickHandler: RowClickHandler;
rowId: RowId;
isSelected: boolean;
}
export class RowSelector<TData> {
private _rowClickHandler: RowClickHandler = null;
private _rowIdGetter: RowIdGetter<TData>;
constructor(rowClickHandler: RowClickHandler, rowIdGetter: (rowData: TData) => RowId) {
this._rowClickHandler = rowClickHandler;
this._rowIdGetter = rowIdGetter;
}
public rowIdToSelect: RowId;
public plugin: GriddlePlugin = {
components: {
RowEnhancer: (OriginalComponent: React.ComponentClass<components.RowProps>) =>
this.rowSelectionEnhancer(OriginalComponent)
}
}
private rowSelectionEnhancer(
OriginalComponent: React.ComponentClass<components.RowProps>
): React.ComponentClass<components.RowProps> {
const rowDataSelector = (state, { griddleKey }) => {
return state
.get('data')
.find(rowMap => rowMap.get('griddleKey') === griddleKey)
.toJSON();
};
return Redux.compose(
connect((state, props) => {
const rowData: TData = rowDataSelector(state, props as { griddleKey });
const rowId: RowId = this._rowIdGetter(rowData);
return {
...props,
rowClickHandler: this._rowClickHandler,
rowId: rowId,
isSelected: rowId.toString() === this.rowIdToSelect.toString()
};
})
)(class extends React.Component<IRowEnhancerProps, any>{
public render() {
return (
<OriginalComponent
{...this.props}
onClick={(event) => this.props.rowClickHandler(event, this.props.rowId)}
className={this.props.isSelected ? "selected" : ""}
/>
);
}
});
}
}
Here's a rough outline of how it's used by a component. (Note that I had to selectively extract this example from a much larger and more complicated component, so there might be some errors/inconsistencies; sorry about that. It should still give a good overall idea of the approach.)
import * as React from "react";
import Griddle, { RowDefinition, plugins, GriddlePlugin} from "griddle-react";
import * as MyGriddlePlugins from "../GriddlePlugins";
export interface IPartInfo {
serialNumber: number,
name: string,
location: string
}
export interface IPartListProps{
parts: IPartInfo[],
selectedSerialNumber: number
}
export class PartList extends React.Component<IPartListProps, void > {
private rowSelector: MyGriddlePlugins.RowSelector<IPartInfo>;
private rowIdGetter: MyGriddlePlugins.RowIdGetter<IPartInfo>;
constructor(props?: IPartListProps, context?: any) {
super(props, context);
this._rowClickHandler = this._rowClickHandler.bind(this);
this.rowSelector = new MyGriddlePlugins.RowSelector(
this._rowClickHandler,
this._rowIdGetter);
}
private _rowClickHandler: MyGriddlePlugins.RowClickHandler =
(event: React.MouseEvent<Element>, selectedSerialNumber: MyGriddlePlugins.RowId) => {
if (selectedSerialNumber !== this.props.selectedSerialNumber) {
/*
Set state, dispatch an action, do whatever. The main point is that you
now have the actual event from the click on the row and the id value from
your data in a function on your component. If you can trigger another
render pass from here and set a fresh value for this.rowSelector.rowIdToSelect
then the "selected" CSS class will be applied to whatever row this click
event just came form so you can style it however you like.
*/
}
}
private _rowIdGetter: (rowData: IPartInfo) => MyGriddlePlugins.RowId =
(rowData: IPartInfo) => rowData.serialNumber;
public render(): JSX.Element {
this.rowSelector.rowIdToSelect = this.props.selectedSerialNumber;
return (
<div>
<Griddle
data={this.props.parts}
plugins={[plugins.LocalPlugin, this.rowSelector.plugin]}
>
<RowDefinition>
<ColumnDefinition id="name" title="Part Name" />
<ColumnDefinition id="location" title="Installed Location" />
<ColumnDefinition id="serailNumber" title="Serial Number" />
</RowDefinition>
</Griddle>
</div>
);
}
}
So, what's actually going on here? The component creates an instance of the plugin class at instantiation time, passing in an event handler to capture the click on the row and an accessor function to retrieve your ID value (not an inscrutable internal ID) from a row of your data. Just before the component returns its rendering, a value is set on the component's instance of the plugin, that way, when Griddle renders the plugin has the data to figure out when it's on a selected row and then adjust the CSS accordingly. The handler function from your component is then assigned to the row's onClick handler so your component can get the data from the click and do whatever it needs to do.
This passes the "It works for me" test (on React 15.6) which in my case is a straightforward master/detail view driven by a traditional table implemented through Griddle. I have no idea how well it would work with some of Griddle's more advanced features.

Related

Value of type '(params: any) => CSSProperties' has no properties in common with type 'Properties<string | number>'. Did you mean to call it?

Why does this property in react CSS not work if it is of type CSSProperties? How can I get it to work with Properties<string | number> ?
export const fields: GridFieldsConfiguration[] = [
{
...defaultColDefs,
field: 'amInitials',
displayNameRule: 'Asset Manager',
flex: 1.1,
minWidth: 75,
cellStyle: (params: any): any => {
getCellStyle(params, 'amInactive')
}
}
];
const isDisabledbStyle = {
color: '#FF0000'
};
const getCellStyle = ((params: any, inactiveCol: string): CSSProperties => {
console.log(params);
if (params?.api?.getValue(inactiveCol, params.node) === true) {
return isDisabledbStyle;
} else {
return isDisabledbStyle;
}
}
);
Here are the types. cellStyle comes from CSSProperties which is an extension of CSS.Properties<string | number>.
export interface GridFieldConfiguration extends FieldConfiguration {
cellStyle?: CSSProperties;
}
export interface CSSProperties extends CSS.Properties<string | number> {
/**
* The index signature was removed to enable closed typing for style
* using CSSType. You're able to use type assertion or module augmentation
* to add properties or an index signature of your own.
*
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
}
Here is Properties
export interface Properties<TLength = string | 0> extends StandardProperties<TLength>, VendorProperties<TLength>, ObsoleteProperties<TLength>, SvgProperties<TLength> {}
Sounds like you are using AG Grid, and trying to configure a cellStyleFunc for the cellStyle option of column definitions?
As shown in the linked documentation, it is indeed possible to provide a function that takes a params argument.
But it looks like in your case, you have an intermediate GridFieldsConfiguration custom type, that expects cellStyle to be of type Properties<string | number> (which is very probably actually React.CSSProperties), which does not accepts the function form, hence the error message.
If the rest of your code that handles GridFieldsConfiguration really expects CSSProperties and not the function form, then you would have to refactor it first, so that it can handle that form.
If all it does is to pass the cellStyle option to AG Grid, then you just need to improve the definition of GridFieldsConfiguration type. You can re-use the actual types grom AG Grid, e.g.:
import { AgGridColumnProps as ColDef } from "ag-grid-react";
export interface GridFieldsConfiguration {
cellStyle?: ColDef["cellStyle"];
}
But note that CSSProperties is actually not type-compatible with cellStyle. To fix it, simply remove the return type assertion on your getCellStyle function. If you want to ensure that the returned objects still resembles a CSS object, you can use the new satisfies operator:
const isDisabledbStyle = {
color: '#FF0000'
} satisfies CSSProperties;
const getCellStyle = (params: any, inactiveCol: string) => {
return isDisabledbStyle;
};
Playground Link

stenciljs: Issue with slot

I have created a simple web (stencil) component AuthGuard, not to be be confused with Angular's AuthGuard.
The purpose of this component is to check if the user is logged in.
If yes, render the slot html.
If not, render the Signup button.
The component code is as follows:
import { Component, Host, h } from '#stencil/core';
import { Build, State } from '#stencil/core';
import { AuthService } from 'auth/auth.service';
import { ConfigService } from 'common/config.service';
#Component({
tag : 'auth-guard',
styleUrl : 'auth-guard.css',
shadow : true,
})
export class AuthGuard {
#State() canRender : boolean = false;
componentWillLoad() {
if (Build.isBrowser) {
const timerId = setInterval(() => {
if (AuthService.isInitialized) {
AuthService.vol$.subscribe(_u => {
this.canRender= true;
});
clearInterval(timerId);
}
}, ConfigService.loadTime);
}
}
render() {
console.log('auth guard :: render', this.canRender, AuthService.me);
return (
<Host>
{
this.canRender ? (
AuthService.me && AuthService.me.id.length > 0 ? (
<slot></slot>
) : (
<ion-button
href="/signup"
routerDirection="forward"
color="danger">
Signup
</ion-button>
)
): null
}
</Host>
);
}
}
Now in the other file, I use the following code:
<auth-guard slot='end'>
<volunteer-mini volunteer={AuthService.me}></volunteer-mini>
</auth-guard>
With this what I am expecting is
Nothing to be rendered, till this.canRender becomes true.
Once this.canRender becomes true, If AuthService.me is valid, render the slot HTML,
If AuthService.me is null, render signup button.
But seems when this.canRender is false, it tried to render volunteer-mini the slot HTML, which is a problem. Since volunteer-mini internally depends on AuthService.me, which is not yet initialized.
But once this.canRender becomes true, other 2 scenarios are working fine.
It's in general a bad idea to write an auth-guard using stencil. The core problem is that your slot exists before your component has initialized.
Therefor, using your current code, you'd have to manually remove the slot after you decided that you don't have the rights.
Additionally, if you do not define a slot position, but still provide a slot-content in your parent, it will still be appended to your inner children.
To resolve this problem, you can refactor your component to an function, like <Host>, but this has other pits to consider.

ReactJS not setting class based on expression

I'm actually trying to toggle a class on an element when user clicked. But unfortunately, my code only set class for single element. It looks like the view is not refreshing for subsequent clicks, even the set class is not removing. But my store is properly updating.
Here is my code.
class MyInterests extends Component {
constructor(props) {
super(props);
this.state = {selected: []};
}
toggleChip(id, index, event){
const { interestChanged } = this.props;
let index = this.state.selected.indexOf(id);
if(index === -1){
this.state.selected.push(id);
}else{
this.state.selected.splice(index, 1);
}
interestChanged(this.state.selected);
}
render() {
const {classes, myinterests: { categories, userinterest } } = this.props;
const getClassNames = (id) => {
return classNames(classes.chip, {[classes.selected]: (userinterest.indexOf(id) !== -1)});
}
return (
/*..... Few element to support styles....*/
{categories.map((data, index) => {
return (
<Chip
key={data._id.$oid}
label={data.name}
onClick={this.toggleChip.bind(this, data._id.$oid, index)}
className={getClassNames(data._id.$oid)}
/>
);
})}
);
}
}
Can anyone tell me what is wrong in this or how can I achieve this?.
Since state is immutable, you cannot use .push on it.
By using this.state.selected.push(id) you're mutating the state thus not signaling react on the change making the change vulnerable to future state updates (remember that setState is asynchronous and changes are batched for single action).
Take a look at this for how to solve it.
In your case, a better way to update the state would be something like this:
// create a copy of the current array
var selected = this.state.selected.slice();
// push the new element to the copy
selected.push(id);
// replace the current array with the modified copy
this.setState({ selected: selected });

Binding to 'style.grid' throws 'sanitizing unsafe style value' warning

I'm creating a mechanism for defining and calculating my own reusable grids. Here's an example of what's returned
[left-bleed-start] 10.245000000001024px [left-bleed-end content-col-start] 1331.8500000001332px [content-col-end right-bleed-start] 10.245000000001024px [right-bleed-end]/[top-bleed-start] 10.245000000001024px [top-bleed-end link-row-start] 81.9600000000082px [content-row-end footer-row-start] 163.9200000000164px [footer-row-end bottom-bleed-start] 10.245000000001024px [bottom-bleed-end]
When applying it like this
<div class="appCompGrid" [style.grid]="Grid.GridCode"></div>
I get the santization warning. However if I copy and paste the value in like this
<div class="appCompGrid" style="grid: (same code);"></div>
everything works. The css class is where I define the display as grid seeing that it'll be consistent no matter what size the screen is. The only thing I could think to do was go back into the function and add + ';' to the end of where the grid code is put together figuring maybe that was throwing something off but it still gives the same error. I tried applying display: grid; inline to see if maybe there was a problem with it reading both from a css class and inline for some odd reason.
I'm using #HostListener to re-calculate the grid as the size or orientation changes, so far I haven't run into a problem with Angular functioning in this manner so I don't understand where to begin with figuring out why this is happening. Here's how I have my component classes set up.
Base Class
export class GridBuilder {
Settings : GridInit = new GridInit();
GridData : Array<GridType> = new Array();
Grid : GridOutput = new GridOutput();
constructor() { this.GridData = GridDefs; }
public buildGrid() {
const coreSettings : GridInit = this.Settings;
const gridData : GridType[] = this.GridData;
const w: number = multiply( coreSettings.Size.Width, coreSettings.Size.PixelRatio );
const h: number = multiply( coreSettings.Size.Height, coreSettings.Size.PixelRatio );
const o: string = checkOrientation( w, h );
const c: CellSpecs = calcCell( o, w );
const t: GridType = gridData.find( a => a.GridName == coreSettings.GridStyle );
const cols: string = calcArea( t.Columns, c );
const rows: string = calcArea( t.Rows, c );
this.Grid.GridCode = cols + '/' + rows + ';';
this.Grid.GridAreas = t.Areas;
}
}
Secondary class for app component/ any top tier container
export class SiteGrid extends GridBuilder {
constructor(){
super();
this.applySizeSettings();
}
applySizeSettings(){
this.Settings.Size.Width = window.innerWidth;
this.Settings.Size.Height = window.innerHeight;
this.Settings.Size.PixelRatio = window.devicePixelRatio;
}
}
the AppComponent
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent extends SiteGrid {
title = 'app';
#HostListener( 'window: resize', [ '$event' ] )onResize( event ){ this.applySizeSettings(); this.buildGrid(); }
constructor(){
super();
this.Settings.GridStyle = 'SiteGridA';
this.buildGrid();
}
}
I don't know how relevant this may be in helping figure out the solution but thought I'd show how things are flowing just incase. Anyone know why this warning is occurring?
You need to implement a sanitizer to cleanse your css, or bypass it...
constructor(private sanitizer: DomSanitizer) {
this.sanitizedCSS = sanitizer.bypassSecurityTrustStyle(Grid.GridCode) ;
}
As for why, this blog explains it pretty well, as does the DomSanitizer documentation.
DomSanitizer helps preventing Cross Site Scripting Security bugs (XSS) by sanitizing values to be safe to use in the different DOM contexts.

flow issue I can't overcome. Passing props buy not confirming to shape

I have a parent component, this a very scaled down version.
I define type State (as example shows)..... and then I pass down a portion of (their values) them into a child component. That child component (in a seperate file) again, defines its props.... but I am getting errors for every single prop in the parent. infuriating.
for each prop being passed to the "MyChildComponent", I am getting this error:
props of React element MyChildComponent. This type is incompatible with object type
type State = {
name: string,
age: number,
shoe: number,
hair: string
}
class Dude extends component<void, Props, State> {
props: Props;
state: State;
// these values get over-written by some ajax call
constructor() {
this.state = {
name: 'george',
age: 999,
hair: 'brown',
shoe: 11
}
displaySomeComponent = (): React.Element<*> => {
const { name, age, hair } = this.state;
return (
// EACH ONE OF THESE GIVES AN ERROR
//Flow: props of React element `MyChildComponent`. This type is incompatible with object type
<MyChildComponent
name={name},
age={age},
hair={hair}
/>
)
}
}
// In a separate file for MyChildComponent
// No errors in this file
type Props = {
name: string,
age: number,
hair: string
}
class MyChildComponent extends component<void, Props, void> {
props: Props;
render() {
// all renders fine
}
}
I am at my wits end. I do not understand what is going on, as I've passed things into many child components, but this is mucking up. I am thinking it might have something to do with the destructuring?
Hmm, it's hard to say. If I fix a few syntax errors, this seems to work fine in Flow 0.54: my version
To track down what is wrong, you might try adding type declarations to see where things go wrong. Like, inside the parent render method, under the destructuring, try adding name: string; and see if Flow complains about that.

Resources