Intersection Observer doesn't observe new elements - vue.js

I need to observe elements in list and mark them as read when they appear in the user's viewport. But when I add new elements in the beginning of the array (unshift), observer doesn't work :(
I use Vue, but I know that the problem does not correlate with it.
Here is observer method, which don't fire for new elements:
onElementObserved(entries) {
entries.forEach(({ target, isIntersecting}) => {
if (!isIntersecting) {
return;
}
this.observer.unobserve(target);
setTimeout(() => {
const i = target.getAttribute("data-index");
this.todos[i].seen = true;
}, 1000)
});
}
codepen

The v-for items have no key specified, so Vue tracks each list element by index. When a new item is unshifted into the list, the new element will have an index of 0, which already exists in the list, so the existing element is simply patched in place. Since no new element is created, the Intersection Observer is not triggered.
To resolve the issue, set a unique key per item in v-for. For example, you could add an id property to each array element, and then bind that id as the key for <todo>:
let nextId = 0;👈
const TodoList = Vue.extend({
template: `
<div>
<ul class="TodoList">
<todo
v-for="(todo, i) in todos"
:todo="todo"
:observer="observer"
:index="i"
:key="todo.id"👈
></todo>
</ul>
<button #click="pushNewTodo()">PUSH NEW</button>
</div>
`,
data() {
return {
todos: [ 👇
{ id: nextId++, seen: false, text: "Add app skeleton" },
{ id: nextId++, seen: false, text: "Add to-do component" },
{ id: nextId++, seen: false, text: "Add to-do list component" },
{ id: nextId++, seen: false, text: "Style the components" },
{ id: nextId++, seen: false, text: "Add the IntersectionObserver" },
{ id: nextId++, seen: false, text: "Mark to-do's as seen" }
],
};
},
methods: {
pushNewTodo() { 👇
this.todos.unshift({ id: nextId++, seen: false, text: "Add app skeleton BLAH BLAH BLAH" })
},
}
})
updated codepen

Related

How do you push an object into another object when the data within it gets changed?

I have a very large array of objects containing various errors logs. There are 1015 objects in total. At the moment I am saving the entire array every time I make a small change to a value in one of the objects. This leads to timeout errors because it takes too long to go through the whole array.
So instead I would like to figure out how I can change it so that the program ONLY saves an object if that object has been changed on the frontend.
So if I have 1015 objects and I only change something in object no. 2, then only object no. 2 should be saved on submit.
I was thinking, maybe it would be possible to first let the program look for any changes. Then IF a change has occured it will move that particular object to a new (empty) object, which I can then submit.
So, in my code example, I would like to have this function implemented on the computed property "Fields" which has the key "done". This contains a checkbox that sets the value error.done to true or false. So I would like for the program to check if this specific value has changed. If it has changed from true to false or vice versa I would like to send this object to a new object.
eg. if errors.done is set from true to false, then move the changed object to a new object called changedValue.
<template>
<b-container>
<b-card class="mt-4">
<h5>{{ $t('events') }}</h5>
<b-table
:items="errors"
:fields="fields"
:per-page="[10, 25, 50]"
selectable
:select-mode="'single'"
#row-selected="onRowSelected"
#row-clicked="showModal"
sort-desc
/>
</b-card>
<error-log-entry-modal ref="errorLogEntryModal" :selected-error-log="selectedRows"/>
<button #click="submit">Submit</button>
</b-container>
</template>
<script>
import {errorService} from '#/services/error';
import ErrorLogEntryModal from '#/components/error-log/ErrorLogEntryModal';
import moment from 'moment';
export default {
components: {
ErrorLogEntryModal,
},
props: {
ownerId: String
},
data() {
return {
errors: null,
selectedRows: []
};
},
computed: {
fields() {
return [
{
key: 'done',
label: '',
thStyle: 'width: 1%',
template: {
type: 'checkbox',
includeCheckAllCheckbox: true,
}
},
{
key: 'priority',
label: this.$t('errorLogs.priority'),
sortable: true,
},
{
key: 'creationDateTime',
label: this.$t('creationDateTime'),
formatter: date => moment(date).locale(this.$i18n.locale).format('L'),
sortable: true,
},
{
key: 'stackTraceShort',
label: this.$t('errorLogs.stackTrace'),
sortable: true,
},
{
key: 'errorMessage',
label: this.$t('message'),
sortable: true
},
]
},
},
methods: {
load(){
errorService.getErrorLogs().then(result => {
result.data.forEach(log => log.stackTraceShort = log.stackTrace.substring(0,30));
this.errors = result.data
})
},
submit(){
return errorService.setStatusOnErrorEntryLog(this.errors).then( result => {
console.log(result)
})
},
onRowSelected(fields){
this.selectedRows = fields
},
showModal(){
if (this.selectedRows) {
this.$refs.errorLogEntryModal.show()
}
},
},
created() {
this.load()
},
};
</script>
If I have understood correctly the selected rows correspond to errors.done ? In this case you can just edit the onRowSelected method like this :
onRowSelected(fields){
this.selectedRows.push(fields)
},
Then replace this.errors by this.selectedRows in you're submit method ?

Vue Draggable Recursive / Nested Dragging Weird Behavior

This question will be bountied as soon as I can, so a big rep reward coming to someone!
EDIT: One more layer to this issue I noticed, The lowest level children when I try to drag it somewhere else it doesn't work, acts like it will but goes back where it was (i.e it's children are empty). But when I drag the parent of a child somewhere else it does one of the following:
Moves the parent and it's parent somewhere else where I drag it, but leaves the child where it was i.e
Initial state on a refresh
After I drag col-2 to container-1
or
Moves the child into the container where I dragged the parent to but leaves the parent where it was i.e
Initial state on a refresh
After dragging col-2 into col-1
Original Post
Hey guys, been building a landing page builder and decided vue-draggable would be a nice addition. That said after 3 days of headaches trying to make this work I'm at a loss. So far I've followed the nested example guide which has been KINDA working, in addition, I followed an issue about the nested guide on here adding an emitter to the children for proper updates. Now my getters and setters are firing BUT I'm still having a problem dragging elements(see the video)
http://www.giphy.com/gifs/ZMiyi8LEcI73nye1ZN
As you can see when I drag stuff around it's strange behavior:
Example cases:
When I drag col 2 label into col 1 it moves the children inside into
col one, does not change col 2s place
When I drag paragraph label
anywhere it will not move, shows like it will but when I release
nothing happens
If I drag row 1 from the original starting state you
saw in the gif into the paragraph I end up with the following:
Just 3 sample cases, references:
https://sortablejs.github.io/Vue.Draggable/#/nested-with-vmodel
https://github.com/SortableJS/Vue.Draggable/issues/701#issuecomment-686187071
my code creating these results:
component-renderer.vue (THERE'S A NOTE IN HERE TO READ)
<draggable
v-bind="dragOptions"
:list="list"
:value="value"
style="position: relative; border: 1px solid red"
:tag="data.tagName"
:class="data.attributes.class + ` border border-danger p-3`"
#input="emitter"
#change="onChange" //NOTE: I've tried setting the group here to row instead of in the computed prop below, didn't work
>
<slot></slot>
<component-renderer
v-for="el in realValue"
:key="el.attributes.id"
:list="el.children"
:data="el"
:child="true"
#change="onChange"
>
<span style="position: absolute; top: 0; left: 0; background: red">{{
`${el.tagName} - ${el.attributes.id}`
}}</span>
{{ el.textNode }}
</component-renderer>
</draggable>
</template>
<script>
import draggable from "vuedraggable";
export default {
name: "ComponentRenderer",
components: {
draggable,
},
props: {
data: {
required: false,
type: Object,
default: null,
},
value: {
required: false,
type: Array,
default: null,
},
list: {
required: false,
type: Array,
default: null,
},
child: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
dragOptions() {
return {
animation: 0,
disabled: false,
ghostClass: "row",
group: "row",
};
},
realValue() {
return this.value ? this.value : this.list;
},
},
methods: {
emitter(value) {
this.$emit("input", value);
},
onChange: function () {
if (this.child === true) {
this.$emit("change");
} else {
this.emitter(this.value);
}
},
},
};
</script>
<style scoped></style>
PageEditor.vue:
<div id="wysiwyg-page-editor">
<ChargeOverNavBar />
<div class="editor">
<ComponentRenderer v-model="elements" :data="elements[0]" />
</div>
<ChargeOverFooter />
</div>
</template>
<script>
import ChargeOverNavBar from "#/components/ChargeOverNavBar";
import ChargeOverFooter from "#/components/ChargeOverFooter";
import InlineEditor from "#ckeditor/ckeditor5-build-inline";
import ComponentRenderer from "#/components/module-editor/ComponentRenderer";
export default {
name: "PageEditor",
components: {
ComponentRenderer,
ChargeOverFooter,
ChargeOverNavBar,
},
data() {
return {
editor: InlineEditor,
editorConfig: {},
activeSection: null,
page: {},
panels: {
pageProperties: true,
seoProperties: true,
sectionProperties: false,
},
};
},
computed: {
elements: {
get() {
console.log("getter");
return JSON.parse(JSON.stringify(this.$store.state.editor.editorData));
},
set(value) {
console.log("setter");
this.$store.dispatch("setEditor", value);
},
},
},
};
</script>
<style lang="scss" scoped>
#use "../assets/scss/components/PageEditor";
</style>
editor.js(store module):
state: {
editorData: [],
editorLoading: false,
},
mutations: {
SAVE_EDITOR(state, data) {
state.editorData = data;
},
TOGGLE_EDITOR_LOAD(state, busy) {
state.editorLoading = busy;
},
},
actions: {
setEditor({ commit }, data) {
commit("SAVE_EDITOR", data);
},
loadEditor({ commit }) {
commit("TOGGLE_EDITOR_LOAD", true);
//TODO: Change me to read API DATA
let fakeData = [
{
tagName: "section",
attributes: {
id: "section-1",
class: "test",
},
children: [
{
tagName: "div",
attributes: {
id: "container-1",
class: "container",
},
children: [
{
tagName: "div",
attributes: {
id: "row-1",
class: "row",
},
children: [
{
tagName: "div",
attributes: {
id: "col-1",
class: "col",
},
children: [],
},
{
tagName: "div",
attributes: {
id: "col-2",
class: "col",
},
children: [
{
tagName: "p",
attributes: {
id: "p-1",
class: "p",
},
textNode: "This is my paragraph",
children: [],
},
],
},
],
},
],
},
],
},
];
commit("SAVE_EDITOR", fakeData);
commit("TOGGLE_EDITOR_LOAD", false);
},
},
getters: {
getEditorData: (state) => state.editorData,
getEditorLoading: (state) => state.editorLoading,
},
};
It seems only dragging labels works for moving stuff around but not like i'd expect. I think this makes sense but why can't I drag the body anywhere? It's not slotted in header or footer and the docs says that's the onlytime it wouldn't be? Can I not use as the tag itself and drag it?
As I'm sure all of you can deduce the behavior I'm expecting, anything should be draggable into any section(in the future I want this to change so only cols can be dragged into rows, rows can only be dragged into sections etc(but for now I'm not sure how to do this so we start at the beginning :D)).
Also yes I know those components are kinda messy atm, until I fix this I'm not cleaning them up as I keep drastically changing the contents of these files trying to make it work, sorry it's hacky atm!
Any help or ideas would be amazing!
Thanks guys!

Vuejs - list - How to pass click functions as string

I have an array of objects, each with a click property (a string) that is passed to a click-event handler. I can print the .click property to the console, but it is not recognized as Vue data. I tried to eval(todo.click), but it didn't work.
html:
<div id="app">
<h2>Todos:</h2>
<ol>
<li v-for="todo in todos">
<label #click="clickMethod(todo)">{{todo.text}}</label>
</li>
</ol>
<br>
<div v-if="infoVisible">infoVisible</div>
<div v-if="tresVisible">tresVisible</div>
</div>
and my js:
new Vue({
el: "#app",
data: {
infoVisible:false,
tresVisible:true,
todos: [
{ text: "Learn JavaScript", done: false, click:'infoVisible=!infoVisible' },
{ text: "Learn Vue", done: false, click:'infoVisible=!infoVisible' },
{ text: "Play around in JSFiddle", done: true , click:'infoVisible=!infoVisible'},
{ text: "Build something awesome", done: true , click:'tresVisible=!tresVisible'}
]
},
methods: {
clickMethod(todo){
console.log(todo.click)
todo.click()
}
}
})
Fiddle
Instead of using strings as functions (which would require eval()), you could define function expressions:
new Vue({
el: "#app",
data: (vm) => ({
infoVisible: false,
tresVisible: true,
todos: [
{ ..., click() { vm.infoVisible = !vm.infoVisible } },
{ ..., click() { vm.infoVisible = !vm.infoVisible } },
{ ..., click() { vm.infoVisible = !vm.infoVisible } },
{ ..., click() { vm.tresVisible = !vm.tresVisible } },
]
}),
methods: {
clickMethod(todo){
todo.click()
}
}
})
Steps:
In todos[], change the type of .click properties from strings to function expressions:
//click: 'infoVisible = !infoVisible' // from strings
click() { infoVisible = !infoVisible } // to function expressions (to be updated in step 3)
In the function body, a reference to the Vue instance is required so that click() can change the data properties (i.e., infoVisible and tresVisible). Update the Vue declaration's data property to be a function that takes an argument (the argument will be the Vue instance itself):
data: (vm) => ({/* ... */})
Update click() to use that argument to reference the target data properties:
click() { vm.infoVisible = !vm.infoVisible }
^^^ ^^^
updated fiddle
eval(todo.click) will work but you need to add "this." to all of the todo properties in the click attributes so they have the right context, that is the context of the Vue instance.
new Vue({
el: "#app",
data: {
infoVisible:false,
tresVisible:true,
todos: [
{ text: "Learn JavaScript", done: false, click:'this.infoVisible=!this.infoVisible' },
{ text: "Learn Vue", done: false, click:'this.infoVisible=!this.infoVisible' },
{ text: "Play around in JSFiddle", done: true , click:'this.infoVisible=!this.infoVisible'},
{ text: "Build something awesome", done: true , click:'this.tresVisible=!this.tresVisible'},
]
},
methods: {
clickMethod(todo){
eval(todo.click)
}
}
})

How to trigger a Vue method by name for each element in a v-for loop?

I'm having troubles triggering a separate Vue instance method by name for each element in a v-for loop on click.
Each action corresponds to a method, but it's not triggered. What am I doing wrong?
Code:
<v-btn v-for="btn in windowControlButtons" :key="btn.id"
#click="btn.action"
>
<v-icon size="20px">{{btn.icon}}</v-icon>
</v-btn>
...
window: remote.getCurrentWindow(),
windowControlButtons: [
{
icon: 'remove',
action: minimizeWindow()
},
{
icon: 'crop_square',
action: maximizeWindow()
},
{
icon: 'close',
action: closeWindow()
}
]
...
methods: {
minimizeWindow() {
this.window.minimize()
},
maximizeWindow() {
this.window.maximize()
},
closeWindow() {
this.window.close()
}
}
UPDATE
I can trigger some code directly in the data(), e.g.:
...
{
icon: 'remove',
action: () => {remote.getCurrentWindow().minimize()}
},
But what if a method wasn't as short?
How do I trigger a method already specified in methods: { }?
btn.action is a string, thus you can't execute it.
Every Vue instance/component method is accessible as a property in the vm.$options.methods object.
I suggest creating another method, say handleClick, to simplify your method calling depending on the button, and invoke the best suitable method from this.$options.methods as shown below.
new Vue({
el: '#app',
data: {
windowControlButtons: [
{id: 1, icon: 'remove', action: 'minimizeWindow'},
{id: 2, icon: 'crop_square', action: 'maximizeWindow'},
{id: 3, icon: 'close', action: 'closeWindow'}
]
},
methods: {
handleClick(button) {
if (this.$options.methods[button.action]) { // guard to prevent runtime errors
this.$options.methods[button.action]();
}
},
minimizeWindow() {
console.log('minimizeWindow');
},
maximizeWindow() {
console.log('maximizeWindow');
},
closeWindow() {
console.log('closeWindow');
}
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<div id="app">
<button v-for="btn in windowControlButtons" :key="btn.id" #click="handleClick(btn)">
<span>{{btn.icon}}</span>
</button>
</div>

VueJS Virtual field on array item

I am building something with VueJS and I have some problem to select an item in a list:
Let's imagine the following VueJS component:
new Vue({
el: '#app',
data: {
list: [
{
id: 1,
title: 'My first Item',
selected: false
},
{
id: 2,
title: 'My second Item',
selected: false
}
]
}
})
With the selected property, I can apply a class or not to the item:
<div id="app">
<ul>
<li #click="item.selected = !item.selected" :class="item.selected ? 'active' : ''" v-for="item in list">{{ item.title }}</li>
</ul>
</div>
But now, let's imagine that I grab my data from an API, I still want to be able to select the items:
new Vue({
el: '#app',
data: {
list: []
},
created: function () {
// Let's imagine that this is an Ajax Call to a webservice
this.$set('list', [
{
id: 1,
title: 'My first Item'
},
{
id: 2,
title: 'My second Item'
}
])
}
})
Now, my html can't work anymore because the data has not a selected property.
So how could I do such a thing?
Here are two JsFiddle that explain the problem:
The working one
The non working one
Please read docs on vue lifecycle :
id prefer you set the list to be a computed property that always checks for returned items : ie ,
new Vue({
el: '#app',
data: {
list: []
},
computed: {
list (){
// Let's imagine that this is an Ajax Call to a webservice
let returned = [
{
id: 1,
title: 'My first Item'
},
{
id: 2,
title: 'My second Item'
}
]
return returned
}
}
})