I'm creating a simple SPFX extension to manage a task list. I have created a list which gets items via REST through pnpjs.
I want to be able to delete said items by clicking on a button. When I click the delete button, a modal (WarningModal) opens, asking for confirmation. I have managed to create a working example, but I'm having a hard time understanding why it works. When I try to follow the guides in the official docs(https://reactjs.org/docs/faq-functions.html), I can't get my code to work.
Render method in parent component:
public render = (): React.ReactElement<ITakenbeheerProps> => {
const {
taken,
showErrorMessage,
errorMessage,
showSuccessMessage,
successMessage,
isDataLoaded,
showTryAgainButton,
showDeleteModal,
deleteModalTitle,
deleteModalContent,
deleteModalOkCallback,
deleteModalCancelCallback,
} = this.state;
const { ShimmerCollection } = this;
let AssignTaskModal = this.AssignTaskModal;
let addTaskIcon: IIconProps = { iconName: "AddToShoppingList" };
return (
<Stack
tokens={this.verticalGapStackTokens}
className={styles.takenBeheer}
>
<ActionButton iconProps={addTaskIcon}>
{strings.NieuweTaakButton}
</ActionButton>
{showErrorMessage ? (
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
onDismiss={this.onResetAllMessageBars}
>
{errorMessage}
</MessageBar>
) : null}
{showSuccessMessage ? (
<MessageBar
messageBarType={MessageBarType.success}
isMultiline={false}
onDismiss={this.onResetAllMessageBars}
>
{successMessage}
</MessageBar>
) : null}
<Stack horizontalAlign={"center"}>
{showTryAgainButton && (
<PrimaryButton
text={strings.TryAgain}
onClick={this._populateList}
/>
)}
</Stack>
<ShimmerCollection />
{isDataLoaded ? (
<div>
{taken.findIndex((t) => t.toegewezenAan.name == null) > -1 ? (
<List
items={taken.filter((t) => {
return t.toegewezenAan.name == null;
})}
onRenderCell={this.onRenderCell}
/>
) : (
<div className={styles.noNewTasks}>{strings.NoNewTasks}</div>
)}
</div>
) : null}
<AssignTaskModal />
<WarningModal
isModalOpen={showDeleteModal}
title={deleteModalTitle}
content={deleteModalContent}
okCallback={deleteModalOkCallback}
cancelCallback={deleteModalCancelCallback}
/>
</Stack>
);};
The warning modal
Screenshot
import * as React from "react";
import * as strings from "CicOvlTakenlijstWebPartStrings";
import {
Modal,
DialogFooter,
DefaultButton,
Button,
getTheme,
mergeStyleSets,
FontWeights,
} from "office-ui-fabric-react";
/* Prop definition */
export interface IWarningModalProps {
isModalOpen: boolean;
title: string;
content: string;
okCallback: any;
cancelCallback: any;
}
/* State definition */
export interface IWarningModalState {}
export default class WarningModal extends React.Component<
IWarningModalProps,
IWarningModalState
> {
public constructor(props: IWarningModalProps) {
super(props);
this.state = {
/* State initialization */
isModalOpen: false,
};
}
public render(): React.ReactElement<IWarningModalProps> {
const {
isModalOpen,
okCallback,
cancelCallback,
title,
content,
} = this.props;
return (
<Modal
isOpen={isModalOpen}
isBlocking={false}
containerClassName={contentStyles.container}
>
<div className={contentStyles.header}>
{title}
</div>
<div className={contentStyles.body}>
{content}
<DialogFooter>
<DefaultButton onClick={okCallback} text={strings.OkMessage} />
<Button onClick={cancelCallback} text={strings.CancelMessage} />
</DialogFooter>
</div>
</Modal>
);
}
}
Why does this work? I would expect the code in the warning modal to be as following, with an arrow function:
<DialogFooter>
<DefaultButton onClick={() => okCallback()} text={strings.OkMessage} />
<Button onClick={() => cancelCallback()} text={strings.CancelMessage} />
</DialogFooter>
But when I click the buttons, nothing happens. The debugger also doesn't show any error message.
Related
I have a simple component with a few nested bits of mark-up:
import React, { ReactElement } from "react";
type MenuItem = {
title: string;
subItems?: Array<string>;
};
type MenuConfig = Array<MenuItem>;
function Item({ item }: { item: MenuItem }): ReactElement {
const [showItem, setShowItem] = React.useState(false);
const handleShowItem = (): void => {
setShowItem(!showItem);
};
return (
<>
{item.subItems && (
<button onClick={() => handleShowItem()}>
{showItem ? "Hide" : "Expand"}
</button>
)}
{showItem && <SubItem item={item} />}
</>
);
}
function SubItem({ item }: { item: MenuItem }): ReactElement {
const { title } = item;
return (
<ul>
{item?.subItems?.map((subitem: string, i: number) => (
<li key={i}>
{subitem}
</li>
))}
</ul>
);
}
function Solution({ menuConfig }: { menuConfig: MenuConfig }): ReactElement {
return (
<>
{menuConfig.map((item: MenuItem, i: number) => (
<div key={i}>
<span>{item.title}</span>
<Item item={item} />
</div>
))}
</>
);
}
export default Solution;
This is what I am passing in:
menuConfig={[
{
title: "About",
},
{
title: "Prices",
subItems: ["Hosting", "Services"],
},
{
title: "Contact",
subItems: ["Email", "Mobile"],
},
]}
Now, it functions as expected, if an item contains subItems then an Expand button will be show which, if clicked, will only expand the relevant list.
How should I go about making sure only one list would be open at a time, given my implementation?
So if the user clicks Expand on a button, the other previously expanded lists should close.
I can't mess with the data that's coming in, so can't add ids to the object, but titles are unique.
I've searched and whilst there are a few examples, they don't help me, I just can't wrap my head around this.
This is a perfect use case for React context. You essentially want a shared state among your menu. You can achieve this with prop drilling however contexts are a much cleaner solution.
Code sandbox
const MenuContext = createContext<{
expandedState?: [number, React.Dispatch<React.SetStateAction<number>>];
}>({});
function Solution(): ReactElement {
const expandedState = useState<number>(null);
const value = { expandedState };
return (
<>
<MenuContext.Provider value={value}>
{menuConfig.map((item: MenuItem, i: number) => (
<div key={i}>
<span>{item.title}</span>
<Item i={i} item={item} />
</div>
))}
</MenuContext.Provider>
</>
);
}
function Item({ item, i }: { item: MenuItem; i: number }): ReactElement {
const [expanded, setExpanded] = useContext(MenuContext)?.expandedState ?? [];
const shown = expanded === i;
return (
<>
{item.subItems && (
<button onClick={() => setExpanded?.(i)}>
{shown ? "Hide" : "Expand"}
</button>
)}
{shown && <SubItem item={item} />}
</>
);
}
So I want when I press the button in the Button Component everything in the 'li section' disappears as well as in the ImageComponent but it not working I would like to know what my mistake is. ButtonComponent is rendered somewhere else.
App Component/Parent
function App({ hideButton }) {
return (
<div className="App">
<ImageComponent hideButton={hideButton} />
</div>
);
}
// ButtonComponent
function ButtonComponent() {
const [hideButton, setHideButton] = React.useState(false)
function handleClick() {
setHideButton(true)
}
return (
{
!hideButton && (
<li>
<img className="image"src="./icons/>
<Button onClick={handleClick} variant="outlined" className="button__rightpage" >Hide</Button>
<caption className="text"> Hide</caption>
</li >
)
}
)
}
// ImageComponent
const ImageComponent = ({ hideButton }) => {
return (
<div>
{
!hideButton && (
<div>
<img src='icons/icon.png' />
<caption>Image </caption>
</div>
)
}
</div>
)
}
you need to lift up the state to the most parent Component be accessible to the ButtonCommponent and the ImageComponent. in this Case App Component. however, if the ButtonComponent is rendered out of the hierarchy under the App Component tree, you should use the context API.
create a context and share the state on it and it will be accessible on the application level.
//#1. create context.
export const HiddenContext = React.createContext(false);
//#2. create the provider and share the state with it.
function HiddenProvider({ children }) {
const [hideButton, setHideButton] = React.useState(false);
function handleClick() {
setHideButton(true);
}
return (
<HiddenContext.Provider value={{ hideButton, handleClick }}>
{children}
</HiddenContext.Provider>
);
}
//#3. render the provider component to the most top parent component
export default function App() {
const { hideButton } = React.useContext(HiddenContext);
return (
<HiddenProvider>
<div className="App">
<ImageComponent hideButton={hideButton} />
<OtherComponentRenderTheButton />
</div>
</HiddenProvider>
);
}
// other component that render the button
function OtherComponentRenderTheButton() {
return <ButtonComponent />;
}
//ButtonComponent
function ButtonComponent() {
const { hideButton, handleClick } = React.useContext(HiddenContext);
return (
<React.Fragment>
{!hideButton && (
<li>
<img className="image" src="./icons" alt="" />
<Button
onClick={handleClick}
variant="outlined"
className="button__rightpage"
>
Hide
</Button>
<caption className="text"> Hide</caption>
</li>
)}
</React.Fragment>
);
}
// ImageComponent
const ImageComponent = () => {
const { hideButton } = React.useContext(HiddenContext);
return (
<div>
{!hideButton && (
<React.Fragment>
<img src="icons/icon.png" alt="" />
<caption>Image </caption>
</React.Fragment>
)}
</div>
);
};
working demo
I've created a React component that takes inputs from other components to display text of various size in a view. Since it is basically a form, what I want to do is pass the current view into another page where I will then post that view to my database as JSON.
Since the state of the input fields are not set in this component, I'm not sure how I would pass them as props to a new view.
This is a condensed version of what my data input component looks like:
INPUTSHOW.JSX
export default class InputShow extends Component {
componentDidMount() {
autosize(this.textarea);
}
render() {
const { node } = this.props;
...
return (
<div className="editor-div" >
{
(node.type === 'buttonA') ?
<textarea
style={hlArea}
ref={a => (this.textarea = a)}
placeholder="type some text"
rows={1}
defaultValue=""
id={node.id} className='editor-input-hl' type="text" onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<textarea
style={subArea}
ref={b => (this.textarea = b)}
placeholder="type some text"
rows={1}
defaultValue=""
id={node.id} className='editor-input-sub' type="text" onChange={this.props.inputContentHandler} />
:
""
}
</div >
)
}
}
This works fine in creating inputs in a current view. I then pass those values to TextAreaField.JSX
export default (props) => {
return (
<>
<button><Link to={{
pathname: '/edit/preview',
text: props.inputsArray
}}>preview</Link></button>
<div className='view'>
{
props.inputsArray.map(
(node, key) => <InputShow key={key} node={node} inputContentHandler={props.inputContentHandler} />
)
}
</div>
</>
)
}
and then finally that is rendered in my Edit.JSX form:
export default class Edit extends React.Component {
constructor(props) {
super(props)
UniqueID.enableUniqueIds(this);
this.state = {
inputs: [],
text: ''
}
}
...
createPage = async () => {
await this.props.postPage(this.state.text)
}
// Handler for listen from button.
buttonCheck = (e) => {
index++;
const node = {
id: this.nextUniqueId() + index,
type: e.target.id,
text: '',
image: true
}
this.setState(
prev => ({
inputs: [...prev.inputs, node]
})
)
console.log(this.state.inputs);
}
inputContentHandler = (e) => {
let newArray = this.state.inputs;
let newNode = newArray.find((node) => {
return (node.id === e.target.id)
})
newNode.text = e.target.value;
this.setState({ inputs: newArray });
console.log(this.state.inputs);
}
render() {
return (
<div>
<InnerHeader />
<div className='some-page-wrapper'>
<div className='row'>
<div className="dash-card-sm">
<br />
<EditButtonContainer buttonCheck={this.buttonCheck} />
<Route path='/edit/form' render={() => (
<TextAreaField
inputsArray={this.state.inputs}
inputContentHandler={this.inputContentHandler}
/>
)}
/>
<Route path='/edit/preview' render={(props) => (
<Preview
inputs={this.state.inputs}
text={this.state.text}
createPage={this.createPage}
/>
)}
/>
<br /> <br />
{/* Button Header */}
</div>
</div>
</div>
</div>
)
}
}
The problem is that I don't know how I should be passing the rendered view to the Preview.jsxcomponent. I'm still new to react (4 months)...Any help in pointing me in the right direction would be appreciated.
I am trying to display multiple instances of a component on a page. Each instance should appear within a Tab.Pane (Semantic UI feature). However in my attempt to achieve this, I am getting some strange behaviour as explained in more detail below.
I have a component; <MultistepFilterSection />
Within this component the state is:
this.state = {
conjunction: "AND" // default
}
This state is modified by a onClick handler function in the component;
handleConjunctionButtonClick = (conjunction, e) => {
this.setState({conjunction: conjunction})
}
This onClick is triggered by clicking on one of two buttons:
<Button.Group vertical>
<Button
onClick={(e) => this.handleConjunctionButtonClick("AND")}
color={this.state.conjunction === "AND" ? "blue" : null}
>
And
</Button>
<Button
onClick={(e) => this.handleConjunctionButtonClick("OR")}
color={this.state.conjunction === "OR" ? "blue" : null}
>
Or
</Button>
</Button.Group>
I am using Semantic UI's Tab component to render 3 instances of this component on a page;
const panes = [
{
menuItem: "Panel Data",
render: () =>
<Tab.Pane attached={false}>
<MultistepFilterSection getAudience={this.getAudience} />
</Tab.Pane>
},
{
menuItem: "Geolocation Data",
render: () =>
<Tab.Pane attached={false}>
<MultistepFilterSection getAudience={this.getAudience} />
</Tab.Pane>
},
{
menuItem: "Combined Audience Selection",
render: () =>
<Tab.Pane attached={false}>
<MultistepFilterSection getAudience={this.getAudience} />
</Tab.Pane>
}
]
In the render method I have:
<Tab
menu={{ secondary: true, pointing: true }}
panes={panes}
/>
Triggering the onClick handler in any one of the tabs changes the state in all instances of the component. Is this normal? I thought that the component state in one instance of the component is exclusive to that instance.
On further investigation, I found that the behaviour is exhibited when I'm instantiating multiple instances of the <MultistepFilterSection /> using Semantic UI's Tab component. When rendering the instances on their own, the exhibit expected behaviour.
The full code of the <MultistepFilterSection /> component:
import React from "react";
import { Grid, Button, Container, Icon, Segment } from "semantic-ui-react";
import { connect } from "react-redux";
import uuid from "uuid";
import _ from "lodash";
import RuleGroup from "../rules/RuleGroup";
import { filterGroupCreated, filterGroupDeleted, filterDeleted } from "../../actions/FiltersAction"
export class MultistepFilterSection extends React.Component {
constructor(props) {
super(props);
this.state = {
conjunction: "AND" // default
}
// create one default group
this.props.filterGroupCreated({filterGroupId: uuid()})
this.handleAddRuleGroupButtonClick = this.handleAddRuleGroupButtonClick.bind(this);
this.renderRuleGroups = this.renderRuleGroups.bind(this);
this.handleConjunctionButtonClick.bind(this)
}
handleAddRuleGroupButtonClick() {
// console.log("called handleAddRuleGroupButtonClick()")
const filterGroupId = uuid()
let data = {}
data.filterGroupId = filterGroupId
this.props.filterGroupCreated(data)
}
handleConjunctionButtonClick = (conjunction, e) => {
this.setState({conjunction: conjunction})
}
renderRuleGroups() {
// console.log("called renderRuleGroups()")
return Object.values(this.props.filterGroups).map(function(ruleGroup) {
const key = ruleGroup.id;
// incomplete
let currentGenderSelected
return (
<React.Fragment key={key}>
<RuleGroup
id={key}
key={key}
deleteRuleGroup={this.deleteRuleGroup}
deleteFilter={this.deleteFilter}
currentGenderSelected={currentGenderSelected}
/>
</React.Fragment>
)
}.bind(this))
}
deleteRuleGroup = (id, e) => {
// console.log("delete rule group called")
this.props.filterGroupDeleted({filterGroupId: id})
}
deleteFilter = (filterGroupId, filterId, e) => {
// console.log("deleteFilter() called")
this.props.filterDeleted({
filterGroupId: filterGroupId,
filterId: filterId
})
}
render() {
const isFilterGroupQuery = true
return(
<Grid padded="vertically">
<Grid.Row stretched>
<Grid.Column verticalAlign={"middle"} width={2}>
{_.size(this.props.filterGroups) > 0 &&
<Segment basic>
<Button.Group vertical>
<Button
onClick={(e) => this.handleConjunctionButtonClick("AND")}
color={this.state.conjunction === "AND" ? "blue" : null}
>
And
</Button>
<Button
onClick={(e) => this.handleConjunctionButtonClick("OR")}
color={this.state.conjunction === "OR" ? "blue" : null}
>
Or
</Button>
</Button.Group>
</Segment>
}
</Grid.Column>
<Grid.Column width={14}>
{this.renderRuleGroups()}
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={2}></Grid.Column>
<Grid.Column width={3}>
<Button
color="grey"
basic
onClick={this.handleAddRuleGroupButtonClick}
>
<Icon name="plus" />Add Rule Group
</Button>
</Grid.Column>
<Grid.Column width={11}>
<Button
color="blue"
onClick={
(isFilterGroupQuery) => this.props.getAudience(
isFilterGroupQuery, this.state.conjunction
)
}
floated={"right"}
>
Run query
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
)
}
}
function mapStateToProps(state) {
return {
filterGroups: state.filters
};
}
export default connect(
mapStateToProps,
{
filterGroupCreated,
filterGroupDeleted,
filterDeleted
}
)(MultistepFilterSection);
After much digging around and eventually posting an issue on Semantic UI's GitHub repo, I learned that I needed to use renderActiveOnly={false} prop on the Tab component. I also had to change my markup so that panes objects were in the following format:
const panes = [
{
key: 1,
menuItem: "Panel Data",
pane: <Tab.Pane><Simple key={1} /></Tab.Pane>
},
{
key: 2,
menuItem: "Geolocation Data",
pane: <Tab.Pane><Simple key={2} /></Tab.Pane>
}
]
I am making a form like
I want the add button to be active whenever user is changing the "Tags" input text.
I am using material-ui and I made a Input component.
const SingleInput = (props) => (
<Fragment>
<FormControl margin="normal" required fullWidth>
<TextField
id={props.id}
type={props.type}
name={props.name}
label={props.label}
value={props.content}
variant={props.variant}
placeholder ={props.placeholder}
onChange={props.controlFunc}>
</TextField>
</FormControl>
</Fragment>
);
export default SingleInput;
and I import this into my form and like:
class AddCompanyForm extends React.Component {
constructor() {
super();
this.state = {
company_name: "",
company_description: "",
company_tag: "",
company_tags: "",
company_contact: "",
disabled: true
};
this.handleOnSubmit = this.handleOnSubmit.bind(this);
this.handleOnChange = this.handleOnChange.bind(this);
this.handleOnSelect = this.handleOnSelect.bind(this);
}
handleOnChange(e) {
e.preventDefault();
this.setState({ [e.target.name]: e.target.value });
this.setState({ disabled: false });
console.log("teg", this.state.company_tag.length);
console.log("cont", this.state.company_contact.length);
if (this.state.company_tag.length == 1) {
this.setState({ disabled: true });
}
}
handleOnSubmit(e) {
e.preventDefault();
this.props.createCompany(this.state);
}
handleOnSelect(e) {
e.preventDefault();
chipsValue.push(this.state.company_tag);
this.setState({
company_tags: chipsValue,
company_tag: "",
disabled: true
});
}
render() {
return (
<Paper style={styles.paper}>
<Avatar>
<LockIcon />
</Avatar>
<Typography variant="headline">Add a New Company</Typography>
<form onSubmit={this.handleOnSubmit}>
<SingleInput
id={"company_name"}
type={"company_name"}
name={"company_name"}
label={"Company Name"}
content={this.state.company_name}
controlFunc={this.handleOnChange}
variant={"filled"}
/>
<SingleInput
id={"company_description"}
type={"company_description"}
name={"company_description"}
label={"Description"}
content={this.state.company_description}
controlFunc={this.handleOnChange}
variant={"filled"}
/>
<SingleInput
id={"company_tags"}
type={"company_tags"}
name={"company_tag"}
label={"Tags (to add dropdown here!)"}
content={this.state.company_tag}
controlFunc={this.handleOnChange}
variant={"filled"}
/>
<Button
disabled={this.state.disabled}
onClick={this.handleOnSelect}
variant="raised"
color="secondary"
>
Add
</Button>
<SingleInput
id={"company_contact"}
type={"company_contact"}
name={"company_contact"}
label={"Contact"}
content={this.state.company_contact}
controlFunc={this.handleOnChange}
variant={"filled"}
/>
<Button type="submit" fullWidth variant="raised" color="primary">
Add Company
</Button>
</form>
</Paper>
);
}
}
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
createCompany
},
dispatch
);
export default connect(
null,
mapDispatchToProps
)(AddCompanyForm);
Now the problem is even when I change "Company Name" or any other input button, the Add button becomes enabled.
Any help is very much appreciated.
Check the example below using React Hooks for the button state, and the onChange property of the TextField to set it.
export default function TextFieldAndButton (props) {
const [btnDisabled, setBtnDisabled] = useState(true)
return (
<>
<TextField
onChange={(text) => setBtnDisabled(!text.target.value)}
/>
<Button disabled={btnDisabled}>OK</Button>
</>
)
}
The problem here is that setState is async and you are using state values inside handleOnChange before it is updated. Either use setState callback to calculate disable or a better way is to calc disabled in render. This approach makes it much simpler and even works while rendering for the first time.
render() {
const disabled = !this.state.company_tag.length;
return (
// ...
<Button
disabled={disabled}
onClick={this.handleOnSelect}
variant="raised"
color="secondary"
>
Add
</Button>
// ...
);
}