I am creating a calendar component that contains 2 Views with FlatList. Each View is associated with a different data source. Specifically, View 1 will display all days in a month where View 2 will display all days in a week.
Link to video demo, button of year is for view switching
For simplicity, each month/week view is represented by a date as an array item in a bigger array.
For example, each item in the month array is a unique data in a month. An array of size 12 will have 12 unique dates of each month. Each Date() object will then be passed down to my children components for a proper rendering. The result of this particular month array is a list of complete calendar of 12 months.
Note: To improve performance, my child component will only render the current month -1, current month, and current month +1, all other months will be rendered conditionally as soon as user scrolls away.
[
...
Wed May 30 2018 21:51:47 GMT+0800 (+08),
Sat Jun 30 2018 21:51:47 GMT+0800 (+08),
Mon Jul 30 2018 21:51:47 GMT+0800 (+08),
...
]
The same applies to View 2, which is the week view. Except that my data is now contained unique days in a week for all weeks in a month.
Before I implement this approach, I tried to hack something out from my child component. For instance, when users click a button to switch view, it would pass down a prop, with the current Date() object to notify the children as to conditionally render a different view. This is basically switching between a View and a FlatList. Nested FlatList was a bad idea as both lists are scrolling towards the same direction.
Not to mention that my calendar component involve jumping to selected month. Another solution I had implemented is to use only month array for both views by hacking out the indexes from keyExtractor. For example, I would reindex the list as soon as user jumps from one month to another but soon I realized this is an anti-pattern an only causes more problem.
Note: I have implemented shouldComponentUpdate in all my children
components. So scrolling in just 1 View should not be a problem, size
of the data source will not affect at all because only the changes
happen to the -1, 0, and +1 month will be reflected. The
bottleneck of my component is only happening when switching views.
Lost story short, now I have resorted to the current solution where I have 2 different data source (array) for 2 different Views in a FlatList. I have a button for the user to switch between modes. The problem is it takes some time for switching mode (setting states) as every time it re-instantiates the view by calling this prop onViewableItemsChanged from FlatList, which involve some complex calculation. Is there a better approach?
Code (Parent)
renderCalendarList = () => {
return (
<FlatList
pageSize={1}
horizontal={true}
pagingEnabled={true}
scrollEnabled={true}
keyExtractor={this.keyExtractor}
ref={(c) => this.calendarList = c}
getItemLayout={this.getItemLayout}
renderItem={this.renderCalendarComponent}
onViewableItemsChanged={this.onViewableItemsChanged}
data={(this.state.weekMode ? this.state.weekRows : this.state.rows)} />
)
}
switchView = () => {
this.setState({ weekMode: !this.state.weekMode });
// next week is going to be month view
if (this.state.weekMode) {
const mYClone = this.state.monthYears.slice(0);
const selectedDay = DateUtils.formatDateInMY(this.props.selectedDay);
for (let i = 0; i < mYClone.length; i++) {
if (selectedDay === mYClone[i]) {
this.setState({ currentMonth: this.props.selectedDay })
this.calendarList.scrollToIndex({ animated: false, index: i });
}
}
} else { // next week is going to be week view
const rowClone = this.state.weekRows.slice(0);
for (let i = 0; i < rowClone.length; i++) {
if (isSameWeek(rowClone[i], this.props.selectedDay)) {
this.setState({ currentMonth: rowClone[i] });
this.calendarList.scrollToIndex({ animated: false, index: i });
}
}
}
}
Code (Children)
render() {
const { container, monthContainer, weekContainer } = styles;
const { currentMonth, firstDay, style, weekMode } = this.props;
if (!weekMode) {
const days = DateUtils.populateMonth(currentMonth, firstDay);
const weeks = [];
while (days.length) {
weeks.push(this.renderWeek(days.splice(0, 7), weeks.length));
}
return (
<View style={[container, style]}>
<View style={[monthContainer]}>{weeks}</View>
</View>
)
} else {
const startDay = subDays(currentMonth, 3); // focus on middle
const endDay = addDays(startDay, 6)
const days = eachDay(startDay, endDay, 1);
const daysToRender = [];
days.forEach((day, dayID) => {
daysToRender.push(this.renderDay(day, dayID))
});
return (
<View style={style}>
<View style={[weekContainer]}>
{daysToRender}
</View>
</View>
)
}
}
Link to video demo, button of year is for view switching
Related
I am using a calendar application that shows the available timeslots for a group. For example if the team has 2 members, and if each member has their own workingHours and own existing bookings; to show the team calendar where the array output is only giving all slots that are commonly (collectively) open among all the members. Code below is incorrectly providing returns that are not consistent with the common open slots.
userSchedule has the working hours and busy times of the 2 members of the team
userSchedule = [{"workingHours":[{"days":[1,2,3,4,5],"startTime":60,"endTime":540}],"busy":[]},{"workingHours":[{"days":[2,3],"startTime":60,"endTime":540},{"days":[5],"startTime":60,"endTime":450},{"days":[1],"startTime":180,"endTime":540}],"busy":[]}]
From this I want to get the working hours of the combined team when both are available first. Then i want to removed the blocked times and only show in team calendar (in an array) the actual days and slots that the combined team is available.
The below logic is not working and showing output where the workingHours are not all days that both are available
const workingHours = userSchedules?.reduce(
(currentValue: ValuesType<typeof userSchedules>["workingHours"], s) => {
// Collective needs to be exclusive of overlap throughout - others inclusive.
if (eventType.schedulingType === SchedulingType.COLLECTIVE) {
// taking the first item as a base
if (!currentValue.length) {
currentValue.push(...s.workingHours);
return currentValue;
}
// the remaining logic subtracts
return s.workingHours.reduce((compare, workingHour) => {
return compare.map((c) => {
const intersect = workingHour.days.filter((day) => c.days.includes(day));
return intersect.length
? {
days: intersect,
startTime: Math.max(workingHour.startTime, c.startTime),
endTime: Math.min(workingHour.endTime, c.endTime),
}
: c;
});
}, currentValue);
} else {
// flatMap for ROUND_ROBIN and individuals
currentValue.push(...s.workingHours);
}
return currentValue;
},
[]
);
I'm trying to use an if statement in my code where I want it to 'open' a Calendar Box if the date of today has occurred as well as for the past days of my calendar to open.
Here is my code where I'm using an useEffect to post it on loading the React Component:
// Call on post method via axios
useEffect(async () => {
console.log(daysData);
const daysDay = daysData.map((day) => day.day);
console.log(daysDay);
if (date + 1 >= daysDay) {
// Url where to post
await axios.post(`http://localhost:5001/open/chocolate`, {
day: date,
});
alert('New day is available to eat!');
}
setOpenCalendarBox('');
}, []);
I'm trying to get an array I've initiated a few lines above of the useEffect function (daysData) and I want the value of the 'day' item inside of the objects inside of the array and then compare the date of today if it is equal to or less than daysDay (day item inside of daysData)
Here is the code for my array:
// Here I initalize the array with useState
const [daysData, setDaysData] = useState([]);
// Here is the port I'm fetching my array from.
useEffect(() => {
fetch('http://localhost:5001/chocolates')
.then((resp) => resp.json())
.then((data) => setDaysData(data));
}, []);
And here is the date code:
// Initiate new Date
const current = new Date();
// Retrieve current day of the month
const date = current.getDate();
I can't seem to get the effect I want. I basically only want to see if the day has passed or if it is today then I want it to post to '/open/chocolate'.
That's probably because the value of daysData is set asynchronously, yet the useEffect block that depends on it does not list it as a dependency. Therefore you are invoking logic, which requires daysData to be populated asynchronously, when the component is loaded at runtime. So daysData will be empty.
A solution is to simply add daysData in the dependency array, so that you will only execute whatever logic that is in there once the array is successfully populated.
On the other hand, you are comparing a number against an array: which will give an unexpected result. If you want any of the day to meet date + 1, use daysDay.some(d => date + 1 >= d). If you want all of the days to meet date + 1, use daysDate.every(d => date + 1 >= d).
useEffect(async () => {
const daysDay = daysData.map((day) => day.day);
// This needs to be fixed, see comment for options
if (daysDay.some(d => date + 1 > d)) {
// Url where to post
await axios.post(`http://localhost:5001/open/chocolate`, {
day: date,
});
}
setOpenCalendarBox('');
}, [daysData]);
I created a Calendar with React hooks, using date-fns,
following this article -> https://medium.com/#w.difulvio523/create-a-custom-hooks-calendar-in-react-e50bbba456e1
And added the functionality to display schedules which I fetch from my backend.
The logic is, if the schedule objects date is same as the cloneDay,
render the extra Link element where user can click and go to the matching schedule page.
The problem is that the schedule is being render on actual schedule day + 1.
ex. if the date is 14th, it will render on 15th
this issue is happening inside of the cell function
const cells = () => {
const monthStart = startOfMonth(currentDate)
const monthEnd = endOfMonth(monthStart)
const startDate = startOfWeek(monthStart, {weekStartsOn: 1})
const endDate = endOfWeek(monthEnd)
const dateFormat = "d"
const rows = []
let days = []
let day = startDate
let formattedDate = ""
//here get the schdules
//and render in cells.
//if the schduels.date is same as 'day' or 'clone day',
//render <Link/> to Exercise element with corresponding ID
while (day <= endDate) {
for (let i = 0; i < 7; i++) {
formattedDate = format(day, dateFormat)
const cloneDay = day
//split and reforamt the cloneDay to compare with schedule object's date
const formatCloneDay = cloneDay.toISOString().split('T')[0]
// console.log('formatCloneDay', formatCloneDay)
const scheduleElements = exerciseScheduleArray.map(schedule => {
//reformat for the compare with formatCloneday
const formatScheduleDate = schedule.date.split('T')[0]
const hasMatchingDays = formatScheduleDate.toString() === formatCloneDay.toString()
if(hasMatchingDays) {
//here it adds 1 day from matching day
return (
<Link className="schedule-link" to="/exercise/:exerciseId" key={schedule._id}>
<span>{schedule.bodySection} {schedule.duration}</span>
</Link>
)
}
else return null
})
days.push(
<div
className={`column cell ${
!isSameMonth(day, monthStart) ? "disabled" :
isSameDay(day, selectedDate) ? "selected" : "" }`}
key={day}
onClick={() => onClickDate(cloneDay)}
>
<span className="number">{formattedDate}</span>
<span className="bg">{formattedDate}</span>
{hasSchedules ? scheduleElements : null}
</div>
)
//this will increase the day value by 1 each time it iterates
day = addDays(day, 1)
}
rows.push(
<div className="row" key={day}> {days} </div>
)
//if 7 days has been pushed to the rows, delete the days
//so it could start a new row with new days
days = []
}
return <div className="body">{rows}</div>
}
I track down the 'cloneDay' value and this +1 behaviour happens inside of the if statement
where it checks the hasMatchingDays. Specifically here in .map function.
const scheduleElements = exerciseScheduleArray.map(schedule => {
//reformat for the compare with formatCloneday
const formatScheduleDate = schedule.date.split('T')[0]
const hasMatchingDays = formatScheduleDate.toString() === formatCloneDay.toString()
if(hasMatchingDays) {
//here it adds 1 day from matching day
return (
<Link className="schedule-link" to="/exercise/:exerciseId" key={schedule._id}>
<span>{schedule.bodySection} {schedule.duration}</span>
</Link>
)
}
else return null
})
I wonder why it is causing that +1 day, and how can I render the schedule on correct date?
_
exerciseScheduleArray looks like this
After a long search,
I found the issue regarding date-fns UTC timezones.
As mentioned in this thread,
https://github.com/date-fns/date-fns/issues/376
this might be the reason why the cell function would render schedules in correct date in USA, or same timeszones,
and would render day + 1, in t.ex Berlin.
For now, I will switch to moment.js to re-create the cell function as a solution.
Hope this helped ppl who encountered same issue as I did.
I have a component which renders x amount of DateInputs based on x number of charges. The date inputs should be rendered with the default value loaded from the server. Once the onChange is triggered it updates the state in the parent and prints out the console with the selected date using the computedProperty. To give more context here is an image of the page:
A you can see, a datepicker is rendered for each charge and the user needs the ability to modify the charge date. So in terms of data flow, the onChange in the child component triggers the method handleChargesDateChange in the parent, as the state needs to be stored in parent.
At the minute, the default charge date is rendered in the dropdown, but when onChange is executed then date input value does not change. However, the method prints out the correct date that was selected. Again, for more context, here is a screenshot of the data stored in parent when child executes onChange on the date picker.
As you can see, the date values get modified when the user selects one from each datepicker. However, the actual datepicker value stays the same. I can get it to work where it renders with an empty date for each datepicker and when the user selects a new date, it will render the datepicker with the selected date. It also stored the value in parent. But I am really struggling to render the defalt value, but when it is changed update the datepicker with the user selected value... I hope this makes sense.. Anyway here's the code. So the method which gets triggered in parent:
handleChargesDateChange = (e) => {
const date = e.target.value;
const momentDate = moment(date);
const chargeId = e.target.name;
console.log(date);
console.log(momentDate);
this.setState({
updatedChargeDates: [
...this.state.updatedChargeDates,
{[chargeId]: date}
]
}, () => console.log(this.state.updatedChargeDates))
}
This then gets passed down (around 3 children deep) via:
<Grid.Row>
<Grid.Column width={16}>
<OrderSummary
order={this.state.order}
fuelTypes={this.state.fuelTypes}
vehicleClasses={this.state.vehicleClasses}
deviceTypes={this.state.deviceTypes}
chargeTypes={this.state.chargeTypes}
featureTypes={this.state.featureTypes}
itemGroups={this.state.itemGroups}
itemTypes={this.state.itemTypes}
updateChargeDates={this.handleChargesDateChange}
chargeDates={this.state.updatedChargeDates}
/>
</Grid.Column>
I will skip the various other children components that the method gets passed to and go straight to the effected component. So the method then lands at Charges:
export class Charges extends Component {
constructor(props) {
super(props);
this.state = {
currentItem: 0,
newDate: '',
fields: {}
}
}
render() {
const {charges, chargeTypes, updateChargeDates, chargeDates} = this.props;
console.log('Props method', charges);
if (charges.length === 0) { return null; }
return <Form>
<h3>Charges</h3>
{charges.map(charge => {
console.log(charge);
const chargeType = chargeTypes.find(type => type.code === charge.type) || {};
return <Grid>
<Grid.Row columns={2}>
<Grid.Column>
<Form.Input
key={charge.id}
label={chargeType.description || charge.type}
value={Number(charge.amount).toFixed(2)}
readOnly
width={6} />
</Grid.Column>
<Grid.Column>
<DateInput
name={charge.id}
selected={this.state.newDate}
***onChange={updateChargeDates}***
***value={chargeDates[charge.id] == null ? charge.effectiveDate : chargeDates[charge.id]}***
/>
</Grid.Column>
</Grid.Row>
</Grid>
})}
<Divider hidden />
<br />
</Form>
}
I have highlighted the culprit code with *** to make it easier to follow. I am using a ternary type operator to determine which value to display, but it doesnt seem to update when i select a value.. I thought that from the code I have, if the initial chargeDates[charge.id] is null then we display the charge.effectiveDate which is what it is doing, but soon as I change the date i would expect chargeDates[charge.id] to have a value, which in turn should display that value...
I really hope that makes sense, im sure im missing the point with this here and it will be a simple fix.. But seems as im a newbie to react, im quite confused..
I have tride to follow the following resources but it doesnt really give me what I want:
Multiple DatePickers in State
Daepicker state
If you need anymore questions or clarification please do let me know!
Looking at your screenshot, updatedChargeDates (chargeDates prop) is an array of objects. The array has indexes 0, 1 and 2, but you access it in the DateInput like this chargeDates[charge.id] your trying to get the data at the index 431780 for example, which returns undefined, and so the condition chargeDates[charge.id] == null ? charge.effectiveDate : chargeDates[charge.id], will always return charge.effectiveDate.
You can fix this in handleChargesDateChange by making updatedChargeDates an object, like this :
handleChargesDateChange = (e) => {
const date = e.target.value;
const momentDate = moment(date);
const chargeId = e.target.name;
console.log(date);
console.log(momentDate);
this.setState({
updatedChargeDates: {
...this.state.updatedChargeDates,
[chargeId]: date
}
}, () => console.log(this.state.updatedChargeDates))
}
I render several sections and one of the sections is a ScrollView. This ScrollView is a horizontal days calendar and each item is arenderRow() that and has View Text and Button wrapped together within one View:
render()
{
//..
let day = this.renderDaysView();
return(
<View style={{ backgroundColor: 'white' }}>
{day}
</View>
)
}
renderDaysView()
{
let renderRow = (rowData, rowID) => {
return (
<View key={rowID}>
<TouchableOpacity ... >
<View ... >
<Text ... >{currentWeekDay}</Text>
</View>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView ref={(scrollview) => this._daysScrollView = scrollview} horizontal= 'true'>
{
this.state.Days.map((item, index) => renderRow(item, index) )
}
</ScrollView>
}
}
Now, when the screen gets displayed, I would like todays day to be right in the middle of the screen.
Following the Docs, scrollTo() can take an object:
scrollTo( y? , object , x?, animated? )
Currently I am using this hack (which is missing one part) where I multiply the current day of month with a specific value:
componentWillUpdate(newProps: Object, newState: Object) {
this._daysScrollView.scrollTo(
{
x: // if first week, do nothin
newProps.currentDate.getDate() < 6 ?
0
: // elseif if last week, scroll to scrollView width (missing)
newProps.currentDate.getDate() > 27 ?
(newProps.currentDate.getDate() * 55 ) - ( 7 * 52) // TODO: should be scrollview width
: // else someday later, put in in the middle
(newProps.currentDate.getDate() - 4) * 55,
animated: true
}
)
}
So I either need to know the width of _daysScrollView, or jump to that object - 4 (to keep it in middle)
If my object is a component, how can I jump to a specific object? (For ex. if today is the 19th, go to index 19)
Or for my hack,
How can I get the width of my _daysScrollView?(_daysScrellView.width) did not work
Thanks
To use the scrollTo method you would have to get the x/y position of the element you wanted to scroll to. An easier solution might be something like this:
componentDidUpdate() {
let location = window.location;
let dayOfMonth = new Date.getDate();
location.hash = `#${dayOfMonth}`;
}
The new Date.getDate(); will return a number 1 - 31 depending on the day in the month. So if you set the id of each day in your calendar to a number corresponding to its day in the month. The above will scroll to it.
location.hash = `#${dayOfMonth}`;
will then scroll the document to the element with that id attribute.
Set the id of each day to the it's numerical position in the month and window.location.hash to route to the anchor of the day within the page.
EDIT: Try replacing your componentWillUpdate() method with this componentDidUpdate.