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);
},
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>
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;
}
I am trying to implement a simple authentication in vuejs. I have a list of objects in which I have authentic username and password. I am iterating over this list and checking with the entered username and password. If there is a match then I am emitting an event and updating my variable authenticated. But the problem is inside the login in the forEarch loop i am not able to access the emit.
This is my Login.vue file
<template>
<div id="login">
<h1>Login</h1>
<b-form-input v-model="input.username" placeholder="Username"></b-form-input>
<br/>
<b-form-input v-model="input.password" placeholder="Password" type="password"></b-form-input>
<br/>
<b-button variant="primary" v-on:click="login()">Submit</b-button>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
input: {
username: "",
password: ""
}
}
},
methods: {
login() {
var enteredUsername = this.input.username;
var enteredPassword = this.input.password;
if(enteredUsername !== "" && enteredPassword !== "") {
this.$parent.mockAccount.forEach(function (element) {
if (enteredUsername === element.username && enteredPassword === element.password) {
this.$emit("authenticated", true)
this.$router.replace({name: "secure"})
}
})
}
}
}
}
</script>
<style scoped>
#login {
width: 500px;
border: 1px solid #CCCCCC;
background-color: #FFFFFF;
margin: auto;
margin-top: 200px;
padding: 20px;
}
</style>
Here is my App.vue file
<template>
<div id="app">
<div id="nav">
<router-link v-if="authenticated" to="/login" v-on:click.native="logout()" replace>Logout</router-link>
</div>
<router-view/>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
authenticated: false,
mockAccount: [
{
username: "a",
password: "a"
},
{
username: "rick",
password: "rick2018"
},
{
username: "nick",
password: "nick2018"
},
{
username: "paul",
password: "paul2018"
}]
}
},
mounted() {
if(!this.authenticated) {
this.$router.replace({ name: "Login" });
}
},
methods: {
setAuthenticated(status) {
this.authenticated = status;
},
logout() {
this.authenticated = false;
}
}
}
</script>
<style>
body {
background-color: #F0F0F0;
}
h1 {
padding: 0;
margin-top: 0;
}
#app {
width: 1024px;
margin: auto;
}
</style>
This is the error i am getting
ES5 functions have their own this, so change
this.$parent.mockAccount.forEach(function (element) {
if (enteredUsername === element.username && enteredPassword === element.password) {
this.$emit("authenticated", true)
this.$router.replace({name: "secure"})
}
})
either to an ES6 arrow function (which have the same this as the context they're defined in)
this.$parent.mockAccount.forEach((element) => {
if (enteredUsername === element.username && enteredPassword === element.password) {
this.$emit("authenticated", true)
this.$router.replace({name: "secure"})
}
})
or use explicit binding with Function.prototype.bind() (ES5):
this.$parent.mockAccount.forEach(function (element) {
if (enteredUsername === element.username && enteredPassword === element.password) {
this.$emit("authenticated", true)
this.$router.replace({name: "secure"})
}
}.bind(this))
or use a closure:
const self = this;
this.$parent.mockAccount.forEach(function (element) {
if (enteredUsername === element.username && enteredPassword === element.password) {
self.$emit("authenticated", true)
self.$router.replace({name: "secure"})
}
})
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>
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>