Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing state and methods to custom components #623

Closed
wasabigeek opened this issue Nov 17, 2017 · 26 comments
Closed

Passing state and methods to custom components #623

wasabigeek opened this issue Nov 17, 2017 · 26 comments

Comments

@wasabigeek
Copy link

wasabigeek commented Nov 17, 2017

It's great that we can specify our own custom components, but they seem to be Uncontrolled. I would like to be able to pass state and methods down to custom components - is there a way to do this?

An example use case - I want to add a button in the Toolbar which will modify some of the events on the calendar.

@wasabigeek wasabigeek changed the title Passing methods to custom components Passing state and methods to custom components Nov 17, 2017
@tobiasandersen
Copy link
Collaborator

There's nothing stopping you from doing that! You'd just have to pass down a function to the toolbar that updates the state where the events live.

@wasabigeek
Copy link
Author

@tobiasandersen how could I do this? I was under the impression it couldn't be done since we're only passing the class to components.

Thanks in advance!

@jquense
Copy link
Owner

jquense commented Nov 17, 2017

you can pass Componets, and functions are valid components (as well as classes)!

@tobiasandersen
Copy link
Collaborator

@wasabigeek here's one way to do it: https://codesandbox.io/s/kw645vwm27

@jquense how would you feel about me making an issue template, with a basic codesandbox that people can use for reproducing bugs etc.?

@wasabigeek
Copy link
Author

wasabigeek commented Nov 17, 2017

@tobiasandersen how would you do it with a Class?

I've extended the default Toolbar so I can still use the regular methods (e.g. this.navigate), but can't figure out how to pass a method from it's parent.

Example below: how could I pass refreshEvents method from <Calendar/> to <CustomToolbar/>?

(Apologies in advance for my noobness - I can't seem to find anything on the topic online, probably don't know the right search terms)

class CustomToolbar extends Toolbar {
  ...
  render() {
    ...
    return (
      <div className='rbc-toolbar'>
        <ButtonGroup className="mr-2">
          <Button onClick={this.navigate.bind(null, navigate.PREVIOUS)}>
            <i className="fa fa-chevron-left"></i>
          </Button>
          <Button onClick={this.navigate.bind(null, navigate.NEXT)}>
            <i className="fa fa-chevron-right"></i>
          </Button>
        </ButtonGroup>
        ...
        <Button onClick={() => this.props.refreshEvents()}>Refresh Events</Button>
      </div>
    );
  }
}


class Calendar extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      events: [],
    }
    this.refreshEvents = this.refreshEvents.bind(this);
  }

  refreshEvents() {
    ...
    this.setState({events: events});
  }

  handleNavigate(date, view, action) {
    this.refreshEvents();
  }

  render() {
    return (
      <BigCalendar
        events={this.state.events}
        views={['month', 'week']}
        components={{toolbar: CustomToolbar}}
        onNavigate={this.handleNavigate}
      />
    );
  }
}

@wasabigeek
Copy link
Author

wasabigeek commented Nov 18, 2017

Ok this seems to work for me, encapsulate the class in another function:

const CustomToolbar = ({refreshEvents}) => {
  return class BaseToolbar extends Toolbar { ... }
}

class Calendar extends React.Component {
   ...
   render() {
    return (
      <BigCalendar
        ...
        components={{toolbar: CustomToolbar({this.refreshEvents})}}
      />
    );
  }
}

If there's a better way to do it I'd love to know - otherwise, thanks for the example!

@DreDasLT
Copy link

DreDasLT commented May 15, 2018

Hello, i tried to use custom toolba as you showed before, but I get this warning:

Warning: Failed prop type: You have provided a view prop to Calendar without an onView handler. This will render a read-only field. If the field should be mutable use defaultView. Otherwise, set onView

Tried to search everywhere for this problem, but i couldn't find anything to help me. I've tried to add 'onView' prop to BigCalendar component, but it doesn't work.

`const CustomToolbar = (courses) => {
return class BaseToolbar extends Toolbar {

    render() {
        console.log(courses);
        let {messages, label} = this.props;

        return (
            <div className="rbc-toolbar">
    <span className="rbc-btn-group">
      <button
          type="button"
          onClick={this.navigate.bind(null, 'TODAY')}
      >
        {messages.today}
      </button>
      <button
          type="button"
          onClick={this.navigate.bind(null, 'PREV')}
      >
        {messages.previous}
      </button>
      <button
          type="button"
          onClick={this.navigate.bind(null, 'NEXT')}
      >
        {messages.next}
      </button>
    </span>

                <span className="rbc-toolbar-label">{label}</span>

                <span className="rbc-btn-group">

                <Input type="select" name="select" id="exampleSelect">
        <option>Kaunas | Pavasario semestras 2018</option>
        <option>Vilnius | Pavasario semestras 2018</option>
        <option>Šiauliai | Pavasario semestras 2018</option>
      </Input>

                    {this.viewNamesGroup(messages)}
                </span>
            </div>
        );
    }


    navigate = (action) => {
        this.props.onNavigate(action)
    };

    view = (view) => {
        this.props.onViewChange(view)
    };

    viewNamesGroup(messages) {
        let viewNames = this.props.views;
       const view = this.props.view;

        if (viewNames.length > 1) {
            return viewNames.map(name => (
                <button
                    type="button"
                    key={name}
                    className={view === name ? 'rbc-active' : ''}
                    onClick={this.view.bind(null, name)}
                >
                    {messages[name]}
                </button>
            ))
        }
    };
}

};`

@wasabigeek
Copy link
Author

@DresDasLT could you edit to include <Calendar/>'s render() code (where <BigCalendar /> is)? It seems like that's where the issue is occuring.

@DreDasLT
Copy link

DreDasLT commented May 16, 2018

`<BigCalendar
selectable
culture='lt'
popup events={events}
step={60}
defaultDate={new Date()}
views={["month", "week", "day", "agenda"]}
view={this.state.currentView}
onSelecting={() => false}
onView={(view) => {
this.setState({currentView: view});
}}

                    eventPropGetter={
                        (event, start, end, isSelected) => {
                            let newStyle = {
                                color: 'white',
                                backgroundColor: event.category.color
                            };
                            return {
                                className: "",
                                style: newStyle
                            };
                        }
                    }
                    messages={
                        {
                            'today': 'Šiandien',
                            'previous': 'Atgal',
                            'next': 'Kitas',
                            'month': 'Mėnuo',
                            'week': 'Savaitė',
                            'day': 'Diena',
                            'showMore': total => `+${total} Žiūrėti daugiau`
                        }
                    }
                    components={{
                        toolbar: CustomToolbar(courses),
                        month: {
                            event: CustomMonthEvent
                        },
                        week: {
                            event: CustomWeekEvent
                        },
                    }}
                    onSelectSlot={s => this.toggle(s)}
                />`

@DreDasLT
Copy link

DreDasLT commented May 16, 2018

There is one problem, i dont' use <Calendar /> component anywhere.

@GavinThomas1192
Copy link

@wasabigeek Can you post a link to your full imports for your example? I am only trying to create a custom toolbar and can't access the original methods, eg this.navigate? I'm also using typescript so I really could use a working example in jsx?

@wasabigeek
Copy link
Author

@GavinThomas1192 sorry for the late response, this is my import statement import Toolbar from 'react-big-calendar/lib/Toolbar';. I manually inspect node_modules/react-big-calendar to find out where the needed file is.

If you don't want to do class inheritance (which admittedly, is not recommended in React), I suggest using the source as a base (i.e. copy it and edit): https://github.com/intljusticemission/react-big-calendar/blob/master/src/Toolbar.js. this.navigate is simply a prop that is passed to the component as this.props.onNavigate.

@GavinThomas1192
Copy link

GavinThomas1192 commented Jun 13, 2018

Hey @wasabigeek , thanks for getting back to me! I am using typescript and the issue was that their types were incorrect and I was calling this.props.navigate(date, PREV) and it should have been reversed. Heres my custom header for reference, since this single post was super important for myself when trying to create a custom header.

import React from 'react' import { Navigate } from 'react-big-calendar' import Icon from 'app/common/components/Icon' const styles = require('./CustomToolbar.scss')
`
interface Props {
onNavigate: (action: Navigate) => void
date: string | Date
label: string
}
export default class CustomToolbar extends React.Component {
prevMonthClick = () => {
this.props.onNavigate('PREV')
}
nextMonthClick = () => {
this.props.onNavigate('NEXT')
}
render() {
const { label } = this.props
return (






{label}





    <div className={styles.viewOptionsContainer}>
      <span className="rbc-btn-group">
        <button>Month</button>
        <button>Day</button>
        <button>Week</button>
      </span>

      <button className="btn btn-back">
        <Icon icon="R" />
      </button>
      <button className="btn btn-back">
        <Icon icon="meet_now" />
      </button>
    </div>
  </div>
)

}
}

@elgandoz
Copy link

elgandoz commented Mar 6, 2019

Hi @wasabigeek, this answer is long time closed but i still cannot figure the syntax to pass the prop to my imported custom class component.
Here's a short version of what I'm trying to do, but it fails:

import React, {Component} from 'react'
import Calendar from 'react-big-calendar'
// my External custom components
import CustomToolbar from './components/CustomToolbar'

class App extends Component {
  // encapsulating the class component, not sure if ok
  CustomToolbarCapsule = (something) => {
    const instance = new CustomToolbar(something)
    return instance.render()
   }

  render() {
    const something = this.state.something
    const components = {
            toolbar: this.CustomToolbarCapsule({something})
    }
  return (<Calendar components={components} />)
}

@elgandoz
Copy link

elgandoz commented Mar 6, 2019

For future reference, I ended up doing this, but I really don't think it's the right way.
Never the less, it works:

import React, {Component} from 'react'
import Calendar from 'react-big-calendar'
// my external custom components
import CustomToolbar from './components/CustomToolbar'

class App extends Component {
  // encapsulating the class component, not sure if ok
  CustomToolbarCapsule = (props) => {
    return class CustomToolbarInstance extends CustomToolbar {
      static defaultProps = { ...props }
    }
   }

  render() {
    const something = this.state.something
    const components = {
            toolbar: this.CustomToolbarCapsule({something})
    }
  return (<Calendar components={components} />)
}

If anybody could advise for a better solution, please let me know.

@wasabigeek
Copy link
Author

@elgandoz I don't really know the terminology or the best practices, but if you look at https://github.com/intljusticemission/react-big-calendar/blob/master/src/Calendar.js#L888 you'll see that the custom toolbar is declared in render() via the <... /> syntax. In React, this can either be a function or a class which is somehow resolved or instantiated through React magic.

In your first example, when you declare this.CustomToolbarCapsule(), you are resolving that function and returning the value (admittedly, I don't know what the value is) only. In the second the function is also resolved, but the resolution of that is to return a class, which will work.

There are other ways you can declare it, e.g. in your first example you should be able to keep everything the same and wrap the custom component in an anonymous function:

    const components = {
            toolbar: () => this.CustomToolbarCapsule({something})
    }

Hope that helps!

@elgandoz
Copy link

elgandoz commented Mar 8, 2019

Thanks for the reply @wasabigeek. I tried your solution but I get the error:
Cannot read property 'messages' of undefined

That's because all the default props of my custom toolbar gets replaced by something. All I wanted was to add a prop on top of the existing ones, basically what I would do calling

<CustomToolbar someprop={something} />

The problem is that I cannot use that syntax when declaring the calendar components, and if my class is in a different file (import CustomToolbar from './components/CustomToolbar'), I can't do it as you show in your encapsulate example.

Below you can find a snipper of what's going on in the CustomToolbar class:

// CustomToolbar.js, imported in App.js
import React from 'react'
import Toolbar from 'react-big-calendar/lib/Toolbar'

class CustomToolbar extends Toolbar {
  render() {
    // someprops is custom, the others are default passed by BigCalendar
    const { someprop, onNavigate, label , localizer: { messages } } = this.props
     [...]
  }
}

In the end, all I want is to pass a prop to an imported class component.

My last example with static defaultProps = { ...props } works but I find it quirky.

@wasabigeek
Copy link
Author

wasabigeek commented Mar 8, 2019

@elgandoz, since the de-structuring of message from localiser is throwing the error, maybe you can trace that? What is the localizer that is being passed in etc.

I realise we missed passing the props down to the custom toolbar, that could be the reason (I think this would be the syntax): toolbar: props => this.CustomToolbarCapsule({ something, ...props })

@elgandoz
Copy link

elgandoz commented Mar 9, 2019

@wasabigeek That could work, but how do I get the default props passed to the toolbar (or to any custom component)?
In this case onNavigate, label , localizer come from big calendar itself.

@mdjuric-itk
Copy link

This issue shouldn't be closed. The problem is as @elgandoz says, the props passed to Custom components this way are overwriting existing components props. Is there a way to keep them both?

@AndreyYevsega
Copy link

I want to get answer to @mdjuric-itk question too.

@azouaoui-med
Copy link

azouaoui-med commented May 14, 2020

I had a similar problem and i fixed it by using a function when adding my custom component

components={{event: (props) => <Event {...props} customProp={youCustomProp} />}}

This will allow you to keep the default props and add your own custom props

@pitops
Copy link

pitops commented Jun 13, 2020

Guys all your suggestions neglect the fact that it WONT RE-RENDER if the props change. This is bad practice and I am puzzled without they haven't fixed it yet.

@trevorbye
Copy link

I had a similar problem and i fixed it by using a function when adding my custom component

components={{event: (props) => <Event {...props} customProp={youCustomProp} />}}

This will allow you to keep the default props and add your own custom props

After hours of scouring all these git issues and PRs, this was the only thing that worked for me. This also allowed me to pass functions as props as well, which is the main thing I was trying to do.

@ArtemGoldsmith
Copy link

@pitops have you found a solution for this?

@cutterbl
Copy link
Collaborator

The best way of handling accessing custom props and methods from within an override component is by using context. Here's an example of how to accomplish this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests