I'm using the Tiptap editor and I have issues accessing the content of the editor.
I need to post the content of the editor to an API so I need the content. Here is my code:
import tippy from "tippy.js";
import { Editor, EditorContent, VueRenderer } from "#tiptap/vue-2";
import Document from "#tiptap/extension-document";
import Paragraph from "#tiptap/extension-paragraph";
import Text from "#tiptap/extension-text";
export default {
components: {
EditorContent
},
props: {
comment_type: String,
comment_text: String,
comment_id: Number,
edit_mode: Boolean
},
data() {
return {
editor: null,
limit: 280,
users: [],
comment_da: {},
edit_comment_da: {}
};
},
watch: {
comment_text(value) {
console.log(value);
this.editor.setContent(value);
}
},
mounted() {
const self = this;
const CustomParagraph = Paragraph.extend({
addKeyboardShortcuts() {
return {
// ↓ your new keyboard shortcut
"Shift-Enter": () => self.addComment()
};
}
});
this.editor = new Editor({
extensions: [
Document,
CustomParagraph,
Text,
],
content: this.comment_text
});
},
beforeDestroy() {
this.editor.destroy();
},
}
<template>
<div>
<editor-content :editor="editor" class="form-control"/>
</div>
</template>
In the parent component I have some properties which I need to pass to the Tiptap editor:
import editor from "./components/editor";
new Vue({
components: {
editor
},
data() {
comment_da: {},
comment_text: "",
}
})
<div class="m-messenger__form-controls">
<editor
v-model="comment_text"
:comment_text="comment_text"
:comment_type="'comment'"
:comment_id="comment_da.id"
/>
</div>
I cannot access the editor content. I've tried this solution but I'm getting this error constantly while typing in it:
Error: "getHTML is not a function"
I found the answer in the Tiptap documentation:
import editor from "./components/editor";
new Vue({
components: {
editor
},
data() {
comment_da: {},
comment_text: "",
}
})
<div class="m-messenger__form-controls">
<editor
v-model="comment_text"
:comment_text="comment_text"
:comment_type="'comment'"
:comment_id="comment_da.id"
/>
</div>
import tippy from "tippy.js";
import { Editor, EditorContent, VueRenderer } from "#tiptap/vue-2";
import Document from "#tiptap/extension-document";
import Paragraph from "#tiptap/extension-paragraph";
import Text from "#tiptap/extension-text";
export default {
components: {
EditorContent
},
props: {
comment_type: String,
comment_text: String,
comment_id: Number,
edit_mode: Boolean
},
data() {
return {
editor: null,
limit: 280,
users: [],
comment_da: {},
edit_comment_da: {}
};
},
watch: {
comment_text(value) {
console.log(value);
this.editor.setContent(value);
}
},
mounted() {
const self = this;
const CustomParagraph = Paragraph.extend({
addKeyboardShortcuts() {
return {
// ↓ your new keyboard shortcut
"Shift-Enter": () => self.addComment()
};
}
});
this.editor = new Editor({
extensions: [
Document,
CustomParagraph,
Text,
Mention.configure({
HTMLAttributes: {
class: "mention"
},
suggestion: {
items: query => {
var self = this;
const search_user = new crud(
"/chat/mention/" + query
);
search_user.get_all_data(true, false, data => {
self.users = data.data;
});
return this.users.filter(item =>
item.name
.toLowerCase()
.startsWith(query.toLowerCase())
);
},
render: () => {
let component;
let popup;
return {
onStart: props => {
component = new VueRenderer(MentionList, {
parent: this,
propsData: props
});
popup = tippy("body", {
getReferenceClientRect:
props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start"
});
},
onUpdate(props) {
component.updateProps(props);
popup[0].setProps({
getReferenceClientRect: props.clientRect
});
},
onKeyDown(props) {
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
}
};
}
}
})
],
content: this.comment_text,
onUpdate() {
// You can access to both HTML and JSON type of your content
const json = this.getJSON();
const html = this.getHTML();
// send the content to an API here
this.comment_text = json.content[0].content[0].text
? json.content[0].content[0].text
: "";
}
});
},
beforeDestroy() {
this.editor.destroy();
},
}
<template>
<div>
<editor-content :editor="editor" class="form-control"/>
</div>
</template>
If you are curious, you can take a look at onUpdate part of my code for get the changes.
Related
I need to render a map using Mapbox only when data is ready.
I have the following code in my Vuex store:
/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
In my App.vue I fetch all APIs requests using an async created method.
App.vue:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
async created() {
await this.$store.dispatch("FETCH_ACTIVITY");
await this.$store.dispatch("FETCH_GEO_IPS");
},
};
</script>
In my Dashboard component I have a conditional rendering to draw the maps component only when geoIps.length > 0
Dashboard.vue:
<template>
<div v-if="geoIps.length > 0">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
geoIps() {
return this.$store.state.geoIps;
},
};
</script>
Then I load the Maps component.
<template>
<q-card class="bg-primary APP__card APP__card-highlight">
<q-card-section class="no-padding no-margin">
<div id="map"></div>
</q-card-section>
</q-card>
</template>
<script>
import "mapbox-gl/dist/mapbox-gl.css";
import mapboxgl from "mapbox-gl/dist/mapbox-gl";
export default {
name: "maps-geo-ips-card",
computed: {
geoIps() {
return this.$store.state.geoIps;
},
},
created() {
mapboxgl.accessToken = process.env.MAPBOX_API_KEY;
},
mounted() {
const mapbox = new mapboxgl.Map({
container: "map",
center: [0, 15],
zoom: 1,
});
this.geoIps.map((geoIp) =>
new mapboxgl.Marker()
.setLngLat([geoIp.longitude, geoIp.latitude])
.addTo(mapbox)
);
},
};
</script>
<style>
#map {
height: 500px;
width: 100%;
border-radius: 25px;
overflow: hidden;
}
</style>
The problem is that when the function resolves the first IP address, the map is drawn showing only one address and not all the others like this:
What is the best way to only draw the map when my FETCH_GEO_IPS function has finished?
Thanks in advance
I think the answer lies in this bit of code:
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
Your map function loops through every element of the array and commits each IP one by one. So when the first one is committed, your v-if="geoIps.length > 0" is true.
A workaround would be to set a flag only when the IPs are set.
This is a proposed solution:
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
isReady: false
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
setIsReady: (state, value) => {
state.isReady = value;
}
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
let tofetch = getters.activityIps.length; // get the number of fetch to do
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
toFetch -= 1; // decrement after each commit
if (toFetch === 0) {
commit("setIsReady", true); // all commits are done
}
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
And in your view:
<template>
<div v-if="isReady">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
isReady() {
return this.$store.state.isReady;
},
};
</script>
Functional Button component:
const Button = createElement('button', {}, [slots().icon ? createElement('span', slots().icon[0].text) : null])
Another functional component that uses Button above:
createElement(Button, {}, ['this goes to children'])
But this renders text this goes to children not inside the span as i wrapped it above.
How can i put contents inside a slot in a Button component from another createElement()?
With templates it's easy enough:
<template>
<Button>
<template #icon>This would be wrapped inside span</template>
</Button>
</template>
UPDATE 1
I've to utilize slot: 'name-of-the-slot' key in the data properties:
const icon = createElement('span', { slot: 'icon' }, 'text inside span')
createElement(Button, {}, [icon])
No success. Does it even work? Created a bug report in a Vue repo: https://github.com/vuejs/vue/issues/11519
SEMI-SOLUTION
With the Posva's help:
export default {
name: "Wrapper",
functional: true,
render(h) {
return h(Button, { scopedSlots: {
icon: () => h('span', {}, 'its from wrapper')
} });
}
};
export default {
name: "Button",
functional: true,
render(createElement, { scopedSlots }) {
return createElement("button", {}, scopedSlots.icon(null));
}
};
ScopedSlots was the key.
Also don't forget to add check, if this slot exists, like:
return createElement("button", {}, scopedSlots.icon ? scopedSlots.icon(null) : null)
A component using template:
MyButton.vue
<template>
<div>
<slot name="left"></slot>
<button>
<slot v-bind:person="person">
<span>按钮</span>
</slot>
</button>
<slot name="right" v-bind:age="person.age"></slot>
</div>
</template>
<script>
export default {
name: 'MyButton',
data() {
return {
person: {
name: 'jack',
age: 23,
},
}
},
}
</script>
use MyButton in template:
<MyButton>
<template #right="{age}">
<span>button rigt side. I am {{ age }} years</span>
</template>
<template v-slot="{ person }">this is a button,{{ person }}</template>
<template #left>
<span>button left side</span>
</template>
</MyButton>
use it in render function:
import MyButton from './MyButton.vue'
export default {
name: 'UseButton',
render(h) {
const slotLeft = h('template', { slot: 'left' }, 'in the left of button')
const slotRight = h('template', { slot: 'right' }, 'right')
const slotDefault = h('template', { slot: 'default' }, 'default slot')
const children = [slotRight, slotLeft, slotDefault]
// normal slots in third parms, any order work well
return h(MyButton, {}, children)
},
}
get data from scoped slot in render function:
import MyButton from './MyButton.vue'
export default {
name: 'UseButton',
render(h) {
const slotLeft = h('template', { slot: 'left' }, 'normal slot with name left')
const children = [slotLeft]
return h(
MyButton,
{
// scopedSlot in the second param
scopedSlots: {
// props come from MyButton inside
default: props => {
console.log(props)
const { person } = props
const text = `defaul scopedSlot,${JSON.stringify(person)}`
// use h
return h('span', {}, text)
},
right: props => {
console.log(props)
const { age } = props
// use jsx
return <span>in the right,I am {age} years</span>
},
},
},
// normal slot
children
)
},
}
implement of MyButton.vue with jsx or js.
MyButton.jsx
export default {
name: 'MyButton',
data() {
return {
person: {
name: 'jack',
age: 23,
},
}
},
render(h) {
const { left, right, default: _defaultSlot } = this.$scopedSlots
// checek the default exist or not
const defaultSlot = (_defaultSlot && _defaultSlot({ person: this.person })) || <span>按钮</span>
const leftSlot = (left && left()) || ''
const rightSlot = right(this.person)
const button = h('button', {}, [defaultSlot])
// jsx make structure more clear
return (
<div>
{leftSlot}
{button}
{rightSlot}
</div>
)
},
}
use functional component with jsx:
FunctionalButton.jsx
export default {
name: 'FunctionalButton',
functional: true,
props: {
person: {
type: Object,
default: () => ({ name: 'jack', age: 23 }),
},
},
// NO DATA in functional component
// data() {
// return {
// person: {
// name: 'jack',
// age: 23,
// },
// }
// },
render(h, { props, scopedSlots }) {
const { left, right, default: _defaultSlot } = scopedSlots
const defaultSlot = (_defaultSlot && _defaultSlot({ person: props.person })) || <span>按钮</span>
const leftSlot = (left && left()) || ''
const rightSlot = right(props.person)
const button = h('button', {}, [defaultSlot])
return (
<div>
{leftSlot}
{button}
{rightSlot}
</div>
)
},
}
Using dynamic component resolved a problem for me, you can simply pass render function into "is" attribute like this
<div v-for="slot in a.tabs"><component :is="slot.renderFn"></component></div>
I created a new unit test for a vue file called badge.vue. When running it I get the following error:
badge.vue
✗ has zero badges
undefined is not a constructor (evaluating 'vm.$el.querySelector('.badge')')
✗ has a trending badge
expected null to not equal null
I think that 'hasBadge' function is somehow the cause. Because when I move it to the child div, the 'has zero badges' check passes but trending badge still fails.
Is there something i'm missing?
I'm using:
Karma v1.7.1
PhantomJS 2.1.1
mocha
badge.vue
<template>
<div v-if="hasBadge" class="badge">
<icon-base :iconColor="iconColor" :icon-name="iconName"><component v-if="iconName" :is="iconName"></component></icon-base>
<div class="badge-copy" v-bind:style="iconColorObj">{{badgeCopy}}</div>
</div>
</template>
<script>
import iconBase from '../icon_base/icon_base';
import iconTrending from '../icons/icon_trending';
import iconClock from '../icons/icon_clock';
import iconEye from '../icons/icon_eye';
import iconTrophy from '../icons/icon_trophy';
import iconStar from '../icons/icon_star';
export default {
components: {
iconBase,
iconTrending,
iconClock,
iconEye,
iconTrophy,
iconStar,
},
data() {
return {
iconMap: {
TRENDING: 'icon-trending',
ALMOST_GONE: 'icon-clock',
TOP_SELLER: 'icon-star',
RECENTLY_VIEWED: 'icon-eye',
DOORBUSTER: 'icon-trophy',
},
};
},
props: [
'deal',
'toStartCase',
],
computed: {
hasBadge() {
const deal = this.deal;
return deal.badges
&& deal.badges.length > 0
&& deal.badges[0]
&& deal.badges[0].badgeType
&& deal.badges[0].text;
},
iconName() {
const iconMap = this.iconMap;
const deal = this.deal;
const badgeType = deal.badges[0].badgeType.toUpperCase();
if (!iconMap[badgeType]) {
return undefined;
}
return iconMap[badgeType];
},
badgeCopy() {
const deal = this.deal;
const badgeText = deal.badges[0].text;
return this.toStartCase &&
this.toStartCase(badgeText.toLowerCase());
},
hasIconColor() {
const deal = this.deal;
return deal.badges
&& deal.badges[0]
&& deal.badges[0].primaryColor;
},
iconColor() {
const deal = this.deal;
if (this.hasIconColor) {
return deal.badges[0].primaryColor;
}
return '#6650D7';
},
iconColorObj() {
return { color: this.iconColor };
},
},
};
</script>
<style scoped>
.badge svg {
display: inline-block;
vertical-align: baseline;
}
.badge svg path {
margin: 0 auto;
width: 12px;
}
.badge-copy {
color: iconColor;
}
</style>
badge.spec.js
import Vue from "vue";
import badge from "./badge";
describe('badge.vue', () => {
/* Badges */
it('has zero badges', () => {
const vm = new Vue({
el: document.createElement('div'),
data: {
content: {
rapi: [
{
"badges": [],
}
],
},
},
components: {
badge
},
template: '<badge :deal="content.rapi[0]" :toStartCase="toStartCase" ></badge>',
});
expect(vm.$el.querySelector('.badge')).to.equal(null);
});
it('has a trending badge', () => {
const vm = new Vue({
el: document.createElement('div'),
data: {
content: {
rapi: [
{
"badges": [
{
"uuid": "84fbb816-3fec-4693-b0a6-ec2f9fb4e400",
"primaryColor": "#6650D7E6",
"text": "TRENDING",
"secondaryColor": "#FFFFFFFF",
"badgeType": "TRENDING"
}
],
},
],
},
},
components: {
badge
},
template: '<badge :deal="content.rapi[0]" ></badge>',
});
expect(vm.$el.querySelector('.badge')).to.not.equal(null);
expect(vm.$el.querySelector('.badge-copy').innerText).to.equal('Trending');
});
});
Turns out before making any assertions, I needed to first use Vue.nextTick() followed by the assertions, and finished with done().
Source
badge.spec.js
import Vue from "vue";
import badge from "./badge";
describe('badge.vue', () => {
/* Badges */
it('has zero badges', () => {
const vm = new Vue({
el: document.createElement('div'),
data: {
content: {
rapi: [
{
"badges": [],
}
],
},
},
components: {
badge
},
template: '<badge :deal="content.rapi[0]" ></badge>',
});
Vue.nextTick(() => {
expect(vm.$el.querySelector('.badge-copy')).to.equal(null);
done();
});
});
it('has a trending badge', () => {
const vm = new Vue({
el: document.createElement('div'),
data: {
content: {
rapi: [
{
"badges": [
{
"uuid": "84fbb816-3fec-4693-b0a6-ec2f9fb4e400",
"primaryColor": "#6650D7E6",
"text": "TRENDING",
"secondaryColor": "#FFFFFFFF",
"badgeType": "TRENDING"
}
],
},
],
},
},
components: {
badge
},
template: '<badge :deal="content.rapi[0]" ></badge>',
});
Vue.nextTick(() => {
expect(vm.$el.querySelector('.badge')).to.not.equal(null);
expect(vm.$el.querySelector('.badge-copy').innerText).to.equal('Trending');
done();
});
});
});
My components looks like:
App.jsx
import MyInput from './MyInput';
const onChangeHandler = (val) => {
console.log(val);
};
export default {
render() {
return (
<MyInput onChange={onChangeHandler} />
);
},
};
and MyInput.jsx
export default {
props: {
onChange: {
type: Function,
},
},
render() {
// as Sphinx suggested it should be this.$props.onChange
return (
<input onChange={this.$props.onChange} />
);
},
};
But this.onChange is undefined:
How to properly use this.onChange prop in MyInput component?
CodePen
Here you can find CodePen with implementation of my problem:
https://codepan.net/gist/13621e2b36ca077f9be7dd899e66c056
Don't start your prop name with on. The 'on' prefix in reserved.
Credits to:
nickmessing - see his answer
Check Vue API: instance property=$props, you should use
_this.$props like below demo:
Vue.config.productionTip = false
Vue.component('child', {
props: {
onChange: {
type: Function,
default: function () {console.log('default')}
},
},
render: function (h) {
let self = this
return h('input', {
on: {
change: function (e) {
var test;
(test = self.$props).onChange(e)
}
}
})
}
})
Vue.component('container1', {
render: function (h) {
return h('child', {
props: {
onChange: this.printSome
}
})
},
methods: {
printSome: function () {
console.log('container 1 custom')
}
}
})
Vue.component('container2', {
render: function (h) {
return h('child', {
props: {
onChange: this.printSome
}
})
},
methods: {
printSome: function () {
console.log('container 2 custom')
}
}
})
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h3>Container 1</h3>
<container1></container1>
<h3>Container 2</h3>
<container2></container2>
</div>
Using ReactJs and react-dnd
I want a user to be able to sort the form fields (a.k.a. properties)
I set up the code almost identical to the source code for the Cards in the simple sort demo. There are no console warnings or errors, and I can't figure out why this won't work. I can neither drag nor drop anything.
What it looks like:
Code:
App.js
import EditForm from './Forms/EditForm.js';
var id = $('#form').data('id');
var source = `/api/forms/${id}?include=type,properties.type`;
React.render(
<EditForm source={source} />,
document.getElementById('form')
);
EditForm.js
import React from 'react/addons';
import update from 'react/lib/update';
import Property from './Property.js';
var EditForm = React.createClass({
mixins: [ React.addons.LinkedStateMixin ],
getInitialState: function() {
return {
id: null,
name: null,
slug: null,
properties: []
}
},
componentDidMount: function() {
this.getFormFromServer();
},
getFormFromServer: function () {
$.get(this.props.source, (result) => {
if (this.isMounted()) {
this.setState({
id: result.id,
name: result.name,
slug: result.slug,
properties: result.properties.data
});
}
});
},
moveProperty: function(id, afterId) {
const { properties } = this.state;
const property = properties.filter(p => p.id === id)[0];
const afterProperty = properties.filter(p => p.id === afterId)[0];
const propertyIndex = properties.indexOf(property);
const afterIndex = properties.indexOf(afterProperty);
this.setState(update(this.state, {
properties: {
$splice: [
[propertyIndex, 1],
[afterIndex, 0, property]
]
}
}));
},
render: function() {
const { properties } = this.state;
var propertiesList = properties.map((property, i) => {
return (
<Property
key={property.id}
id={property.id}
type={property.type.name}
name={property.name}
moveProperty={this.moveProperty} />
);
});
return (
<div>
<h1>Form</h1>
<form>
<div className="form-group">
<label>Name:</label>
<input type="text" name="name" valueLink={this.linkState('name')} className="form-control" />
</div>
<div className="form-group">
<label>Properties:</label>
<div className="list-group properties-list">
{propertiesList}
</div>
</div>
</form>
</div>
);
}
});
export default EditForm;
Property.js
import React, { PropTypes } from 'react/addons';
import { DragDropMixin } from 'react-dnd';
import ItemTypes from './ItemTypes';
const dragSource = {
beginDrag(component) {
return {
item: {
id: component.props.id
}
};
}
};
const dropTarget = {
over(component, item) {
component.props.moveProperty(item.id, component.props.id);
}
};
var Property = React.createClass({
mixins: [ React.addons.LinkedStateMixin, DragDropMixin ],
propTypes: {
id: PropTypes.any.isRequired,
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
moveProperty: PropTypes.func.isRequired
},
statics: {
configureDragDrop(register) {
register(ItemTypes.PROPERTY, {
dragSource,
dropTarget
});
}
},
render: function () {
const { type } = this.props;
const { name } = this.props;
const { isDragging } = this.getDragState(ItemTypes.PROPERTY);
const opacity = isDragging ? 0 : 1;
return (
<a className="list-group-item"
{...this.dragSourceFor(ItemTypes.PROPERTY)}
{...this.dropTargetFor(ItemTypes.PROPERTY)}>
{type}: {name}
</a>
);
}
});
export default Property;
ItemTypes.js
module.exports = {
PROPERTY: 'property'
};
If anybody could help I would greatly appreciate it. It's kind of sad how much time I've actually spent trying to figure this out.
Reference links:
My code on github
Demo example
Demo source on github
After spending over a day trying to get the drag and drop working I fixed it with one single line of code.
import React from 'react/addons';
How it compiled and rendered at all without that, I don't even know.