Changing custom control style dynamically - reactjs

Changing custom control style dynamically
I am trying to use react to put red border around each custom field that is empty. array this.state.Fields contains all the controls to be checked.
I want to check every required control and if its value not set, change its style property. Since properties cannot be changed, I tried to use state but the problem is I'd need to have a separate vriable for each control:
<Control ref="controlLabel" name="controlLabel" type="1" onComponentMounted={this.register} label="Control Label:" required="1" value={this.state.controlLabel} localChange={this.handleControlLabelChange} inputStyle={{border: this.state.errControlLabelStyle}} />
I was wondering if there is a more elegant way to do that? Here is my code:
this.state.Fields.forEach((field) => {
if(field.props.required === "1"){
var validField = (field.props.value != '' && field.props.value != undefined);
if(!validField){
//set the field style dynamically
}
}
validForm=validForm && validField;
});

You could add validation logic inside Control itself.
var Control = React.createClass({
isValid: function() {
if (!this.props.required) {
return true;
}
return this.props.value !== '' && this.props.value !== undefined;
},
render: function() {
let value = this.props.value;
return <div className={this.isValid() ? 'item valid' : 'item invalid'}>{value}</div>;
}
});
var App = React.createClass({
render: function() {
return (
<div className="container">
{[
{
required: true,
value: ''
},
{
required: true,
value: 'abc'
},
{
required: false,
value: ''
}
].map((v, i) => <Control key={i} required={v.required} value={v.value} />)}
</div>
);
}
});
ReactDOM.render(
<App />,
document.getElementById('container')
);
.valid {
border-color: green;
}
.invalid {
border-color: red;
}
.item {
width: 200px;
height: 50px;
border-width: 1px;
border-style: solid;
margin: 1px;
display: flex;
}
.container {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>

Related

Issue in removing Grandchild in a recursive component

What I have been trying to achieve?
Create a nested context menu that is driven by a config.
Where am I stuck:
Sub menus are rendering correctly, but if there is more than 2 level, the change in root level only affects its sub-menu and not its entire tree
Here is the sandbox link for you to check.
Steps to reproduce:
On load, a menu is displayed (say menu)
Click on File, it will open its sub menu (say sub-menu1).
Click on Open in the sub-menu1, again another sub menu (say sub-menu2) is open.
Now when you click on Edit in menu, sub-menu1 disappears but not sub-menu2
I think, I know the problem. sub-menu2 is not refreshing because props or state is not changed. To hide it, we will need to trickle down some prop but can't think of elegant way to do it without state management system.
You'll have a better time if the ContextMenu component is responsible for state management and recursion is flattened into iteration.
function ContextItem({ item, onClick }) {
return (
<div className="menu-item" onClick={() => onClick(item)}>
<p className="menu-title">{item.title}</p>
{item.children && item.children.length > 0 ? <i className="right-icon">{">"}</i> : null}
</div>
);
}
function MenuList({ list, onClick }) {
return (
<div className="menu-container">
{list.map((listItem) => (
<ContextItem item={listItem} key={listItem.title} onClick={onClick} />
))}
</div>
);
}
const ContextMenu = ({ list }) => {
const [openSubmenus, setOpenSubmenus] = React.useState([]);
const clickHandler = React.useCallback((item, level) => {
if (item.children && item.children.length) {
setOpenSubmenus((oldItems) => {
return [...oldItems.slice(0, level), item.children];
});
} else {
setOpenSubmenus([]); // item selected, clear submenus
alert(item.title);
}
}, []);
const menus = [list, ...openSubmenus];
return (
<div className="menu">
{menus.map((menu, level) => (
<MenuList
key={level}
list={menu}
level={level}
onClick={(item) => clickHandler(item, level)}
/>
))}
</div>
);
};
const menuList = [{
title: "File",
children: [{
title: "Close",
children: [],
action: "fileClose",
}, {
title: "Open",
children: [{
title: "A:\\",
children: [],
action: "",
}, {
title: "C:\\",
children: [],
action: "",
}, {
title: "\\",
children: [],
action: "",
}],
action: "",
}, {
title: "Find",
children: [{
title: "here",
children: [],
}, {
title: "elsewhere",
children: [],
}],
action: "",
}, {
title: "Backup",
children: [],
action: "backup",
}],
action: "",
}, {
title: "Edit",
children: [],
action: "edit",
}];
function App() {
return <ContextMenu list={menuList} />;
}
ReactDOM.render(<App />, document.getElementById("root"));
.menu {
display: flex;
flex-direction: row;
}
.menu-container {
display: flex;
flex-direction: column;
background-color: #eee;
border: 1px solid gray;
border-radius: 4px;
}
.menu-item {
display: flex;
flex-direction: row;
margin: 2px;
max-width: 200px;
line-height: 30px;
padding: 5px 10px;
}
.menu-title {
min-width: 80px;
height: 30px;
flex-grow: 1;
margin: 0;
vertical-align: middle;
}
.menu-title.active {
background-color: blue;
color: white;
}
.right-icon {
width: 25px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You could use the label as the key to reset the ContextMenu state when selectedItem change (assuming the label is unique at a given depth, but it seems reasonable, otherwise you could add unique ids).
export const ContextMenu = ({list}) => {
const [selectedItem, setSelectedItem] = useState();
return (
<div className="menu">
<div className="menu-container">
{list.map((listItem) => {
return (
<ContextItem
item={listItem}
key={listItem.title}
onClick={setSelectedItem}
/>
);
})}
</div>
{selectedItem?.children.length > 0 && <ContextMenu
key={selectedItem.title}
list={selectedItem.children}/>}
</div>
);
};

React Star Widget - why do all stars update on single click?

I am trying to create a star widget. I have a state array for each star, but when I click one of the stars, ALL of the stars set themselves to that state. I am very lost on this, please halp. I have added a lot of debugging logs. The moment I set newStars[i] = currentStar;, the entire newStars array gets updated, but I'm failing to see why.
Also, here is the code pen link: https://codepen.io/trismi/pen/zYZpvQq?editors=1111
HTML:
<div id="root">
</div>
CSS (plus the awesome fonts stylesheet linked in the codepen)
.star {
display: inline-block;
width: 30px;
text-align: center;
color: #ddd;
font-size: 20px;
transform: scale(.8);
transition: transform 50ms ease;
&:hover,
&.semi-active {
color: gold;
transform: scale(1);
}
&.selected {
color: orange;
transform: scale(1);
}
}
JAVASCRIPT
function Star(props) {
console.log(props);
console.log(props.index);
let classes = 'star' + (props.selected ? ' selected' : '') + (props.hover ? ' semi-active' : '');
return (
<div className={classes} onClick={props.onClick}>
<i className="fas fa-star"></i>
</div>
);
}
class RatingWidget extends React.Component {
constructor(props){
super(props);
this.state = {
stars: Array(5).fill({
selected: false,
hover: false,
}),
}
}
handleClick(currentStar, index) {
console.log('\n\n\n******CLICK');
console.log("star state on click", currentStar);
console.log("index", index);
let newStars = this.state.stars.slice();
let newStar = newStars[index];
console.log("new star ", newStar);
newStar.selected = !newStar.selected;
newStars[index] = newStar;
console.log("stars", newStars);
this.setState({
stars: newStars
});
}
render() {
let stars = this.state.stars.map((rating, index) => {
return (
<Star
key={index}
index={index}
onClick={() => this.handleClick(rating, index)}
selected={rating.selected}
hover={rating.hover}
/>);
});
return (
<div className="RatingWidget">
Future rating widget
{stars}
</div>
);
}
}
ReactDOM.render(<RatingWidget />, document.getElementById('root'));
The problem is here:
Array(5).fill({
selected: false,
hover: false,
})
you are filling the same object (same reference) to each element of the array.
Try using:
Array(5).fill(null).map(() => ({
selected: false,
hover: false,
}))
Or use Array.from():
Array.from({length: 5}, () => ({ selected: false, hover: false}))
You can have the below handleClick function
I updated let newStar = newStars[index]; to let newStar = {...newStars[index]};
handleClick(currentStar, index) {
console.log('\n\n\n******CLICK');
console.log("star state on click", currentStar);
console.log("index", index);
let newStars = this.state.stars.slice();
let newStar = {...newStars[index]};
console.log("new star ", newStar);
newStar.selected = !newStar.selected;
newStars[index] = newStar;
console.log("stars", newStars);
this.setState({
stars: newStars
});
}

Footer buttons on dynamic dialog

I'm using a dynamic dialog and I want to have some buttons on footer, however it seems that only text is allowed for footer in this component.
const ref = this.dialogService.open(TermsComponent, {
data: {
entity: response.IDEntity,
user: response.IDUser
},
header: this.translate.instant('resTerminosCond'),
width: '70%',
footer: `
<button mz-button class="btnLoginAgree" (click)="termsAccepted()" translate>
resAceptar
</button>
<button mz-button class="btnLoginDisagree" (click)="onAcceptTerms(false);" translate>
resRechazar
</button>`
});
Any help is appreciated!
use like this for latest version PrimeNG p- instead of ui- it will work.
<div class="p-dialog-footer">
Currently primeng does not have a feature that allows that.
But you can do this:
First set a styleClass for the dynamic Dialog.
this.ref = this.dialogService.open(TermsComponent, {
data: {
entity: response.IDEntity,
user: response.IDUser
},
header: this.translate.instant('resTerminosCond'),
width: '70%',
styleClass: 'dynamicDialog',
});
Then in your app.component.scss remove the padding:
::ng-deep .dynamicDialog .p-dialog-content{
padding: 0
}
Then in your TermsComponent.html add 2 classes. One for the content and another one for the footer.
<div class="container">
<!-- your content-->
</div>
<div class="footer">
<button mz-button class="btnLoginAgree" (click)="termsAccepted()" translate>
resAceptar
</button>
<button mz-button class="btnLoginDisagree" (click)="onAcceptTerms(false);" translate>
resRechazar
</button>
</div>
And finally TermsComponent.scss:
:host {
.container {
padding: 3rem 1.5rem 2rem 1.5rem;
}
.footer {
border-top: 0;
background: white;
padding: 1rem;
text-align: right;
border-bottom-right-radius: 2px;
border-bottom-left-radius: 2px;
display: flex;
justify-content: flex-end;
}
.footer button {
margin: 0 .5rem 0 0;
width: auto;
}
}
In order to add buttons in the DynamicDialog footer you can do the following using renderer2:
Define new custom type
export type PrimeBtnObj = {text:string,icon:string,btnClass:string,callBack:(event:any) => void}
In Dialog component Define array of new type PrimeBtnObj
btns: PrimeBtnObj[] = [
{
btnClass: 'p-button-text',
icon: 'pi pi-check',
text: 'OK',
callBack: () => this.close() //function you want to call on click this button
},
{
btnClass: 'p-button-rounded',
icon: 'pi pi-times',
text: 'Hello',
callBack: () => this.ss() //function you want to call on click this button
}
]
Wrap your content html of the dialog component inside div element with id e.g:'content'
Declare the following function in dialog component
addBtnToDialogFooter(btns: PrimeBtnObj[]) {
//get parent element of content
const content = document.getElementById('content').parentNode.parentNode.parentNode
const divFooter = this._renderer2.createElement('div')
this._renderer2.setAttribute(divFooter, 'class', 'p-dialog-footer')
for (let index = 0; index < btns.length; index++) {
const element = btns[index];
const button = this._renderer2.createElement('button')
const spanIcon = this._renderer2.createElement('span')
const spanText = this._renderer2.createElement('span')
const textToSpan = this._renderer2.createText(element.text)
this._renderer2.appendChild(spanText, textToSpan)
this._renderer2.setAttribute(spanText, 'class', 'p-button-label')
this._renderer2.setAttribute(spanIcon, 'class', `p-button-icon p-button-icon-left ${element.icon}`)
this._renderer2.setAttribute(button, 'class', `p-ripple p-button ${element.btnClass}`)
this._renderer2.setAttribute(button, 'type', 'button')
this._renderer2.appendChild(button, spanIcon)
this._renderer2.appendChild(button, spanText)
this._renderer2.appendChild(divFooter, button)
content.appendChild(divFooter)
this._renderer2.listen(button, 'click', element.callBack)
}
}
call addBtnToDialogFooter(this.btns) inside ngOnInit()
For DynamicDialog, you can just use primeNg class "ui-dialog-footer" and place the buttons inside it.
<div class="ui-dialog-footer">
<button mz-button class="btnLoginAgree" (click)="termsAccepted()" translate>
resAceptar
</button>
<button mz-button class="btnLoginDisagree" (click)="onAcceptTerms(false);" translate>
resRechazar
</button>
</div>
So, this may be a late WORKAROUND, and not the best solution,but to make the Dynamic Dialog footer look alike the Dialog footer I created a custom dialog service:
import { Injectable, Type } from '#angular/core';
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
#Injectable({
providedIn: 'root',
})
export class CustomDialogService{
dialogRef!: DynamicDialogRef;
constructor(private _dialogService: DialogService) { }
/**
* #param content any date you need to pass
* #param component component to open as dialog
* #param header dialog title
* #param customFooter if true have a custom footer else not
* #returns dialog reference
*/
openDialog(
content: any,
component: Type<any>,
header: string,
customFooter?: boolean
): DynamicDialogRef {
let data = undefined;
if (content) data = { content: content };
this.dialogRef = this._dialogService.open(component, {
header: header,
width: '70%',
contentStyle: { overflow: 'auto' },
baseZIndex: 10000,
maximizable: true,
data: data,
footer: customFooter ? '---' : undefined,
});
return this.dialogRef;
}
}
The customFooter flag defines if there will be a custom footer over the default one. This help the scroll overflow not get behind the custom footer.
Don`t forget to place it in your "app.module" in the providers.
To open the dialog:
...
constructor(private _cDialogService: CustomDialogService) { }
clickButton() {
this._cDialogService.openDialog({}, YourComponent, 'Title', true);
}
...
We will also need a css class to add to the footer div, and guarantee the scroll overflow wont be behind the custom footer.
I added this code in the style.scss, you can do it in the component that will be opened.
.app-dialog-footer {
display: block !important;
position: absolute !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
padding: 8px !important;
}
If placing it in the style.scss you will need to add "!important" at the end.
Now in the component html:
<div class="p-dialog-footer app-dialog-footer">
<!-- your html content -->
</div>

React-select, open sub-menu when hover over an option

I'm trying to build a submenu inside a main menu with React-select, it should be something like this:
When hovering over an option from the main menu, it triggers the submenu to open at the side.
Is there a way to do this using react-select? I couldn't find any example or documentation on this, is there a function like ```optionOnMouseover`` for this? Thank you in advance!
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
...
<Select
value={...}
onChange={...}
options={options}
/>```
This is on click, but if you need on hover,
just modify it
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Select, { components } from "react-select"
const CustomOption = (props) => {
const [submenu, setSubmenu] = useState(false)
const [height, setHeight] = useState(0)
const handleOption = (e) => {
if(submenu) {
setSubmenu(false)
} else {
setHeight(e.clientY)
setSubmenu(true)
}
}
const handleSubOption = (e) => {
console.log('clicked')
}
const { data } = props;
return data.custom ? (
<>
<div onClick={handleOption} className="customs">
{data.label} <span className="caret"/>
{
submenu && (
<div className="dropdown-submenu">
<div className="drops" onClick={handleSubOption}>
Test dropdown 1
</div>
<div className="drops" onClick={handleSubOption}>
Test dropdown 2
</div>
<div className="drops" onClick={handleSubOption}>
Test dropdown 3
</div>
</div>
)
}
</div>
<style jsx>{`
.customs {
height: 36px;
padding: 8px;
position: relative;
}
.drops {
height: 36px;
padding: 8px;
}
.customs:hover, .drops:hover {
background-color: #17cf76;
}
.dropdown-submenu {
position: fixed;
top: ${height - 10}px;
left: 410px;
min-height: 36px;
overflow: auto;
border: 1px solid hsl(0,0%,80%);
border-radius: 4px;
color: #212529;
}
`}</style>
</>
) : (
<components.Option {...props} />
);
};
const options = [
{ custom: true, label: "I'm a custom link", value: "cust" }
];
function App() {
return (
<>
<Select classNamePrefix="category-select" className="w-30" components={{ Option: CustomOption }} options={options} />
<style jsx global>{`
* {
font-family: sans-serif;
text-align: center;
}
.w-30 {
width: 30% !important;
}
`}</style>
</>
)
}
export default App

Navigation menu not working in React

Please help me to fix this navigation menu.Something here is not working.It has to change the clicked cell after click. I would be very grateful if you show me where is the problem
class MenuExample extends React.Component{
constructor(props) {
super(props);
this.state = {focused: 0};
}
clicked(index){
this.setState({focused: index});
};
render: {
return (
<div>
<ul>{ this.props.items.map(function(m, index){
var style = '';
if(this.state.focused == index){ style = 'focused'; }
return <li className={style} onClick={this.clicked.bind(this)}>{m}</li>;
}) }
</ul>
<p>Selected: {this.props.items[this.state.focused]}</p>
</div>
);
}
};
ReactDOM.render(
<MenuExample items={ ['Home', 'Services', 'About', 'Contact us'] } />,
document.getElementById('root')
);
Its a binding issue, you forgot to bind the map callback method, here:
this.props.items.map(function(m, index){.....})
Use arrow function to maintain the context, like this:
this.props.items.map((m, index) => {.....})
Check the working code:
class MenuExample extends React.Component{
constructor(){
super();
this.state = { focused: 0 };
}
clicked(index){
this.setState({focused: index});
}
render() {
return (
<div>
<ul>{ this.props.items.map((m, index) => {
var style = '';
if(this.state.focused == index){
style = 'focused';
}
return <li className={style} onClick={this.clicked.bind(this, index)}>{m}</li>
}) }
</ul>
<p>Selected: {this.props.items[this.state.focused]}</p>
</div>
);
}
}
ReactDOM.render(
<MenuExample items={ ['Home', 'Services', 'About', 'Contact us'] } />,
document.getElementById('container')
);
* {
padding:0;
margin:0;
}
html{
font:14px normal Arial, sans-serif;
color:#626771;
background-color:#fff;
}
body{
padding:60px;
text-align: center;
}
ul{
list-style:none;
display: inline-block;
}
ul li{
display: inline-block;
padding: 10px 20px;
cursor:pointer;
background-color:#eee;
color:#7B8585;
transition:0.3s;
}
ul li:hover{
background-color:#beecea;
}
ul li.focused{
color:#fff;
background-color:#41c7c2;
}
p{
padding-top:15px;
font-size:12px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='container'/>
Working Fiddle.

Resources