I made a simple application (in react js) that has two lists and you can add elements to that list. One is a list of input elements and other is list of spans.
Upon adding new element, the list of span renders perfectly but the list of inputs renders differently.
This is how my react class looks like
var App = React.createClass({
getInitialState: function(){
return {
'app': {
'data': this.props.data,
'data2': this.props.data2
}
};
},
onclick: function(){
var dat = this.state.app.data;
var val = this.refs.input.getDOMNode().value;
dat.splice(0, 0, val);
var dat2 = this.state.app.data2;
dat2.splice(0, 0, val);
this.setState({'app': {
'data': dat,
'data2': dat2
}});
},
renderElement: function(i){
return(
<input defaultValue={i} />
);
},
renderElement2: function(i){
return(
<span>{i}</span>
);
},
render: function(){
var self = this;
return(
<div>
<input type="text" ref="input" placeholder="Enter a value"/>
<input type="button" value="Add value" onClick={this.onclick} />
<div className="col2">
{this.state.app.data.map(function(page, i){
return(
<div key={i}>{self.renderElement(page)}</div>
);
})}
</div>
<div className="col2">
{this.state.app.data2.map(function(page, i){
return(
<div key={i}>{self.renderElement2(page)}</div>
);
})}
</div>
</div>
);
}
});
This is how the App is rendered
var data = [1,2,3];
var data2 = [1,2,3];
React.render(<App data={data} data2={data2}/>, document.getElementById("main"));
Here is the DEMO
My test case:
I add 55 and expect the results to look like this
55
1
2
3
55
1
2
3
But I get
1
2
3
3
55
1
2
3
Am I missing something basic here ?
First render looks (roughly) like this:
<div>
<div key={0}><input defaultValue="1" /></div>
<div key={1}><input defaultValue="2" /></div>
<div key={2}><input defaultValue="3" /></div>
</div>
<div>
<div key={0}><span>1</span></div>
<div key={1}><span>2</span></div>
<div key={2}><span>3</span></div>
</div>
Cool, no problems. Then let's say I put 'foo' in the box and click Add Value. The state updates by inserting it before the first item (side note: splice(0,0,x) is unshift(x)), you then render this:
<div>
<div key={0}><input defaultValue="foo" /></div>
<div key={1}><input defaultValue="1" /></div>
<div key={2}><input defaultValue="2" /></div>
<div key={3}><input defaultValue="3" /></div>
</div>
<div>
<div key={0}><span>foo</span></div>
<div key={1}><span>1</span></div>
<div key={2}><span>2</span></div>
<div key={3}><span>3</span></div>
</div>
Now it's time for React to take these two, and figure out what changed. To do this it compares the component (div, span, or input here), and the keys.
Starting with the span section, it sees that all the tags are the same, but there's a new key it hasn't seen before at the end. It also sees that the value of div[0] span, div[1] span, and div[2] span have all changed. It inserts the new elements with the text 3 at the end, and updates the other spans. Inefficient, but it works.
Now for the inputs... it does roughly the same thing. The keys and tags for the first three inputs are the same, it tells each of the inputs that they're updating, they check if they have a value prop, and if not, kindly deny to do anything.
It sees that there's a new input at the end. It is at div[3] input and has a defaultValue of "3". React inserts the new input, sets the value to 3, and the update is complete.
As you continue, the above procedure is the same, and it continues updating the spans, and inserting a new div span and div input each time.
The main problem here, aside from defaultValue which I don't think should ever be used, is the keys are upside down! Items are being inserted at the beginning, so keys should descend, not ascend. This can be fixed by using the length of the items and subtracting the index from that. If length is 4, the keys will be 4, 3, 2, 1.
this.state.app.data.map(function(page, i, items){
return(
<div key={items.length - i}>{self.renderElement(page)}</div>
);
})
React then says 'oh, there's a new item at the beginning, I should just insert a node there', and everyone's happy. The end.
Sorry, I don't know the exact reason why this is happening, but if you include a value for the key attribute on your inputs, it resolves the issue:
renderElement: function(i){
return(
<input key={i} defaultValue={i} />
);
},
My guess is that the lack of 'key' and use of 'defaultValue', instead of 'value', makes it difficult for React to correctly diff the change.
Related
Here’s my code
<form>
<div v-for="(inputAvion, index) in inputsAvion" :key="inputAvion.id" :id="`avion-${index}`">
<input placeholder="origin" name="data" />
<input placeholder="destination" />
<div class="ui button small green" #click="addAvion">+</div>
<div
class="ui button small red"
#click="removeAvion(inputAvion)"
v-show="inputsAvion.length > 1"
>
-
</div>
{{ index }}
</div>
</form>
And the script part :
<script>
export default {
mounted() {
this.inputsAvion.push(this.inputsAvion.length + 1);
//this.inputsAvion.push(this.inputsAvion);
},
data() {
return {
inputsAvion: [],
};
},
methods: {
addAvion() {
this.inputsAvion.push(this.inputsAvion.length + 1);
},
removeAvion(index) {
this.inputsAvion.splice(index, 1);
console.log(this.inputsAvion)
console.log(index)
}
},
};
I would like to add the div element each time the button “+” is clicked and assign a unique id to the div (I willl then add autocomplete google map places to calculate distance between 2 locations)
I think I did it correctly … hope so ^^
But I’d like to use also a remove button to delete the selected line. I’ve tried a lot of things but only managed to remove the last “div” added not the one I clicked.
If someone could help me I’d be grateful a lot ! :)
Thanks
You should pass the index instead of inputAvion
#click="removeAvion(index)"
then
removeAvion(index) {
this.inputsAvion.splice(index, 1);
...
I'm working with a portfolio project, where users can upload a picture, provide a description below it, and then click the "add" button to add another image and description.
I'm trying to add a character counter to the description input, which is a textarea field. Usually I can add the name of the v-model into the function, and it works fine, but this textarea is in a for-loop, so I'm not sure how to get this function to work.
Template:
<div class="newPortfolioList">
<div class="newPortfolioItem" v-for="(item, index) in this.portfolioItems" v-bind:key="index">
....
<div class="newPortfolioDescription">
<textarea v-model="item.portfolioDescription" #keyup='remaincharCount()' maxlength="1000" placeholder="Item Description..."></textarea>
</div>
<!-- Displaying the remaining characters -->
<span style="text-align:left; padding: 10px;">{{ remaincharactersText }}</span>
</div>
...
Script:
export default {
data () {
return {
portfolioItems:[],
maxcharacter: 1000,
remaincharactersText: "1000 characters remaining"
}
},
methods: {
createPortfolioItem () {
this.portfolioItems.push({
portfolioDescription: ''
})
},
remaincharCount () {
if (this.foo.length > this.maxcharacter) {
this.remaincharactersText = "Exceeded "+this.maxcharacter+" characters limit.";
} else {
var remainCharacters = this.maxcharacter - this.foo.length;
this.remaincharactersText = remainCharacters + " characters remaining";
}
}
}
}
There are a number of ways you could do this.
In this case I think your best option is to introduce a new component to represent a single portfolio item. Each of these components can manage their own message. From their perspective there is no loop to consider.
So you'd have something like this for your list template:
<div class="newPortfolioList">
<my-new-portfolio-item
v-for="(item, index) in portfolioItems"
:key="index"
:portfolio-item="item"
/>
</div>
Two side notes. I've dropped the v-bind prefix on your key, a : will suffice. I've also removed the this. before portfolioItems as that's also unnecessary. The Vue linting rules can be used to help keep this stuff in check.
There are alternatives to introducing a new component. You could generate the value of remaincharactersText within the template rather than keeping it in state. It could still be a method call but it wouldn't be precalculated. Something like this:
<span style="text-align:left; padding: 10px;">{{ remaincharCount(item) }}</span>
A further (even more painful) alternative would be to make remaincharactersText an array of values and then grab the relevant one by index:
<span style="text-align:left; padding: 10px;">{{ remaincharactersText[index] }}</span>
But, to reiterate, introducing a separate component for the items within the v-for is probably the best way to go here.
You should be aware that the textarea already has its maxlength set to 1000, so the label Exceeded N characters limit isn't possible (unless you check for fewer than 1000). Currently, the label always would display N reminaing characters.
Option 1: Display count calculation inline
Instead of storing the character count (unnecessarily taking up extra memory), you could display the calculation inline with string interpolation:
<template>
<div>
<textarea v-model="item.portfolioDescription" maxlength="1000"></textarea>
<span>{{ 1000 - item.portfolioDescription.length }} remaining characters</span>
</div>
</template>
demo 1
Option 2: Display count from item variable
If you prefer storing the character count (e.g., for some internal processing), you could add a new property to the item data:
<script>
const MAXLEN = 1000
export default {
methods: {
createPortfolioItem() {
this.portfolioItems.push({
remainChars: MAXLEN, // <--
})
},
}
}
</script>
Then, update item.remainChars upon the textarea's input-event, and display item.remainChars inline.
<template>
<div>
<textarea v-model="item.portfolioDescription" maxlength="1000"
#input="item.remainChars = 1000 - item.portfolioDescription.length">
</textarea>
<span>{{ item.remainChars }} remaining characters</span>
</div>
</template>
demo 2
Option 3: Display computed text
You could compute the character-count labels in a separate array that corresponds to portfolioItems:
<script>
const MAXLEN = 1000
export default {
computed: {
remainingCharsText() {
return this.portfolioItems.map(item => `${MAXLEN - item.portfolioDescription.length} remaining characters`)
},
}
}
</script>
Then, update your template to reference this computed array by index:
<template>
<div>
<textarea v-model="item.portfolioDescription" maxlength="1000">
</textarea>
<span>{{ remainingCharsText[index] }}</span>
</div>
</template>
demo 3
I am using REACT JS with a FLUX IMPLEMENTATION called ALT.
I started with a working React Component:
ConfirmEditModal = React.createClass({
getInitialState(){
console.log('ConfirmEditModal - getInitialState');
var state = {sample: 'MY_SAMPLE_STATE'};
console.log(state);
return state;
},
onChange(state){
console.log('ConfirmEditModal - onChange');
console.log(state);
this.setState(state);
},
componentDidMount(){
console.log('ConfirmEditModal - componentDidMount');
//SeedStore.listen(this.onChange);
},
componentWillUnmount(){
//SeedStore.unlisten(this.onChange);
},
render(){
return (
<div className="ui modal edit">
<i className="close icon"></i>
<div className="header">Form For Editing</div>
<div className="content">
<div className="ui form">
<h4 className="ui dividing header">Birth Information</h4>
<div className="field">
<label>Name</label>
<input name="name" type="text" placeholder="Name"></input>
</div>
</div>
</div>
</div>
);
}
});
Pls note the commented out code of SeedStore.listen(this.onChange) and SeedStore.unlisten(this.onChange).
the above code works as long as those 2 lines are commented out.
Those 2 lines are the code required to listen to the store using ALT (FLUX IMPLEMENTATION).
If I uncomment those 2 lines I will end up with this code:
ConfirmEditModal = React.createClass({
getInitialState(){
console.log('ConfirmEditModal - getInitialState');
var state = {sample: 'MY_SAMPLE_STATE'};
console.log(state);
return state;
},
onChange(state){
console.log('ConfirmEditModal - onChange');
console.log(state);
this.setState(state);
},
componentDidMount(){
console.log('ConfirmEditModal - componentDidMount');
SeedStore.listen(this.onChange);
},
componentWillUnmount(){
SeedStore.unlisten(this.onChange);
},
render(){
return (
<div className="ui modal edit">
<i className="close icon"></i>
<div className="header">Form For Editing</div>
<div className="content">
<div className="ui form">
<h4 className="ui dividing header">Birth Information</h4>
<div className="field">
<label>Name</label>
<input name="name" type="text" placeholder="Name"></input>
</div>
</div>
</div>
</div>
);
}
});
and I get this error:
Error: Invariant Violation: findComponentRoot(..., .0.1.1.0.4.2.0.1.1): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a <tbody> when using tables, nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an <svg> parent. Try inspecting the child nodes of the element with React ID ``.
I would like to understand what is going on behind the scenes that would be causing this error, because as far as I can understand it....listening to a store just means that you listen to any state changes in the store and that's it....it has got nothing to do with parent child components and FINDING ELEMENTS.......
What's the explanation behind this error?
I would not have thought that listening to a store would affect finding of elements as the error suggests.
Other stackOverflow articles Q/A on this suggests that there are changes in the DOM that is not intended.....but listening to a store should not affect the DOM right??? thus my confusion.....
What's the logic and explanation behind this error?
In my AngularJS directive, I want to apply focus to the first child of type <pre> of the last child of type <div>.
That is if my document looks like this:
<div main-div>
<div>
</div>
<div>
<div>
<pre></pre>
</div>
</div>
<div>
<div>
<pre the-one></pre>
</div>
</div>
the <pre> it should select is the last one, with an attribute the-one.
Is there any way to do this?
if you have the structure above as a jqLite element (however you get it a hold of it), you could do the following:
var divs = element.children().find("div");
var theOne;
if (divs.length > 0){
theOne = angular.element(divs[divs.length-1]).find("pre");
}
I started using Firebase (AngularFire) for synchronizing my data for my application. It's a Card tool for Scrum that adds cards to an array. You can manipulate the input fields.
In the first place I used localStorage, which worked really well. Now that I basically implemented Firebase, I got the following problem: After typing a single key into one field, the application stops and the only way of resuming typing is to click in the input field again.
Do you know why this is? Thank you very much in advance!
That's my basic implementation in my Controller:
Card = (#color, #customer, #points, #number, #projectName, #story) ->
$scope.cards = []
reference = new Firebase("https://MYACCOUNT.firebaseio.com/list")
angularFire(reference, $scope, "cards")
$scope.reset = ->
$scope.cards = []
$scope.addCardRed = (customer) ->
$scope.cards.push new Card("red", customer)
That's my Markup:
<div class="card card-{{ card.color }}">
<header>
<input class="points" contenteditable ng-model="card.points"></input>
<input class="number" placeholder="#" contenteditable ng-model="card.number"></input>
<input class="customerName" contenteditable ng-model="card.customer.name"></input>
<input class="projectName" placeholder="Projekt" contenteditable ng-model="card.projectName"></input>
</header>
<article>
<input class="task" placeholder="Titel" contenteditable ng-model="card.task"></input>
<textarea class="story" placeholder="Story" contenteditable ng-model="card.story"></textarea>
</article>
<footer>
<div class="divisions">
<p class="division"></p>
<button ng-click="deleteCard()" class="delete">X</button>
</div>
</footer>
</div>
<div class="card card-{{ card.color }} backside">
<article>
<h2 class="requirement">Requirements</h2>
<textarea class="requirements" placeholder="Aspects" contenteditable ng-model="card.requirements"></textarea>
</article>
</div>
I ran into this as well. This is because it's recalculating the entire array. Here's how I fixed it:
Bind your input to an ng-model and also add this focus directive
<input class="list-group-item" type="text" ng-model="device.name" ng-change="update(device, $index)" ng-click="update(device, $index)" ng-repeat='device in devices' focus="{{$index == selectedDevice.index}}" />
I set the selectedDevice like this
$scope.update = function(device, index) {
$scope.selectedDevice = device
$scope.selectedDevice.index = index
}
Now create this directive.
angular.module('eio').directive("focus", function() {
return function(scope, element, attrs) {
return attrs.$observe("focus", function(newValue) {
return newValue === "true" && element[0].focus();
});
};
});
Update Sorry for the delay, had a few things to tend to.
The reason why this works is because it is constantly saving the index value of the item in the array you are currently selecting. Once focus is lost, focus is returned immediately by going to that index.
If we're talking about multiple arrays, however, you'll need to refactor the setSelected code to say which array it is.
So you'd want to change
focus="{{$index == selectedDevice.index}}"
to something like
focus="{{$index == selectedDevice.index && selectedDevice.kind == 'points'}}"
Where points is the category of the array where the code appears.
I sorted this one by downloading the most recent version of angularFire.js, seems like bower installed the on that didn't have this fix. now my contentEditable is!