Creating Vue Component Dropdown with Popper Js - javascript

I'm on progress to make a dropdown component with vue and popperjs. To be known i'm using vuejs v.2.6.12 and popperjs v.2.9.2 and here is the code
<template>
<button type="button" #click="show = true">
<slot />
<portal v-if="show" to="dropdown">
<div>
<div
style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2"
#click="show = false"
/>
<div
ref="dropdown"
style="position: absolute; z-index: 99999;"
#click.stop="show = autoClose ? false : true"
>
<slot name="dropdown" />
</div>
</div>
</portal>
</button>
</template>
<script>
import { createPopper } from "#popperjs/core";
export default {
props: {
placement: {
type: String,
default: "bottom-end"
},
boundary: {
type: String,
default: "scrollParent"
},
autoClose: {
type: Boolean,
default: true
}
},
data() {
return {
show: false
};
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = createPopper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: [
{
name: "preventOverflow",
options: {
boundary: this.boundary
}
}
]
});
});
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100);
}
}
},
mounted() {
document.addEventListener("keydown", e => {
if (e.keyCode === 27) {
this.show = false;
}
});
}
};
</script>
When i'm trying to run the code, i get error message Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.
I don't know why i got this error, i think i've put the reference this.$el and the popper this.$refs.dropdown on the right place.
Could anybody here to help me for solving this problem?
Thank You

I created a snippet that works without the mentioned errors.
you used vue-portal: if it's not globally imported in your code, then you have to import it to the SFC
you have to create <portal-target> somewhere, otherwise this.$refs.dropdown won't exist, as it's the default content of the portal.
const createPopper = Popper.createPopper
/* import {
createPopper
} from "#popperjs/core"; */
Vue.component('DropDown', {
props: {
placement: {
type: String,
default: "bottom-end"
},
boundary: {
type: String,
default: "scrollParent"
},
autoClose: {
type: Boolean,
default: true
}
},
data() {
return {
show: false
};
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = createPopper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: [{
name: "preventOverflow",
options: {
boundary: this.boundary
}
}]
});
});
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100);
}
}
},
mounted() {
document.addEventListener("keydown", e => {
if (e.keyCode === 27) {
this.show = false;
}
});
},
template: `
<button type="button" #click="show = true">
<slot />
<portal
v-if="show"
to="dropdown"
>
<div>
<div
style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2"
#click="show = false"
/>
<div
ref="dropdown"
style="position: absolute; z-index: 99999;"
#click.stop="show = autoClose ? false : true"
>
<slot name="dropdown" />
</div>
</div>
</portal>
</button>
`
})
new Vue({
el: "#app",
template: `
<div>
<drop-down>
<template
v-slot:default
>
Dropdown default slot
</template>
<template
v-slot:dropdown
>
This is the dropdown slot content
</template>
</drop-down>
<portal-target
name="dropdown"
/>
</div>
`
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<script src="http://unpkg.com/portal-vue"></script>
<script src="https://unpkg.com/#popperjs/core#2"></script>
<div id="app"></div>

Related

vuejs onblur directive not working as expected, while should hide the list

I'm trying to make a simple dropdown search select list.
I'd like to hide the name list by clicking outside of it. And the issue is that when I click outside the list of search items by using #blur directive the appropriate list item doesn't fill the input field. It's assumingly because of the #click="selectCategory(category)" is triggered later than #blur="isVisible = false" in the template.
<template>
<div сlass="search-bar" :style="{'position' : (isVisible) ? 'absolute' : 'fixed'}">
<input
type="text"
v-model="input"
#focus="isVisible = true"
// #blur="isVisible = false" doesn't work as required
/>
<div class="search-bar-options" v-if="isVisible">
<div v-for="category in filteredUser" :key="category.id" #click="selectCategory(category)">
<p>{{ category.name }}</p>
</div>
<div v-if="filteredUser.length === 0">
<p>No results found!</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
input: "",
selectedItem: null,
categoriesDynamic: [],
isVisible: false,
};
},
mounted() {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((json) => {
this.categoriesDynamic = json;
});
},
filteredUser() {
const query = this.input.toLowerCase();
if (this.input === "") {
return this.categoriesDynamic;
} else {
return this.categoriesDynamic.filter(category => {
return category.name.toLowerCase().includes(query);
});
}
},
},
methods: {
selectCategory(category) {
this.input = category.name;
this.isVisible = false;
},
},
};
</script>
<style scoped>
.pointer {
cursor: pointer;
}
.show {
visibility: show;
}
.hide {
visibility: hidden;
}
</style>
One solution could be to detect the clicked element. If the clicked element is not an input or a category element, then the dropdown can be closed.
Here is a working demo in which I gave a class "category" to each list item for element detecting purposes.
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data() {
return {
input: "",
selectedItem: null,
categoriesDynamic: [],
isVisible: false,
};
},
mounted() {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((json) => {
this.categoriesDynamic = json;
});
},
created() {
document.addEventListener('click', (e) => {
let isInput = e.target instanceof HTMLInputElement;
let isCategoryEl = e.target.classList.contains('category');
if (isInput || isCategoryEl) return;
this.isVisible = false;
})
},
computed: {
filteredUser() {
const query = this.input.toLowerCase();
if (this.input === "") {
return this.categoriesDynamic;
} else {
return this.categoriesDynamic.filter(category => {
return category.name.toLowerCase().includes(query);
});
}
},
},
methods: {
selectCategory(category) {
this.input = category.name;
this.isVisible = false;
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div сlass="search-bar" :style="{'position' : (isVisible) ? 'absolute' : 'fixed'}">
<input
type="text"
v-model="input"
#focus="isVisible = true"
/>
<div class="search-bar-options" v-if="isVisible">
<div v-for="category in filteredUser" :key="category.id" #click="selectCategory(category)">
<p class="category">{{ category.name }}</p>
</div>
<div v-if="filteredUser.length === 0">
<p>No results found!</p>
</div>
</div>
</div>
</div>

Vue.js rendering after 2nd onClick

I have a form that uses dropdowns to allow the user to select their configuration and when they click apply a highchart is rendered. At first glance this works perfectly.
My problem is that if after the chart is rendered you open a dropdown again to make a change without closing the dropdown and click apply we get the chart with the previous configuration. I have to click apply a second time to get the new configuration to show up.
What am I missing?
Here is part of the complete code:
<template>
<div class="dropdown multiple-select">
<GlobalEvents v-if="open" #click="handleClick" #keydown.esc="close" />
<button
ref="button"
class="btn btn-block btn-multiple-select"
type="button"
:disabled="disabled"
#click="toggle"
>
<span v-if="internalValue.length === 0"> {{ emptyLabel }} </span>
<span v-else> {{ internalValue.length }} Selected </span>
<b-icon :icon="open | icon" :scale="0.5" />
</button>
<div ref="dropdown" class="dropdown-menu" :class="{ show: open }">
<template v-if="!loading">
<div class="px-4 pt-1">
<div class="form-group">
<b-form-input
v-model="search"
debounce="500"
placeholder="Search"
/>
</div>
</div>
<div class="scroll px-4">
<b-form-checkbox v-model="selectAll" class="py-1" #change="toggleAll">
Select all
</b-form-checkbox>
<b-form-checkbox
v-for="item in filtered"
:key="item.id"
v-model="internalValue"
:value="item"
class="py-1"
#input="checkSelectAllStatus"
>
{{ item.name }}
</b-form-checkbox>
<p v-if="filtered.length === 0">No results.</p>
</div>
</template>
<div v-else class="text-center my-2">
<b-spinner />
</div>
</div>
</div>
</template>
<script>
import { createPopper } from '#popperjs/core';
import GlobalEvents from 'vue-global-events';
export default {
components: {
GlobalEvents
},
filters: {
icon(item) {
return item ? 'caret-up-fill' : 'caret-down-fill';
}
},
model: {
prop: 'value',
event: 'change'
},
props: {
emptyLabel: {
type: String,
default: () => 'None Selected'
},
disabled: {
type: Boolean,
default: () => false
},
loading: {
type: Boolean,
default: () => false
},
options: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
}
},
data() {
return {
internalValue: this.value,
open: false,
popper: null,
search: '',
selectAll: false
};
},
computed: {
filtered() {
return this.options.filter((item) =>
item.name.toLowerCase().includes(this.search.toLowerCase())
);
},
showAll() {
return (
this.internalValue.length > 0 &&
this.internalValue.length === this.options.length
);
}
},
watch: {
options() {
this.checkSelectAllStatus();
},
internalValue() {
this.checkSelectAllStatus();
},
value(value) {
this.internalValue = value;
this.$emit('change', this.internalValue);
}
},
methods: {
checkSelectAllStatus() {
this.selectAll = this.internalValue.length === this.options.length;
},
close() {
this.open = false;
this.search = '';
this.$emit('change', this.internalValue);
},
create() {
this.popper = createPopper(this.$refs.button, this.$refs.dropdown, {
placement: 'bottom-start',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 10]
}
}
]
});
},
destroy() {
if (this.popper) {
this.popper.destroy();
this.popper = null;
}
},
handleClick(event) {
if (!this.$el.contains(event.target)) {
this.close();
}
},
toggle() {
this.open = !this.open;
if (this.open) {
this.$emit('open');
this.create();
} else {
this.destroy();
}
},
toggleAll(checked) {
if (checked) {
this.internalValue = this.options;
} else {
this.internalValue = [];
}
}
}
};
</script>
Found a solution that works with what we already have. My MultipleSelect component was invoking #input="checkSelectAllStatus"
I added this.$emit('change', this.internalValue); to the checkSelectAllStatus method and it worked.
You seem to be using vuelidate - is there a good reason why you aren't accessing your form values directly but are going through your validation model instead?
Without knowing your config, your code should look more like this:
async apply() {
try {
this.chartOptions.utilityMetric = this.form.metric;
this.chartOptions.title = this.getTitle();
this.loading = true;
this.$v.$touch()
if (this.$v.$error) throw new Error('form not valid')
const response = await await this.$api.organizations.getAnnualWidget(
this.organizationId,
this.parentUtility,
this.requestParams
);
this.chartOptions.categories = response.data.categories;
this.chartOptions.series = response.data.series;
this.chartOptions.yAxis = response.data.yAxis;
this.$forceUpdate();
} catch (error) {
const message = getErrorMessage(error);
this.$bvToast.toast(message, {
title: 'Error',
toaster: 'b-toaster-top-center',
variant: 'danger'
});
} finally {
this.loading = false;
}
}
and
requestParams() {
return {
calendarization: this.form.calendarization,
metrics: this.form.metric.map((metric) => metric.id),
meterIds: this.form.meterIds,
Years: this.form.fiscalYears.map((year) => year.value),
waterType: this.form.waterType,
includeBaseline: this.form.baseLine,
showDataLabels: this.form.showDataLabels
};
},
Again, without knowing your complete code it's difficult to tell, but I am guessing it's the pointing to vuelidate that's causing the issues

Autocomplete - select option with mouse click - vuejs

Some time ago I created an autocomplete component in vue for a project in which I am involved.
But today I detected a small bug.
When I select the option I want with the click of the mouse, the option does not get transmitted, as you can see in the console.log () that is in the example. If I click on another option again, what will appear in console.log () is the option previously selected.
If I put a setTimeout( () => {}, 200) it already detects and emit the option, but I think it is not the best solution for this case.
Any suggestion?
example
const Autocomplete = {
name: "autocomplete",
template: "#autocomplete",
props: {
items: {
type: Array,
required: false,
default: () => Array(150).fill().map((_, i) => `Fruit ${i+1}`)
},
isAsync: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isOpen: false,
results: [],
search: "",
isLoading: false,
arrowCounter: 0
};
},
methods: {
onChange() {
console.log( this.search)
// Let's warn the parent that a change was made
this.$emit("input", this.search);
},
setResult(result, i) {
this.arrowCounter = i;
this.search = result;
this.isOpen = false;
},
showAll() {
this.isOpen = !this.isOpen;
(this.isOpen) ? this.results = this.items : this.results = [];
},
},
computed: {
filterResults() {
// first uncapitalize all the things
this.results = this.items.filter(item => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
return this.results;
},
},
watch: {
items: function(val, oldValue) {
// actually compare them
if (val.length !== oldValue.length) {
this.results = val;
this.isLoading = false;
}
}
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
destroyed() {
document.removeEventListener("click", this.handleClickOutside);
}
};
new Vue({
el: "#app",
name: "app",
components: {
autocomplete: Autocomplete
}
});
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.autocomplete {
position: relative;
width: 130px;
}
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto;
width: 100%;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 2px;
cursor: pointer;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4aae9b;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<autocomplete />
</div>
<script type="text/x-template" id="autocomplete">
<div class="autocomplete">
<input type="text" #blur="onChange" v-model="search" #click="showAll" />
<ul id="autocomplete-results" v-show="isOpen" ref="scrollContainer" class="autocomplete-results">
<li class="loading" v-if="isLoading">
Loading results...
</li>
<li ref="options" v-else v-for="(result, i) in filterResults" :key="i" #click="setResult(result, i)" class="autocomplete-result" :class="{ 'is-active': i === arrowCounter }">
{{ result }}
</li>
</ul>
</div>
</script>
You were using onblur event, but its fired when you click outside and before the onclick's item listener, so the value wasn't updated.
Use onchange event to capture data if user types anything in the input and call onChange() method inside setResult().
const Autocomplete = {
name: "autocomplete",
template: "#autocomplete",
props: {
items: {
type: Array,
required: false,
default: () => Array(150).fill().map((_, i) => `Fruit ${i+1}`)
},
isAsync: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isOpen: false,
results: [],
search: "",
isLoading: false,
arrowCounter: 0
};
},
methods: {
onChange() {
console.log( this.search)
// Let's warn the parent that a change was made
this.$emit("input", this.search);
},
setResult(result, i) {
this.arrowCounter = i;
this.search = result;
this.isOpen = false;
// Fire onChange, because it won't do it on blur
this.onChange();
},
showAll() {
this.isOpen = !this.isOpen;
(this.isOpen) ? this.results = this.items : this.results = [];
},
},
computed: {
filterResults() {
// first uncapitalize all the things
this.results = this.items.filter(item => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
return this.results;
},
},
watch: {
items: function(val, oldValue) {
// actually compare them
if (val.length !== oldValue.length) {
this.results = val;
this.isLoading = false;
}
}
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
destroyed() {
document.removeEventListener("click", this.handleClickOutside);
}
};
new Vue({
el: "#app",
name: "app",
components: {
autocomplete: Autocomplete
}
});
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.autocomplete {
position: relative;
width: 130px;
}
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto;
width: 100%;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 2px;
cursor: pointer;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4aae9b;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<autocomplete />
</div>
<script type="text/x-template" id="autocomplete">
<div class="autocomplete">
<input type="text" #change="onChange" v-model="search" #click="showAll" />
<ul id="autocomplete-results" v-show="isOpen" ref="scrollContainer" class="autocomplete-results">
<li class="loading" v-if="isLoading">
Loading results...
</li>
<li ref="options" v-else v-for="(result, i) in filterResults" :key="i" #click="setResult(result, i)" class="autocomplete-result" :class="{ 'is-active': i === arrowCounter }">
{{ result }}
</li>
</ul>
</div>
</script>
blur is the wrong event to use here, and I think you're over complicating it. Simply call your emit in setResult:
setResult(result, i) {
this.arrowCounter = i;
this.search = result;
this.isOpen = false;
this.$emit("input", this.search);
},

class binding always active

I am having a minor issue, that I simply can't figure out.
I am trying to activate class active, when selecting a profession. Can you help me spot my mistake? The class I am targeting is image-rect.
<div
v-for="(profession, index) in professions"
:key="index"
>
<div
class="profession-btn"
:class="(selected === index) ? 'active' : ''"
#click="toggleProfession(index, profession)"
>
<div class="wrapper">
<div
class="image-rect"
:class="(selected === index) ? 'active' : ''"
>
<img :src="profession.icon">
</div>
</div>
<div
class="profession-name"
:style="[selected === index ? { opacity: 1 } : { opacity: 0.3 }]"
>
{{ profession.name }}
</div>
</div>
</div>
script
export default {
props: {
professions: {
type: Array,
default: () => []
},
selectedProfession: {
type: Object,
default: () => {}
}
},
data: () => ({
checked: false,
selected: 0
}),
updated() {
console.log('selectedProfession', this.selectedProfession)
if (this.selectedProfession) {
this.selected = this.professions.findIndex(profession => profession.name === this.selectedProfession.name)
}
},
methods: {
toggleProfession(index, profession) {
this.checked = !this.checked
this.selected = index
const price = this.checked ? profession.price : 0
const name = this.checked ? profession.name : ''
this.$parent.getProfession(price, name)
}
}
}
CSS
.image-rect {
width: 40px;
height: 40px;
background-color: white;
padding: 0px;
border-radius: 10px;
margin: 0 auto;
}
.image-rect.active {
background-color: #ef793c;
}

Component not rerendering - ReactJS

I have something similar to a notes app, and want to be able to drag and drop cards from one group to another (by using react-dnd). Naturally, after a card is dropped, I want to remove it from the source group and add it to the target group. Removing works fine, but the card is not being rendered in the target group. Here is the relevant code:
App = React.createClass({
getInitialState: function() {
...
return {
appState: appState
}
}
removeCard: function(card) {
var content = this.state.appState[card.groupId].content;
content.splice(content.indexOf(card), 1);
this.setState({ appState: this.state.appState });
},
addCard: function(card, target) {
var content = this.state.appState[target.groupId].content;
content.splice(content.indexOf(target) + 1, 0, card);
this.setState({ appState: this.state.appState });
},
onCardDrop: function(source, target) {
this.addCard(source, target); // didn't work
this.removeCard(source); // worked
},
render: function() {
var that = this;
var appState = this.state.appState;
return (
<div>
{_.map(appState, function(group) {
return (
<Group
key={group.id}
id={group.id}
group={group}
onCardDrop={that.onCardDrop} />
)
})}
</div>
)
}
});
So, the card is removed from the source group, but it never appears in the target group even though the console.log of the target group shows the card is there. Is it possible that for some reason the component is not rerendering.
The Group and Card components are rendering ul and li respectively.
I took some time to make a working example based on the code you provided... but it did work. No problems in the code you provided. This indicates that the problem lies elsewhere in your code.
I cannot give you a complete answer because the snippet you provided does not follow the Minimal, Complete, and Verifiable example rule. Though it is minimal, it's incomplete, and also not verifiable.
What I can do is paste the whole code that I made here and hope that it will be useful to you.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://npmcdn.com/react-dnd-html5-backend#2.1.2/dist/ReactDnDHTML5Backend.min.js"></script>
<script src="https://npmcdn.com/react-dnd#2.1.0/dist/ReactDnD.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<style>
ul {
display: inline-block;
padding: 10px;
width: 100px;
border: 1px solid gray;
vertical-align: top;
}
li {
display: block;
padding: 0;
width: 100px;
text-align: center;
box-sizing: border-box;
position: relative;
}
li.group {
}
li.card {
height: 100px;
border: 1px solid black;
line-height: 100px;
margin-top: 5px;
font-size: 25px;
font-weight: bold;
cursor: move;
}
li > span {
vertical-align: middle;
display: inline-block;
}
</style>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
window.ItemTypes = {
CARD: "card",
GROUP_TITLE: "group-title"
};
</script>
<script type="text/babel">
var cardSource = {
beginDrag: function (props) {
return { cardId: props.id, groupId: props.groupId, card: props.card };
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
var cardTarget = {
drop: function (props, monitor) {
var item = monitor.getItem();
console.log(item.card)
console.log(props.card)
props.onCardDrop(item.card, props.card);
},
canDrop: function (props, monitor) {
var item = monitor.getItem();
return item.cardId != props.id;
}
};
function collectTgt(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
};
}
window.Card = React.createClass({
propTypes: {
connectDragSource: React.PropTypes.func.isRequired,
isDragging: React.PropTypes.bool.isRequired,
isOver: React.PropTypes.bool.isRequired,
canDrop: React.PropTypes.bool.isRequired
},
renderOverlay: function (color) {
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
zIndex: 1,
opacity: 0.5,
backgroundColor: color,
}} />
);
},
render: function() {
var connectDragSource = this.props.connectDragSource;
var isDragging = this.props.isDragging;
var connectDropTarget = this.props.connectDropTarget;
var isOver = this.props.isOver;
var canDrop = this.props.canDrop;
return connectDropTarget(connectDragSource(
<li className="card" style={{opacity: isDragging ? 0.5 : 1}}
><span>{this.props.card.name}-{this.props.card.groupId}</span>
{isOver && !canDrop && this.renderOverlay('red')}
{!isOver && canDrop && this.renderOverlay('yellow')}
{isOver && canDrop && this.renderOverlay('green')}
</li>
));
}
});
window.Card = ReactDnD.DragSource(ItemTypes.CARD, cardSource, collect)(window.Card);
window.Card = ReactDnD.DropTarget(ItemTypes.CARD, cardTarget, collectTgt)(window.Card);
</script>
<script type="text/babel">
window.Group = React.createClass({
render: function() {
console.log(this.props.group)
var that = this;
return (
<ul>
<li className="group">Group #{this.props.group.id}</li>
{_.map(this.props.group.content, function(card) {
return (
<Card
key={card.name}
id={card.name}
groupId={card.groupId}
card={card}
onCardDrop={that.props.onCardDrop}
/>
)
})}
</ul>
);
}
});
</script>
<script type="text/babel">
window.App = React.createClass({
getInitialState: function() {
return {
appState: [
{
id: 0,
content: [
{
groupId: 0,
name: "C1"
},
{
groupId: 0,
name: "C2"
},
{
groupId: 0,
name: "C3"
},
{
groupId: 0,
name: "C4"
}
]
},
{
id: 1,
content: [
{
groupId: 1,
name: "C5"
},
{
groupId: 1,
name: "C6"
},
{
groupId: 1,
name: "C7"
},
{
groupId: 1,
name: "C8"
}
]
}
]
};
},
removeCard: function(card) {
var content = this.state.appState[card.groupId].content;
content.splice(content.indexOf(card), 1);
this.setState({ appState: this.state.appState });
},
addCard: function(card, target) {
var content = this.state.appState[target.groupId].content;
content.splice(content.indexOf(target) + 1, 0, card);
card.groupId = target.groupId;
this.setState({ appState: this.state.appState });
},
onCardDrop: function(source, target) {
this.removeCard(source); // worked
this.addCard(source, target); // worked
},
render: function() {
var that = this;
var appState = this.state.appState;
return (
<div>
{_.map(appState, function(group) {
return (
<Group
key={group.id}
id={group.id}
group={group}
onCardDrop={that.onCardDrop}
/>
)
})}
</div>
)
}
});
window.App = ReactDnD.DragDropContext(ReactDnDHTML5Backend)(window.App);
</script>
<script type="text/babel">
ReactDOM.render(
<App />,
document.getElementById('example')
);
</script>
</body>
</html>

Categories

Resources