Attempts to add an icon to option in react-select. I imported svg icons from the files england.svg, germany.svg. I created customSingleValue and put it in
<Select components={{ SingleValue: customSingleValue }} />
Labels are displayed, but the icons are not.
Demo here: https://stackblitz.com/edit/react-q19sor
import Select from 'react-select'
import { ReactComponent as IconFlagEngland } from "./england.svg";
import { ReactComponent as IconFlagGermany } from "./germany.svg";
const options = [
{ value: 'England', label: 'England', icon: <IconFlagEngland/> },
{ value: 'Germany', label: 'Germany', icon: <IconFlagGermany/> }
]
const customSingleValue = ({ options }) => (
<div className="input-select">
<div className="input-select__single-value">
{ options.icon && <span className="input-select__icon">{ options.icon }</span> }
<span>{ options.label }</span>
</div>
</div>
);
class App extends Component {
constructor() {
super();
this.state = {
name: 'React'
};
}
render() {
return (
<Select
defaultValue={ options [0] }
options={ options }
/*styles={ selectCustomStyles }*/
/*onChange={ changeSelectHandler }*/
components={ {SingleValue: customSingleValue } }
/>
);
}
}
render(<App />, document.getElementById('root'));
I found a workaround how to solve it. My technique is similar to #canda:
import React, { Component } from "react";
import { render } from "react-dom";
import "./style.css";
import Select, { components } from "react-select";
const options = [
{ value: "England", label: "England", icon: "england.svg" },
{ value: "Germany", label: "Germany", icon: "germany.svg" }
];
const { Option } = components;
const IconOption = props => (
<Option {...props}>
<img
src={require('./' + props.data.icon)}
style={{ width: 36 }}
alt={props.data.label}
/>
{props.data.label}
</Option>
);
class App extends Component {
constructor() {
super();
this.state = {
name: "React"
};
}
render() {
return (
<Select
defaultValue={options[0]}
options={options}
components={{ Option: IconOption }}
/>
);
}
}
render(<App />, document.getElementById("root"));
Just in case anybody want to use icons with multiple selection in react-select, below code can be used:
const { MultiValue } = components;
const MultiValueOption = (props) => {
return (
<MultiValue {...props}>
<img
src={require("./" + props.data.icon)}
style={{ width: 36 }}
alt={props.data.label}
/>
<span>{props.data.value}</span>
</MultiValue>
);
};
<Select
options={options}
components={{
Option: IconOption,
MultiValue: MultiValueOption,
}}
isMulti={true}
></Select>;
I think the problem is that you are not actually importing the SVGs. If you try to use <IconFlagGermany/> directly in your code anywhere, it will crash hard with this message :
Element type is invalid: expected a string (for built-in components)
or a class/function (for composite components) but got: undefined. You
likely forgot to export your component from the file it's defined in,
or you might have mixed up default and named imports.
It is currently not crashing because I think your customSingleValue function is not working as you intend it to (have not looked into it, but pretty sure it is bugged).
If you want to be able to import SVGs in this manner, you need to setup an appropriate loader in Webpack (or your chosen bundler). Maybe something like this : https://www.npmjs.com/package/react-svg-loader
However, another solution is to properly export your SVGs as components, like in this demo forked from your code : https://stackblitz.com/edit/react-5gvytm
This is how I did with the help of #Penny Lui
import React from 'react';
import Select, { components } from 'react-select';
import { ReactComponent as MyIcon } from './my-icon.svg';
const options = [
{
value: 'hello',
label: 'Hello',
Icon: MyIcon,
},
// ...
];
const { Option } = components;
function IconOption(props: any) {
const {
data: { label, Icon },
} = props;
return (
<Option {...props}>
<div className="flex items-center gap-2">
{Icon && <Icon />}
<span>{label}</span>
</div>
</Option>
);
}
export default function App() {
return (
<Select
options={options}
components={{ Option: IconOption }}
// ...
/>
);
}
This method also worked
<SelectComponent
options={options}
getOptionLabel={(props: any) => {
const { Icon, label } = props;
return (
<div tw="flex items-center gap-2">
{Icon && <Icon />}
<span>{label}</span>
</div>
) as unknown as string;
}}
/>
Related
I'm using ant design pro.
The idea is In the platform we have 2 languages to choose from Fr(French) and En(English),
I want the user when he logs in and change the language to English for example when he logs out and log in again the language should be saved to English so he would be able to see the content in English, I managed to do it, when i login in the backend the preferedLanguage = en, the language toggle in the also changes to en, the only problem the web page content stays in French it's only change in English when i reload the page.
I think the issue is related to the login page, the login page is set to French as default , let's say my preferred language now is English, if i login from the login page. The page content loaded in French only changes when i reload.
-This is the umijs documentation : https://umijs.org/docs/max/i18n#setlocale-%E8%AE%BE%E7%BD%AE%E8%AF%AD%E8%A8%80
-LanguageDropdown (the toggle where you select the language (Fr or En)
import type { FC } from 'react';
import React, { useState, useEffect } from 'react';
import { Menu, Dropdown } from 'antd';
import { CaretDownOutlined, CaretUpOutlined, GlobalOutlined } from '#ant-design/icons';
import styles from './index.less';
import { setLocale, getLocale, getAllLocales } from 'umi';
interface Props {
setUpdateLang: any;
currentLang: string;
}
const LanguageDropdown: FC<Props> = ({ currentLang, setUpdateLang }) => {
const [langvisible, setLangVisible] = useState<boolean>(false);
const [localesList, setLocalesList] = useState<any[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>('');
// useEffect(() => {
// if (currentLang) {
// setLocalesList(getAllLocales());
// setCurrentLocale(getLocale());
// setLocale(currentLang === 'fr' ? 'fr-FR' : 'en-US');
// }
// alert(currentLang);
// alert(getLocale());
// }, [currentLang]);
useEffect(() => {
setLocalesList(getAllLocales());
setCurrentLocale(getLocale());
if (currentLang) {
// alert(222);
const selectedLang = currentLang === 'fr' ? 'fr-FR' : 'en-US';
// setNewLang(selectedLang);
setLocale(selectedLang, false);
setCurrentLocale(getLocale());
}
}, [currentLang]);
const onLangVisibleChange = (visibleLang: boolean) => {
setLangVisible(visibleLang);
};
const langNameHandler = (lang: string) => {
if (lang === 'en-US') return 'EN';
else if (lang === 'fr-FR') return 'FR';
return 'FR';
};
const setNewLang = (lang: string) => {
setUpdateLang({ lang: langNameHandler(lang).toLocaleLowerCase(), updated: true });
setLocale(lang);
};
const Langmenu = (
<Menu>
{localesList?.map((lang: any) => (
<Menu.Item key={lang}>
<a onClick={() => setNewLang(lang)}>{langNameHandler(lang)}</a>
</Menu.Item>
))}
</Menu>
);
return (
<div className={styles.profileDropdownContainer}>
<Dropdown
overlay={Langmenu}
placement="bottomLeft"
trigger={['click']}
onVisibleChange={onLangVisibleChange}
className={styles.dropdown}
>
<div className={styles.langContainer}>
<span>
<GlobalOutlined /> {langNameHandler(currentLocale)}
</span>
{!langvisible ? <CaretDownOutlined /> : <CaretUpOutlined />}
</div>
</Dropdown>
</div>
);
};
export default LanguageDropdown;
-RightContext
import { Tag } from 'antd';
import type { Settings as ProSettings } from '#ant-design/pro-layout';
import React, { useEffect, useState } from 'react';
import type { ConnectProps } from 'umi';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { ConnectState } from '#/models/connect';
import Avatar from './AvatarDropdown';
import styles from './index.less';
import LanguageDropdown from '../languageDropdown';
import moment from 'moment';
export type GlobalHeaderRightProps = {
dispatch: Dispatch;
theme?: ProSettings['navTheme'] | 'realDark';
auth: any;
users: any;
platformLanguage: any;
data: any;
} & Partial<ConnectProps> &
Partial<ProSettings>;
const ENVTagColor = {
dev: 'orange',
test: 'green',
pre: '#87d068',
};
const GlobalHeaderRight: React.FC<GlobalHeaderRightProps> = (props) => {
const [updateLang, setUpdateLang] = useState<{ lang: string; updated: boolean }>({
lang: '',
updated: false,
});
const [currentLang, setCurrentLang] = useState<any>(null);
const { theme, layout, auth, platformLanguage, data, dispatch } = props;
let className = styles.right;
useEffect(() => setCurrentLang(platformLanguage), [platformLanguage]);
useEffect(() => {
if (updateLang.updated) {
const {
organization,
roles,
email,
deleted,
department,
createdById,
organizationId,
...rest
} = data;
const birthdate = moment(rest.birthdate).format('YYYY-MM-DD');
const workversary = moment(rest.workversary).format('YYYY-MM-DD');
dispatch({
type: 'users/updateMe',
payload: {
data: { ...rest, birthdate, workversary, platformLanguage: updateLang.lang },
userId: auth?.currentUser.id,
},
});
setUpdateLang({ ...updateLang, updated: false });
setCurrentLang(updateLang.lang);
}
}, [updateLang]);
if (theme === 'dark' && layout === 'top') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
{currentLang ? (
<LanguageDropdown currentLang={currentLang} setUpdateLang={setUpdateLang} />
) : null}
<Avatar />
{REACT_APP_ENV && (
<span>
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
</span>
)}
</div>
);
};
export default connect(({ settings, auth, users }: ConnectState) => ({
theme: settings.navTheme,
layout: settings.layout,
auth,
users,
platformLanguage: auth?.currentUser?.membership?.platformLanguage,
data: auth?.currentUser?.membership,
}))(GlobalHeaderRight);
-LoginLayout
import React, { useEffect, useState } from 'react';
import type { ConnectState } from '#/models/connect';
import type { MenuDataItem } from '#ant-design/pro-layout';
import { getMenuData, getPageTitle } from '#ant-design/pro-layout';
import { useIntl, connect } from 'umi';
import type { ConnectProps } from 'umi';
import { Col, Row } from 'antd';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import LoginImage from '../assets/loginImage.png';
import SignUpAdminImage from '../assets/adminSignup.svg';
import styles from './LoginLayout.less';
import LanguageDropdown from '#/components/languageDropdown';
import SignupSideText from '#/components/SignupSideText';
export type UserLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
} & Partial<ConnectProps>;
const LoginLayout: React.FC<UserLayoutProps> = (props) => {
const [layoutImage, setLayoutImage] = useState(LoginImage);
const [updateLang, setUpdateLang] = useState<{ lang: string; updated: boolean }>({
lang: '',
updated: false,
});
const [currentLang, setCurrentLang] = useState<any>('');
useEffect(() => {
if (updateLang.updated) {
setUpdateLang({ ...updateLang, updated: false });
setCurrentLang(updateLang.lang);
}
}, [updateLang]);
useEffect(() => {
if (window.location.pathname === '/user/adminSignup/step1') {
setLayoutImage(SignUpAdminImage);
} else if (window.location.pathname === '/user/adminSignup/step2') {
setLayoutImage('TextSignup');
} else setLayoutImage(LoginImage);
}, [window.location.pathname]);
const {
route = {
routes: [],
},
} = props;
const { routes = [] } = route;
const {
children,
location = {
pathname: '',
},
} = props;
const { formatMessage } = useIntl();
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
formatMessage,
breadcrumb,
...props,
});
return (
<>
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<Col
xl={{ span: 12, order: 1 }}
xs={{ span: 0, order: 2 }}
md={{ span: 0, order: 2 }}
lg={{ span: 0, order: 2 }}
style={{ backgroundColor: '#00bfa5' }}
>
{layoutImage === 'TextSignup' ? (
<SignupSideText />
) : (
<img alt="logo" width="100%" height="100%" src={layoutImage} />
)}
</Col>
<Col
xl={{ span: 12, order: 2 }}
lg={{ span: 24, order: 2 }}
sm={{ span: 24, order: 1 }}
xs={{ span: 24, order: 1 }}
>
{' '}
<Row justify="end" className="languageRow">
<LanguageDropdown currentLang={currentLang} setUpdateLang={setUpdateLang} />
</Row>
{children}
</Col>
</div>
</HelmetProvider>
</>
);
};
export default connect(({ settings }: ConnectState) => ({ ...settings }))(LoginLayout);
I am working in React Hooks and have encountered a problem with the useContext method. My context class for a MapStyle (using Google Maps API) is as follows:
import React, { createContext, useState } from 'react';
const MapStyleContext = createContext();
function MapStyleProvider(props) {
const [selectedMapStyle, setSelectedMapStyle] = useState();
return (
<MapStyleContext.Provider value={{ selectedMapStyle, setSelectedMapStyle}}>
{props.children}
</MapStyleContext.Provider>
);
}
export { MapStyleContext, MapStyleProvider };
I have the map component attempting to access the selectedMapStyle property:
...
export default function Map() {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.REACT_APP_MAPS_API_KEY
});
const { selectedMapStyle } = useContext(MapStyleContext);
...
Finally, in the MapStyle component, I am attempted to use the setter setSelectedMapStyle whenever the user changes the value of the radio button:
import React, { useContext } from 'react';
import { Card, CardBody, Button, FormGroup, Label, Input } from 'reactstrap';
import { MenuContext } from '../../MenuContext';
import { MapStyleContext } from '../mapStyle/MapStyleContext'
import '../Tool.css';
export default function MapStyle() {
const {setSelectedItem} = useContext(MenuContext);
const {setSelectedMapStyle} = useContext(MapStyleContext);
const mapStyles = [
{ 'label': 'Map (default)', 'value': 'default' },
{ 'label': 'Grayscale (Clean)', 'value': 'cleanGray' },
{ 'label': 'Subtle Grayscale', 'value': 'subtleGray' },
{ 'label': 'Ultra light', 'value': 'ultralight' },
{ 'label': 'Clean (no labels)', 'value': 'cleanNoLabel' },
{ 'label': 'Clean (roads)', 'value': 'cleanRoads' }
];
return (
<div className="tool">
<Card>
<CardBody>
<div className="clearfix">
<h3 className="float-left mb-0 mr-2">Map Style</h3>
<Button onClick={() => setSelectedItem(null)} className="float-right" close />
</div>
<hr />
<FormGroup tag="fieldset">
{mapStyles.map(mapStyle => (
<FormGroup key={mapStyle.value} check>
<Label check>
<Input type='radio' name='mapStyle' value={mapStyle.value} onChange={setSelectedMapStyle(mapStyle.value)}/>
{mapStyle.label}
</Label>
</FormGroup>
))}
</FormGroup>
</CardBody>
</Card>
</div>
);
}
I am not quite sure where I am going wrong, but the context props never seem to load in correctly (giving me the type error) when I use this context.
The problem you've faced here is that the value of the useContext(MapStyleContext) is null as of initialisation and you've never wrapped the parent component or the current component inside the Context.Provider.
Here is an example:
function Map() {
return (
<Context.Provider>
{/*children */}
<Context.Provider>
);
}
I have an application with a table, the table has a checkbox to set a Tenant as Active, this variable is a global variable that affects what the user does in other screens of the application.
On the top right of the application, I have another component called ActiveTenant, which basically shows in which tenant the user is working at the moment.
The code of the table component is like this:
import React, { Component } from 'react';
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
class ListTenants extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
const results= responseJson.map(row => ({
key: row.id,
TestSiteCollectionUrl: row.TestSiteCollectionUrl,
TenantName: row.TenantName,
Email: row.Email
}))
this.setState({ data: results });
}
})
.catch(error => {
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
const columns = [
{
title: 'TenantName',
dataIndex: 'TenantName',
key: 'TenantName',
},
{
title: 'TestSiteCollectionUrl',
dataIndex: 'TestSiteCollectionUrl',
key: 'TestSiteCollectionUrl',
},
{
title: 'Email',
dataIndex: 'Email',
key: 'Email',
}
];
// rowSelection object indicates the need for row selection
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].TenantName != undefined){
console.log(selectedRows[0].TenantName);
const options = {
method: 'post'
};
adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+selectedRows[0].TenantName.toString(), options)
.then(response =>{
if(response.status === 200){
Notification(
'success',
'Tenant set to active',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not activated',
error
);
console.error(error);
});
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
return (
<Table rowSelection={rowSelection} columns={columns} dataSource={this.state.data} />
);
}
}
export default ListTenants;
And the code of the ActiveTenant component its also very simple
import React, { Component } from 'react';
import authAction from '../../redux/auth/actions';
import { adalApiFetch } from '../../adalConfig';
class ActiveTenant extends Component {
constructor(props) {
super(props);
this.state = {
tenant: ''
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant/GetActiveTenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
this.setState({ tenant: responseJson.TenantName });
}
})
.catch(error => {
this.setState({ tenant: '' });
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
return (
<div>You are using tenant: {this.state.tenant }</div>
);
}
}
export default ActiveTenant;
The problem is, if I have multiple tenants on my database registered and I set them to active, the server side action occurs, and the state is changed, however on the top right its still showing the old tenant as active, UNTIL I press F5 to refresh the browser.
How can I achieve this?
For the sake of complete understandment of my code I will need to paste below other components:
TopBar which contains the active tenant
import React, { Component } from "react";
import { connect } from "react-redux";import { Layout } from "antd";
import appActions from "../../redux/app/actions";
import TopbarUser from "./topbarUser";
import TopbarWrapper from "./topbar.style";
import ActiveTenant from "./activetenant";
import TopbarNotification from './topbarNotification';
const { Header } = Layout;
const { toggleCollapsed } = appActions;
class Topbar extends Component {
render() {
const { toggleCollapsed, url, customizedTheme, locale } = this.props;
const collapsed = this.props.collapsed && !this.props.openDrawer;
const styling = {
background: customizedTheme.backgroundColor,
position: 'fixed',
width: '100%',
height: 70
};
return (
<TopbarWrapper>
<Header
style={styling}
className={
collapsed ? "isomorphicTopbar collapsed" : "isomorphicTopbar"
}
>
<div className="isoLeft">
<button
className={
collapsed ? "triggerBtn menuCollapsed" : "triggerBtn menuOpen"
}
style={{ color: customizedTheme.textColor }}
onClick={toggleCollapsed}
/>
</div>
<ul className="isoRight">
<li
onClick={() => this.setState({ selectedItem: 'notification' })}
className="isoNotify"
>
<TopbarNotification locale={locale} />
</li>
<li>
<ActiveTenant />
</li>
<li
onClick={() => this.setState({ selectedItem: "user" })}
className="isoUser"
>
<TopbarUser />
<div>{ process.env.uiversion}</div>
</li>
</ul>
</Header>
</TopbarWrapper>
);
}
}
export default connect(
state => ({
...state.App.toJS(),
locale: state.LanguageSwitcher.toJS().language.locale,
customizedTheme: state.ThemeSwitcher.toJS().topbarTheme
}),
{ toggleCollapsed }
)(Topbar);
App.js which contains the top bar
import React, { Component } from "react";
import { connect } from "react-redux";
import { Layout, LocaleProvider } from "antd";
import { IntlProvider } from "react-intl";
import { Debounce } from "react-throttle";
import WindowResizeListener from "react-window-size-listener";
import { ThemeProvider } from "styled-components";
import authAction from "../../redux/auth/actions";
import appActions from "../../redux/app/actions";
import Sidebar from "../Sidebar/Sidebar";
import Topbar from "../Topbar/Topbar";
import AppRouter from "./AppRouter";
import { siteConfig } from "../../settings";
import themes from "../../settings/themes";
import { themeConfig } from "../../settings";
import AppHolder from "./commonStyle";
import "./global.css";
import { AppLocale } from "../../dashApp";
import ThemeSwitcher from "../../containers/ThemeSwitcher";
const { Content, Footer } = Layout;
const { logout } = authAction;
const { toggleAll } = appActions;
export class App extends Component {
render() {
const { url } = this.props.match;
const { locale, selectedTheme, height } = this.props;
const currentAppLocale = AppLocale[locale];
return (
<LocaleProvider locale={currentAppLocale.antd}>
<IntlProvider
locale={currentAppLocale.locale}
messages={currentAppLocale.messages}
>
<ThemeProvider theme={themes[themeConfig.theme]}>
<AppHolder>
<Layout style={{ height: "100vh" }}>
<Debounce time="1000" handler="onResize">
<WindowResizeListener
onResize={windowSize =>
this.props.toggleAll(
windowSize.windowWidth,
windowSize.windowHeight
)
}
/>
</Debounce>
<Topbar url={url} />
<Layout style={{ flexDirection: "row", overflowX: "hidden" }}>
<Sidebar url={url} />
<Layout
className="isoContentMainLayout"
style={{
height: height
}}
>
<Content
className="isomorphicContent"
style={{
padding: "70px 0 0",
flexShrink: "0",
background: "#f1f3f6",
position: "relative"
}}
>
<AppRouter url={url} />
</Content>
<Footer
style={{
background: "#ffffff",
textAlign: "center",
borderTop: "1px solid #ededed"
}}
>
{siteConfig.footerText}
</Footer>
</Layout>
</Layout>
<ThemeSwitcher />
</Layout>
</AppHolder>
</ThemeProvider>
</IntlProvider>
</LocaleProvider>
);
}
}
export default connect(
state => ({
auth: state.Auth,
locale: state.LanguageSwitcher.toJS().language.locale,
selectedTheme: state.ThemeSwitcher.toJS().changeThemes.themeName,
height: state.App.toJS().height
}),
{ logout, toggleAll }
)(App);
I think this should be enough to illustrate my question.
You're not using redux correctly there. You need to keep somewhere in your redux state your active tenant; That information will be your single source of truth and will be shared among your components throughout the app. Every component that will need that information will be connected to that part of the state and won't need to hold an internal state for that information.
Actions, are where you would call your API to get your active tenant information or set a new tenant as active. These actions wil call reducers that will change your redux state (That includes your active tenant information) and those redux state changes will update your app components.
You should probably take some time to read or re-read the redux doc, it's short and well explained !
Goal: Search must work correctly and then be able to modify the found input. I'm trying to figure out why I can't do both.
Observation: the crazy thing I found is it works if I change the key from key={index} to key={variable.value} which doesn't make any sense.
Can someone tell me what I'm doing wrong, or if there's any way to do this better?
You'll understand better if you look at the codesandbox DEMO
index.js
import React from "react";
import ReactDOM from "react-dom";
import Input from './Input'
const dummyVariablies = [
{
name: 'barrack',
value: 11
},
{
name: 'putin',
value: 22
},
{
name: 'trump',
value: 33
}
]
class App extends React.Component {
state = {
search: ''
}
handleSearch = (e) => {
this.setState({
search: e.target.value
})
}
getFilteredVariables = (variables) => {
const { search } = this.state;
return variables.filter(
variable => variable.name.toString().toLowerCase().includes(search.toString().toLowerCase())
);
}
render() {
const variables = this.getFilteredVariables(dummyVariablies || [])
return (
<div>
Goal: Search must work correctly and then be able to modify the found input
<br /> <br /> <br />
Search: <input onChange={this.handleSearch} />
<br /> <br /> <br />
{variables.map((variable, index) => {
return <Input variable={variable} key={index} />
})
}
</div>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Input.js
import React from 'react';
export default class Input extends React.Component {
state = {
name: '',
value: ''
}
componentDidMount() {
this.setState(
{
value: this.props.variable.value,
name: this.props.variable.name
}
)
}
// static getDerivedStateFromProps(nextProps) {
// return {
// value: nextProps.variable.value,
// name: nextProps.variable.name
// }
// }
handleChange = (e) => {
this.setState({ value: e.target.value });
}
render() {
const { value, name } = this.state;
return (
<div>
{name}
<input type="text"
value={value}
onChange={this.handleChange}
/>
</div>
);
}
}
I forked your sandbox here
As you can see I changed the input:
import React from "react";
export default class Input extends React.Component {
state = {
value: this.props.variable.value
};
handleChange = e => {
this.setState({ value: e.target.value });
};
render() {
return (
<div>
{this.props.variable.name}
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
</div>
);
}
}
The problem with your code is that you're sending props to your Input but you're not handling the updates correctly. You're only doing it once, because componentDidMount runs only once when the component is loaded. I simplified it and I'm just passing the props down. This way the filtration works fine.
Keep in mind that if you want to save the values correctly after you edit the inputs you'll have to insert dummyVariablies inside the state of App and then use lifting state up from your Input component. Good example can be found here.
React/Redux newbie here. I have a form input that allows a user to enter a doctor issue. It returns a list of doctors from the server via Redux action, and displays the doctor list and a marker for each doctor's location on the map (react-google-maps).
When I click submit, the list of doctors for the correct issue displays, the map is there, but with no markers. I can get the markers on the map to display ONLY after submitting the form, THEN clicking on a doctor from the list to display their details.
Want: Enter a doctor issue and render both the list of doctors and markers on the map when the user clicks submit. Then, select a doctor to see their details page (that's another question, routing to dynamic a detail page).
I think, I need to use a life-cycle method but not sure how to implement it. Or, is there a better way to handle this with Redux?
Doctor component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DoctorSearchForm from '../../containers/doctors/DoctorSearchForm';
import DoctorList from './DoctorList';
import Map from '../maps/Map';
class Doctors extends Component {
constructor(props) {
super(props);
this.state = {
markers: [],
isMarkerShown: false
}
}
componentDidMount() {
this.getMarkers();
}
getMarkers = () => {
let practices = this.props.doctors.map(function(doctor, index) {
return {
title: doctor.profile.first_name + ' ' + doctor.profile.last_name,
location: {
lat: doctor.practices[0].visit_address.lat,
lng: doctor.practices[0].visit_address.lon
}
}
});
this.setState({ markers: practices, isMarkerShown: true });
}
render() {
const { doctors, match } = this.props;
return (
<div>
<DoctorSearchForm getMarkers={this.getMarkers} />
<div className="row">
<div className="col-md-4">
<DoctorList doctors={doctors} match={match} />
</div>
<div className="col-md-8">
<Map
isMarkerShown={this.state.isMarkerShown}
center={{ lat: 45.6318,lng: -122.6716 }}
zoom={12}
markers={this.state.markers}
/>
</div>
</div>
</div>
);
}
}
Doctors.propTypes = {
doctors: PropTypes.array.isRequired,
match: PropTypes.object.isRequired
}
export default Doctors;
DoctorList component:
import React from "react";
import { Route } from 'react-router-dom';
import DoctorItem from './DoctorItem';
import DoctorView from './DoctorView';
class DoctorList extends React.Component {
render() {
const { doctors, match } = this.props;
const linkList = doctors.map((doctor, index) => {
return (
<DoctorItem doctor={doctor} match={match} key={index} />
);
});
return (
<div>
<h3>DoctorList</h3>
<ul>{linkList}</ul>
<Route path={`${match.url}/:name`}
render={ (props) => <DoctorView data= {this.props.doctors} {...props} />}
/>
</div>
);
}
}
export default DoctorList;
DoctorItem component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link, Route } from 'react-router-dom';
import DoctorView from './DoctorView';
const DoctorItem = (props) => {
const { doctor, match } = props;
return (
<li>
<Link to={{ pathname: `${match.url}/${doctor.profile.first_name}-${doctor.profile.last_name}` }}>
{doctor.profile.first_name} {doctor.profile.last_name}
</Link>
</li>
)
}
DoctorItem.propTypes = {
doctor: PropTypes.object.isRequired,
};
export default DoctorItem;
DoctorView component:
import React from 'react';
const DoctorView = ({ data, match }) => {
const doctor = data.find(p => `${p.profile.first_name}-${p.profile.last_name}` === match.params.name);
let doctorData;
if (doctor) {
const mappedSpecialties = Object.entries(doctor.specialties).map(([index, specialty]) => {
return <li key={index} id={index}>{specialty.description}</li>;
});
doctorData =
<div>
<h5><strong>{doctor.profile.first_name} {doctor.profile.last_name}</strong> - {doctor.profile.title}</h5>
<img src={doctor.profile.image_url} alt={"Dr." + doctor.profile.first_name + " " + doctor.profile.last_name} />
<ul>{mappedSpecialties}</ul>
<p>{doctor.profile.bio}</p>
</div>;
}
return (
<div>
{doctorData}
</div>
)
}
export default DoctorView;
Map component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withScriptjs, withGoogleMap, GoogleMap, Marker } from 'react-google-maps';
import { compose, withProps } from "recompose";
export default Map = compose(
withProps({
googleMapURL:
"https://maps.googleapis.com/maps/api/js?key={MY_KEY}&v=3.exp&libraries=geometry,drawing,places",
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `400px` }} />,
mapElement: <div style={{ height: `100%` }} />
}),
withScriptjs,
withGoogleMap
)(props => (
<GoogleMap defaultZoom={9} defaultCenter={{ lat: 45.6318,lng: -122.6716 }}>
{props.markers.map((doctor, index) => {
const marker = {
position: { lat: doctor.location.lat, lng: doctor.location.lng },
title: doctor.title
}
return <Marker key={index} {...marker} />;
})}
</GoogleMap>
));
I've spent several days trying and searching for answers but no luck. Any help would be greatly appreciated!
Just like you calculate the markers when the component mounts, you need to recalculate your markers when you receive new props:
componentWillReceiveProps(nextProps) {
this.getMarkers(nextProps);
}
This will require you to change your getMarkers signature a bit so that it can accept an argument and use that instead of this.props in your map operation:
getMarkers = (props = this.props) => {
let practices = props.doctors.map(function(doctor, index) {
return {
title: doctor.profile.first_name + ' ' + doctor.profile.last_name,
location: {
lat: doctor.practices[0].visit_address.lat,
lng: doctor.practices[0].visit_address.lon
}
}
});
this.setState({ markers: practices, isMarkerShown: true });
}
Assuming you are calling getMarkers() in your DoctorSearchForm component, you can remove that since it will automatically update the markers when receiving new props -- or you could bypass state altogether and just calculate it on the fly in render based on the incoming props.