AngularJS : Two-way binding between a textarea and ng-repeat-ed inputs - angularjs

I was going to ask this as a question, but I figured out a solution. So at this point, I'm looking for a critique of my solution.
I've got a static textarea, and an input with an ng-repeat directive.
As the user types a sentence into the textarea, a input is rendered for each word in the sentence.
Then if the user updates the text in any input, the corresponding word in the textarea sentence is updated (really the whole sentence is recreated).
Demo: http://plnkr.co/edit/bSjtOK?p=preview
Questions
Keeping in mind that I'm only 2 weeks into my AngularJS learning:
Did I write this in the "angular" way?
Is there something I could have done better?
Am I violating any no-nos?
Abbreviated Code
HTML
<textarea ng-model="sentence" ng-change="parseSentence()" style="width: 100%; height: 15em;"></textarea>
<input type="text" ng-repeat="w in words" ng-model="w.word" ng-change="buildSentance(w)" />
JavaScript
function WordCtrl($scope, debounce) {
$scope.words = [];
$scope.sentence = 'Hello there how are you today?';
// this is called when the textarea is changed
// it splits up the textarea's text and updates $scope.words
$scope.parseSentence = function() {
var words = $scope.sentence.split(/\s+/g);
var wordObjects = [];
for (var i=0;i<words.length;i++) {
wordObjects.push({word: words[i]});
}
if ((words.length == 1) && (words[0] === '')) {
$scope.words = [];
} else {
$scope.words = wordObjects;
}
};
$scope.parseSentenceDebounced = debounce($scope.parseSentence, 1000, false);
$scope.buildSentance = function(w) {
var words = [];
for (var i=0;i<$scope.words.length;i++) {
var word = $scope.words[i].word;
if (word.replace(/\s+/g,'') !== '') {
words.push(word);
}
}
$scope.sentence = words.join(' ');
// if the user puts a space in the input
// call parseSentence() to update $scope.words
if (w.word.indexOf(' ') > -1) {
$scope.parseSentenceDebounced();
}
}
$scope.parseSentence();
}

Interesting issue you are having. I put your code on my page and the first thing I noticed is that you cannot pass debounce in the controller method.
Next Problem I noticed is that you have an ng-change that changes the values on another box with ng-change. I changed the event to Keypress to stop the digest in a digest.
Here it is working in JSFiddle enter link description here
The code:
HTML
<body ng-app="portal">
<div ng-controller="WordCtrl">
<textarea ng-model="sentence" ng-keypress="parseSentence()" style="width: 100%; height: 15em;"></textarea>
<input type="text" ng-repeat="w in words" ng-model="w.word" ng-keypress="buildSentance(w)" />
</div>
</body>
Javascript
angular.module("portal",[]).controller("WordCtrl",function($scope) {
$scope.words = [];
$scope.sentence = 'Hello there how are you today?';
$scope.parseSentence = function () {
var words = $scope.sentence.split(/\s+/g);
var wordObjects = [];
for (var i = 0; i < words.length; i++) {
wordObjects.push({ word: words[i] });
}
if ((words.length == 1) && (words[0] === ''))
{
$scope.words = [];
}
else
{
$scope.words = angular.copy(wordObjects);
}
}
$scope.buildSentance = function (w) {
var words = [];
for (var i = 0; i < $scope.words.length; i++) {
var word = $scope.words[i].word;
if (word.replace(/\s+/g, '') !== '') {
words.push(word);
}
}
$scope.sentence = words.join(' ');
// if the user puts a space in the input
// call parseSentence() to update $scope.words
if (w.word.indexOf(' ') > -1) {
$scope.parseSentenceDebounced();
}
}
$scope.parseSentence();
});
Hope this solves your issue.

Related

Filter on string

I'm learning angularjs and got an exercise that wants me to Use angular filter to show a title in the following format :
first letter of each word upper cased and each other letter lower cased also
remove any non-English letters from the title. For example:
A title with the name
“##THIS is a Title!!”
should be changed to
“This Is A Title”
I'm getting each title from an array of objects and present them like so.
<div ng-repeat="obj in objects">
<h3 class="panel-title">{{obj.Title}}</h3>
</div>
i understand that filter receives an array and filters through it . but this requires me to filter the string.
been searching for a while, how can i do this?
please refer below fiddle
http://jsfiddle.net/HB7LU/28315/
<div ng-controller="MyCtrl">
Hello, {{ name | ordinal|capitalize }}
</div>
var myApp = angular.module('myApp',[]);
//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});
function MyCtrl($scope) {
$scope.name = 'Super hero!!12##3';
}
myApp.filter('ordinal', function() {
// Create the return function
// set the required parameter name to **number**
return function(strTitle) {
// Ensure that the passed in data is a number
// If the data is not a number or is less than one (thus not having a cardinal value) return it unmodified.
strTitle=strTitle.replace(/[^a-zA-Z ]/g, "")
return strTitle;
}
});
myApp.filter('capitalize', function() {
return function(input){
if(input.indexOf(' ') !== -1){
var inputPieces,
i;
input = input.toLowerCase();
inputPieces = input.split(' ');
for(i = 0; i < inputPieces.length; i++){
inputPieces[i] = capitalizeString(inputPieces[i]);
}
return inputPieces.toString().replace(/,/g, ' ');
}
else {
input = input.toLowerCase();
return capitalizeString(input);
}
function capitalizeString(inputString){
return inputString.substring(0,1).toUpperCase() + inputString.substring(1);
}
};
});
angular.module('app', []).filter('myFilter', function(){
return function(input){
if(!input)
return;
var out = '';
var english = /^[A-Za-z0-9 ]*$/;
for(var letter of input)
if(english.test(letter))
out += letter;
var result = '';
for(var i = 0; i < out.length; i++)
result += out[i][(i === 0 || out[i-1] == ' ') ? 'toUpperCase' : 'toLowerCase']();
return result;
}
})
<script src="//code.angularjs.org/snapshot/angular.min.js"></script>
<body ng-app="app">
<input ng-init='text="##THIS is a Title!!"' type='text' ng-model='text'>
<p>{{text | myFilter}}</p>
</body>

How to Display the first 20 messages in the DB

I found this code in the internet, it functions as a message box. The code works perfectly fine. However, it display all the messages stored in the file message.db.
My question is, how can I make it display only the last 20 posted messages? I'm sorry for asking this. I have no knowledge about programming. I appreciate any help...
Here is the snippet of the code:
<div class="container" ng-controller="MessageBoardCtrl">
<form>
<input type="email" placeholder="Email Address" ng-model="email" required/>
<textarea placeholder="Advertise your link here for Free..." rows="5" style="width:90%" ng-model="message" required=""></textarea>
<button class="btn btn-primary" ng-click="sendMessage()">Post</button>
</form>
<p>{{item.message}}</p>
<script>
function MessageBoardCtrl($scope, $http, $timeout) {
$scope.items = [];
$scope.message = '';
$scope.email = '';
$scope.lastTime = 0;
$scope.refreshMessages = function() {
$http.get('../templates/faucet.php/messages?time=' + $scope.lastTime).success(function(data) {
for(id in data) {
item = data[id];
$scope.items.unshift(item);
if($scope.lastTime<item.time)
$scope.lastTime = item.time;
}
});
}
$scope.sendMessage = function() {
if(!$scope.message)
return;
$http.post('../templates/faucet.php/add_message', {message: $scope.message, email: $scope.email}).success(function() {
$scope.message = '';
$scope.refreshMessages();
});
}
$scope.periodicRefresh = function() {
$scope.refreshMessages();
$timeout($scope.periodicRefresh, 5000, true);
}
$scope.periodicRefresh();
}
</script>
I found this function to limit the result:
function limitToFilter(){
return function(array, limit) {
if (!(array instanceof Array)) return array;
limit = int(limit);
var out = [],
i, n;
// check that array is iterable
if (!array || !(array instanceof Array))
return out;
// if abs(limit) exceeds maximum length, trim it
if (limit > array.length)
limit = array.length;
else if (limit < -array.length)
limit = -array.length;
if (limit > 0) {
i = 0;
n = limit;
} else {
i = array.length + limit;
n = array.length;
}
for (; i<n; i++) {
out.push(array[i]);
}
return out;
}
}
Now, I just need help on how to combine them together to make the script work. I hope someone can help. I really do not have any idea... Thanks.

Field update after autocompletion with angularJS

I'm quite new to AngularJS and struggling a bit to have some input fields updated after an autocompletion event using google maps.
The idea is that when the user inputs his city/zip code, I would update 3 fields which are themselves linked to an object.
So far, I managed to have a working code except that sometimes the fields are not updated immediately : I have to autocomplete twice so that the good value will appear in the fields.
I've tweaked an existing angular directive in order to get what I want but since this is new to me, I dont know if I'm using the correct approach.
Below is the JS directive I use :
angular.module( "ngVilleAutocomplete", [])
.directive('ngAutocomplete', function($parse) {
return {
scope: {
details: '=',
ngAutocomplete: '=',
options: '=',
data: '='
},
link: function(scope, element, attrs, model) {
//options for autocomplete
var opts
//convert options provided to opts
var initOpts = function() {
opts = {}
if (scope.options) {
if (scope.options.types) {
opts.types = []
opts.types.push(scope.options.types)
}
if (scope.options.bounds) {
opts.bounds = scope.options.bounds
}
if (scope.options.country) {
opts.componentRestrictions = {
country: scope.options.country
}
}
}
}
initOpts()
//create new autocomplete
//reinitializes on every change of the options provided
var newAutocomplete = function() {
scope.gPlace = new google.maps.places.Autocomplete(element[0], opts);
google.maps.event.addListener(scope.gPlace, 'place_changed', function() {
scope.$apply(function() {
scope.details = scope.gPlace.getPlace();
//console.log(scope.details)
var HasCP = false;
for (var i=0 ; i<scope.details.address_components.length ; i++){
for (var j=0 ; j<scope.details.address_components[i].types.length ; j++){
if (scope.details.address_components[i].types[j] == 'postal_code' && scope.data.CP != 'undefined'){
scope.data.CP = scope.details.address_components[i].long_name;
HasCP = true;
} else if (scope.details.address_components[i].types[j] == 'locality' && scope.data.Ville != 'undefined') {
scope.data.Ville = scope.details.address_components[i].long_name;
} else if (scope.details.address_components[i].types[j] == 'country' && scope.data.Pays != 'undefined') {
scope.data.Pays = scope.details.address_components[i].long_name;
}
}
}
if (!HasCP){
var latlng = {lat: scope.details.geometry.location.lat(), lng: scope.details.geometry.location.lng()};
var geocoder = new google.maps.Geocoder;
geocoder.geocode({'location': latlng}, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {
for (var i=0 ; i<results[0].address_components.length ; i++){
for (var j=0 ; j<results[0].address_components[i].types.length ; j++){
if (results[0].address_components[i].types[j] == 'postal_code' && scope.data.CP != 'undefined'){
scope.data.CP = results[0].address_components[i].long_name;
console.log('pc trouvé :' + scope.data.CP);
}
}
}
}
});
}
//console.log(scope.data)
scope.ngAutocomplete = element.val();
});
})
}
newAutocomplete()
//watch options provided to directive
scope.watchOptions = function () {
return scope.options
};
scope.$watch(scope.watchOptions, function () {
initOpts()
newAutocomplete()
element[0].value = '';
scope.ngAutocomplete = element.val();
}, true);
}
};
});
The matching HTML code is below :
<div class="form-group">
<lable>Code postal : </label>
<input type="text" id="Autocomplete" class="form-control" ng-autocomplete="cities_autocomplete" details="cities_autocomplete_details" options="cities_autocomplete_options" data="client" placeholder="Code postal" ng-model="client.CP" />
</div>
<div class="form-group">
<lable>Ville : </label>
<input type="text" id="Autocomplete" class="form-control" ng-autocomplete="cities_autocomplete" details="cities_autocomplete_details" options="cities_autocomplete_options" data="client" placeholder="Ville" ng-model="client.Ville" />
</div>
<div class="form-group">
<lable>Pays : </label>
<input type="text" class="form-control" name="Pays" ng-model="client.Pays" placeholder="Pays" />
</div>
You'll see that I pass the "client" object directly to my directive which then updates this object. I expected angular to update the html page as soon as the values of the client object are updated but I will not always be the case :
If I search twice the same city, the values are not updated
If I search a city, Google wont send me a zip code so I have to do another request to the geocoding service and I get the zipcode in return but while my client.CP field is correctly updated, changes are not visible in the CP input field until I do another search.
Thanks in advance for any advice on what I'm doing wrong.

ui-select multiselect is very slow in displaying the choices

I ran into this problem, and I don't know how to solve it. I have used a ui-select multiselect in my page. First, a http.get request is made to a url which gets the data, then the ui-select choices are populated. The data is big - the length of the data is 2100. This data is to be shown as choices. (The data is fetched at the beginning during the loading of the page and is stored in an array)
But the problem is each time I click on the multiselect to select a choice, it takes 4-5 seconds to populate the list and the page becomes very slow. What do I do to reduce this time?
The choices data is stored in an array, the datatype is array of strings.
<ui-select multiple ng-model="selectedFields.name" style="width: 100%;">
<ui-select-match placeholder="Select fields...">{{$item}}</ui-select-match>
<ui-select-choices repeat="fields in availableFields | filter:$select.search">
{{fields}}
</ui-select-choices>
</ui-select>
in the controller,
$scope.selectedFields = {};
$scope.selectedFields.name = [];
$scope.init = function() {
$http.get(url)
.success( function(response, status, headers, config) {
availableFields = response;
})
.error( function(err) {
});
};
$scope.init();
If not this way, is there any other options/choice I can work with which doesn't delay showing the select-choices?
This is a known issue in ui-select. I tried the following ways, both work
1) There is a workaround for this - use
| limitTo: 100
This limits the choice display to 100 but all the choices can be selected. Look at this thread for more details.
2) Since some of the time, there is a need to display the entire list in the choices, 1) is not a viable option. I used a different library - selectize.js. Here's a plunker demo given in the page
Here is complete solution that decorates uiSelectChoices directive.
Items are populated progressively as the user scrolls.
Also takes care of searches in the scrolls.
Also works for all values of position={auto, up, down}
Example
<ui-select-choices
position="up"
all-choices="ctrl.allTenThousandItems"
refresh-delay="0"
repeat="person in $select.pageOptions.people | propsFilter: {name: $select.search, age: $select.search} ">
<div ng-bind-html="person.name | highlight: $select.search"></div>
<small>
email: {{person.email}}
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
</small>
</ui-select-choices>
Working Plnkr
Also with With v0.19.5
The directive
app.directive('uiSelectChoices', ['$timeout', '$parse', '$compile', '$document', '$filter', function($timeout, $parse, $compile, $document, $filter) {
return function(scope, elm, attr) {
var raw = elm[0];
var scrollCompleted = true;
if (!attr.allChoices) {
throw new Error('ief:ui-select: Attribute all-choices is required in ui-select-choices so that we can handle pagination.');
}
scope.pagingOptions = {
allOptions: scope.$eval(attr.allChoices)
};
attr.refresh = 'addMoreItems()';
var refreshCallBack = $parse(attr.refresh);
elm.bind('scroll', function(event) {
var remainingHeight = raw.offsetHeight - raw.scrollHeight;
var scrollTop = raw.scrollTop;
var percent = Math.abs((scrollTop / remainingHeight) * 100);
if (percent >= 80) {
if (scrollCompleted) {
scrollCompleted = false;
event.preventDefault();
event.stopPropagation();
var callback = function() {
scope.addingMore = true;
refreshCallBack(scope, {
$event: event
});
scrollCompleted = true;
};
$timeout(callback, 100);
}
}
});
var closeDestroyer = scope.$on('uis:close', function() {
var pagingOptions = scope.$select.pagingOptions || {};
pagingOptions.filteredItems = undefined;
pagingOptions.page = 0;
});
scope.addMoreItems = function(doneCalBack) {
console.log('new addMoreItems');
var $select = scope.$select;
var allItems = scope.pagingOptions.allOptions;
var moreItems = [];
var itemsThreshold = 100;
var search = $select.search;
var pagingOptions = $select.pagingOptions = $select.pagingOptions || {
page: 0,
pageSize: 20,
items: $select.items
};
if (pagingOptions.page === 0) {
pagingOptions.items.length = 0;
}
if (!pagingOptions.originalAllItems) {
pagingOptions.originalAllItems = scope.pagingOptions.allOptions;
}
console.log('search term=' + search);
console.log('prev search term=' + pagingOptions.prevSearch);
var searchDidNotChange = search && pagingOptions.prevSearch && search == pagingOptions.prevSearch;
console.log('isSearchChanged=' + searchDidNotChange);
if (pagingOptions.filteredItems && searchDidNotChange) {
allItems = pagingOptions.filteredItems;
}
pagingOptions.prevSearch = search;
if (search && search.length > 0 && pagingOptions.items.length < allItems.length && !searchDidNotChange) {
//search
if (!pagingOptions.filteredItems) {
//console.log('previous ' + pagingOptions.filteredItems);
}
pagingOptions.filteredItems = undefined;
moreItems = $filter('filter')(pagingOptions.originalAllItems, search);
//if filtered items are too many scrolling should occur for filtered items
if (moreItems.length > itemsThreshold) {
if (!pagingOptions.filteredItems) {
pagingOptions.page = 0;
pagingOptions.items.length = 0;
} else {
}
pagingOptions.page = 0;
pagingOptions.items.length = 0;
allItems = pagingOptions.filteredItems = moreItems;
} else {
allItems = moreItems;
pagingOptions.items.length = 0;
pagingOptions.filteredItems = undefined;
}
} else {
console.log('plain paging');
}
pagingOptions.page++;
if (pagingOptions.page * pagingOptions.pageSize < allItems.length) {
moreItems = allItems.slice(pagingOptions.items.length, pagingOptions.page * pagingOptions.pageSize);
}
for (var k = 0; k < moreItems.length; k++) {
pagingOptions.items.push(moreItems[k]);
}
scope.calculateDropdownPos();
scope.$broadcast('uis:refresh');
if (doneCalBack) doneCalBack();
};
scope.$on('$destroy', function() {
elm.off('scroll');
closeDestroyer();
});
};
}]);
As stated, ui-select is having quite a few performance issues, but there is a workaround for the limit issue.
If you follow akashrajkn's approach then you will notice that it will actually cut out important pieces of data because it will only render 100 at a time. There is a fix that has passed the unit tests and it can be found on the thread here:
https://github.com/angular-ui/ui-select/pull/716
Basically, if you are storing the javascript file locally, then you can adjust the unminified version. All you need to do is implement the changes he made in the pull request and it should help out significantly. In order to apply the limiting factor, take a look at the below, modified example:
<ui-select multiple ng-model="selectedFields.name" limit = "10" style="width: 100%;">
<ui-select-match placeholder="Select fields...">{{$item}}</ui-select-match>
<ui-select-choices repeat="fields in availableFields | filter:$select.search | limitTo:$select.limit ">
{{fields}}
</ui-select-choices>
</ui-select>
The above will limit your data in the drop down while also maintaining the level of consistency needed.
Because I cannot leave a comment (not enough rep) I write this as an answer and I am sorry it is no answer for the problem.
#bhantol I changed the following line of code to your solution which is working perfectly for me so far
for (var k = 0; k < moreItems.length; k++) {
pagingOptions.items.push(moreItems[k]);
}
for (var k = 0; k < moreItems.length; k++) {
if (pagingOptions.items.indexOf(moreItems[k]) == -1){
pagingOptions.items.push(moreItems[k]);
}
}
This prevents duplicated items from showing up if the user is starting to write a filter and then deletes it.
Also I just figured out that if the list is smaller than 20 items it will not work so I changed:
if (pagingOptions.page * pagingOptions.pageSize < allItems.length) {
moreItems = allItems.slice(pagingOptions.items.length, pagingOptions.page * pagingOptions.pageSize);
}
to:
if (pagingOptions.page * pagingOptions.pageSize < allItems.length) {
moreItems = allItems.slice(pagingOptions.items.length, pagingOptions.page * pagingOptions.pageSize);
}
else{ moreItems = allItems;}
Maybe this will help you somehow and sorry again for not answering the question.

how to wrap text in angularjs and save the position

I would like to wrap text with span tag and save the position.
I know how to do it with JS but i dont know how to do it with angularjs
Here is what i have done:
http://jsfiddle.net/ymeaL06j/1/
This function gives me the position of the text in the DIV
function getSelectionPosition() {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(document.getElementById("code"));
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
};
I take the start and end positions and insert them as an attribute in the span tag
After this i would like to save all the marked positions and load it later, i have a function that select text and then i can wrap it (i hope that there is a better solution)
function setSelection(savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(document.getElementById("code"), 0);
range.collapse(true);
var nodeStack = [containerEl], node, foundStart = false, stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
You can use the same code that you illustrated above and put it inside of your angularjs controller.
Refer to my plunker code; it is a simple angularjs version of your jsfiddle code.
For example, suppose that a snippet of the index.html looks like this:
<body ng-controller="MainCtrl">
<div id="code">This is <b>some text</b> bla bla bla</div>
<br />
<input type="button" value="Mark!" ng-click="markText()" />
<input type="button" value="Remove marks!" ng-click="removeMarks()" />
</body>
Then the example angularjs controller, MainCtrl, could look like this:
app.controller('MainCtrl', function($scope) {
var getSelectionPosition = function () {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(document.getElementById("code"));
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
}
$scope.markText = function() {
var currPosition = getSelectionPosition();
var selection = window.getSelection().getRangeAt(0);
var selectedText = selection.extractContents();
var span = document.createElement("span");
span.className = "Mark";
span.setAttribute("PosStart", currPosition.start);
span.setAttribute("PosEnd", currPosition.end);
span.appendChild(selectedText);
selection.insertNode(span);
};
$scope.removeMarks = function() {
$(".Mark").each(function () {
$(this).contents().unwrap();
});
};
});
Notice that the MainCtrl is the angularjs controller for the body. The ng-click on the buttons reference the markText and removeMarks functions in the controller's scope. The logic in the functions are exactly the same as you referenced in your question (and jsfiddle).
None of your JS code changed other than moving the functions inside of the controller. Again, check out the plunker above to see the actual code working.

Resources