Property in custom Angular directive link function always null - javascript

Plunkr here
I have a custom directive I'm build to display basic user data. The HTML template is able to render the user data using {{xxx}} syntax, but I also need to use one of the properties from the user data object to retrieve a photo of the user.
My problem is that within the link function the personData value is always null. I added a $observe on person, but it is always null also.
Is there a way observe changes to the personData object and perform action on change?
Directive Code:
app.directive('graphPerson', ['$http', function() {
return {
restrict: 'E',
scope: {
personData: '=person'
},
link: function($scope, element, attrs, ctrl) {
console.log("Directive was linked");
console.log($scope.personData);
attrs.$observe('person', function(newValue, oldValue) {
console.log($scope.personData);
if ($scope.personData) {
//hard-coded photo for demo purposes
$scope.myPhoto = "http://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Blank_woman_placeholder.svg/315px-Blank_woman_placeholder.svg.png";
}
});
},
templateUrl: 'graph-person.html',
};
}]);

I am assuming that your intent is to monitor changes made to the personData object and not to the actual value of the DOM attribute person in your directive. Is that correct?
I would use $scope.$watch() here.
See: http://plnkr.co/edit/SU5x1F5tZKQWk4VK5OGO
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.personData = null;
$scope.people = [
{
"displayName": "Katie Jordan",
"givenName": "Katie",
"jobTitle": "Auditor",
"mail": "KatieJ#demo.com",
"mobilePhone": null,
"officeLocation": "12/1110",
"preferredLanguage": "en-US",
"surname": "Jordan",
"userPrincipalName": "KatieJ#demo.com",
"id": "a0da13e4-1866-492e-96e6-8a2a4b60f650"
},
{
"displayName": "James Jordan",
"givenName": "James",
"jobTitle": "Auditor",
"mail": "JamesJ#demo.com",
"mobilePhone": null,
"officeLocation": "12/1110",
"preferredLanguage": "en-US",
"surname": "Jordan",
"userPrincipalName": "JamesJ#demo.com",
"id": "b0da13e4-1866-492e-96e6-8a2a4b60f651"
}
];
$scope.clickMe = function(index) {
$scope.personData = $scope.people[index];
}
});
app.directive('graphPerson', ['$http', function() {
function getImageUrl(id)
{
console.log("I should get the URL For: " + id);
return id;
}
return {
restrict: 'E',
scope: {
personData: '=person'
},
link: function($scope, element, attrs, ctrl) {
console.log("Directive was linked");
$scope.$watch('personData', function(newValue) {
console.log($scope.personData);
if ($scope.personData) {
$scope.myPhoto = getImageUrl($scope.personData.id);
}
});
},
templateUrl: 'graph-person.html',
};
}]);

Related

How to trigger ng-change on md-select when model is changed?

I'm using md-select and need to trigger certain code when the value changes (building a Country/State selector). I have it working fine when I change the value through the control but I also need to have the controls reflect the values properly when the model is changed from code. I'm using ng-change to trigger the change code (need ng-change as user can change the value from the keyboard without clicking on it). The problem is that when the value is changed from the code, the event isn't fired. To complicate things a bit more, the md-selects live in a directive to allow me to use the setup in several places.
Here's my directive template:
<md-input-container class="md-block">
<label>Country</label>
<md-select name="country" ng-model="countryState.countryModel" ng-change="countryState.onCountrySelected()">
<md-option ng-repeat="country in countryState.countries" ng-value="country.itemId">
{{ country.name | translate }}
</md-option>
</md-select>
</md-input-container>
<md-input-container class="md-block">
<label>State</label>
<md-select name="state" ng-model="countryState.stateModel" ng-disabled="countryState.countryModel == null">
<md-option ng-repeat="state in countryState.states" ng-value="state.itemId">
{{ state.name | translate }}
</md-option>
</md-select>
</md-input-container>
Here's the directive code:
angular.module('myapp.shared')
.directive('countryStateInput', [function () {
return {
restrict: 'E',
templateUrl: 'app/shared/inputs/countryState/country-state-input.directive.html',
transclude: false,
scope: {
coordsForm: '=',
countryModel: '=',
stateModel: '='
},
bindToController: true,
controllerAs: 'countryState',
controller: ['$scope', '$document', 'OptionService', function($scope, $document, OptionService) {
var ctrl = this;
// Properties
ctrl.countries = null;
ctrl.states = [];
ctrl.selectedCountryIndex = null;
ctrl.onCountrySelected = function() {
// Get the index of the country
for (var i = 0; i < ctrl.countries.length; i++) {
if (ctrl.countryModel === ctrl.countries[i].itemId) {
// If a different country was chosen, clear the selected state
if (i !== ctrl.selectedCountryIndex) {
ctrl.stateModel = null;
angular.copy(ctrl.countries[i].states, ctrl.states);
};
// Save the index of the selected country
ctrl.selectedCountryIndex = i;
return;
}
}
};
// Initialization
var initialize = function () {
OptionService.getCountries().then(
function (result) {
ctrl.countries = result;
});
};
(function () {
$document.ready(function () {
initialize();
})
})();
}]
}
}]);
Here's a usage example:
<country-state-input country-model="app.location.countryId" state-model="app.location.stateId" input-form="locationsForm"></country-state-input>
And OptionService.getCountries() returns something like this (states lists are shortened):
[
{
"itemId": 1,
"name": "CA",
"states": [
{
"itemId": 1,
"name": "CA_ON",
"abbreviation": "ON"
},
{
"itemId": 2,
"name": "CA_QC",
"abbreviation": "QC"
}
]
},
{
"itemId": 2,
"name": "US",
"states": [
{
"itemId": 14,
"name": "US_AL",
"abbreviation": "AL"
},
{
"itemId": 15,
"name": "US_AK",
"abbreviation": "AK"
}
]
}
]
Basically, I'm trying to figure out if there's a way to trigger onCountrySelected that will cover all 3 use cases.
You could use $scope.$watch
$scope.$watch(
function valueGetter(){
return smth;
},
function onChange(newSmth, oldSmth){
}
)

Angular scope.$watch not working in directive

I have an angular directive with a scope.$watch which is not working, but I know the value is changing. Here's the directive:
var StepFormDirective = function ($timeout, $sce, dataFactory) {
return {
replace: false,
restrict: 'AE',
scope: {
currentStep: "=",
title: "="
},
template: '<h3>{{title}}</h3><form class="step-form"></form>',
compile: function (tElem, attrs) {
return function (scope, elem, attrs) {
scope
.$watch(
function(){return scope.currentStep;},
function (newValue) {
var stepFormFields = Array();
stepFormFields.push($sce.trustAsHtml("<p>Fields</p>"));
alert(scope.currentStep);
}
);
};
}
};
};
Here's the tag:
<div title="'test'" currentStep="currentContext.currentStep"></div>
I know the currentContext.currentStep is changing, because I also have this on my page and it updates:
<pre>{{currentContext.currentStep | json}}</pre>
The alert gets called the first time, but then when the value changes (evidenced by the bit in the pre tags) the alert does not get called again and I have no js console errors.
The output for the step (it's data type) is:
{
"identifier": "830abacc-5f88-4f9a-a368-d8184adae70d",
"name": "Test 1",
"action": {
"name": "Approval",
"description": "Approve",
"instructions": "Select 'Approved' or 'Denied'",
"validOutcomes": [
{
"outcome": "Approved",
"display": "Approved",
"id": "Approved"
},
{
"outcome": "Denied",
"display": "Denied",
"id": "Denied"
}
]
...
Try to use $watch method with third argument set by true (objectEquality):
$watch('currentStep', function(newValue){...}, true);
Or use $watchCollection:
$watchCollection('currentStep', function(newValue){...})

angularjs directive to build widget according to arbitrary js Object with json schema

I'm trying to build a widget to render arbitrary data (javascript object) on ui. And the schema of the data is defined by a json schema file. And the model must be two-way binding since I want use this widget to input/display data.
Is directive the right way to do this? Is there anyone can give me some clue to achieve this?
Yes, a directive is the right way. You can see that the angular-schema-form mentioned in the comments is using directives.
If you'd like to create your own directive. Please have look at the demo below or this jsfiddle.
The example with the comments is probably not the best because there you wouldn't need the two-way binding of the data.
But it should give you an idea how this could work.
How does the code work?
It loops over schema.items.properties and creates a template with ng-model / ng-restrict and all bindings added to the template.
After the loop is done the $compile service is adding the current scope to the template.
Replace the html with the compiled template.
The demo is probably pretty basic compared to the angular-schema-form code but it's easier to understand and easier to modifiy to your needs.
angular.module('demoApp', [])
.controller('mainController', MainController)
.directive('awDisplayJsonView', DisplayJsonView)
.directive('awDateParser', DateParser);
/**
* Translates unix time stamp into readable dates
* from model/view & view/model
*/
function DateParser() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelController) {
ngModelController.$parsers.push(function(data) {
//convert data from view format to model format
return new Date(data).getTime(); //converted
});
ngModelController.$formatters.push(function(data) {
//convert data from model format to view format
return new Date(parseInt(data)).toUTCString(); //converted return UTC
});
}
}
}
function MainController() {
angular.extend(this, {
/*jsonView: {
"text": { // name of model
type: "input",
label: "Two way binded input",
}
},*/
jsonModel: [{
"id": 1,
"name": "Test User",
"text": "I am a comment.",
"date": "1435427542904",
}, {
"id": 2,
"name": "Antother User",
"text": "I am the second comment.",
"date": "1435427605064",
}],
jsonSchema: {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Comments",
"type": "array",
"items": {
"title": "Comment",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a comment.",
"type": "number",
"format": "hidden" // not sure if this is the right place
},
"name": {
"title": "Name",
"type": "string"
},
"text": {
"title": "Comment",
"type": "string"
},
"date": {
"title": "Date (format y/m/d hh:mm:ss GMT)",
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"name",
"text"]
}
}
});
}
function DisplayJsonView($compile) {
var templatesObj = {
string: function (hidden) {
return $('<input/>')
.attr('type', hidden)
.addClass('form-control');
},
checkbox: function (hidden) {
return $('<input/>')
.attr('type', hidden || 'checkbox')
.addClass('form-control');
},
number: function(hidden) {
return $('<input/>')
.attr('type', hidden)
.addClass('form-control');
}
};
function render(schema, model, index) {
var outTemplate = $(document.createDocumentFragment()),
tempTmpl, hidden; // temporary template, hidden input
angular.forEach(schema.items.properties, function (prop, key) {
//console.log(key, prop.type, prop.format, templatesObj[prop.type]);
hidden = prop.format == 'hidden'? 'hidden': null;
tempTmpl = templatesObj[prop.type](hidden); // get template based on type
tempTmpl.attr({
'ng-model':
'model[' + index + '].' + key, // add current model
'ng-required': schema.items.required.indexOf(key) != -1 // check if it is required
});
if (prop.format == 'date-time')
tempTmpl.attr('aw-date-parser', ''); // add directive if we have a date
outTemplate.append($('<div/>').addClass('form-group')
.append(!hidden ? $('<label/>').text(prop.title || key) : null) //add label if not hidden
.append(tempTmpl));
});
//console.log(model, outTemplate);
return outTemplate;
}
return {
restrict: 'EA',
scope: {
//view: '=', // angular schema form does implement this
model: '=',
schema: '='
},
template: '<div>{{schema |json:2}}</div>',
link: function (scope, element, attrs) {
var out = $('<form/>');
angular.forEach(scope.model, function(item, index) {
//console.log(item);
out.append(render(scope.schema, item, index));
});
var compiled = $compile(out)(scope);
//console.log(scope.model, scope.schema);
element.replaceWith(compiled);
}
};
}
DisplayJsonView.$inject = ['$compile'];
body {
padding: 0.5em;
}
.form-group {
padding-bottom: 0.25em;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<div ng-app="demoApp" ng-controller="mainController as ctrl">
<h3>Comments list with schema json directive</h3>
<form name="form" aw-display-json-view="" data-model="ctrl.jsonModel" data-schema="ctrl.jsonSchema" ng-cloak=""></form>
<div ng-cloak>
{{form|json}}
{{ctrl.jsonModel}}
</div>
</div>

Accessing parent-children objects in directive [AngularJS]

I am creating a basic page that shows and lets you edit/add/remove children of a tree. I've made a recursive directive to print them out, but I am really confused on how should I be going about removing an array element, as I need to know its parent to call splice.
Any help really appreciated, been scratching my head for hours now :/
Should I be passing the parent element down as an attribute?
app.controller('treeController', ['$scope', function($scope)
{
//Root object
$scope.tree = {
name : "Root",
expand : true,
children : []
};
//Temp data filler.
$scope.fillData = function()
{
var child1 = {
name : "Element 1",
expand : false,
children : []
};
var child2 = {
name : "Element 2",
expand : false,
children : []
};
var subChild1 = {
name : "Child 1",
expand : false,
children : []
};
var subChild2 = {
name : "Child 2",
expand : false,
children : []
};
var grandChild1 = {
name : "grandChild 1",
expand : false,
children : []
};
var grandChild2 = {
name : "grandChild 2",
expand : false,
children : []
};
// Add the children
$scope.tree.children.push(child1);
$scope.tree.children.push(child2);
$scope.tree.children[0].children.push(subChild1);
$scope.tree.children[1].children.push(subChild1);
$scope.tree.children[0].children.push(subChild2);
$scope.tree.children[1].children.push(subChild2);
$scope.tree.children[0].children[0].children.push(grandChild1);
$scope.tree.children[0].children[1].children.push(grandChild1);
$scope.tree.children[1].children[0].children.push(grandChild1);
$scope.tree.children[1].children[1].children.push(grandChild1);
$scope.tree.children[0].children[0].children.push(grandChild2);
$scope.tree.children[0].children[1].children.push(grandChild2);
$scope.tree.children[1].children[0].children.push(grandChild2);
$scope.tree.children[1].children[1].children.push(grandChild2);
}
}]);
app.directive('collection', function () {
return {
restrict: "E",
replace: true,
scope: {
collection: '='
},
template: "<ul><member ng-repeat='member in collection.children' member='member'></member></ul>"
}
})
app.directive('member', function ($compile) {
return {
restrict: "E",
replace: true,
scope: {
member: '='
},
//template: "<li></li>",
templateUrl: 'node.html',
link: function (scope, element, attrs) {
var collectionSt = '<collection collection="member"></collection>';
//check if this member has children
if (angular.isArray(scope.member.children))
{
$compile(collectionSt)(scope,
function(cloned, scope)
{
element.append(cloned);
});
}
scope.deleteMe = function(index)
{
//var index = array.indexOf(member);
alert(array.indexOf($scope.member))
};
}
}
});

passing different array variables to directive

In a controller I have defined two different arrays and I need to pass different arrays to directives, but the same item array is getting passed only.
Actually this is just snippets. In my actual code this directive I am using inside another.
Is that causing problem or any other way?
<div my-sticky tags="items" template_name="test2.html"></div>
<div my-sticky tags="kititems" template_name="test2.html"></div>
JS:
app.controller('MyCtrl', function($scope, $http) {
$scope.items = [
{ id: 18, text: '1' },
{ id: 28, text: '2' },
{ id: 38, text: '3' }
];
$scope.kititems = [
{ id: 0, text: '001' },
{ id: 028, text: '002' },
{ id: 038, text: '003' }
];
});
app.directive("mySticky", function($http, $compile) {
return {
restrict : "A",
scope : {
templateName: "#",
tags: "=",
},
templateUrl : function(el, attrs) {
return attrs.templateName;
},
controller : function($http, $scope, $element, $sce, $templateCache, $compile, $attrs) {
$scope.drawerStyle = {left: '140px'};
//$scope.CommonArray=$attrs.tags;
},
replace : false,
transclude : false,
link : function(scope, element, attrs) {
// change element just to show we can
}
};
});
Try this one
Working Demo
Inside your templateURL(test2.html) should like this i.e,
<div ng-repeat="tag in tags"> {{tag.id}} {{tag.text}} </div>
You might try to access items or kititems both are not in current scope because you have used isolated scope here.For clarity template used here instead of templateURL.
HTML Markup:
<div ng-controller="MyCtrl">
MyCtrl
<div my-sticky tags="items"></div>
<hr/>
<div my-sticky tags="kititems"></div>
</div>
angular modules
var app = angular.module("myApp",[]);
app.controller('MyCtrl', function($scope) {
$scope.items = [
{ id: 18, text: '1' },
{ id: 28, text: '2' },
{ id: 38, text: '3' }
];
$scope.kititems = [
{ id: 0, text: '001' },
{ id: 028, text: '002' },
{ id: 038, text: '003' }
];
});
app.directive("mySticky", function($http, $compile) {
return {
restrict : "A",
scope : {
templateName: "#",
tags: "=",
},
template :'<div ng-repeat="tag in tags"> {{tag.id}} {{tag.text}} </div>',
controller : function($http, $scope, $element, $sce, $templateCache, $compile, $attrs) {
$scope.drawerStyle = {left: '140px'};
// console.log($scope);
//$scope.CommonArray=$attrs.tags;
},
replace : false,
transclude : false,
link : function(scope, element, attrs) {
// change element just to show we can
}
};
});

Categories