I am trying to get drag and drop function working in the vue.js app using vue-draggable https://vuejsexamples.com/vuejs-drag-and-drop-library-without-any-dependency/
The library has few events you can listen to and I would like to execute some logic once the item is dropped. However, I am not able to access vue component 'this' along with the data and methods. I've tried to use this.$dispatch('symDragged', event); but it is not working for the same reason. 'this' is not a vue instance but rather instance of draggable element.
Here is the code:
export default {
components: {
ICol,
SymptomsChooser, MultiSelectEditor, TempPressureChooser, BodyPartsEditor, MandatorySymptomsChooser},
data() {
return {
// data ommited...
options: {
dropzoneSelector: 'ul',
draggableSelector: 'li',
excludeOlderBrowsers: true,
showDropzoneAreas: true,
multipleDropzonesItemsDraggingEnabled: true,
onDrop(event) {
// delete symptom from old basket and add it to new one
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
//this is not working
//this.symDragged(this.draggedSymId, oldBasket, newBasket);
},
onDragstart(event) {
this.draggedSymId = event.items[0].accessKey;
}
}
}
},
methods: {
symDragged(symId, oldBasketId, newBasketId) {
console.log("symDragged!");
let draggedSym = this.getSymById(symId);
let basketOld = this.getBasketById(oldBasketId);
this.delSym(basketOld, draggedSym);
this.addSym({baskedId: newBaskedId, sym: draggedSym});
}
//other methods ommited
}
}
So, what is the correct way to call the vue component method from callback event? Or maybe I need to create another event so that vue instance could listen to it?
Thanks for you help!
The problem you are facing is that with this you are referencing to the returned data object scope and not component scope. The best way to solve this is to make reference to the component instance, so later on you can call anything attached to that instance. You can also take a look at codesandbox example https://codesandbox.io/embed/7kykmmmznq
data() {
const componentInstance = this;
return {
onDrop() {
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
let draggedItemsAccessKeys = event.items.map(element => element.accessKey);
componentInstance.symDragged(
draggedItemsAccessKeys,
oldBasket,
newBasket
);
}
}
}
Related
I am new to front-end testing, and I am trying to get a good grasp on it.
In the process, inside one project, I am creating a test for a Vue component. I want to test that the behaviour of the code is correct (The component has code inside the mounted() hook that has to perform basically some checks and an API call).
I want to check that the code reaches one method. Previously to that, the code creates a click event listener to one element in the DOM.
My test emulates a click event (triggers it), but it cannot assert that the proper method has been called after the click event.
This is due to it not finding the element in the DOM to which it has to add the event listener. It seems that the code cannot find anything inside the document (using .getElementById()).
I wonder why, and how I would resolve this, since I have been stuck here for hours and I haven't found any solution that could work here, even when I have learned some interesting things in the process. I will leave a code example with the code structure I have built:
Inside the component:
<template>
// ...
<button id = "myButton">Add</button>
</template>
<script>
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile ";
let classIESF = new classInExternalScriptsFile();
export default {
methods: {
setup: function () {
classIESF.setupMethod();
},
},
mounted() {
this.setupMethod();
},
};
</script>
Inside the scriptsFile
export class classInExternalScriptsFile {
setupMethod() {
let myButton = document.getElementById("myButton") // <-- getElementById() returns a null here
if (typeof myButton !== "undefined" && myButton !== null) {
myButton.onclick = () => { // <-- The test code complains because it cannot enter here
// Some lines...
this.mySuperMethod()
}
}
}
mySuperMethod() {
// API call etc.
}
}
Inside the .spec.js test file:
// imports...
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile.js";
describe("description...", () => {
const mySuperMethodMock = vi
.spyOn(classInExternalScriptsFile.prototype, "mySuperMethod")
.mockImplementation(() => {});
test("That the button performs x when clicked", () => {
let wrapper = mount(myComponent, {
props: ...,
});
let myButton = wrapper.find('[test-id="my-button"]');
myButton.trigger("click");
expect(mySuperMethodMock).toHaveBeenCalled(); // <-- The test fails here
}
}
can anyone provide a solution to the problem that I'm currently encountering? I created a custom element where this custom element must have been detected on the dom, but I need to have the data contained in this custom element loaded, so my program code is like this.
import './menu-item.js';
class MenuList extends HTMLElement {
// forEach cannot be used if I use the ConnectedCallback () method
connectedCallback() {
this.render()
}
// my data can be from this method setter
set menus(menus) {
this._menus = menus;
this.render();
}
render() {
this._menus.forEach(menu => {
const menuItemElement = document.createElement('menu-item');
menuItemElement.menu = menu;
this.appendChild(menuItemElement);
});
}
}
customElements.define('menu-list', MenuList);
and this is the data I sent in the main.js file
import '../component/menu/menu-list.js';
import polo from '../data/polo/polo.js';
const menuListElement = document.querySelector('menu-list');
menuListElement.menus = polo;
please give me the solution.
The connectedCallback runs before the menus=polo statement.
So there is no this._menus declared.
If all the menus setter does is call render, then why not merge them:
set menus(menus) {
this.append(...menus.map(menu => {
const menuItemElement = document.createElement('menu-item');
menuItemElement.menu = menu;
return menuItemElement;
}));
}
I am trying to access a DOM element with Vue, using the $refs functionality, but I am having trouble getting it to work.
My element looks like so below. The plateId is generated dynamically, so it will not always be the same number:
<textarea :ref="plateId + '-notes'">
My Vue function looks like so:
/* This does not work */
addNotes: function(plateId) {
console.log(this.$refs.plateId + '-notes');
}
Whenever I run this code and the function is activated, it just reads undefined in my console. I've also tried this, which also does not work and reads undefined:
/* This does not work */
addNotes: function(plateId) {
var plateIdNotes = plateId + '-notes';
console.log(this.$refs.plateIdNotes);
}
Replacing var with const (I am using ES6 and transpiling the code) doesn't work either:
/* This does not work */
addNotes: function(plateId) {
const plateIdNotes = plateId + '-notes';
console.log(this.$refs.plateIdNotes);
}
I know the ref is binding correctly to the element, because when I do this below, I can see all of my other refs in the console, as well as the plateId-notes ref:
/* This works */
addNotes: function(plateId) {
console.log(this.$refs);
}
How can I access the plateId ref using the parameter in my function?
you can use the [] notation:
methods: {
foo (id) {
alert(this.$refs[id + '-test'].innerText)
}
}
A complete working example: https://jsfiddle.net/drufjsv3/2/
also, you can acces to all the $refs rendered in view by accessing
vm.$children.forEach( child => {
var tag = child.$vnode.data.ref;
console.log(vm.$refs[tag]);
vm.$refs[tag].loadData();
});
// loadData() is a method to implement when mounted or when you want to reload data
////////////////////////////////////
<script>
export default {
data() {
return {
notifications: []
}
},
mounted() {
this.loadData();
},
methods: {
loadData: function() {
axios.get('/request-json/notifications').then(res => {
this.notifications = res.data;
});
},
.....
I am trying to use .bind() when using a method in my component.
The reason is simple: In a loop I am returing Components and extend them with a property which is calling a method. But for every loop-item this I want to extend the this Object with some information (like a key).
Example:
Items.jsx
Items = React.createClass({
eventMethod() {
console.log('this event was triggered by key:', this.key);
},
items() {
let items = [];
let properties = {};
_.each(this.props.items, (itemData, key)=>{
properties.eventMethodInItem = this.eventMethod.bind(_.extend(this, {
key
}));
let {...props} = properties;
let item = <Item {...props} key={key} />;
items.push(item);
});
return items;
},
render() {
return(<div>{this.items()}</div>);
}
});
Item.jsx
Item = React.createClass(...);
In this case (and its working) when the Item Component is triggering the prop "eventMethodInItem" my method "eventMethod" will be called and this.key has the correct value!
So - whats now the question ? Its working perfect, right ?
Yes.
But ReactJS does not want me to do this. This is what ReactJS is telling me as a console log.
Warning: bind(): You are binding a component method to the component. React does this for you automatically in a high-performance way, so you can safely remove this call. See Items
Maybe you think its a "bad" way to add children to the component like I am doing it but in my special case I need to do this in this way - so I need to bind new information to a method.
I'm not going to pretend that I understand what you are trying to do here, but maybe I can help clear it up anyway.
React takes all of the top level methods found on each component and automagically binds them to the context of the component.
This prevents other methods from overriding the context of this and as a result, if you try to rebind the method, React says "Hey don't bother. I already got it" — which is the warning you are seeing.
Assuming that you really want do this (each time you are mutating the outer properties object by overriding the eventMethodInItem property).
properties.eventMethodInItem = this.eventMethod.bind(_.extend(this, {
key
}));
Then I can't see any reason that the eventMethod has to live on the component, rather than just in the scope of the items function.
items() {
const eventMethod = function() {
console.log('this event was triggered by key:', this.key);
}
// ...
_.each(this.props.items, (itemData, key)=>{
properties.eventMethodInItem = eventMethod.bind(_.extend(this, {
key
}));
// ...
});
},
That way you don't have to fight React to get your program to work.
React is already autobinding this when using React.createClass http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#under-the-hood-autobinding-and-event-delegation
Change your binding to
properties.eventMethodInItem = this.eventMethod.bind(null,key);
and your eventMethod to
eventMethod(key) {
console.log('this event was triggered by key:', key);
}
I also suggest using _.map instead of _.each
items() {
return _.map(this.props.items, (itemData, key) => {
return <Item
handleEventMethod={this.eventMethod.bind(null,key)}
key={key} />;
});
},
Good pattern
https://www.newmediacampaigns.com/blog/refactoring-react-components-to-es6-classes
Before :
class ExampleComponent extends React.Component {
constructor() {
super();
this. _handleClick = this. _handleClick.bind(this);
this. _handleFoo = this. _handleFoo.bind(this);
}
// ...
}
After :
class BaseComponent extends React.Component {
_bind(...methods) {
methods.forEach( (method) => this[method] = this[method].bind(this) );
}
}
class ExampleComponent extends BaseComponent {
constructor() {
super();
this._bind('_handleClick', '_handleFoo');
}
// ...
}
another good hacks for this topic http://egorsmirnov.me/2015/08/16/react-and-es6-part3.html
I have 2 components - addProjectForm and listProjects. They are both nested components inside the root module. Whenever I add a project using the form, I want it to appear in the list straight away.
To achieve this, I had to pass down the controller instance to each component like this:
var RootComponent = {};
rootComponent.controller = function() {
this.example = 'test variable';
}
rootComponent.view = function(ctrl) {
return [
m.component(addProjectForm, ctrl),
m.component(listProjects, ctrl)
];
}
and then the listProjectscomponent for example, looks like this:
var listProjects = {
controller: function(root) {
this.root = root;
},
view: function(ctrl) {
console.log(ctrl.root.example);
}
};
So this way I keep calling methods on the top level, but I don't quite like passing down the controller instance like this. Is there any other way I should be doing it?
I think this is what you're looking for:
Mithril.js: Should two child components talk to each other through their parent's controller?
A newer way of solving this common problem is to use a Flux like architecture developed by Facebook:
https://facebook.github.io/flux/
Writing your own dispatcher is semi-trivial. Here's an example that someone else built alongside Mithril:
https://gist.github.com/MattMcFarland/25fb4f0241530d2f421a
The downside with this approach is it would be somewhat anti-Flux to use m.withAttr, as views aren't supposed to write directly to models in the dispatcher paradigm.
The problem you have is the difference between passing by reference or by value. In JS all primitive types are passed by value. Thats why you can't pass the string directly since it's cloned during pass. You have multiple options here:
You can use m.prop and just pass the variable down to the components, m.props stores the value in function that is always passed by reference.
var RootComponent = {};
rootComponent.controller = function() {
this.example = m.prop('test variable');
}
rootComponent.view = function(ctrl) {
return [
m.component(addProjectForm, ctrl.example),
m.component(listProjects, ctrl.example)
];
}
If the variable is an array, it will be passed by reference anyways.
Second option is to keep the list in the root context and add a callback to the second component.
var RootComponent = {};
rootComponent.controller = function() {
var projects = this.projects = [];
this.addProject = function(project) {
projects.push(project);
}
}
rootComponent.view = function(ctrl) {
return [
m.component(addProjectForm, {
onsubmit: ctrl.addProject
}),
m.component(listProjects, ctrl.projects)
];
}