AngularJS How do I access JSON from a directive - javascript

I have written the following directive:
var gameOdds = function(){
return {
template: '{{games["#homeTeam"]}} vs {{games["#awayTeam"]}}',
scope: {
games: '#'
}
};
};
<div game-odds games="{{games}}">
This uses the following JSON data (part of the json is below):
{
#id: "69486",
#homeTeam: "Home Team",
#awayTeam: "Away Team",
otherNormalValues : {
etc: "normal..."
}
}
I know that the method of selecting these keys preceded with an # symbol works when put directly into the HTML bound to a controller. But in my directive I cannot select the fields in this ["#field"] way.
Does anyone know how to do this?

Instead of using the attribute object notation, #, you can use the = instead.
DEMO
JAVASCRIPT
.directive('gameOdds', function() {
return {
template: '{{games.homeTeam}} vs {{games.awayTeam}}',
scope: {
games: '='
}
}
});
HTML
<div game-odds games="games"></div>
Update: Sorry for the late reply, as what the accepted answer had mentioned, you can access them with the [] notation, if the key starts with special characters in it:
.directive('gameOdds', function() {
return {
template: '{{games['#homeTeam']}} vs {{games['#awayTeam']}}',
scope: {
games: '='
}
}
});

The # symbol on scope transforms whatever you pass to the attribute games into text, and passes it into your directive. If you use the = symbol, you can pass a scope variable into the directive.
With #, scope.games will be a string
With =, scope.games will be your json object
var gameOdds = function(){
return {
template: '{{games["#homeTeam"]}} vs {{games["#awayTeam"]}}',
scope: {
games: '='
}
};
};
<div game-odds games="games">

Related

Checking Input to a Custom Directive in AngularJS

I have a very large form, which was getting difficult to read and follow when editing the HTML. I decided that I would try and make the most of AngularJS and use custom directives to compress the amount of repeated text. Here is the original directive I wrote:
app.directive("formField", function () {
return {
restrict: 'E',
scope: {
fieldData: '=field',
fieldName: '=name',
fieldType: '=type'
},
template: <SOME HTML>
}
});
And I would use this directive to add form fields to my page as follows:
<form-field field="some_data" type="text" name="other_data"></form-field>
I was using the type variable to differentiate between dateTime input, text input, numbers, etc, as they were distinguished in my code by only one keyword (by the input's type attribute.)
However now I have encountered a need to include checkboxes, which, thanks to my layout, require significantly different code to be structured properly. Based on this, when the type "checkbox" is passed into the directive I would like to return a different template value. I have tried variations of this kind of thing:
app.directive("formField", function () {
return {
restrict: 'E',
scope: {
fieldData: '=field',
fieldName: '=name',
fieldType: '=type'
},
template: function () {
if(fieldType == 'checkbox') {
return <CHECKBOX HTML>;
} else {
return <REGULAR FIELD HTML>;
}
}
});
This doesn't work. Can anybody tell me how to check the value coming in for the type field so that I can compare it in the directive's returned object? Thanks.
In the template, you can check for the element's attributes.
Your template should look like:
template: function (element, attrs) {
if(attrs.type == 'checkbox') {
return <CHECKBOX HTML>;
} else {
return <REGULAR FIELD HTML>;
}
}
The isolate scope attribute definitions for fieldData, fieldName, and fieldType are available in the template return string (using expressions), but they are not available in the template's logic. Ex:
template: '<p>{{ fieldData }}</p>'

Angular: afterCellEdit without scope

I'm trying to implement the afterCellEdit function inside my gridOption.onREgisterApi function. I'm not using $scope in my program as is recommended to do in the guidelines.
In fact, my question is exactly the same as this one : question
Sadly it is not answered.
When I use null as one of the answer suggest I got an error.
Here is the code :
vm.gridOptions.onRegisterApi = function(gridApi){
vm.gridApi = gridApi;
vm.gridApi.edit.on.afterCellEdit(null,function(rowEntity, colDef, newValue, oldValue){
alert("afterCellEdit");
});
};
And here is my error :
typeError: Cannot read property '$on' of null
at Object.feature.on.(anonymous function) [as afterCellEdit]
Thanks !
Edit : for #SaurabhTiwari here is my $scope.gridData alternative
function onCustomerListComplete(data) {
vm.gridOptions.data = data;
}
function OnError(reason) {
$log.error(reason);
}
function activate() {
customerService.getCustomerList()
.then(onCustomerListComplete, OnError);
}
vm.gridOptions = {
enablePaginationControls: false,
useExternalSorting: true,
enableHorizontalScrollbar : uiGridConstants.scrollbars.NEVER,
enableVerticalScrollbar : uiGridConstants.scrollbars.WHEN_NEEDED,
columnDefs: [
// will be modified once the data model is set
{ field: 'id', name: '', cellTemplate: 'content/customerList/rowEditor/customerList.rowEditor.editButton.html', width: 34 },
{ name: 'customerNumber', },
{ name: 'customerName' },
{ name: 'city' },
{ name: 'creditLimit' },
{ name: 'postalCode' },
]
};
It might be late to provide some help here and you possibly have found a solution. But I would like to explain your condition using the below code.
function (scope, handler, _this) {
if ( scope !== null && typeof(scope.$on) === 'undefined' ){
gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters. It is legitimate to pass null, but you\'ve passed something else, so you probably forgot to provide scope rather than did it deliberately, not registering');
return;
}
var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this);
//track our listener so we can turn off and on
var listener = {handler: handler, dereg: deregAngularOn, eventId: eventId, scope: scope, _this:_this};
self.listeners.push(listener);
var removeListener = function(){
listener.dereg();
var index = self.listeners.indexOf(listener);
self.listeners.splice(index,1);
};
//destroy tracking when scope is destroyed
if (scope) {
scope.$on('$destroy', function() {
removeListener();
});
}
return removeListener;
}
This is the original code from the ui-grid Api. You can search for it in the library or from a proper scope in application you can just try doing:
$scope.gridApi.edit.on.afterCellEdit.toString()
So the point here is that this function explicitly requires a scope or a null. So You do need to pass a scope to this listener.
The point you mentioned in your question that you are not using $scope in your code is somewhat vague. There always is a scope associated if you have a controller. Angular is pretty much about scopes (atleast Angular 1 was).
What the referred guidelines says is that you should avoid heaping everything on your $scope. The guidelines also says you should use $scope only for listening and watching, which is exactly the case here

Object property becomes null after chrome.storage.sync.set

I'm building a chrome extension and have encountered a bug I cannot wrap my head around. The problem is a single object property that becomes null in chromes' storage.
I'm testing this by doing:
console.log("pre-storage", settings);
var obj = {};
obj[storage_key] = settings;
chrome.storage.sync.set(obj, function() {
chrome.storage.sync.get(storage_key, function(data) {
console.log("post-storage", data[storage_key]);
});
});
This is the output:
pre-storage, Object {
...
soundClip: Object {
options: Array[5],
selected: Object {
label: "Soft2",
value: "snd/soft2.wav"
}
}
}
post-storage, Object {
...
soundClip: Object {
options: Array[5],
selected: null
}
}
Storing JSON.parse(JSON.stringify(obj)) instead of obj directly seems to fix this. Anyone have any ideas what might cause this? Any help is appreciated!
Edit: Making a deep copy of obj does not fix it.
Edit2: I should expand on how settings.soundClip is set. I'm using Angular (1.x) and I'm using a custom select directive. The stripped down directive looks like this:
function mySelect() {
return {
restrict: "E",
templateUrl: "mySelect.html",
scope: {
options: "=",
selected: "="
},
link: function (scope) {
scope.select = function (item) {
scope.selected = item;
};
}
}
}
Directive template view (mySelect.html):
<div>
<div ng-repeat="item in options track by $index"
ng-click="select(item)">
</div>
</div>
The properties are then two-way bound like this:
<my-select selected="settings.soundClip.selected"
options="settings.soundClip.options">
</my-select >
Since calling JSON.parse(JSON.stringify(obj)) seems to fix it, my guess is that you're having a problem with encoding your settings object with a variable instead of a string. See the answer here which might help.
Is it possible that the total quota (or per item) is being hit? Consider displaying the runtime.lastError on the set callback to see if there are any error messages.
chrome.storage.sync.set(obj, function() {
console.log('Error', runtime.lastError);
chrome.storage.sync.get(storage_key, function(data) {
console.log("post-storage", data[storage_key]);
});
});
See the limits here chrome.storage.sync.set

Angular ng-model dynamic getters and setters

I want to use ng-model with an external model-service. This model has two methods: getValue(variable) and setValue(variable).
So in my html I want to be able to do:
<input type="text" ng-model="balance">
Note: balance is not defined on $scope in my controller. And because we are dealing with more then 4000 different variables, I don't want to define them all on $scope.
And then on change it must call the setValue() method of the model. So in my controller I would like to have something like:
$catchAllGetter = function(variable) { // e.g. variable = 'balance'
var value = Model.getValue(variable);
return value;
}
$catchAllSetter = function(variable, value) { // called on change
Model.setValue(variable, value);
}
Is something like this possible with Angular?
My approach is similar to #Dan Prince, but the implementation differs a bit
Create a directive, that accepts name of the model variable, and then inject your model service in the directive itself to perform the getting and setting.
Edit : As specified by #Werlang, writing an attribute that replaces
ngModel will refrain you from features like validation, formatting,
debounced update, ng-change etc. So instead of writing a replacement,
we will instead wire up a supplementary attribute
.
app.directive('dynamicInput', function() {
return {
restrict: 'A',
link: function(scope, el, attr) {
scope.variableName = angular.copy(attr.ngModel); // Saving the variable name
scope[attr.ngModel] = (attr.ngModel + '_1'); // Setting a dummy value in the scope variable.
// In your case it will look something like scope[attr.ngModel] = Model.getValue(attr.ngModel);
scope.$watch(attr.ngModel, function(newValue, oldValue) {
console.log(scope.variableName + " ==> " + newValue);
//Model.setValue(scope.variableName, newValue);
});
}
};
})
Then in your HTML :
<input ng-model='balance' dynamic-input />
You can create a new directive which implements this behaviour.
<input model-getter='getFn()' model-setter='setFn($value)' />
This would be fairly straightforward to implement:
app.directive('modelGetter', function() {
return {
restrict: 'A',
scope: {
get: '&modelGetter',
set: '&modelSetter'
},
link: function(scope, element) {
element.val(scope.get());
element.on('change', function() {
var val = element.val();
scope.set({ $value: val });
});
}
};
})
look at example, i created for you. I hope I have understood you correctly
$scope.$watch('variables', function(newValue) {
console.log("triggers on variables change");
angular.forEach(newValue, function(value, key) {
Model.setValue(key, value);
});
}, true);
ngModel supports getter and setters. Here's how it works:
<input ng-model="balance" ng-model-options="{ getterSetter: true }">
This works if balance is a getter/setter function:
$scope.balance(100); // sets 100
var b = $scope.balance(); // returns 100
You don't need to expose each variable on the scope - you could just expose the Model service that you use in your example:
$scope.Model = Model;
then, in the View, bind to whatever property you need:
<input ng-model="Model.balance" ng-model-options="{ getterSetter: true }">
Have all your variables in an object array:
[
{key: "Variable 1", value: 1, kind: "number"},
{key: "Variable 2", value: "some text", kind: "text"},
{key: "Variable 3", value: new Date(), kind: "date"}
]
Then in your view you shall create them with the help of an ng-repeat:
<div ng-repeat="variable in myVariables">
<input type="{{variable.kind}}" ng-model="variable.value" ng-change="changed(variable)">
</div>
If you need to update your external service, implement a method changed(variable) in your controller.
ES5 Object properties to the rescue:
Object.defineProperty($scope, 'balance', {
enumberable: true,
get: function () {
// -- call your getter here
},
set: function (val) {
// -- call the setter here
}
});
This is native Javascript, so it does not get faster than this.
You can evaluate your model function dynamically, e.g.
<input type="text" ng-model="myModel(var)">
And in the controller:
$scope.myModel = function(var) {
return function(newValue) {
// this is a regular model function but you can use 'var' here
...
}
}

How to make Automated Dynamic Breadcrumbs with AngularJS + Angular UI Router

One key component to web applications is breadcrumbs/navigation. With Angular UI Router, it would make sense to put the breadcrumb metadata with the individual states, rather than in your controllers. Manually creating the breadcrumbs object for each controller where it's needed is a straight-forward task, but it's also a very messy one.
I have seen some solutions for automated Breadcrumbs with Angular, but to be honest, they are rather primitive. Some states, like dialog boxes or side panels should not update the breadcrumbs, but with current addons to angular, there is no way to express that.
Another problem is that titles of breadcrumbs are not static. For example, if you go to a User Detail page, the breadcrumb title should probably be the user's Full Name, and not a generic "User Detail".
The last problem that needs to be solved is using all of the correct state parameter values for parent links. For example, if you're looking at a User detail page from a Company, obviously you'll want to know that the parent state requires a :companyId.
Are there any addons to angular that provide this level of breadcrumbs support? If not, what is the best way to go about it? I don't want to clutter up my controllers - I will have a lot of them - and I want to make it as automated and painless as possible.
Thanks!
I did solve this myself awhile back, because nothing was available. I decided to not use the data object, because we don't actually want our breadcrumb titles to be inherited by children. Sometimes there are modal dialogs and right panels that slide in that are technically "children views", but they shouldn't affect the breadcrumb. By using a breadcrumb object instead, we can avoid the automatic inheritance.
For the actual title property, I am using $interpolate. We can combine our breadcrumb data with the resolve scope without having to do resolves in a different place. In all of the cases I had, I just wanted to use the resolve scope anyway, so this works very well.
My solution also handles i18n too.
$stateProvider
.state('courses', {
url: '/courses',
template: Templates.viewsContainer(),
controller: function(Translation) {
Translation.load('courses');
},
breadcrumb: {
title: 'COURSES.TITLE'
}
})
.state('courses.list', {
url: "/list",
templateUrl: 'app/courses/courses.list.html',
resolve: {
coursesData: function(Model) {
return Model.getAll('/courses');
}
},
controller: 'CoursesController'
})
// this child is just a slide-out view to add/edit the selected course.
// It should not add to the breadcrumb - it's technically the same screen.
.state('courses.list.edit', {
url: "/:courseId/edit",
templateUrl: 'app/courses/courses.list.edit.html',
resolve: {
course: function(Model, $stateParams) {
return Model.getOne("/courses", $stateParams.courseId);
}
},
controller: 'CourseFormController'
})
// this is a brand new screen, so it should change the breadcrumb
.state('courses.detail', {
url: '/:courseId',
templateUrl: 'app/courses/courses.detail.html',
controller: 'CourseDetailController',
resolve: {
course: function(Model, $stateParams) {
return Model.getOne('/courses', $stateParams.courseId);
}
},
breadcrumb: {
title: '{{course.name}}'
}
})
// lots more screens.
I didn't want to tie the breadcrumbs to a directive, because I thought there might be multiple ways of showing the breadcrumb visually in my application. So, I put it into a service:
.factory("Breadcrumbs", function($state, $translate, $interpolate) {
var list = [], title;
function getProperty(object, path) {
function index(obj, i) {
return obj[i];
}
return path.split('.').reduce(index, object);
}
function addBreadcrumb(title, state) {
list.push({
title: title,
state: state
});
}
function generateBreadcrumbs(state) {
if(angular.isDefined(state.parent)) {
generateBreadcrumbs(state.parent);
}
if(angular.isDefined(state.breadcrumb)) {
if(angular.isDefined(state.breadcrumb.title)) {
addBreadcrumb($interpolate(state.breadcrumb.title)(state.locals.globals), state.name);
}
}
}
function appendTitle(translation, index) {
var title = translation;
if(index < list.length - 1) {
title += ' > ';
}
return title;
}
function generateTitle() {
title = '';
angular.forEach(list, function(breadcrumb, index) {
$translate(breadcrumb.title).then(
function(translation) {
title += appendTitle(translation, index);
}, function(translation) {
title += appendTitle(translation, index);
}
);
});
}
return {
generate: function() {
list = [];
generateBreadcrumbs($state.$current);
generateTitle();
},
title: function() {
return title;
},
list: function() {
return list;
}
};
})
The actual breadcrumb directive then becomes very simple:
.directive("breadcrumbs", function() {
return {
restrict: 'E',
replace: true,
priority: 100,
templateUrl: 'common/directives/breadcrumbs/breadcrumbs.html'
};
});
And the template:
<h2 translate-cloak>
<ul class="breadcrumbs">
<li ng-repeat="breadcrumb in Breadcrumbs.list()">
<a ng-if="breadcrumb.state && !$last" ui-sref="{{breadcrumb.state}}">{{breadcrumb.title | translate}}</a>
<span class="active" ng-show="$last">{{breadcrumb.title | translate}}</span>
<span ng-hide="$last" class="divider"></span>
</li>
</ul>
</h2>
From the screenshot here, you can see it works perfectly in both the navigation:
As well as the html <title> tag:
PS to Angular UI Team: Please add something like this out of the box!
I'd like to share my solution to this. It has the advantage of not requiring anything to be injected into your controllers, and supports named breadcrumb labels, as well as using resolve: functions to name your breadcrumbs.
Example state config:
$stateProvider
.state('home', {
url: '/',
...
data: {
displayName: 'Home'
}
})
.state('home.usersList', {
url: 'users/',
...
data: {
displayName: 'Users'
}
})
.state('home.userList.detail', {
url: ':id',
...
data: {
displayName: '{{ user.name | uppercase }}'
}
resolve: {
user : function($stateParams, userService) {
return userService.getUser($stateParams.id);
}
}
})
Then you need to specify the location of the breadcrumb label (displayname) in an attribute on the directive:
<ui-breadcrumbs displayname-property="data.displayName"></ui-breadcrumbs>
In this way, the directive will know to look at the value of $state.$current.data.displayName to find the text to use.
$interpolate-able breadcrumb names
Notice that in the last state (home.userList.detail), the displayName uses the usual Angular interpolation syntax {{ value }}. This allows you to reference any values defined in the resolve object in the state config. Typically this would be used to get data from the server, as in the example above of the user name. Note that, since this is just a regular Angular string, you can include any type of valid Angular expression in the displayName field - as in the above example where we are applying a filter to it.
Demo
Here is a working demo on Plunker: http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview
Code
I thought it was a bit much to put all the code here, so here it is on GitHub: https://github.com/michaelbromley/angularUtils/tree/master/src/directives/uiBreadcrumbs
I made a Angular module which generate a breadcrumb based on ui-router's states. All the features you speak about are included (I recently add the possibility to ignore a state in the breadcrumb while reading this post :-) ) :
Here is the github repo
It allows dynamic labels interpolated against the controller scope (the "deepest" in case of nested/multiple views).
The chain of states is customizable by state options (See API reference)
The module comes with pre-defined templates and allows user-defined templates.
I do not believe there is built in functionality, but all the tools are there for you, take a look at the LocationProvider. You could simply have navigation elements use this and whatever else you want to know just inject it.
Documentation
After digging deep into the internals of ui-router I understood how I could create a breadcrumb using resolved resources.
Here is a plunker to my directive.
NOTE: I couldn't get this code to work properly within the plunker, but the directive works in my project. routes.js is provided merely for example of how to you can set titles for your breadcrumbs.
Thanks for the solution provided by #egervari. For those who need add some $stateParams properties into custom data of breadcrumbs. I've extended the syntax {:id} for the value of key 'title'.
.state('courses.detail', {
url: '/:courseId',
templateUrl: 'app/courses/courses.detail.html',
controller: 'CourseDetailController',
resolve: {
course: function(Model, $stateParams) {
return Model.getOne('/courses', $stateParams.courseId);
}
},
breadcrumb: {
title: 'course {:courseId}'
}
})
Here is an Plunker example. FYI.

Categories

Resources