I'm building a simple weekly calendar component for my app and I'm struggling to find a way for creating the weeks navigation. Here's what I have so far:
/week-view/component.js
import Ember from 'ember';
export default Ember.Component.extend({
firstDay: moment().startOf('week'),
days: Ember.computed('firstDay', function() {
let firstDay = this.get('firstDay').subtract(1, 'days');
let week = [];
for (var i = 1; i <= 7; i++) {
var day = firstDay.add(1, 'days').format('MM-DD-YYYY');
week.push(day);
}
return week;
}),
actions: {
currentWeek() {
this.set('firstDay', moment().startOf('week'));
},
previousWeek() {
this.set('firstDay', moment().startOf('week').subtract(7, 'days'));
},
nextWeek() {
this.set('firstDay', moment().startOf('week').add(7, 'days'));
}
}
});
/week-view/template.hbs
<button {{action "previousWeek"}}>previous</button>
<button {{action "currentWeek"}}>current week</button>
<button {{action "nextWeek"}}>next</button>
<ul>
{{#each days as |day|}}
<li>{{day}}</li>
{{/each}}
</ul>
At the moment it works to navigate one week before and one after the current week only. Any idea on how to make this work for an unlimited number of weeks is greatly appreciated. Thanks in advance.
I think you shouldn't change your firstDay property while prepairing week array (in the computed function). It overrides momentjs state. Computed property in this case should just read firstDay property without affecting changes to it.
Also in your previous and next week actions you don't have to create new momentjs objects. You can easily operate on previously created firstDay property, for example.
this.get('firstDay').subtract(7, 'days');
But in this case, state of momentjs have changed, but emberjs doesn't see any changes. That is because your firstDay property doesn't really changed (and computed property is set to check only firstDay, it doesn't work deeply). In fact firstDay property is just reference to momentjs object, and that object has been changed, not the reference. But luckily you can manually force emberjs to reload any computed properties based on any property in this way:
this.notifyPropertyChange('firstDay');
So little bit refactored working example can looks like:
import Ember from 'ember';
export default Ember.Component.extend({
selectedWeek: moment().startOf('week'),
days: Ember.computed('selectedWeek', function() {
let printDay = this.get('selectedWeek').clone();
let week = [];
for (var i = 1; i <= 7; i++) {
week.push(printDay.format('MM-DD-YYYY'));
printDay.add(1, 'days');
}
return week;
}),
actions: {
currentWeek() {
this.set('selectedWeek', moment().startOf('week'));
},
previousWeek() {
this.get('selectedWeek').subtract(7, 'days');
this.notifyPropertyChange('selectedWeek');
},
nextWeek() {
this.get('selectedWeek').add(7, 'days');
this.notifyPropertyChange('selectedWeek');
}
}
});
Related
I am working on a form in React that uses a date picker, which uses a ref. However, when multiple of these date pickers are put onto one form, the refs on each datepicker instance seems to refer to the last one defined on the form. For example, choosing a date on the first or second date picker would only update the last one but not itself.
Where I define the ref:
<div className={styles.datePicker} ref={this.setWrapperRef}>
<div className={styles.mdpInput} onClick={()=> this.showDatePicker(true)}>
<input type='date' onChange={this.updateDateFromInput} ref={inputRef}/>
</div>
...rest of code
</div>
Setting the ref:
setWrapperRef(node){
this.wrapperRef = node;
}
I also made sure to bind the function:
constructor(props) {
super(props);
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth();
this.state = {
year,
month,
selectedDay: todayTimestamp,
monthDetails: this.getMonthDetails(year, month)
}
this.setWrapperRef = this.setWrapperRef.bind(this);
}
I just learned about refs and I am fairly new to React, so there might be a simple answer that I am overlooking or unaware of. Thanks!
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
I've got a vue component that will show a calendar week. The component is meant to be modular so it will not know what days are populated with what dates until it's parent component (the month) passes in the data.
My template looks like this:
<div class="cs-week">
<div class="day" v-for="n in 7">
<!-- I'm still building it out, so for now I jsut want to show the date -->
{{ dayLabels[n] }}
</div>
</div>
The Vue Component looks like this:
module.exports = {
props:[
'events',
'weekdata',
'weeknumber'
],
data: function (){
return {
// initializing each of the day label slots so the view doesn't blow up for not having indexed data when rendered
dayLabels: [
null,
null,
null,
null,
null,
null,
null
]
}
},
methods:{
loadWeek: function (){
for(
var i = this.weekdata.days[0],
j = this.weekdata.dates[0];
i <= this.weekdata.days[1];
i++, j++
){
this.dayLabels[i] = j;
}
},
test: function (){
this.loadWeek();
}
}
}
The data being passed to the component from the parent tells it the range of the days to fill and the dates to use:
weekdata: {
days: [3,6], // start on wednesday, end on saturday
dates: [1,3] // use the 1st through the 3rd for the labels
}
When I fire this method, the data updates, but the bound elements never update:
The thing is, if I hard code an update to the labels array before I iterate through the loop...
loadWeek: function (){
debugger;
this.dayLabels = [1,2,3,3,2,1]; // arbitrary array data assigned
for(
var i = this.weekdata.days[0],
j = this.weekdata.dates[0];
i <= this.weekdata.days[1];
i++, j++
){
this.dayLabels[i] = j;
}
},
... the bound elements will update:
Is there a reason why it won't work without the arbitrary assignment before the loop??
When you change an array by setting a value in it, Vuejs cannot detect the change and wont fire any on-change methods. See here: http://vuejs.org/guide/list.html#Caveats
You can use the $set() method to change an object in an array, and that will force Vue to see the change. So in your for-loop
this.dayLabels.$set(i, j);
EmberJSBin here
I have a component which needs to perform a given operation when any of its values are changed, but the observer I created does not fire:
export default Ember.Component.extend({
taxes: null,
NOAYears: 2,
NOAValues: Ember.computed("NOAYears", function() {
var NOAs = [],
lastYear = new Date().getFullYear() - 1;
for (var year = 0; year < this.get("NOAYears"); year++) {
NOAs.push({year: lastYear - year, value: 0});
}
return NOAs;
}),
NOAYearsChanged: function() {
// does not fire when any of the values change
}.observes("NOAValues.#each.value")
});
In the component template, I am binding via the {{#each}} iterator:
{{#each NOAValues as |year|}}
<label for="{{year.year}}-value">{{year.year}}</label>
{{input min="0" step="any" value=year.value required=true placeholder="Yearly Income"}}
{{/each}}
How can I get my observer to fire when any of the value properties in NOAValues is changed?
This issue has been verified as a bug, caused by legacy code, which interprets any property name beginning with a capital letter (i.e. PascalCase) as either a global or a Class name reference... rendering the property unobservable.
Source: https://github.com/emberjs/ember.js/issues/10414
It seems like efforts will be made to fix it in some upcoming releases.
In order to observe property changes, you need to use a setter for a given property. I think you introduce a NOA model that extends Ember.Object it should be sufficient. For example:
// file app/model/noa.js
export default Ember.Object.extend({
year: undefined,
value: undefined
});
and then replace this:
for (var year = 0; year < this.get("NOAYears"); year++) {
NOAs.push({year: lastYear - year, value: 0});
}
with
for (var year = 0; year < this.get("NOAYears"); year++) {
NOAs.push(app.model.noa.create({year: lastYear - year, value: 0}));
}
You should see some property changes.
I have view model with 3 fields
dateStart = ko.observable();
dateEnd = ko.observable();
days = ko.observable();
assuming startDate is selected, whenever endDate is selected days field needs to be updated (days = endDate - startDate).
Also when days field is updated i need to calculate endDate (endDate = startDate + days).
how can this be done with knockoutjs ?
Thank You!
I've tried
http://jsfiddle.net/NfG4C/6/, but my js always throws too musch recursion exception.
From what I understand, you basically need 2 things.
You want to calculate the "days" field whenever someone selects the
"endDate" [assuming they have selected the "startDate" ofcourse]
You want to recalculate the "endDate" field whenever someone changes the
"days" field
One way to solve this would be to use a "writeable" computed Observable [http://knockoutjs.com/documentation/computedObservables.html]. Please go through the link for details, but in general term, a 'writeable computed observable' is something whose value is 'computed' based on some 'other' observable(s) and vice-versa.
I took the liberty to modify your fiddle and change the "days" as a computed observable. Please take a look: http://jsfiddle.net/dJQnu/5/
this.days = ko.computed({
read: function () {
//debugger;
// here we simply need to calculate the days as => (days = endDate - startDate)
if (that.dateStart() && that.dateEnd()) {
var vacDayCounter = 0;
for (var curDate = new Date(that.dateStart()); curDate <= that.dateEnd(); curDate = curDate.addDays(1)) {
if (isDateCountsAsVacation(curDate)) {
vacDayCounter++;
}
}
//that.days(vacDayCounter);
return vacDayCounter;
}
},
write: function (newDays) {
if (newDays && !isNaN(newDays) && that.dateStart()) {
var tmpEndDate = new Date(that.dateStart())
appliedDays = 0;
while (appliedDays < newDays) {
if (isDateCountsAsVacation(tmpEndDate)) {
appliedDays++;
}
tmpEndDate = tmpEndDate.addDays(1);
}
if (tmpEndDate) {
that.dateEnd(tmpEndDate);
}
}
}
});
If you notice, I simply reused your code (logic) for the read and write part. During read, we are "computing" the value for the observable itself, in this case the "days" and during write (which fires anytime the user changes the actual "days" input value) we are recalculating the "dateEnd" field.
Please let me know if you have any other question.
Hope this helps.
Thanks.
You get a recusive problem because when updating the observables from a subscription will also trigger that observables subscribe method.
You need to add a fourth member, updatedFromSubscriber
set it to false from tbe beginning, in each subscribe method add
if(this.updatedFromSubscriber)
return;
and just before updating the observable do
this.updatedFromSubscriber = true
set it to false after updating the observable