(Vue.js) V-model array. Having difficulty running function - arrays

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

Related

Add and remove clicked html elements from array in vue

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);
...

Problem with an angular filter of objects without using routes (Angular TS)

I need some help about the code: I have to make a filter for a list of objects and I have to use the observables (the exercise only includes the front end part) and the objects are in a database.
with the code written as soon as I insert a letter in the search bar, the array is emptied and only the last letter remains (for example insert C and after I insert E, in the filter only the E remains)
in TS:
Search(name:any):void{
this.arraycopy=this.mylist
})
this.arraycopy=this.mylist.filter(res =>{
return res.description.includes(name.key) ;
})
}
IN HTML:
<div class="row">
<div class="col-2" *ngFor="let object of arraycopy">
<div class="card">
<div class="card-block">
<p class="card-text">
<a class="breadcrumbLabelStyle" href="{{list.listCode}}" title="access to {{list.description}}">{{list.description}}
</a>
</p>
</div>
</div>
</div>
</div>
Are you sure you're updating your Observable with the full search-bar input and not just the last key pressed?
You are probably updating it based on the keypress event which returns the last key pressed.
Update on Answer:
Try this:
Search-bar:
<input type="text" [ngModel]="searchInput" (keyup)="Search()">
In TS:
public searchTerm = new BehaviorSubject('');
public searchInput: string;
constructor() {
this.searchTerm.subscribe((text: string) => {
this.arraycopy = this.mylist.filter(res => {
return res.description.includes(text);
});
});
}
public Search(): void {
this.searchTerm.next(this.searchInput);
}
This should work, but with this you're not using Observables so good.

React js: Input element renders differently compared to other elements

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.

Angular JS truncate text and add read more

I have an ng-repeat that is outputting a number of <p>. I would like to truncate the text and add a read more button that expands when you click it.
This is what I have so far:
//NG-repeat
<div class="col-xs-4 mbm" ng-repeat="wine in wines">
<p readMore> {{wine.copy|truncate: textLength }}
<a ng-click="changeLength()" class="color3"><strong>More</strong></a>
</p>
</div>
//NG-click
$scope.changeLength = function() {
$scope.textLength = 9999;
}
I have a custom directive that is able to truncate the length of the string. But when trying trying to modify the text length via and ng-click I am finding all the of items in the ng-repeat are modified.
Is there a way to change a single ng-repeat item?
Target the wine for the ng-click:
<p readMore> {{wine.copy|truncate: wine.textLength }}
<a ng-click="changeLength(wine)" class="color3"><strong>More</strong></a>
</p>
And then only truncate the targeted text:
$scope.changeLength = function(wine) {
wine.textLength = 9999;
}

Firebase makes me stop typing after one keystroke in my AngularFire/AngularJS project

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!

Resources