AngularJS Converting Model Value - javascript

I'm using Formly to create my input pages and currently have a simple testing page setup where I can test any types or wrappers I'm creating.
It's currently defined as,
Html:
<div data-ng-controller="npTestingCtrl as vm" style="height:100%">
<formly-form form="vm.form" model="vm.model" fields="[
{
key: 'things',
type: 'checkedListBox',
}]"></formly-form>
</div>
Controller:
(function () {
'use strict';
angular.module('app.testing').controller('npTestingCtrl', npTestingCtrl);
npTestingCtrl.$inject = [];
function npTestingCtrl() {
var vm = this;
vm.model = {
things: { }
}
}
})();
I've then declared a "checkedListBox" type as the following:
Type:
angular.module('app.formly.checkedListBox', ['formly'])
.run(function (formlyConfig) {
formlyConfig.setType({
name: 'checkedListBox',
template: '<np-checked-list-box></np-checked-list-box>'
});
});
The directive 'np-checked-list-box' is then declared as:
Directive:
angular.module('app.formly.checkedListBox').directive('npCheckedListBox', function () {
return {
restrict: 'E',
scope: true,
templateUrl: 'checkedListBox.html',
link: function (scope, element, attr) {
scope.items = [{ identifier: 13, text: 'Tom' }, { identifier: 57, text: 'Dick' }, { identifier: 4, text: 'Harry' }];
}
}
});
Directive Html:
<div style="overflow-y:auto;height:{{to.height == undefinied ? 350 : to.height}}px">
<div class="checkbox" ng-repeat="item in items">
<input id="{{options.id}}_{{item.identifier}}"
type="checkbox"
ng-model="model[options.key][item.identifier]"
value="{{item.checked}}">
<label for="{{options.id}}_{{item.identifier}}">{{item.text}}</label>
</div>
</div>
This is working correctly, in so much as when I click on any of the checkboxes a property is added to the things object in my model with either true or false as a value, e.g.
things: {
13: true,
57: false
}
I would now like to convert the things object into an array which stores only the items which are true. E.g. I want to end up with an array of identifiers I can post to the server.
As this type will be used in multiple places I only want to have the conversion logic once, e.g. in the directive so have tried changing my Formly template to:
<np-checked-list-box ng-Model="model[options.key]"></np-checked-list-box>
I then injected the ngModelCtrl into the directive, adding a function to both the $formatters and $parsers. This doesn't work however as the functions are never called so I can't manipulate the values. I assume this is because the object it's self isn't changed, it's just has properties add or within it changed.
Is what I'm trying to do possible and if so what do I need to change to make it work?
If it's not possible is there a way to change my model bindings to do as I've described?

FYI for anyone who comes across this in the end I done the following:
Modified the model of my testing controller to:
vm.model = {
things: []
}
E.g. turnings things from an object into an array.
Modified the HTML of the 'np-checked-list-box' directive to:
<div style="overflow-y:auto;height:{{to.height == undefinied ? 350 : to.height}}px">
<div class="checkbox" ng-repeat="item in items">
<input id="{{options.id}}_{{item.identifier}}"
type="checkbox"
value="{{item.checked}}"
data-ng-model="model[options.key]"
data-np-checked-list-box-item
data-np-checked-list-box-item-identifier="{{item.identifier}}">
<label for="{{options.id}}_{{item.identifier}}">{{item.text}}</label>
</div>
</div>
Notice the data-ng-model="model[options.key]" is binding directly to the array rather than an element in that array. I have also added another directive 'data-np-checked-list-box-item' and an attribute 'data-np-checked-list-box-item-identifier' it will use.
Created a new directive 'data-np-checked-list-box-item':
angular.module('app.formly.checkedListBox').directive('npCheckedListBoxItem', function () {
return {
require: 'ngModel',
restrict: 'A',
scope: {
identifier: "#npCheckedListBoxItemIdentifier"
},
link: function (scope, element, attr, ngModelCtrl) {
var model = [];
ngModelCtrl.$formatters.push(function (modelValue) {
model = modelValue;
return model[scope.identifier] == true;
});
ngModelCtrl.$parsers.push(function (viewValue) {
if (viewValue) {
model.push(scope.identifier);
} else {
for (var i = model.length - 1; i >= 0; i--) {
if (model[i] == scope.identifier) {
model.splice(i, 1);
break;
}
}
}
return model;
});
}
}
});
The directive is just a wrapper around the array which on a per identifier basis (e.g. for each checkbox) will return 'true' if there is a matching identifier in the array or otherwise false.
When updating the model it will add the identifier to the array when the checkbox is checked or remove it when unchecked.
E.g. if the checkboxes for "Tom" and "Dick" where checked, my model would look like.
model: {
things: ["13", "57"]
}
It is storing the entries as strings but for my purposes this is fine.

Related

AngularJS 1.6: Directive inside Directive Template works only first time

I hava a directive "avatar", which takes a 'user' object and displays an image of the user.
This works fine.
Now I have another directive 'user', which displays the name of given 'user' object and includes withing its template the directive.
This works.. THE FIRST TIME.
when I update the 'user' object, only the name changes, the image (avatar) does NOT change.
My Problem : How can I make it work ?
Avatar directive :
(link function : if user object does have a 'ext' property, it calculates an image path ('src'), otherwise it displays a standard template.jpeg)
directive('avatarSmall', function() {
return {
restrict: "E",
replace: true,
templateUrl: "scripts/directives/avatar/small.html",
link: function(scope, element, attrs) {
var r = "images/avatars/";
var ext = scope.user.ext;
r += ext != undefined && ext.length >= 3 ? scope.user.ID + "." + ext : "template.jpeg";
scope.src = r;
},
scope: {
user: '='
}
}
})
avatar template :
<div class="circle-wrapper-small">
<img class="circle-img-small"
ng-src="{{src}}">
</div>
user directive :
directive('user', function($compile) {
return {
restrict: "E",
replace: true,
templateUrl: "scripts/directives/user/template.html",
scope: {
user: '='
}
}
})
user template :
<div>
<div class="media-left">
<avatar-small user="user" />
</div>
<div class="media-body">
<h4 class="media-heading">{{user.name}} {{user.surname}}</h4>
</h5>
</div>
</div>
Because your avatar directive's code executes only ones, on directive init. If you want to update changes, you should $broadcast event to your directive and execute that code on $broadcast event.
For more info about $emit, $broadcast and $on event you can look through this post: Usage of $broadcast(), $emit() And $on() in AngularJS
Something like this:
Parent controller
$scope.user = {
// object properties
}
// watch "$scope.user" for changes
$scope.$watch('user', function(newVal, oldVal){
if(newVal !== oldVal) {
// send new "$scope.user" to your directive
$scope.$broadcast('userObjChanged', $scope.user);
}
}, true);
And inside your directive
// catch event from parent's controller with new user object
$scope.$on('userObjChanged', function(event, data) {
console.log(data); // here is the new user object from parent's scope
})

AngularJS directive two way binding for a configurable number of variables

When creating a directive, while defining isolate scope with two way binding using = is there any way that I can bind an array of scope variables. ie. if in my controller I have objects defined like $scope.one, $scope.two etc. and there can be any number of those - I want the directive to be able to handle a configurable number of them. How could I do that?
I can't do this, since another controller that uses the directive may have ten, so I want it to be flexible:
.directive("example", function () {
return {
scope: {
one: "=",
two: "=",
three: "="
},
...
Off course it is:
.directive('example', function() {
return {
scope: {
config: '='
},
link: function(scope) {
var firstOption = scope.config[0];
var secondOption = scope.config[1];
//...
}
}
}
The array options would have to be stored at a fixed index, so it would be less readable than passing a config object
.directive('example', function() {
return {
scope: {
config: '='
},
link: function(scope) {
var firstOption = scope.config.firstOption;
var secondOption = scope.config.secondOption;
//...
}
}
}

Update an angular treeview directive when a new object is added to the collection

I have a treeview directive credit to http://codepen.io/bachly/pen/KwWrzG for being my starting block. that I am trying to update when I add objects to the collection. I can update the object and insert the new objects but the treeview directive is never called once the $scoped item is updated.
Ultimately the data used will come from a service at this point I am just testing with mock data.
The original collection looks like this
$scope.myList = {
children: [
{
name: "Event",
children: [
{
name: "Event Date",
parent:"Event",
children: [
{
name: "2008",
filterType: '_eventStartDate',
parent: 'Event'
},
{
name: "2009",
filterType: '_eventStartDate',
parent: 'Event'
}
]
},
{
name: "Event Attendee",
parent: "Event",
children: [
{
name: "Person 1",
filterType: '_eventAttenddeeName',
parent: 'Event Attendee'
},
{
name: "Person 2",
filterType: '_eventAttenddeeName',
parent: 'Event Attendee'
}
]
}
]
}]
};
var TheOtherCollection = {
children: [
{
name: "A New Event",
children: [
{
name: "The Other Date",
parent: " A New Event",
children: [
{
name: "2010",
FilterType: '_eventStartDate',
Parent: '_event'
},
{
name: "2011",
FilterType: '_eventStartDate',
Parent: '_event'
}
]
}
]
}]
};
This generates a tree view with checkboxes using the following directive and html
app.directive('tree', function () {
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&'
},
controller: 'treeController',
template: '<ul><branch ng-repeat="c in t.children track by $index" src="c" filter="doSomething(object, isSelected)"></branch></ul>'
};
});
app.directive('branch', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&',
checked: '=ngModel'
},
template: '<li><input type="checkbox" ng-click="innerCall()" ng-model="b.$$hashKey" ng-change="stateChanged(b.$$hashKey)" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
var clicked = '';
var hasChildren = angular.isArray(scope.b.children);
scope.visible = hasChildren;
if (hasChildren) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (hasChildren) {
element.toggleClass('collapsed');
}
});
scope.stateChanged = function(b) {
clicked = b;
};
scope.innerCall = function() {
scope.filter({ object: scope.b, isSelected: clicked });
};
}
};
});
And then the html
<div ng-controller="treeController">
<tree src="myList" iobj="object" filter="doSomething(object, isSelected)"></tree>
<a ng-click="clicked()"> link</a>
</div>
When a checkbox is clicked the new collection is added to the existing one using lodashjs
ng-click event
$scope.doSomething = function (object, isSelected) {
if (isSelected) {
var item = object;
console.log(item);
nestAssociation(object, $scope.myList, TheOtherCollection);
}
}
which creates the new array and adds it within the children array
function nestAssociation(node, oldCollection, newAggregates) {
// var item = fn(oldCollection, node.parent);
var updatedArray = _.concat(oldCollection.children, newAggregates);
console.log(updatedArray);
if (updatedArray != null)
updateMyList(updatedArray);
}
I can see in the output I have a new object but I can't get the treeview to update. I have tried within the directive to add a $compile(element) on the click event in the directive but since the array is not built yet nothing changes.
Do I need to add a $watch to this directive and if so where or is there some other way I can get the directive to re-render and display the new nested collection?
Update
Base on some of the feedback and questions here is a little more detail around the question. The issue I am seeing is not in the directive as far as moving data around the issue is I cannot get the treeview to re-render once an array is added to the existing model.
The following link is a working plunker that shows the project as it currently works.
Running chrome dev tools I can see in the output the model is updated after a checkbox is selected
While I see the object is updated, the directive never updates to show the new array added to the object. This is the part that I need help understanding.
thanks in advance
You pass the function to the inner directives (which is the best practice), but you have access to scope.filter. Not doSomethingFunction. This one is undefined there.
filter="doSomething(object, isSelected)"
=>
filter="filter(object, isSelected)"
app.directive('tree', function () {
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&'
},
controller: 'treeController',
template: '<ul>
<branch ng-repeat="c in t.children track by $index"
src="c" filter="filter(object, isSelected)">
</branch>
</ul>'
};
});
Next :
You can never access $$ variables in angularJS, because they are private. Maybe you should make one from your DB..., but the $$hashkey seems a easy solution though.
checked attribute might throw an error, because ngModel does not exist on your tree directive template. (put at least a ? before)
A checkbox can not have as model a $$hashkey.
Ng-change and ng-click will always be called at the same time, use the simplest one.
app.directive('branch', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&'
},
template: '<li><input type="checkbox" ng-click="innerCall(b.$$hashKey)" ng-model="isChecked" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
scope.isChecked = false;
var hasChildren = angular.isArray(scope.b.children);
scope.visible = hasChildren;
if (hasChildren) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (hasChildren) {
element.toggleClass('collapsed');
}
});
scope.innerCall = function(hash) {
if(scope.isChecked){
scope.filter({ object: scope.b, isSelected: hash });
}
};
}
};
});
UPDATE
You have the same treeController in your tree directive and in your index.html view.
This is what causes the view not to update!
I deleted the one in your directive, otherwise you'll have a controller for each child.
You saw the good console.log message in your controller, but it was in a controller for ONE directive.
You were not accessing the controller of the index.html.
Then I fixed the filter function communication between childs :
You forgot to communicate the filter function when you append new tree's :
element.append('<tree src="b" filter="filter({ object: object, isSelected: isSelected })"></tree>');
Also, in your parent directive template, you also need the hash to send parameters to the function :
filter="filter({ object: object, isSelected: isSelected })"
I edited your Plunker HERE without changing the code with the above comments I made.
(I'm not sure what you write is not what you want and because you did not comment I rather not change it so you still undertand your code fast)
But the view is updating now!
I think a little debug with what you want and the comments above should be enough.
EDIT 2
You forgot to return an object with the property chrilden. You returned an array, which caused the problem.
function updateMyList(data) {
var transformed = { children : data };
$scope.myList = transformed;
}
Here is a working PLUNKER.
Try $scope.$apply() after adding

Dynamically including directive in an AngularJS partial

I have a list of directives (normally form fields and custom form controls). Now I will get the list of directive names from backend to use to build a form.
It basically creates a dynamic form where I don't know what all form fields are in the form (it depends on the JSON config file I get from the backend).
Sample JSON:
field1 : {
type: 'text',
directive : 'directive1'
},
field2: {
type : 'dropdown',
directive : 'dropdown-directive'
}
Can I do something similar in AngularJS, and if possible, how?
Use the $compile service against the scope. This will allow you to compile angular code which can be appended to a container.
See jsfiddle: http://jsfiddle.net/p8jjZ/1/
HTML:
<div ng-app="myApp" ng-controller="MainController">
<div custom-elements="myData.elements"></div>
<p>{{user}}</p>
</div>
JavaScript:
var mod = angular.module("myApp", []);
mod.controller("MainController", function ($scope) {
$scope.myData = {};
$scope.myData.elements = {
field1 :{ type: 'text', directive : 'directive1' },
field2: { type : 'dropdown', directive : 'dropdown-directive' }
};
});
mod.directive("customElements", function ($compile) {
return {
restrict: "A",
scope: {
customElements: "="
},
link: function (scope, element, attrs) {
var prop,
elems = scope.customElements,
currElem,
compiled;
for (prop in elems) {
currElem = elems[prop];
console.log("Working on " + prop);
//compile input against parent scope. Assuming directives are attributes, but adapt to your scenario:
compiled = $compile('<div ' + currElem.directive + '></div>')(scope.$parent);
//append this to customElements
element.append(compiled);
}
}
}
});
mod.directive("directive1", function () {
return {
restrict: "A",
template: '<div>Whoa! I am directive 1<br><input type="text" ng-model="user.name"></div>'
}
});
mod.directive("dropdownDirective", function () {
return {
restrict: "A",
template: '<div>I am another directive<br><select ng-model="user.color"><option value="blue">blue</option><option value="green">Green</option></div>'
}
});
The customElement directive just creates the directive as if it were an attribute on an element. This is a very simple example, but should get you started on what you are looking to do where you can update the logic that builds the elements/directive accordingly.

Angularjs: How to update parent scope in directive without using isolated scope when the attribute is passed in within ngRepeat

I have a simple angularjs directive that uses JQuery to convert a template to a draggable dialog
var myApp = angular.module("myApp", []);
myApp.controller('myCtrl', function ($scope) {
$scope.tasks = [{
name: 'learn angular',
show: false
}, {
name: 'build an angular app',
show: false
}];
$scope.showBox = function (taskname) {
for (var i = 0; i < $scope.tasks.length; i++) {
if ($scope.tasks[i].name === taskname) {
$scope.tasks[i].show = !$scope.tasks[i].show;
}
}
}
});
myApp.directive("draggableDialog", function () {
return {
template: 'task: {{task.name}}',
link: function (scope, element, attrs) {
element.dialog({
title : "My Dialog",
autoOpen: false
});
element.bind("dialogclose", function () {
if (!scope.$$phase) {
scope.$apply(function () {
scope[attrs.draggableDialog] = false; //here is the problem
});
}
});
scope.$watch(attrs.draggableDialog, function (v) {
if (v) {
element.dialog("open");
} else {
element.dialog("close");
}
});
}
}
});
I am using this directive in a ngRepeat
<div>
<h2>Draggable Dialog</h2>
<div ng-controller="myCtrl">
<ul class="unstyled">
<li ng-repeat="task in tasks">
<button ng-click="showBox(task.name)">show {{task.name}}</button>{{task.show}}
<div draggable-dialog="task.show">test</div>
</li>
</ul>
</div>
</div>
Refer to this fiddle: http://jsfiddle.net/tianhai/BEtPk/#base
When the user manually close the dialog, I can detect the event and I want to set $scope.task[i].show in myCtrl to false. How can I do it? I am not able to use isolated scope two way binding as I am using this directive together with another directive also taking in $scope.task.
You have attrs.draggableDialog set to "task.show" so when you do
scope[attrs.draggableDialog] = false you end up with a element attached to scope that you could access with scope['task.show'] which is different than scope['task']['show'] or scope.task.show
To generically set a parent variable to false you need to eval a string containing the assignment. For you it would look like this:
scope.$eval(attrs.draggableDialog + ' = false;');
Hope this helped

Categories

Resources