Add a wrapper around a fieldGroup - angularjs

I have create the following jsbin
Notice how the fieldGroup does not display because of the wrapper setting. If you remove/comment out the wrapper then the fields properly display.
Ultimately, I am pushing objects into this fieldGroup from a service call. I want each of these items within the group to be a <li> to the overall <ul>. So I wrapped each individual field with the <li> wrapper and I was planning on wrapping the entire fieldGroup with the <ul> wrapper. But Formly doesn't seem to like this approach.
What am I doing wrong? Or is there a better way of making this list?

Unfortunately fieldGroup doesn't support wrappers, and adding them would mean adding complexity to angular-formly.
Fortunately there's a simple way to accomplish the same results using custom templates:
app.config(function (formlyConfigProvider) {
// set templates here
formlyConfigProvider.setType({
name: 'nested',
template: '<formly-form model="model[options.key]" fields="options.data.fields"></formly-form>'
});
formlyConfigProvider.setWrapper({
name: 'panel',
types: ['nested'],
templateUrl: 'panel.html'
});
});
See full example: http://angular-formly.com/#/example/other/nested-formly-forms

I could be mistaken, but are you trying to dynamically create a list based on data that is pulled in from a service?
If you are...
Create a custom formly type, that has a template with an ng-repeat on the li, within the ul. You can then put a controller on the type and pass in your service to iterate on.
Here is an example of what I am thinking:
formlyConfig.setType({
name: 'myList',
templateUrl: 'myList.view.html',
controller: ['ListService', function(ListService) {
var vm = this;
ListService.getList().then(function(list) {
vm.list = list.data
vm.specific = list.data.specificItems
}
}]
});
<div type="text/ng-template", id="myList.view.html">
<ul>
<li ng-repeat="item for item in vm.list">{{item.name}}</li>
</ul>
<ul>
<li ng-repeat="item for item in vm.specific">{{item.name}}</li>
</ul>`
</div>`

Related

Expose element of parent component to child

I have a main component that handles the toolbar and sidnav of my angular application. I would like to make a div inside the toolbar available to child components (and controllers) to customize so that they can do things like change the toolbar title text and add contextual buttons. This feels sort of like the opposite of transposition where a parent component can customize part of a child component (e.g. a menu component customizing the content of a button). One option would be to have the toolbar managed by a service, but even then I can't think of a great way to customize the content of the toolbar without doing a decent amount of javascript that builds up dom elements (one of the things I always try to avoid in angular).
In Angular 1.6.x components use isolate scope only:
Components only control their own View and Data: Components should
never modify any data or DOM that is out of their own scope. Normally,
in AngularJS it is possible to modify data anywhere in the application
through scope inheritance and watches. This is practical, but can also
lead to problems when it is not clear which part of the application is
responsible for modifying the data. That is why component directives
use an isolate scope, so a whole class of scope manipulation is not
possible.
So, to make this work you would need to use a directive instead of a component. The div you want to include would need to be a directive itself to be able to alter the parent scope of the toolbar directive. What you would be doing is transcluding one directive inside another, and using shared scope to alter the parent scope.
This article is a really good resource for what you are trying to accomplish. I would start here: https://www.airpair.com/angularjs/posts/transclusion-template-scope-in-angular-directives
I have altered the example Codepen from that article to show you how it might work: http://codepen.io/jdoyle/pen/aJQpYo
If you select items in the list you can see that the header name changes to whatever is selected.
angular.module("ot-components", [])
.controller("AppController",($scope)=> {
//Normally, this data would be wrapped in a service. For example only.
$scope.header = "Marketing";
$scope.areas = {
list: [
"Floorplan",
"Combinations",
"Schedule",
"Publish"
],
current: "Floorplan"
};
})
.directive("otList", ()=> {
return {
scope: false, // this is one of the major changes
template:
`<ul class="ot-list">
<li class="ot-list--item"
ng-repeat="item in items"
ng-bind="item"
ng-class="{'ot-selected': item === selected}"
ng-click="selectItem(item)">
</li>
</ul>`,
link: (scope, elem, attrs) => {
scope.items = JSON.parse(attrs.items);
scope.selected = attrs.selected;
scope.selectItem = (item) => {
scope.selected = item;
scope.$parent.header = item; // this is the other major change
};
}
};
})
.directive("otSite", ()=> {
return {
scope: true, // another major change
transclude: true,
template:
`<div class="ot-site">
<div class="ot-site--head">
<img class="ot-site--logo" src="//guestcenter.opentable.com/Content/img/icons/icon/2x/ot-logo-2x.png">
<h1>{{header}}</h1>
</div>
<div class="ot-site--menu">
</div>
<div class="ot-site--body" ng-transclude>
</div>
<div class="ot-site--foot">
© 2015 OpenTable, Inc.
</div>
</div>`
};
});

Delimiting some sibling nodes without additional element

What is the equivalent angularjs template for the following handlebar template? Is there any way to achieve same result without wrapping the if block with another tag?
(foo is false)
<ul>
<li>a</li>
{{if foo}}
<li>b</li>
…
<li>c</li>
{{/if}}
<li>d</li>
</ul>
The rendered template should be exactly:
<ul>
<li>a</li>
<li>d</li>
</ul>
ng-if with one time binding(if you are in version 1.3.x else resort to some other libraries like bindonce to avoid any unnecessary watches) might be more appropriate for you. But ideally it is clearly unclear because you can solve this with many ways in angular. It does not even has to get to the view, you could just filter it out from the controller itself while setting up the view model which is used to repeat (ng-repeat) the lis. ng-show can also be used if you are trying to show and hide them. Difference between ng-if and ng-show/ng-hide is that ng-if removes the element completely from dom (and it cannot be animated with nganimate). ng-show just sets the css property display:none if condition set is false.
<ul>
<li>a</li>
<li ng-if="::foo">b</li><!-- Using :: for one time binding V1.3.x so no more watchers -->
<li ng-if="::foo">c</li>
<li>d</li>
</ul>
Update based on the comment that OP is looking for "a block statement to show/hide a bunch of elements together without adding a container tag".
Angular is not just a templating library like handlebars. So first thing before providing any specific answer is to recommend to learn how angular works. It is much more than a templating engine, it binds data to DOM that is already rendered and view is more of a reflection of the view model/model built from the controller. So in your case, as i explained earlier you just have to filter out the data based on a specific condition. Take a look at ng-repeat, event DOM filters that can be used with ng-repeat. So in short looking for a a block statement to show/hide a bunch of elements together without adding a container tag in angular (just what you would in handlebars) is thinking in wrong direction in my opinion. A possible solution for you can as well just be to identify when foo becomes true do not event provide those items (to be filtered out) to be rendered to view (or works case use filters in the view). And adding a block statement can just result in an invalid html in your case and browser will just strip it off before even angular has a chance to process it (unlike handlerbars where you transform your template to html before even rendering).
Here is one possible, better way (Using view filters are bad if filtering is one time, if it is just one time do the filtering in the controller) to do this in my opinion.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.items = [{
name: 'a',
hideWhenFoo: false
}, {
name: 'b',
hideWhenFoo: false
}, {
name: 'c',
hideWhenFoo: true
}, {
name: 'd',
hideWhenFoo: true
}, {
name: 'e',
hideWhenFoo: true
}, {
name: 'f',
hideWhenFoo: false
}, {
name: 'g',
hideWhenFoo: false
}];
$scope.foo = true; // due to some condition
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<ul>
<li ng-repeat="item in items | filter:{hideWhenFoo:!foo}">{{item.name}}</li>
</ul>
</div>
The following works, similar to ng-repeat-start and ng-repeat-end. However, I did not find it on the docs.
<ul>
<li>a</li>
<li ng-if-start='foo'>b</li>
…
<li ng-if-end>c</li>
<li>d</li>
</ul>

What is the "right" way in Angularjs of doing "master-detail" inter directive communication

I have a directive that displays a list of "master" items and when the user clicks on one of these items I want any "details" directives on the page (there could be more than one) to be updated with the details of the currently selected "master" item.
Currently I'm using id and href attributes as a way for a "details" directive to find its corresponding master directive. But my impression is that this is not the angular way, so if it's not, what would be a better solution?
I appreciate that typically when the issue of inter-communication between directives is raised then the obvious solutions are either to use require: "^master-directive" or to use a service, but in this case the directives are not in the same hierarchy and I don't think using a service is appropriate, as it would make the solution more complicated.
This is some illustrative code showing what I'm doing currently.
<div>
<master-list id="master1"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
In the master-list directive when an item is selected I set an attribute to indicate the currently selected master item:
attrs.$set('masterListItemId',item.id);
In the details-item directive's link function I do:
if (attrs.href) {
var id = attrs.href.split('#')[1];
var masterList = angular.element(document.getElementById(id));
if (masterList) {
var ctrl = masterList.controller('masterList');
ctrl.attrs().$observe('masterListItemId',function(value) {
attrs.$set('detailItemId',value);
});
}
}
attrs.$observe('detailItemId',function(id) {
// detail id changed so refresh
});
One aspect that put me off from using a service for inter-directive communication was that it is possible (in my situation) to have multiple 'masterList' elements on the same page and if these were logically related to the same service, the service would end up managing the selection state of multiple masterList elements. If you then consider each masterList element had an associated detailItem how are the right detailItem elements updated to reflect the state of its associated masterList?
<div>
<master-list id="master1"></master-list>
</div>
<div>
<master-list id="master2"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
<div>
<details-item href="#master2" ></details-item>
</div>
Finally I was trying to use directives, rather than using controller code (as has been sensibly suggested) as I'd really like the relationship between a masterList and its associated detailItems to be 'declared' in the html, rather than javascript, so it is obvious how the elements relate to each other by looking at the html alone.
This is particularly important as I have users that have sufficient knowledge to create a html ui using directives, but understanding javascript is a step too far.
Is there a better way of achieving the same thing that is more aligned with the angular way of doing things?
I think I would use a service for this. The service would hold the details data you care about, so it would look something like this.
In your master-list template, you might have something like a list of items:
<ul>
<li ng-repeat"item in items"><a ng-click="select(item)">{{item.name}}</a></li>
</ul>
...or similar.
Then in your directives, you would have (partial code only)
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick(item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.item;
}
};
})
And then use data in your details template:
<div>Details for {{data.name}}</div>
<ul>
<li ng-repeat="detail in data.details">{{detail.description}}</li>
</ul>
Or something like that.
I would not use id or href, instead use a service to retrieve, save and pass the info.
EDIT:
Here is a jsfiddle that does it between 2 controllers but a directive would be the same idea
http://jsfiddle.net/u3u5kte7/
EDIT:
If you want to have multiple masters and details, leave the templates unchanged, but change your directive controllers and services as follows:
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick($scope.listId,item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.get($scope.listId).item;
}
};
})
.factory('DetailsService',function(){
var data = {};
return {
pick: function(id,item) {
data[id] = data[id] || {item:{}};
// set data[id].item to whatever you want here
},
get: function(id) {
data[id] = data[id] || {item:{}};
return data[id];
}
};
})
I would opt for a different approach altogether without directives. Directives are ideal for DOM manipulation. But in this case I would stick to using just the template and a controller that manages all the data and get rid of the directives. Use ng-repeat to repeat the items
Check out this fiddle for an example of this: http://jsfiddle.net/wbrand/2xrne4k3
template:
<div ng-controller="ItemController as ic">
Masterlist:
<ul><li ng-repeat="item in ic.items" ng-click="ic.selected($index)">{{item.prop1}}</li></ul>
Detaillist:
<ul><li ng-repeat="item in ic.items" >
{{item.prop1}}
<span ng-if="item.selected">SELECTED!</span>
</li></ul>
</div>
controller:
angular.module('app',[]).controller('ItemController',function(){
this.items = [{prop1:'some value'},{prop1:'some other value'}]
this.selectedItemIndex;
this.selected = function(index){
this.items.forEach(function(item){
item.selected = false;
})
this.items[index].selected = true
}
})

Conditionally apply hasDropdown directive on ng-repeated element

I'm working on a project where I use both angularJS and foundation, so I'm making use of the Angular Foundation project to get all the javascript parts of foundation working. I just upgraded from 0.2.2 to 0.3.1, causing a problem in the top bar directive.
Before, I could use a class has-dropdown to indicate a "top-bar" menu item that has a dropdown in it. Since the menu items are taken from a list and only some have an actual dropdown, I would use the following code:
<li ng-repeat="item in ctrl.items" class="{{item.subItems.length > 0 ? 'has-dropdown' : ''}}">
However, the latest version requires an attribute of has-dropdown instead of the class. I tried several solutions to include this attribute conditionally, but none seem to work:
<li ng-repeat="item in ctrl.items" has-dropdown="{{item.subItems.length > 0}}">
This gives me a true or false value, but in both cases the directive is actually active. Same goes for using ng-attr-has-dropdown.
this answer uses a method of conditionally applying one or the other element, one with and one without the directive attribute. That doesn't work if the same element is the one holding the ng-repeat so i can't think of any way to make that work for my code example.
this answer I do not understand. Is this applicable to me? If so, roughly how would this work? Due to the setup of the project I've written a couple of controllers and services so far but I have hardly any experience with custom directives so far.
So in short, is this possible, and how?
As per this answer, from Angular>=1.3 you can use ngAttr to achieve this (docs):
If any expression in the interpolated string results in undefined, the
attribute is removed and not added to the element.
So, for example:
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
angular.module('app', []).controller('testCtrl', ['$scope',
function ($scope) {
$scope.ctrl = {
items: [{
subItems: [1,2,3,4], name: 'Item 1'
},{
subItems: [], name: 'Item 2'
},{
subItems: [1,2,3,4], name: 'Item 3'
}]
};
}
]);
<div ng-app="app">
<ul ng-controller="testCtrl">
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
{{item.name}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
Ok, I made a directive. All <li> will need an initial attr of:
is-drop-down="{{item.subItems.length > 0}}"
Then the directive checks that value and for somereason its returning true as a string. Perhaps some onc can shed some light on that
app.directive('isDropDown', function () {
return {
link: function (scope, el, attrs) {
if (attrs.isDropDown == 'true')
{
return el.attr('has-dropdown', true); //true or whatever this value needs to be
}
}
};
});
http://jsfiddle.net/1qyxrcd3/
If you inspect test2 you will see it has a has-dropdown attribute. There is probably a cleaner solution, but this is all I know. I'm still new to angular.
edit I noticed a couple extra commas in my example json data..take note, still works, but they shouldn't be there.

Read existing data from DOM into model, ng-model for static data?

I'm currently playing around with rewriting the functionality of an existing page using Angular. The gist of it is that I have a plain HTML page with a list of stuff, like this:
<ul>
<li class="item">
<h1>Foo</h1>
<ul class="categories">
<li class="category">Bar</li>
</ul>
</li>
...
</ul>
This is augmented by some Javascript which parses this data once and adds a dynamic category filter menu to the page. I.e. it extracts all li.category elements and displays a menu with them, and clicking on one of these categories filters the item list to display only items with the chosen category.
I've replicated the basics of that in Angular with a lot less code than I had before. However, I'm still doing a lot of jQuery traversing of the .item elements to build that initial list of categories:
myApp.controller('MyController', function ($scope) {
$scope.categories = [];
angular.element('.item').each(function () {
angular.element(this).find('.categories .category').each(function () {
var category = this.textContent;
for (var i = 0, length = $scope.categories.length; i < length; i++) {
if ($scope.categories[i].name == category) {
$scope.categories[i].count++;
$scope.categories[i].items.push(this);
return;
}
}
$scope.categories.push({ name : category, count : 1, items : [this] });
});
});
});
This does not seem to be in the spirit of Angular, and I'd like to replace it with something like:
<ul>
<li class="item" ng-item>
<h1>Foo</h1>
<ul class="categories">
<li class="category" ng-category>Bar</li>
</ul>
</li>
...
</ul>
A directive should then be able to parse all ng-item/ng-category elements and add them to the model/scope once. Something like ng-model, but for static data.
I have virtually no experience with Angular, how can I accomplish this; or shouldn't I want to do something entirely different in the first place?
For creating your own ng-item and ng-category directives, I suggest that you can go through Creating Custom Directives part in Angular Offical Develop Guide:
http://docs.angularjs.org/guide/directive
It will tell you how to begin creating your directive from add directive to module like this:
.directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
Edit:
This two is also useful tutorial:
http://www.ng-newsletter.com/posts/directives.html
http://www.befundoo.com/university/tutorials/angularjs-directives-tutorial/
Edit2:
To answer your comment:
Do you have a concrete sample of how I'd write a directive that reads data from its element and modifies the controller's scope?
I thought that it has clear explanation in Angular Official Guide:
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
});
In this example:
restrict: 'E' : directive name match element name
So directive look like this:
<my-customer></my-customer>
scope: { customerInfo: '=info'}
<my-customer info="myInfo"></my-customer>
this will bind myInfo to scope just like this expression:
$scope.customerInfo = myInfo;
this is a concrete sample of how to read data from its element and modify the controller's scope.

Resources