Angular directive to dynamically set attribute(s) on existing DOM elements - javascript

I'm somewhat new to Angular, so feedback on alternative approaches is certainly welcome.
I have created a directive called "serverMaxLengths". When the directive is placed on an ng-form, it will fetch database fields lengths from a REST API and then will walk the contents of all input elements contained within the form controller, and will set the "maxlength" attribute accordingly. The directive is as follows:
myApp.directive('serverMaxLengths', function ($log,$http,$compile) {
return {
restrict: 'A',
require: '^form',
link: function (scope, elem, attrs, formController) {
if (!formController) return;
var httpConfig = {
method: 'GET',
url: myAppRestURL + "/validator-rest?action=getDBFieldLengths"
};
$http(httpConfig)
.success(function (data, status, headers, config) {
if (typeof data.isValid != 'undefined') {
if(data.isValid){
var inputElem = elem.find('input');
angular.forEach(inputElem, function (value, key) {
var thisElement = angular.element(value);
if (typeof thisElement[0] !== 'undefined') {
if(typeof data.dbFieldLengths[thisElement[0].id] !== 'undefined'){
if(data.dbFieldLengths[thisElement[0].id] > 0){
thisElement.prop("maxlength", data.dbFieldLengths[thisElement[0].id]);
thisElement.prop("ng-maxlength", data.dbFieldLengths[thisElement[0].id]);
thisElement.prop("ng-minlength", 0);
$compile(thisElement)(scope);
}
}
}
});
}else{
...
}
}else{
...
}
}).error(function (data, status, headers, config) {
...
});
}
};});
This works. Insofar as I understand, $compile is replacing the existing element(s) when the directive is executed.
I'm wondering what a better "Angular" way of achieving this might be? I wanted a very simple solution that doesn't require the directive to be placed on any of the actual input elements(I want everything to happen in one go).
Lastly, one of the fields that is getting the max length set has a UI Bootstrap Typeahead directive assigned to it. Prior to the application of the "maxlength", the directive works as expected. However, post application of the "maxlength" being set on the field via the aforementioned method, type ahead renders a "TypeError: Cannot read property 'length' of undefined" error when the input looses focus(otherwise it works). This has me concerned about this approach, and what's happening behind the scenes.
*Note: The type ahead error is resolved by doing:
$compile(thisElement.contents())(scope);
Instead of:
$compile(thisElement)(scope);
Thanks for any feedback/suggestions/thoughts.

The addition of $compile(thisElement.contents())(scope); resolved the issue that was of primary concern.

Related

How to make filters work with array in angularjs

Iam trying to create a custom filter to filter matching array of values in angularjs. Array Structure below
["tag1","tag2"]
Now I need to filter all objs having tags matching id1,id2.. Below is the filter I have tried
var autoFilter = angular.module("autoFilters",[]);
autoFilter.filter('arrayData', function (){
return function(){
return ["id1","id2"];
}
//$scope.arrayValues = ["id1","id2"];
});
and UI code below
<li style="cursor:pointer" ng-cloak class="list-group-item" ng-repeat="values in suggestionResults | arrayData">{{values.id}} -- {{values.title}}</li>
But Data is not showing up. Can you help me out where Iam doing wrong. Plunker Code available below
plunker here
see the code below :) This is not the best approach in my opinion and will definitely have some performance issue with larger lists, but it does the work (now I used indexOf(2) but there you can pass any truthy/falsy argument)
var autoFilter = angular.module("autoFilters",[]);
autoFilter.controller("filterController",['$scope','$http', function ($scope,$http) {
$scope.searchSuggest = function(){
//$http({method: 'GET', url: 'json/searchSuggestions.json'}).success(function(data) {
$http.get("assets.json").then(function(response) {
//var str = JSON.stringify(response);
//var arr = JSON.parse(str);
$scope.suggestionResult = response.data;
console.log($scope.suggestionResult);
//$scope.arrayData = ["asset_types:document/data_sheet","asset_types:document/brochure"];
}).catch(function activateError(error) {
alert('An error happened');
});
}
$scope.showProduct = function(){
}
}]);
autoFilter.filter('arrayData', function (){
return function(data){
// if you are using jQuery you can simply return $.grep(data, function(d){return d.id.indexOf('2') >-1 });
return data.filter(function(entry){
return entry.id.indexOf('2') > -1
})
}
});
Having experienced working with large lists I would, however, suggest you to avoid using a separate filter for this and rather manipulate it in the .js code. You could easily filter the data when you query it with your $http.get like:
$scope.suggestionResult = response.data.filter(function(){
return /* condition comes here */
}
This way you are not overloading the DOM and help the browser handling AngularJS's sometimes slow digest cycle.
If you need it to be dynamic (e.g. the filtering conditions can be changed by the user) then add an ng-change or $watch or ng-click to the modifiable information and on that action re-filter $scope.suggestionResult from the original response.data

Add custom errors to angularjs validation directive and display all errors in a popup

I wrote an angularjs directive that does validation, this is the code:
app.directive('modelValidation', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attr, ctrl) {
var modelFieldName = attr.ngModel;
var model = scope.validationModel;
var valid;
// scope.validationModel is being loaded async so we need to watch when the promise is resolved.
scope.$watch('validationModel', function (newVal, oldVal) {
if (newVal) { model = scope.validationModel; }
}, true);
//For model -> DOM validation
ctrl.$formatters.unshift(function (value) {
valid = true;
if (model != undefined) {
if (modelFieldName in model) {
var errorMessage = checkFieldValidation(model, modelFieldName, value);
if (errorMessage.length > 0) {
valid = false;
}
}
}
ctrl.$setValidity('modelValidation', valid);
return valid ? value : undefined;
});
}
};
});
The field scope.validationModel holds a json object returned from the server and inside this json I have the validation data such as the regEx that I need to test and the error message to be displayed if the validation fails.
The checkFieldValidation returns the desired error message if the validation fails, else it returns an empty string.
I want to use this directive for all of my form elements, and I want to display all of the returned errors inside a popup.
The validation itself works, but I can't seem to find how to use those error messages as I desire.
I don't know if using angular's directive is the best solution for my case, I am open to another solutions that will solve my problem.
You should use the ng-messages directive.
This way beneath every input you can display the correct error message. You can check this codepen
For the dialog, you can use form.$error to get all the errors and their correct error message.
You can see this codepen to see how it all works. If you want to set messages in $error, just set them on your controller using the error name like :
$scope.userForm.name.error_required = "This is required !"
$scope.userForm.username.error_minlength = "This is too short !"
$scope.userForm.username.error_maxlength = "This is too long !"
$scope.userForm.email.error_email = "This is not an email.."
then use a Object.keys() function in html like (note that you have to link it in your scope elsewise it won't be defined in your html file, hence my getKeys function) :
{{userForm.email['error_' + getKeys(userForm.email.$error)]}}

Push object to empty array after condition is met

I've created a couple of services that grab data from a REST API. These services return objects with names, ids, and some unique keys pertaining to a foo or a bar name. I also have a service doing the same for businesses, also with names, ids, and what foo/bar is tied to that business.
Unfortunately, the data model for this is...not ideal. Rather than just showing which foo/bar is attached to that business, it has every single foo or bar for every single business with a published: true/false key/val pair.
What I'm attempting to do is grab the URL name, loop through my foo object, check to see if the name from the current URL and the data match, and if they do store that object in $scope.results. From here, I want to loop through my businesses object and check to see if its conditionData id matches that of the new $scope.results array's id. Once this condition is met, I want to store those businesses in a $scope.businesses array. As it stands right now, I'm getting all businesses returned, rather than just the ones that have the same id as the current $scope.results id. I suspect the issue is either a) I'm a noob (most likely) or b) the published: true/false is creating issues.
Thanks in advance for any help, let me know if I need to clarify anything else. I'm still pretty new to Angular and JS as a whole, so I'm not sure if how I'm attempting to do this is super optimal. I'm open to better ideas if anyone has any.
.controller('ResultsController', function($scope, $location, getData) {
$scope.businesses = [];
$scope.results = [];
var url = $location.path().split('/')[2]; // we do this because it's always going to follow a pattern of /:base/:name
function init() {
getData.getConditions().success(function(data) {
var tempCondition = data;
var tempData;
for (var condition in tempCondition) {
tempData = tempCondition[condition];
if (url === tempData.name) {
$scope.results = tempData;
}
}
})
.error(function(data, status, headers, config) {
console.log('err: ' + data);
});
getData.getBusinesses().success(function(data) {
var tempBusinesses = data,
tempConditionData;
for (var business in tempBusinesses) {
tempConditionData = tempBusinesses[business].conditionData;
for (var condition in tempConditionData) {
if (tempConditionData[condition].id === $scope.results.id) {
$scope.businesses.push(tempBusinesses[business]);
}
}
}
})
.error(function(data, status, headers, config) {
console.log('err: ' + data);
});
}
init();
});
I find myself using SO as a rubber duck most of the time, I figured it out basically as soon as I finished typing the question. It was due to the published: true/false key/val pair.
All I had to do was change
for (var condition in tempConditionData) {
if (tempConditionData[condition].id === $scope.results.id) {
$scope.businesses.push(tempBusinesses[business]);
}
}
to
for (var condition in tempConditionData) {
if (tempConditionData[condition].id === $scope.results.id && tempConditionData[condition].published === true ) {
$scope.businesses.push(tempBusinesses[business]);
}
}
The two http calls you are using may also be problematic as they depend one each other. what if the first calls takes some time, your second http call returns first.

AngularJS - load images using REST API calls

I am writing an application where I need to display car inventory. I ping an API to get all cars matching search criteria such as Car Make, Model and Year. I need to display an image of each car along with the other information. Once the JSON data is available, it also has an ID (StyleID) for each car in my results that I need to use to make another API call to request images for that car.
After reading a few articles (such as this one) I figured I need to use a custom directive in order to query and insert each car's image in a specific spot when looping over the results.
I read this custom directive tutorial by Jim Lavin to create my sample. I was hoping that this approach will work however I must be missing something as it simply doesn't execute my custom directive and display the car image as I want it to.
Can someone please help?
Here's the plunker that shows my code:
http://plnkr.co/edit/5DqAspT92RUPd1UmCIpn?p=preview
Here's the information about the specific media call to Edmunds API that I am trying to use.
And here's the URL to the media endpoint
Repeating my code :
My HTML code :
<div firstImageOfMyCar data-styleid="style.id"></div>
or
<firstImageOfMyCar data-styleid="style.id"></firstImageOfMyCar>
And here's my custom directive:
// Custom Directive to get first image of each car.
app.directive('firstImageOfMyCar', function() {
return {
restrict: "E",
link: function(scope, elm, attrs) {
// by default the values will come in as undefined so we need to setup a
// watch to notify us when the value changes
scope.$watch(attrs.styleid, function(value) {
//elm.text(value);
// let's do nothing if the value comes in empty, null or undefined
if ((value !== null) && (value !== undefined) && (value !== '')) {
// get the photos for the specified car using the styleID.
// This returns a collection of photos in photoSrcs.
$http.get('https://api.edmunds.com/v1/api/vehiclephoto/service/findphotosbystyleid?styleId=' + value + '&fmt=json&api_key=mexvxqeke9qmhhawsfy8j9qd')
.then(function(response) {
$scope.photoSrcs = response.photoSrcs;
// construct the tag to insert into the element.
var tag = '<img alt="" src="http://media.ed.edmunds-media.com' + response.photoSrcs[0] + '" />" />'
// insert the tag into the element
elm.append(tag);
}, function(error) {
$scope.error3 = JSON.stringify(error);
});
}
});
}
};
});
Angular normalizes an element's tag and attribute name to determine which elements match which directives. We typically refer to directives by their case-sensitive camelCase normalized name (e.g. ngModel). However, since HTML is case-insensitive, we refer to directives in the DOM by lower-case forms, typically using dash-delimited attributes on DOM elements (e.g. ng-model).
Try
<div first-image-of-my-car data-styleid="style.id"></div>
or
<first-image-of-my-car data-styleid="style.id"></first-image-of-my-car>
Note: if you use the first, with the attribute, you will need to change the restrict in the directive to restrict: "A", (or "AE" to cover both cases)
Also, $http, and$scope are not defined in your directive. You can simply add $http to the directive function and DI will inject it. You probably want to use scope instead of $scope.
There were also some other things wrong with the example provided. Here is a working version: http://plnkr.co/edit/re30Xu0bA1XrsM0VZKbX?p=preview
Note that $http's .then() will call the provided function with data, status, headers, config, data will have the response you are looking for. (response.data[0].photoSrcs[0])
Please look at the answer by #TheScharpieOne. But i also played around with your code and api. And I would like to add, that your code might benefit from using angular services to wrap the api calls.
Here is an Example for a service:
app.service('VehicleService', function ($q, $http) {
this.getAllMakes = function () {
var deferred = $q.defer();
var url = 'https://api.edmunds.com/api/vehicle/v2/makes?state=new&view=basic&fmt=json&api_key=mexvxqeke9qmhhawsfy8j9qd'
$http.get(url).then(function (response) {
deferred.resolve(response.data.makes);
}, function (error) {
deferred.reject(new Error(JSON.stringify(error)));
});
return deferred.promise;
}
this.getCar = function (makeName, modelName, year) {
var deferred = $q.defer();
var url = 'https://api.edmunds.com/api/vehicle/v2/' + makeName + '/' + modelName + '/' + year + '?category=Sedan&view=full&fmt=json&api_key=mexvxqeke9qmhhawsfy8j9qd'
$http.get(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject(new Error(JSON.stringify(error)));
});
return deferred.promise;
};
});
You could use it like this:
function CarCtrl($scope, VehicleService, VehiclePhotoService) {
// init make select
VehicleService.getAllMakes().then(function (value) {
$scope.makes = value;
});
$scope.getCars = function () {
VehicleService.getCar($scope.make.niceName, $scope.model.niceName, $scope.year.year)
.then(function (value) {
console.log(value);
$scope.myCars = value;
})
}
}
Here is a complete working jsfiddle: http://jsfiddle.net/gkLbh8og/

save $location parameters state AngularJS

How do I save URL parameters state throughout lifecycle of application using pushState?
Page load.
Go to "/search" via href
submitSearch() through filter fields where $location.search(fields)
Go to "/anotherPage" via href
Go back to "/search" via href
Search paramters are set back to what they last were.
Is this a built in feature somewhere?
If not what's the best way to go about this?
If you're planning on a mostly single page website through pushState, you might want to get an intimate understanding of $routeProvider (http://docs.angularjs.org/api/ngRoute.%24routeProvider).
To go further down the rabbit hole, I would recommend looking at the ui-router module: (https://github.com/angular-ui/ui-router). $stateProvider (from ui-router) and $routeProvider work very similar, so sometimes the ui-router docs can give insights that you can't find in the poor documentation of the $routeProvider.
I reccomend going through the five page ui-router documentation (https://github.com/angular-ui/ui-router/wiki) page by page.
After all that preamble, here's the practical: you would set up a factory that holds history data and use the controller defined in your $routeProvider/$stateProvider to access and manipulate that data.
Note: the factory is a service. A service is not always a factory. The namespace goes:
angular.module.<servicetype[factory|provider|service]>.
This post explains the service types: https://stackoverflow.com/a/15666049/2297328. It's important to remember that they're all singletons.
Ex:
var myApp = angular.module("myApp",[]);
myApp.factory("Name", function(){
return factoryObject
});
The code would look something like:
// Warning: pseudo-code
// Defining states
$stateProvider
.state("root", {
url: "/",
// Any service can be injected into this controller.
// You can also define the controller separately and use
// "controller: "<NameOfController>" to reference it.
controller: function(History){
// History.header factory
History.pages.push(History.currentPage);
History.currentPage = "/";
}
})
.state("search", {
url: "/search",
controller: function(History, $routeParams) {
History.lastSearch = $routeParams
}
});
app.factory('<FactoryName>',function(){
var serviceObjectSingleton = {
pages: []
currentPage: ""
lastSearch: {}
}
return serviceObjectSingleton
})
If you're wondering what the difference between $routeProvider and $stateProvider is, it's just that $stateProvider has more features, mainly nested states and views... I think.
The easiest way is using cookies, angularjs provides a wrapping service for that.
Simply when you go to "/search" save your current URL parameters with "$cookieStore.put()" and once you've back you've got what you need with "$cookieStore.get()".
See the documentation at angularjs cookie store
I made a locationState service, you simply give it the values you want to persist and it stores them in the URL. So you can store all the state you want across all routes in your app.
Use it like this:
angular.module('yourapp')
.controller('YourCtrl', function ($scope, locationState) {
var size = locationState.get('size');
;
// ... init your scope here
if (size) {
$scope.size = size;
}
// ...and watch for changes
$scope.$watch('size', locationState.setter('size'));
}
Here's the code:
// Store state in the url search string, JSON encoded per var
// This usurps the search string so don't use it for anything else
// Simple get()/set() semantics
// Also provides a setter that you can feed to $watch
angular.module('yourapp')
.service('locationState', function ($location, $rootScope) {
var searchVars = $location.search()
, state = {}
, key
, value
, dateVal
;
// Parse search string
for (var k in searchVars) {
key = decodeURIComponent(k);
try {
value = JSON.parse(decodeURIComponent(searchVars[k]));
} catch (e) {
// ignore this key+value
continue;
}
// If it smells like a date, parse it
if (/[0-9T:.-]{23}Z/.test(value)) {
dateVal = new Date(value);
// Annoying way to test for valid date
if (!isNaN(dateVal.getTime())) {
value = dateVal;
}
}
state[key] = value;
}
$rootScope.$on('$routeChangeSuccess', function() {
$location.search(searchVars);
});
this.get = function (key) {
return state[key];
};
this.set = function (key, value) {
state[key] = value;
searchVars[encodeURIComponent(key)] = JSON.stringify(value);
// TODO verify that all the URI encoding etc works. Is there a mock $location?
$location.search(searchVars);
};
this.setter = function (key) {
var _this = this;
return function (value) {
_this.set(key, value);
};
};
});

Categories

Resources