Related
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>
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);
},
I created a carousel in html, css and js. The below are the respective codes.
HTML:
<div class="doorHardwareTile" id="dh">
<img class="mySlidesDH slide-to-right" src="images/Folder1/1.jpg?" alt="" />
<img class="mySlidesDH slide-to-right" src="images/Folder1/2.jpg?" alt="" />
<img class="mySlidesDH slide-to-right" src="images/Folder1/3.jpg?" alt="" />
<img class="mySlidesDH slide-to-right" src="images/Folder1/4.jpg?" alt="" />
<img class="mySlidesDH slide-to-right" src="images/Folder1/5.jpg?" alt="" />
<img class="mySlidesDH slide-to-right" src="images/Folder1/6.jpg?" alt="" />
<div class="overlayDH">
<img src="images/Folder1/0.png?" alt="" />
</div>
</div>
CSS:
.slide-to-right {
position: relative;
animation: animateright 0.5s;
}
#keyframes animateright {
from {
right: -300px;
opacity: 0;
}
to {
right: 0;
opacity: 1;
}
}
.doorHardwareTile {
display: flex;
overflow: hidden;
width: 560px;
height: 373px;
min-width: auto;
min-height: auto;
margin-bottom: 16px;
}
.mySlidesDH {
display: none;
}
.overlayDH {
position: absolute;
width: 560px;
height: 373px;
z-index: 800;
opacity: 0;
transition: 0.5s ease;
}
.overlayDH img {
object-fit: cover;
width: 100%;
height: 100%;
align-content: center;
}
.doorHardwareTile:hover .overlayDH {
opacity: 1;
}
JS:
$(function doorHardwareSS() {
var myIndex = 0;
carouselDH();
function carouselDH() {
var i;
var totalElements = document.getElementsByClassName("mySlidesDH");
for (i = 0; i < totalElements.length; i++) {
totalElements[i].style.display = "none";
}
myIndex++;
if (myIndex > totalElements.length) {
myIndex = 1;
}
totalElements[myIndex - 1].style.display = "block";
setTimeout(carouselDH, 5000);
}
});
This worked perfectly in the html, css and js.
However, when I tried to replicate the same in REACT. It throws error in the following line
totalElements[i].style.display = "none";
The error is "Uncaught TypeError: Cannot read property 'style' of undefined."
The images that I want in the carousel are retrieved from DB in a class based component.
I'm just a beginner in REACT. I would be thankful for any help in achiving the same result.
The below is the REACT code which is called in a class based component.
import React from "react";
const ImageSlide = props => {
if (
props.imagePath === undefined ||
props.imagePath === null ||
props.imagePath.length === 0
)
return null;
return (
<div className={props.styles}>
{props.imagePath.map(image => {
const path = props.svgsArray.find(str => str.indexOf(image.hash) > 1);
// console.log(path);
return (
<img
className="mySlidesDH slide-to-right"
key={image.id}
src={path}
alt={props.styles}
/>
);
})}
<div className={props.styles2}>
<img src={require("./images/1_Door_hardware/0.png?")} alt="" />
</div>
</div>
);
};
export default ImageSlide;
Please note that value doorHardwareTile is passed through props.styles and value overlayDH is passed through props.styles2
For React try the following package:
https://github.com/FormidableLabs/react-animations
For React Native:
https://facebook.github.io/react-native/docs/animations
Here is an example but its for react-native it might have the same idea of how you could do a simple right slide, but still you need a function to fade out or set opacity to zero of the container, once it is slide to right and have another function to set it to its original position with replacing picture inside the container.
First set position of container inside the this.state:
this.state = {
containerRightSlideAnim: new Animated.ValueXY({x: 0, y: 0})
}
And inside the constructor set position you would like for X:
this.containerRightSlide = Animated.timing(this.state.containerRightSlideAnim, {
toValue: {x: 200, y: 0},
duration: 10000,
//speed: 0.1,
easing: Easing.in(Easing.ease)
})
Make a function that triggers the right slide animation:
triggerRightSlide(){
this.containerRightSlide.start();
}
Add inside render:
render() {
const animatableRightSlideStyle = this.state.containerSlideAnim.getTranslateTransform()
return (<View>
<Animated.View style={[animatableRightSlideStyle]}></Animated.View>
</View>)
}
Here is a working prototype for Carousel with pagination in ReactJS Live Demo here
HTML
<h2>Prototype Carousel with pagination in ReactJS</h2>
<div id="app"></div>
JS React
import React from 'react'
import ReactDOM from 'react-dom'
class Pagination extends React.Component {
constructor( props ){
super( props );
}
paginationRender = ( source, activeItem, handleEvent ) => {
const items = source.map(( item, i ) => {
let itemClass = 'page-item';
if( item.id === activeItem ){
itemClass += ' active';
}
return <li key={i} className={ itemClass }>
<a className="page-link" href="#"
onClick={ e => handleEvent( e, 'clickItem', item )}>
{ i + 1 }</a>
</li>;
});
return <ul className="pagination pagination-sm justify-content-center">
<li className="page-item">
<a className="page-link" href="#"
onClick={e => handleEvent( e, 'prevItem', {}, items )}>Prev</a>
</li>
{items}
<li className="page-item">
<a className="page-link" href="#"
onClick={e => handleEvent( e, 'nextItem', {}, items )}>Next</a>
</li>
</ul>;
};
render() {
const { itemsSrc, activeItem, handleEvent } = this.props;
//console.info('MenuContent->render()', { source });
return <div>{this.paginationRender( itemsSrc, activeItem, handleEvent ) }</div>;
}
}
class Carousel extends React.Component {
constructor( props ){
super( props );
}
carouselRender = ( source, activeItem, handleEvent ) => {
//console.info('Carousel->carouselRender [0]', { source, state: this.state });
const indicators = source.map(( item, i ) => {
let itemClass = '';
if( item.id === activeItem ){
itemClass += ' active';
}
//console.info('Carousel->carouselRender [3]', { id: item.id, item, pageItemClass, activeItem: activeItem });
return <li key={i} data-target="#demo" data-slide-to="1" className={ itemClass }
onClick={ e => handleEvent( e, 'clickItem', item )}>></li>;
});
const imgs = source.map(( item, i ) => {
let itemClass = 'carousel-item';
if( item.id === activeItem ){
itemClass += ' active';
}
//console.info('Carousel->carouselRender [5]', { id: item.id, item, pageItemClass, activeItem: activeItem });
return <div key={i} className={ itemClass }>
<img src={item.src} className="img-fluid" alt="New York" />
</div>;
});
//console.info('Carousel->carouselRender [7]', { });
return <div id="demo" className="carousel slide" data-ride="carousel">
<ul className="carousel-indicators">
{ indicators }
</ul>
<div className="carousel-inner">
{ imgs }
</div>
<a className="carousel-control-prev" href="#demo" data-slide="prev">
<span className="carousel-control-prev-icon"
onClick={e => handleEvent( e, 'prevItem', {}, source )}>
</span>
</a>
<a className="carousel-control-next" href="#demo" data-slide="next">
<span className="carousel-control-next-icon"
onClick={e => handleEvent( e, 'nextItem', {}, source )}>
</span>
</a>
</div>;
};
render() {
const { itemsSrc, activeItem, handleEvent } = this.props;
//console.info('MenuContent->render()', { source });
return <div>{this.carouselRender( itemsSrc, activeItem, handleEvent ) }</div>;
}
}
const inputProps = {
itemsSrc: [
{ id: 0,
name: 'Los Angeles',
level: 'basic',
src: 'https://www.w3schools.com/bootstrap4/la.jpg'
},
{
id: 1,
name: 'Chicago',
level: 'basic',
src: 'https://www.w3schools.com/bootstrap4/chicago.jpg'
},
{
id: 2,
name: 'New York',
level: 'advanced',
src: 'https://www.w3schools.com/bootstrap4/ny.jpg'
},
],
};
class Wrapper extends React.Component {
constructor( props ){
super( props );
this.state = {
activeItem: 0,
};
}
handleEvent = ( e, actionType, item, items ) => {
e.preventDefault();
let itemsLength, activeItem;
switch( actionType ){
case 'clickItem':
//console.info('MenuContent->paginationRender', { actionType, id: item.id, item });
this.setState({ activeItem: item.id });
break;
case 'prevItem':
activeItem = this.state.activeItem;
if ( activeItem === 0 ){
break;
}
activeItem -= 1;
this.setState({ activeItem });
break;
case 'nextItem':
itemsLength = items.length;
activeItem = this.state.activeItem;
if (activeItem === itemsLength -1) {
break;
}
activeItem += 1;
this.setState({ activeItem });
break;
}
//console.info('MenuContent->handleEvent()', { actionType, item, items });
}
render(){
let props = this.props;
const { activeItem } = this.state;
props = { ...props, handleEvent: this.handleEvent, activeItem };
return <div className="App">
<div className="container-fluid">
<div className="row">
<div className="col-1">
</div>
<div className="col-10">
<Pagination { ...props }/>
<Carousel { ...props }/>
</div>
<div className="col-1">
</div>
</div>
</div>
</div>;
}
}
ReactDOM.render(<Wrapper { ...inputProps }/>, document.getElementById('app'));
I have made a rating component where user can rate but there is a problem. The user can rate from 0 to 4.5(0, 0.5, 1, 1.5 till 4.5) which is unexpected behavior, instead, I want the user to rate from 0.5 till 5. How do i make it so? Here is the component for star rating
Here is the workaround
https://codesandbox.io/s/9y14x3704
class Rating extends React.Component {
constructor(props) {
super(props);
this.state = {
rating: props.defaultRating || null,
maxRating: props.maxRating || null,
temp_rating: null
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.defaultRating !== this.props.defaultRating) {
this.setState({
rating: nextProps.defaultRating,
maxRating: nextProps.maxRating
});
}
}
handleMouseover(rating) {
this.setState(prev => ({
rating: rating / 2,
temp_rating: prev.rating
}));
}
handleMouseout() {
// this.state.rating = this.state.temp_rating;
// this.setState({ rating: this.state.rating });
this.setState(prev => ({
rating: prev.temp_rating
}));
}
rate(rating) {
this.setState(
{
rating: rating / 2,
temp_rating: rating / 2
},
() => this.props.handleRate(this.state.rating)
);
}
render() {
const { rating } = this.state;
let stars = [];
for (let i = 0; i < 11; i++) {
let klass = "icon-star-o";
if (this.state.rating >= i / 2 && this.state.rating !== null) {
klass = "icon-star";
}
stars.push(
<i
style={{
display: "inline-block",
width: "10px",
overflow: "hidden",
direction: i % 2 === 0 ? "ltr" : "rtl"
}}
className={klass}
key={i}
onMouseOver={() => this.handleMouseover(i)}
onClick={() => this.rate(i)}
onMouseOut={() => this.handleMouseout()}
/>
);
}
return <div className="rating">{stars}</div>;
}
}
const props = {
defaultRating: 2,
maxRating: 5,
handleRate: (...args) => {
console.log(args)
}
}
ReactDOM.render(<Rating {...props} />, document.querySelector('.content'))
.rating{
border: 1px solid gray
padding: 5px;
}
i[class^='icon-star'] {
border: 1px solid black;
margin: 4px;
padding: 5px;
height: 10px;
}
.icon-star {
background: gray;
}
.icon-star-o {
background: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div class='content'></div>
Can anyone help me at this, please?
Ok, Have a look at updated codebox
I've changed the count to be UNTIL 10 and reversed the stars
Here's the updated render() method:
render() {
const { rating } = this.state;
let stars = [];
for (let i = 1; i <= 10; i++) { /* note starting at one UNTIL 10 */
let klass = "fa fa-star-o";
if (rating >= i / 2 && rating !== null) {
klass = "fa fa-star";
}
stars.push(
<i
style={{
display: "inline-block",
width: "8px",
overflow: "hidden",
direction: i % 2 ? "ltr" : "rtl" /* reverse the half stars */
}}
className={klass}
key={i}
onMouseOver={() => this.handleMouseover(i)}
onClick={() => this.rate(i)}
onMouseOut={() => this.handleMouseout()}
/>
);
}
return <div className="rating">
{stars}<br />
{rating}
</div>;
}
I have following UI element on the top of my page:
|[Static1] [Dynamic1] [Dynamic2] [Dynamic3] [Static2]|
So Static1 is some logo component that sticks to the left, Static2 is some user menu component that sticks to the right.
Now inside of it I have a collection component that displays several dynamic elements loaded from the DB.
All is good, if there are not too much of those components, but if there are more, I don't wan't any linebreaks, only some fort of "More" menu, something like:
|[Static1] [Dynamic1] [Dynamic2] [Dynamic3] [Dynamic4][...][Static2]|
and when I click the [...] button I wan't a vertical list of the dynamic components.
The list of dynamic items is stored in an ElementList component, with following code:
React.createClass({
render() {
return (
<div ref="listparent">
{ this.props.elements.map((element) => {
return <Element
ref={"element-"+element.name}
key={element.name}
})}
</div>
)
}
});
this.props.elements is a collection passed as a prop. I tried something allong those lines, but it either didn't work or worked but not on each page refresh:
export default React.createClass({
getInitialState(){
return {
visibleElements: this.props.elements,
hiddenElements: []
}
},
componentDidMount() {
this.rearrange();
},
componentDidUpdate(){
this.rearrange();
},
rearrange(){
var element = ReactDOM.findDOMNode(this.refs.listparent);
let visibleElements = [];
let hiddenElements = [];
for(var i=0; i< this.props.elements.length; i++)
{
var currentElement = this.props.elements[i];
var domElement = ReactDOM.findDOMNode(this.refs["element-"+element.name]);
if(domElement) {
if (domElement.offsetTop + domElement.offsetHeight >
element.offsetTop + element.offsetHeight ||
domElement.offsetLeft + domElement.offsetWidth >
element.offsetLeft + element.offsetWidth - 200) {
hiddenElements.push(currentElement);
}
else {
visibleElements.push(currentElement);
}
}
}
if(this.state.visibleElements.length != visibleElements.length) {
this.setState({
visibleElements: visibleElements,
hiddenElements: hiddenElements
})
}
},
render() {
return (
<div ref="listparent">
{ this.state.visibleElements.map((element) => {
return <Element
ref={"element-"+element.name}
key={element.name} />
})}
{ this.state.hiddenElements.length >0 &&
<DropdownMenu
Header="..."
>
{ this.state.hiddenElements.map((element) => {
return <Element
ref={"element-"+element.name}
key={element.name} />
})}
</DropdownMenu>
}
</div>
)
}
});
Here is a rough jsFiddle with what I want to do: https://jsfiddle.net/3uf9r8ne/
Got it working, I don't know if that's the best solution, or how robust it is, but works for me at least for now.
JsFiddle: https://jsfiddle.net/1w6m1n6h/
var Dropdown = React.createClass({
getInitialState(){
return {
isOpen: false
}
},
componentWillMount() {
document.addEventListener('click', this.handleClick, false);
},
componentWillUnmount() {
document.removeEventListener('click', this.handleClick, false);
},
handleClick: function (e) {
var component = ReactDOM.findDOMNode(this.refs.component);
if (e.target == component || $(component).has(e.target).length) {
// Inside the component
}
else{
// Outide
this.setState({ isOpen: false});
}
},
render()
{
return (
<div ref="component" className="dropdown">
<div className="dropdown-button" onClick={() => this.setState({ isOpen : !this.state.isOpen})}>{this.props.Header}</div>
{
this.state.isOpen && (
<div className="dropdown-menu" onClick={() => this.setState({ isOpen : false})}>
{React.Children.map(this.props.children, (item) => item)}
</div>
)
}
</div>
);
}
});
var Card = React.createClass({
render() {
let className = "card";
if(this.props.isHidden)
className += " is-hidden";
return (
<div className={className}>{this.props.name}</div>
)
}
});
var Cards = React.createClass({
getInitialState() {
return {
vCards: [],
hCards: [],
lastSetCards: [],
preMounted: false,
laidOut: false
};
},
rearrange() {
_.throttle(this.setState({laidOut: false, preMounted: false}), 100);
},
componentDidMount() {
window.addEventListener('resize', this.rearrange);
},
componentWillUnmount() {
window.removeEventListener('resize', this.rearrange);
},
componentDidUpdate() {
if(this.props.cards.length != this.state.lastSetCards || !this.state.preMounted) {
this.setState({
lastSetCards: this.props.cards.length,
vCards: this.props.cards,
preMounted: true,
laidOut: false
});
}
if(this.state.preMounted && !this.state.laidOut) {
var element = ReactDOM.findDOMNode(this.refs.listparent);
let visibleCards = [];
let hiddenCards = [];
for(var i=0; i< this.props.cards.length; i++)
{
var card = this.props.cards[i];
var cardElement = ReactDOM.findDOMNode(this.refs["card-"+card]);
if(cardElement) {
if (cardElement.offsetTop + cardElement.offsetHeight >
element.offsetTop + element.offsetHeight ||
cardElement.offsetLeft + cardElement.offsetWidth >
element.offsetLeft + element.offsetWidth - 160) {
hiddenCards.push(card);
}
else {
visibleCards.push(card);
}
}
}
this.setState({
vCards: visibleCards,
hCards: hiddenCards,
laidOut: true
});
}
},
render() {
return (<div className="cards-top" ref="listparent">
<div className="cards" >
{this.state.vCards.map((c)=> <Card ref={"card-"+c} key={c} name={c} />)}
</div>
<Dropdown Header="MORE">
{this.state.hCards.map((c)=> <Card isHidden={true} key={c} name={c} />)}
</Dropdown>
</div>
)
}
});
var Hello = React.createClass({
getInitialState() {
return {
cards: ["one", "two" ]
};
},
componentDidMount() {
this.setState({
cards: ["one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve", "thirteen", "fourteen"]
});
},
render: function() {
let addNew = () => {
this.state.cards.push("additional_"+this.state.cards.length);
this.setState({
cards: this.state.cards
})
};
return (
<div>
<div className="header">
<div className="logo">Logo</div>
<div className="user">User</div>
<Cards cards={this.state.cards} />
<div className="clear"></div>
</div>
<br/><br/>
<button onClick={addNew}>Add</button>
</div>);
}
});
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
.logo
{
float: left;
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
.user
{
float: right;
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
.header{
position: relative;
max-height: 10px;
height: 10px;
width: 100%;
}
.cards
{
position: relative;
display: inline-block;
vertical-align: top;
white-space: nowrap;
}
.clear
{
clear: both;
}
.card
{
display: inline-block;
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
.cards-top
{
display: block;
white-space: nowrap;
vertical-align: top;
width: 100%;
border: green 1px solid;
}
.dropdown
{
display: inline-block;
}
.is-hidden
{
display: block;
}
.dropdown-button
{
margin: 5px;
padding: 5px;
border: solid 1px blue;
}
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>