Vue doesn't update data local to component during method call - javascript

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);

Related

How to initialize data in component according to the received props and don't lose reactivity

I have a problem with initialization data before component is created. My actual question depends on that problem: I'm losing reactivity in one of my data properties because I initialize it in lifecycle hook. But I don't know how to initialize an array from data(){} with a length which I receive from props. If I make it in lifecycle hook, then I'm losing reactivity, as I metioned before.
Here is some more details about my component:
In my Vue.js learning I'm trying to implement a stepper component. I decided to make it a little more dynamic and to be with a flexible size. So in my props of stepper component I receive an Object with such structure:
stepperData: {
steps: 3, //maybe later I'll add more options to stepperData, so I decided to implement it as an Object, not Array of content.
content: [
{
header: "Stepper #1",
text: "Hello World 1!"
},
{
header: "Stepper #2",
text: "Hello World 2!"
},
{
header: "Stepper #3",
text: "Hello World 3!"
}
]
}
Than in my stepper component I am using a steps field to determine a length of another array which hold data about marked or unmarked steps. Here is a code which I am using to initialize that array of marked steps:
methods: {
initializeMarkedSteps() {
this.markedSteps = [];
for (var i = 0; i < this.dataStepper.steps; i++) {
this.markedSteps[i] = false;
}
}
},
created: function() {
this.initializeMarkedSteps();
}
markedSteps is an empty array in data(){}
So, after that, I had an array of false values. In my template I have a v-bind:class
<div class="circle" v-bind:class="{markedCircle: markedSteps[s]}">
Thanks to it all of the steps are unmarked and they can became marked after user clicks "next" button.
<my-btn #click="nextStep">Next</my-btn>
my-btn is my wrapper component for simple button.
Code in nextStep():
nextStep() {
for (let i = 0; i < this.dataStepper.steps; i++) {
if (this.markedSteps[i] === false) {
this.markedSteps[i] = true;
console.log(this.markedSteps);
return;
}
}
}
BUT, when I click button, markedCircle class is not assigned as I expect despite the fact, that acual value of markedSteps[i] was changed to true after button was clicked.
I am very frustrated with this stuff with which I am so messed up. Any help will be appreciated. I have already checked docs on this theme and also I've read "Reactivity in Depth" section but I didn't saw an answer.
There are multiple problems
In your examples you don't show how you initialize your data() but assuming from the code this.markedSteps = []; in initializeMarkedSteps I think you have no markedSteps in data(). That's problem number 1. Properties in data are only reactive if they existed when the instance was created (add markedSteps: [] into data())
Due to limitations in JavaScript, Vue cannot detect changes to an array when you directly set an item with the index - use Vue.set(this.markedSteps, i, true) instead

I'm trying to use jquery to create a div containing columns but I can't get my array to format correctly

I have an array that contains dates. and for some reason I can't get it to show on my screen I've been debugging for a few days now and I've tracked it down to a single line, but the line has worked before and I can't figure out what the issue might be.
The array looks like this:
var selectItems =
[ "05-26-2017", "06-02-2017", "06-09-2017",
"06-16-2017", "06-23-2017", "06-30-2017", "07-07-2017", "07-14-2017",
"07-21-2017", "07-28-2017"...];
It's passed as an argument from another function, but that's how it's showing in console.log().
I might be going about this the wrong way, maybe even a lot further around then I need to but this is what I've come up with:
1. function setTHead(selectItems) {
2 var formatString;
3. for (var x = 0; x < 12; x++) {
4. formatString = selectItems[x].replace(/[^0-9/-]/g, "").toString();
5. console.log(selectItems);
6. $('#datTab').append("<div id='col" + x + "' class='column'>'" + formatString + "'</div>");
7. }
8. }
the array up top is what's showing from the console.log 5 lines down.
the sixth line is what is seeming to give me issues. Nothing is put on the page at all.
I'm getting a console error saying:
jQuery.Deferred exception: selectItems is undefined setTHead#http://localhost/mySite/script.js:136:9
startUp2#http://localhost/mySite/script.js:146:5
#http://localhost/mySite/table.php:19:9
mightThrow#http://localhost/mySite/lib/jquery.js:3586:52
resolve/</process<#http://localhost/mySite/lib/jquery.js:3654:49
setTimeout handler*resolve/<#http://localhost/mySite/lib/jquery.js:3692:37
fire#http://localhost/mySite/lib/jquery.js:3320:30
fireWith#http://localhost/mySite/lib/jquery.js:3450:29
fire#http://localhost/mySite/lib/jquery.js:3458:21
fire#http://localhost/mySite/lib/jquery.js:3320:30
fireWith#http://localhost/mySite/lib/jquery.js:3450:29
ready#http://localhost/mySite/lib/jquery.js:3923:13
completed#http://localhost/mySite/lib/jquery.js:3933:9
EventListener.handleEvent*#http://localhost/mySite/lib/jquery.js:3949:9
#http://localhost/mySite/lib/jquery.js:39:9
#http://localhost/mySite/lib/jquery.js:17:3
undefined
followed by:
TypeError: selectItems is undefined
and thats pointing to line 6.
if anyone has any advice I would be very much appreciative. Thank you in advance.
EDIT: A little more code:
function startTblView(defSel) {
if (defSel === true) {
setCookie('defSel', true, 7);
} else{
setCookie('defSel', false, 7);
}
saveSelected();
window.open('table.php', '_self');
defSel = getCookie('defSel');
if (defSel) {
selectItems = getDefDates();
}else {
selectItems = reGetSelected();
}
setTHead(selectItems);
}
defSel, is a boolean passed from my last page stating whether I'm doing a default view or a custom view, the custom view is passed from saveSelected();
saveSelected is a function for just saving the selected global value as a cookie so I can pull it out on the next page.
getDefDates pulls the default values for the array
reGetSelected, gets the selected array from the cookie.
I apologize for wonky naming conventions. I'm the only one working on this site and I'm just making sure the names don't overlap.
You can do this :
HTML code
<div id="datTab"></div>
JS code
var selectItems =
[ "05-26-2017", "06-02-2017", "06-09-2017",
"06-16-2017", "06-23-2017", "06-30-2017", "07-07-2017", "07-14-2017",
"07-21-2017", "07-28-2017"];
function setTHead(selectItems) {
var formatString;
$.each( selectItems, function( index, value ){
formatString = value.replace(/[^0-9/-]/g, "").toString();
$('#datTab').append("<div id='col" + index + "' class='column'>'" + value + "'</div>");
});
};
You can use $.each, its better than 'for' with javascript.
The .each() method is designed to make DOM looping constructs concise
and less error-prone. When called it iterates over the DOM elements
that are part of the jQuery object. Each time the callback runs, it is
passed the current loop iteration, beginning from 0. More importantly,
the callback is fired in the context of the current DOM element, so
the keyword this refers to the element.
I did a JsFiddle
Here.

Vue.js v-for increment some variable in looped component

I'm trying to display a list of items on an event's agenda.
The event has a start_date end each item on the agenda has a duration in minutes, for example:
event:{
start_date: '2017-03-01 14:00:00',
agendas:[
{id:1,duration:10},
{id:2,duration:15},
{id:3,duration:25},
{id:4,duration:10}
]
}
Now, in my event component, I load agendas with a v-for:
<agenda v-for="(agenda,index) in event.agendas"
:key="agenda.id"
:index="index"
:agenda="agenda">
In agenda component, I want to increment the time at which each item starts:
<div class="agenda">
//adding minutes to start_date with momentJs library
{{ moment(event.start_date).add(agenda.duration,'m') }} //this should increment, not add to the fixed event start date
</div>
Currently it only adds to the fixed event start_date... I would like to show the times 14:00 for event 1, 14:10 for event 2, 14:25 for event 3 and 14:50 for event 4.
How can I increment the value in a v-for directive in Vue.js 2.0?
It looks like you already got an answer that works for you but I'll post this here in case anyone is looking for an alternate solution. The accepted answer might be good for an initial render of the agendas but will start to break if the agendas array is mutated or anything causes the list to re-render because the start time calculation is based on a stored value that gets incremented every iteration.
The code below adds a computed property (based on event.agendas) that returns a new array of of agenda objects, each with an added start property.
Vue.component('agenda', {
template: '<div>Agenda {{ agenda.id }} — {{ agenda.duration }} min — {{ agenda.start }}</div>',
props: {
agenda: Object
}
});
new Vue({
el: "#root",
data: {...},
computed: {
agendas_with_start() {
const result = [];
let start = moment(this.event.start_date);
for(let agenda of this.event.agendas) {
result.push({
id: agenda.id,
duration: agenda.duration,
start: start.format('HH:mm')
});
start = start.add(agenda.duration, 'm');
}
return result;
}
}
});
Then, in the template, the agendas_with_start computed property is used in the v-for:
<div id="root">
<h3>Event Start: {{ event.start_date }}</h3>
<agenda v-for="agenda in agendas_with_start"
:key="agenda.id"
:agenda="agenda"></agenda>
</div>
Here's a working codepen. The benefit of this approach is that if the underlying agendas array is mutated or re-ordered or the event start time changes or anything causes Vue to re-render the DOM, this computed property will be re-evaluated and the start times will be re-calculated correctly.
Vue.js will actually let you bind prop values to methods on the parent's scope, so the easiest way will be to do something like this:
<agenda class="agenda" v-for="(agenda,index) in event.agendas"
:key="agenda.id"
:index="index"
:agenda="agenda"
:start-time="get_start_time(agenda.duration)">
</agenda>
And then the get_start_time() is a method within the parent's scope:
new Vue({
el: '#app',
data: {
time_of_day: '',
event:{
// ...
},
},
methods: {
get_start_time(duration) {
if (this.next_event_start === '') {
this.next_event_start = moment(this.event.start_date);
}
this.event_start = this.next_event_start.format('h:mm a');
this.next_event_start.add(duration, 'minutes');
return this.event_start;
},
},
});
I've made a quick CodePen as a basic example to show it in action, but you'll need to update the actual code to compensate for multiple days.

EmberJS Observer Not Firing

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.

Is it possible to .sort(compare) and .reverse an array in angularfire?

Update: The problem I'm having is doing a combination of three things:
Adding a header to an array when the $priority (set to date created) changes. This is so I can group tasks by week and day in an ng-repeat.
Resorting that list when a task is checked. Checked tasks should go to the bottom.
When creating new tasks, I need to add them to the top of the list instead of the bottom.
Here is a plnkr of all the code: http://plnkr.co/edit/A8lDKbNvhcSzbWVrysVm
I'm using a priorityChanged function to add a header based on comparing the dates on a task:
//controller
var last = null;
$scope.priorityChanged = function(priority) {
var current = moment(priority).startOf('day');
var changed = last === null || !last.isSame(current);
last = current;
return changed;
};
//view
<li ng-repeat="task in list track by task.$id">
<h3 ng-show="priorityChanged(task.$priority)">{{getDayName(task.$priority)}}</h3>
and to move a task to the bottom of the list when a task is completed I am using a .sort function when I populate the task list:
var populateTasks = function(start, end) {
$scope.start = start;
$scope.end = end;
var ref = new Firebase('https://plnkr.firebaseio.com/tasks').startAt(start).endAt(end);
var list = $firebase(ref).$asArray();
list.sort(compare);
list.$watch(function() {
list.sort(compare);
});
function compare(a, b) {
return a.completeTime - b.completeTime;
}
$scope.list = list;
};
It seems as though these approaches will not work together. Is there a way of combining them so that when the list is re-sorted the ng-repeat will run through the tasks again and add the necessary headers? Is that the ideal solution? Can the header be separate?
Update: I moved the ng-init functionality directly into the h3 to try to get that to run again but it does not display the header in that case.
Update2: The header does seem to show up if at least two of the $priority dates are unique but I still have the problem of deleting or moving the associated list item removing the connected header.
USING A DIRECTIVE
You can create a directive to simplify things by nesting your client contents. demo
app.directive('repeatByWeek', function($parse, $window) {
return {
// must be an element called <repeat-by-week />
restrict: 'E',
// replace the element with template's contents
replace: true,
templateUrl: 'repeat.html',
// create an isolate scope so we don't interfere with page
scope: {
// an attribute collection="nameOfScopeVariable" must exist
'master': '=collection'
},
link: function(scope, el, attrs) {
// get the global moment lib
var moment = $window.moment;
scope.weeks = [];
updateList();
// whenever the source collection changes, update our nested list
scope.master.$watch(updateList);
function updateList() {
scope.weeks = sortItems(parseItems(scope.master));
}
function sortItems(sets) {
var items = [];
// get a list of weeks and sort them
var weeks = sortDescending(Object.keys(sets));
for(var i=0, wlen=weeks.length; i < wlen; i++) {
var w = weeks[i];
// get a list of days and sort them
var days = sortDescending(Object.keys(sets[w]));
var weekEntry = {
time: w,
days: []
};
items.push(weekEntry);
// now iterate the days and add entries
for(var j=0, dlen=days.length; j < dlen; j++) {
var d = days[j];
weekEntry.days.push({
time: d,
// here is the list of tasks from parseItems
items: sets[w][d]
});
}
}
console.log('sortItems', items);
return items;
}
// take the array and nest it in an object by week and then day
function parseItems(master) {
var sets = {};
angular.forEach(master, function(item) {
var week = moment(item.$priority).startOf('week').valueOf()
var day = moment(item.$priority).startOf('day').valueOf();
if( !sets.hasOwnProperty(week) ) {
sets[week] = {};
}
if( !sets[week].hasOwnProperty(day) ) {
sets[week][day] = [];
}
sets[week][day].push(item);
});
console.log('parseItems', sets);
return sets;
}
function sortDescending(list) {
return list.sort().reverse();
}
}
}
});
The repeat.html template:
<ul>
<!--
it would actually be more elegant to put this content directly in index.html
so that the view can render it, rather than needing a new directive for
each variant on this layout; transclude should take care of this but I
left it out for simplicity (let's slay one dragon at a time)
-->
<li ng-repeat="week in weeks">
<h3>{{week.time | date:"MMMM dd'th'" }}</h3>
<ul>
<li ng-repeat="day in week.days">
<h4>{{day.time | date:"MMMM dd'th'" }}</h4>
<ul>
<li ng-repeat="task in day.items">
<input type="checkbox" ng-model="task.complete" ng-change="isCompleteTask(task)">
<input ng-model="task.title" ng-change="updateTask(task)">
<span ng-click="deleteTask(task)">x</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
OTHER IDEAS
Most likely, you just need to move your changed out of ng-init. I don't think that is re-run when elements move/resort.
<li ng-repeat="task in list">
<h3 ng-show="priorityChanged(task.$priority)">{{getDayName(task.$priority)}}</h3>
<!-- ... -->
</li>
Since your list may resort several times, you can probably also get a pretty significant speed boost by using track by
<li ng-repeat="task in list track by task.$id">
If that doesn't resolve the problem, it might be time to think about writing your own directive (these are more fun than they sound) and possibly to consider setting aside AngularFire and going right to the source.
You really want a more deeply nested data structure here you can iterate at multiple levels, and you may need to structure that yourself either on the client or the server, now that you have a sense of how you want them organized (essentially a group by week functionality).
you could use "unshift" javascript function
var fruits = ["1", "2", "3", "4"];
fruits.unshift("5","6");
Result
[ '5', '6', '1', '2', '3', '4' ]

Categories

Resources