I want to create a component that provides a list of selections to the user but allows only one selection to be made at any given time. The functionality of the mat-radio-group seems to fit that bill the best, but I don't want the actual radio button to be rendered next to the labels within my radio group. I want to expand the label and make any (change) event fire from a click event on the label itself.
What is the least "hacky" way to eliminate the radio buttons from my radio group, while keeping the labels as they were?
you can also make a custom form control. The idea is that has a .html like
<select-component [(ngModel)]="valor">
<div select value="1">One</div>
<div select>Two</div>
</select-component>
We are going to make a directive that the selector was [select]
#Directive({
selector: '[select]',
})
export class SelectDirective implements AfterViewInit {
#Input('value')value:any;
control:any;
#HostBinding('class.selected')
get isSelected(){
return this.control && this.control.value==this.value?true:undefined
}
#HostBinding('class.select')setClass(){return true}
#HostListener('click') onclick() {
console.log(this.value);
if (this.control)
this.control.setValue(this.value)
}
constructor(private el:ElementRef){}
ngAfterViewInit()
{
this.value=this.value ||this.el.nativeElement.innerHTML
}
}
See that, in ngAfterViewInit we give value to this.value as the innerHTML of the div case you has not defined the value
There're two class binding, one .select -this alow us give .css to our component from app.main.component, one .selected, when the div was "selected".
The SelectComponent is a tipical custom form control, the "interesting" is that, in ngAfterViewInit, we ask about the "select" directive inside to allow comunicate the directive and the component
#Component({
selector: 'select-component',
template: `<ng-content></ng-content>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectComponent),
multi: true
}
]
})
export class SelectComponent implements ControlValueAccessor,AfterViewInit {
#ContentChildren(SelectDirective)selects:QueryList<SelectDirective>;
value:any;
disabled:boolean=false;
onChange:any;
onTouched:any;
writeValue(value: any[]|any): void {
this.value=value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled=isDisabled
}
ngAfterViewInit()
{
this.selects.forEach(x=>{
x.control=this
})
}
setValue(value)
{
this.value=value
this.onChange(value)
}
}
And, voila!, the stackblitz
Related
So I'm using a data table which has an active element. When that active elment changes I store the name of the active element in a property of my polymer element. Then I display this String property in a div.
Now I know for certain that the property change works, because I console.log it after a change, the div displaying the property doesn't update and continually displays the default value I have set.
export class ProjectsOverview extends PolymerElement {
static get template() {
return html`
...
<div>{{currentProject}}</div>
...
`
}
static get properties() {
return {
currentProject: {
type: String,
value: "placeholder",
notify: true,
reflectToAttribute: true
}
};
}
connectedCallback() {
super.connectedCallback();
const grid = this.shadowRoot.querySelector('vaadin-grid');
grid.addEventListener('active-item-changed', function(event) {
const item = event.detail.value;
grid.selectedItems = [item];
if (item) {
this.set('currentProject', item.name);
} else {
this.set('currentProject', '');
}
console.log(this.currentProject);
});
}
}
My expected result would be that every time the currentProject property is updated, the div displaying the property updates as well.
The active-item-changed callback does not have its context bound to the Polymer instance (i.e., this is the grid and not the Polymer component). Instead of the function expression, use an arrow function to automatically bind this to the correct context.
// grid.addEventListener('active-item-changed', function(event) { // DON'T DO THIS
grid.addEventListener('active-item-changed', (event) => {
/* this is the Polymer instance here */
this.set('currentProject', ...);
})
Your scope is wrong. You're using an anonymous function so when you try to set currentProject, you do that when your this is your anonymous function. Use .bind(this) to fix your problem.
grid.addEventListener('active-item-changed', function(event) {
const item = event.detail.value;
grid.selectedItems = [item];
if (item) {
this.set('currentProject', item.name);
} else {
this.set('currentProject', '');
}
console.log(this.currentProject);
}.bind(this));
I am trying to get my head around a scenario with CSS components:
I have a react component that uses its own classes. This component has a little helper subcomponent that also has its own classes. Now: When a specific state in the main component is set and a specific class is applied then the helper component's css should react on that class.
For instance:
Component A uses Component B to show something.
Component A gets clicked on and react sets a "clicked"-class on that component
Component B should then visually react on that class
In plain CSS (or similar) I would do this:
Component A:
.component {
height: 10px;
}
.component.clicked {
height: 5px;
}
Component B
.clicked {
.subComponent {
background-color: orange;
}
}
I know that there is a react way to do this. This kind of thing should be done with states and props which are being passed between the components so that this kind of situation gets avoided altogether. But I am currently refacturing a project that still has these issues and I don't really get how to do this properly with react-css-modules.
By the way: My current workaround uses :global but I'd really, really like to avoid this...
Component B:
.clicked:onclick, .subComponent {
// code ...
}
This should do it.
If not I'm just bad at css, or confused about your question.
Parent:
var ComponentA = React.createClass({
getInitialState: function() {
return {
isClicked: false
}
},
onClick: function() {
this.setState({ isClicked: !this.state.isClicked });
}),
render() {
return (
<div className={this.state.isClicked ? "component clicked" : "component"}>
<ComponentB isClicked={this.state.isClicked}/>
</div>
);
}
});
Child:
var ComponentB = React.createClass({
getDefaultProps: function() {
return {
isClicked: false
}
},
render() {
return (
<div className={this.props.isClicked ? "subComponent clicked" : "subComponent"}>
I am the subComponent
</div>
);
}
});
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.
I am developing an Angular2 app, and I faced a problem:
I have a set of different objects that can be selected using UI. Each of this objects has a set of options (different for different objects) that could be edited using UI. Now, I am using DynamicComponentLoader to insert a specific component for currently selected object, so it can handle its options correctly.
The problem is that I don't know how to bind data of currently selected object to a dynamically inserted options component.
#Component({
selector: 'dynamic',
template: `<div>Options:</div>
<div>Property1: <input type="number" /></div>
<div>Property2: <input type="text" /></div>`
// template: `<div>Options:</div>
// <div>Property1: <input type="number" [(ng-model)]="currentSelection.property1" /></div>
// <div>Property2: <input type="text" [(ng-model)]="currentSelection.property1" /></div>`
})
class DynamicComponent {
}
#Component({
selector: 'my-app',
template: `
<div>
<h2>Selected: {{currentSelection.name}}!</h2>
<div #container></div>
</div>
`
})
class App {
currentSelection = {name: 'Selection1', property1: 10, property2: 'test'};
constructor(private loader: DynamicComponentLoader, private elementRef: ElementRef) {
loader.loadIntoLocation(DynamicComponent, elementRef, 'container');
}
}
Here is a plunker to help you understand my question:
With angular2 and Rxjs, "Observables" are almost always the answer.
If i understood your problem correctly, you need to make your DynamicComponent an "Observer" and your container "an Observable or even better a Subject (In case your container needs to subscribe to another observable to receive selections from)". Then, after loading your dynamic component, subscribe it to your container.
Whenever the selection changes on your container, you push the new selection to your subscribers. This way, you can load multiple dynamic components and all will receive your pushes.
The Container:
class App {
currentSelection = {};
selections = [
{name: 'Selection1', property1: 10, property2: 'test'},
{name: 'Selection2', property1: 20, property2: 'test2'}
];
subject:Subject<any> = new Subject();
constructor(private loader: DynamicComponentLoader, private elementRef: ElementRef) {
}
ngOnInit(){
this.loader.loadIntoLocation(DynamicComponent, this.elementRef, 'container', this.injector)
.then(compRef =>this.subject.subscribe(compRef.instance));
// subscribe after loading the dynamicComponent
}
// set the new selection and push it to subscribers
changeSelection(newSelection){
this.currentSelection = newSelection;
this.subject.next(this.currentSelection);
}
}
The Observer:
class DynamicComponent implements Observer{
public currentSelection = {};
next(newSelection){
this.currentSelection = newSelection;
}
}
Here is your plunker working after my edits, "provided I changed the imports to the newest angular beta.6"
I know this is a quite old question. But hopefully someone will benefit from this answer.
Here is what you can do, move your code from constructor to ngOnInit and use promises for assigning dynamic value.
ngOnInit(){
this.dynamicComponentLoader.loadIntoLocation(DynamicComponent, this.elementRef,'container').then((component)=>{
component.instance.currentSelection = currentSelection;
});
}
I am trying to dynamically set the padding on a grid panel I have showing some data. On the event that my checkbox was clicked, it should apply the padding cls.
Here is the relevant ExtJS code:
var permissionsGrid = Ext.create('Ext.grid.Panel', {
//...
items: [{
xtype: 'checkbox',
name: 'EditRoles',
boxLabel: 'Edit User Roles',
handler: function(field, value) {
userRoleFilter = '';
permissionsGrid.removeCls('permissions_panel_nopadding');
console.log(permissionsGrid.hasCls('permissions_panel_nopadding'));
permissionsGrid.addCls('permissions_panel_padding');
console.log(permissionsGrid.hasCls('permissions_panel_padding'));
}
}],
//...
});
Here is my CSS
.permissions_panel_nopadding {
padding: 0px;
}
.permissions_panel_padding {
padding: 5px;
}
When the checkbox is clicked, currently nothing happens. I tried to use:
permissionsGrid.getView().refresh();
...but to no prevail.
You are always removing one class and always adding another class. You need to "switch" the classes. If you don't need special styling when the checkbox isn't clicked, you should add/remove only one class.
handler: function(field, value) {
userRoleFilter = '';
if ( value === true) {
permissionsGrid.removeCls('permissions_panel_nopadding');
permissionsGrid.addCls('permissions_panel_padding');
}
else {
permissionsGrid.addCls('permissions_panel_nopadding');
permissionsGrid.removeCls('permissions_panel_padding');
}
console.log(permissionsGrid.hasCls('permissions_panel_padding'), permissionsGrid.hasCls('permissions_panel_nopadding'));
}
Here is my fiddle with basic panel and only one CSS class: https://fiddle.sencha.com/#fiddle/ne3
EDIT:
ExtJS 6.0+
Since ExtJS 6.0.0, you can use the toggleCls method:
handler: function(field, value) {
permissionsGrid.toggleCls('permissions_panel_padding', value);
permissionsGrid.toggleCls('permissions_panel_nopadding', !value);
console.log(permissionsGrid.hasCls('permissions_panel_padding'), permissionsGrid.hasCls('permissions_panel_nopadding'));
}