Passing a binding to transcluded scope in component - angularjs

In AngularJS 1.5, I want to pass a binding from a component into the (multi-slot) transcluded scope - for a reference in the template (in either one specific or all of them - no either way is fine).
This is for creating a generic custom-select list
// Component
.component('mySelect', {
bind: {
collection: '<'
},
transclude:{
header: 'mySelectHeader',
item: 'mySelectItem'
},
templateUrl: 'my-select-template',
controller: function(){
.....
}
});
...
// Component template
<script id="my-select-template" type="text/ng-template">
<ol>
<li ng-transclude="header"> </li>
<li ng-transclude="item"
ng-click="$ctrl.select($item)"
ng-repeat"$item in $ctrl.collection">
</li>
</ol>
</script>
...
// Example usage
<my-select collection="[{id: 1, name: "John"}, {id: 2, name: "Erik"}, ... ]>
<my-select-head></my-select-head>
<!-- Reference to $item from ng-repeate="" in component -->
<my-select-item>{{$item.id}}: {{$item.name}}</my-select-item>
</my-select>
Is this possible from a .component()? with custom-directives for the transclusion ?

In your parent component my-select keep a variable like "selectedItem"
In your child component my-select-item, require your parent component like below
require: {
mySelect: '^mySelect'
}
And in your my-select-item component's controller, to access your parent component
$onInit = () => {
this.mySelectedItem= this.mySelect.selectedItem; // to use it inside my-select-item component.
};
select($item) {
this.mySelect.selectedItem = $item; // to change the selectedItem value stored in parent component
}
So that the selected item is now accessible from
<my-select-item>{{selectedItem.id}}: {{selectedItem.name}}</my-select-item>

I ran into this problem as well, and building upon salih's answer, I came up with a solution (disclaimer--see bottom: I don't think this is necessarily the best approach to your problem). it involves creating a stubbed out component for use in the mySelect component, as follows:
.component('item', {
require: { mySelect: '^mySelect' },
bind: { item: '<' }
})
then, tweaking your template:
<script id="my-select-template" type="text/ng-template">
<ol>
<li ng-transclude="header"> </li>
<li ng-click="$ctrl.select($item)"
ng-repeat"$item in $ctrl.collection">
<item item="$item" ng-transclude="item"></item>
</li>
</ol>
</script>
this will mean there's always an item component with the value bound to it. now, you can use it as a require in a custom component:
.component('myItemComponent', {
require: {
itemCtrl: '^item',
}
template: '<span>{{$ctrl.item.id}}: {{$ctrl.item.name}}</span>',
controller: function() {
var ctrl = this;
ctrl.$onInit = function() {
ctrl.item = ctrl.itemCtrl.item;
}
}
});
and to use it:
<my-select collection="[{id: 1, name: "John"}, {id: 2, name: "Erik"}, ... ]>
<my-select-head></my-select-head>
<my-select-item>
<my-item-component></my-item-component>
</my-select-item>
</my-select>
after I implemented this, I actually decided to change my strategy; this might work for you as well. instead of using a transclude, I passed in a formatting function, i.e.:
.component('mySelect', {
bind: {
collection: '<',
customFormat: '&?'
},
transclude:{
header: 'mySelectHeader'
},
templateUrl: 'my-select-template',
controller: function(){
var ctrl = this;
ctrl.format = function(item) {
if(ctrl.customFormat) {
return customFormat({item: item});
} else {
//default
return item;
}
}
.....
}
});
then in the template, just use:
<script id="my-select-template" type="text/ng-template">
<ol>
<li ng-transclude="header"> </li>
<li ng-click="$ctrl.select($item)"
ng-repeat"$item in $ctrl.collection"
ng-bind="::$ctrl.format($item)">
</li>
</ol>
</script>
let me know if you have any feedback or questions!

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

angular js ng-class shows expression as class instead of processing it

I'm trying to make highlighted menu items by using angular js. I've read this question and tried implementing the anwser, but instead of angular evaluating the expression, it just shows it as the class name. I don't know what's going on.
I have the menu items listed as JSON, and the iterate trough it with ng-repeat. Once the list items are created, I want the angular to add a class of 'active', if the location url is the same as the link.href attribute of a menu item (it's a json attribute, not the html one).
Here's the relevant html:
<div class="header" ng-controller="NavbarController">
<ul>
<li ng-repeat="link in menu" ng-class="{ active: isActive({{ link.href }}) }"><a ng-href="{{ link.href }}">{{ link.item }}</a>
</li>
</ul>
</div>
and my controller:
.controller('NavbarController', function ($scope, $location) {
// navbar links
$scope.menu = [
{
item: 'PTC-Testers',
href: '#/PTC-Testers'
},
{
item: 'articles',
href: '#/articles'
},
{
item: 'PTC sites',
href: '#/sites'
},
{
item: 'account reviews',
href: '#/account_reviews'
},
{
item: 'forum',
href: '#/forum'
},
{
item: 'contact us',
href: '#/contact'
},
{
item: 'login',
href: '#/login'
}
]; // end $scope.menu
$scope.isActive = function (viewLocation) {
return viewLocation === $location.path();
};
});
This is the navbar part of a bigger project, and I tried only inserting the relevant code. If you need further info to understand the question properly, please let me know.
It should be ng-class="{'active' : isActive(link.href)}"
You didn't end the curly brace in ng-class and its better to put class name inside quotes

Get one item from a list in parent scope

I have the following Angular controllers:
function TagListController($scope) {
$scope.model = {
tags: ['tag 1', 'tag 2', 'tag 3' ],
template: 'list'
}
$scope.showTemplate = function (template) {
$scope.model.template = template;
};
}
function TagEditController($scope) {
$scope.tagToEdit = $scope.$parent.model ???
}
And I have the following HTML to show a list of tags and to edit a tag:
<ul ng-if="model.template == 'list'" ng-controller="TagListController">
<li data-ng-repeat="tag in model.tags">
<span data-ng-bind="tag.name"></span></br>
<a href="" data-ng-click="show('edit')"</a>
</li>
</ul>
<script type="text/ng-template" id="edit">
<div ng-controller="TagEditController">
Edit template
</div>
</script>
<div ng-if="model.template == 'edit'" ng-include="'edit'"></div>
Using model.template I am able to show the Edit template.
But how can I, in EditTagController, use the tag which Edit link was clicked?
I know it will be one of the items in $scope.$parent.model.tags.
I just don't know how to select it and the best way to select it.
This would be a good place to use a factory or service.
See this jsBin
You want to store common data inside of your factory instead of dealing with $parent or $rootScope.
You could create a factory like this:
function tagService() {
var observerCallbacks = [];
var currentTag;
function getCurrentTag() {
return currentTag;
}
function setCurrentTag(tag) {
currentTag = tag;
notifyObservers();
}
function notifyObservers() {
angular.forEach(observerCallbacks, function(callback) {
callback();
});
}
function registerObserverCallback(callback) {
this.observerCallbacks.push(callback);
notifyObservers();
}
return {
getCurrentTag: getCurrentTag,
setCurrentTag: setCurrentTag,
registerObserverCallback: registerObserverCallback,
notifyObservers: notifyObservers
};
}
Here, I'm also using the observer pattern to notify anybody who is listening for changes any time setCurrentTag is called. That way, if you change tags, both controllers know.
Your TagListController might look like:
function TagListController($scope, tagService) {
$scope.model = {
tags: ['tag 1', 'tag 2', 'tag 3' ],
template: 'list'
};
$scope.editTag = function(tag) {
tagService.setCurrentTag(tag);
$scope.model.template = 'edit';
};
}
And your TagEditController like:
function TagEditController($scope, tagService) {
tagService.registerObserverCallback(function() {
$scope.tagToEdit = tagService.getCurrentTag();
});
}
How about using Angular routing? You could set up a route like "yourapp/tags/{tag}" to your EditTagController and that way you will get the current tag as a parameter.
Something like this

How to render dynamic directives, functions and params in AngularJS

I have an object that I'm iterating over with ngRepeat. This object includes information on what kind of directives to render in the markup and also what values to give those directives. I haven't been able to do this type of dynamic rendering with Angular so far, it would really make the templates feel like reusable templates.
Controller
$scope.myFunk = function(obj) {
alert(obj.target);
};
$scope.fruits = [
{
label: 'Banana',
params: { target: 'ground' },
action: {
directive: 'ng-click',
value: $scope.myFunk
}
}
];
Template
<ul>
<li ng-repeat="fruit in fruits">
<!-- Expecting to see "ground" in an alert() when clicking -->
<a href {{fruit.action.directive}}="{{fruit.action.value}}{{(fruit.params)}}">
{{fruit.label}}
</a>
</li>
</ul>
Here's a plunker
I'm trying to get angular to evaluate something like this:
<li>
<a href ng-click="myFunk({ target: 'ground' })">Banana</a>
</li>
Usually I'd simply declare directives in the markup the general angular-way, but here I really need to have the flexibility of rendering arbitrary directives with individual values that may be functions with their own parameters.
How can I achieve this?
Does this fit the bill?
app.directive('arbitraryThing', function(){
return {
restrict: 'EA',
scope: {
params: '='
},
link: function(scope, element, attr)
{
element.on(scope.params.action.directive, function(event){
event.preventDefault();
scope.params.action.value({ target: scope.params.params.target})
})
}
}
})
http://plnkr.co/edit/slr9M0YK0JuzcMPWrMqr?p=preview

Menu and different submenu on click

I am trying to build a menu and a submenu in angular.
What I want to do is to have two arrays of objects
Menu
menu = [{name: 'Name1', link: '/link1'}, {name: 'Name2', link: '/link2'}]
submenu = [[{name: 'SubName1', link: '/Sublink1'}, {name: 'SubName1', link: '/sublink1'}],
[[{name: 'SubName2', link: '/Sublink2'}, {name: 'SubName2', link: '/sublink2'}]]
So when I click Name1 the first array of SubMenu will be selected and when clicking Name2 the second array will be selected.
How I can create two Directives one for the main menu and one for the second and be able to communicate between them on click. I have tried building this in a controller, I was able to select the submenu by using the $index, but the submenu can't be moved around as I like because it needs to be under the controller.
I finally managed to solve my problem here is the solution: http://jsfiddle.net/4kjjyL4s/4/
How can I improve my solution?
Don't reinvent the wheel :) UI router is a prepackaged solution that handles nested routing for you.
If you have a menu of items and you want to display another menu of items when one of the items is selected UI router does exactly that. https://github.com/angular-ui/ui-router
Can't give you the exact answer because information is lacking, but for example if you're using the directives with different menu items at other places in your app, I'd recommend to pass the menu array from controller (ng-controller, not directive's controller) through directive's scope.
Also, you're looking for kinda standard way for directives to communicate directly (in your case, communication between menu and submenu directive to notify the item selection change), use directive's controller. Here's a good tutorial.
https://thinkster.io/egghead/directive-to-directive-communication/
To communicate between controllers or directives, you should use services.
From the angular guide ( https://docs.angularjs.org/guide/services ):
Angular services are substitutable objects that are wired together using dependency injection (DI). You can use services to organize and share code across your app.
I checked the code you posted on jsfiddle ( http://jsfiddle.net/4kjjyL4s/4/ )and I tried to keep the most of it. Below are my changes in the JavaScript file ( please, read the comments in the code ).
var app = angular.module("app",[]);
app.controller('main', function(){});
// The service will be responsible for the shared objects and logic
app.service('MenuService', function () {
var list = [
{
name: "Menu1", link: "#menu1",
submenu: [
{ name: "Menu1Sub1", link: "#submenu1" },
{ name: "Menu1Sub2", link: "#submenu2" }
]
},
{
name: "Menu2", link: "#menu2",
submenu: [
{ name: "Menu2Sub1", link: "#submenu1" },
{ name: "Menu2Sub2", link: "#submenu2" }
]
}
];
var selected = [];
// methods and attributes published under the **this**
// keyword will be publicly available
this.getMenu = function () {
return list;
};
this.getSubmenu = function () {
return selected;
};
this.select = function ( menuItem ) {
// this does the *trick*!
// if we use the assignment operator here, we would replace the
// reference returned in the getSubmenu() method, so as the original
// reference did not change, angular's dirty checking would not detect it.
// using angular.copy() method, we are copying the contents of the
// selected submenu over the same reference returned by getSubmenu()
angular.copy( menuItem.submenu, selected );
};
});
// No $rootScope injection results in better re-usability. When you were
// relying in $rootScope sharing, both directives should live in the
// $rootScope, so if you add them inside a ng-controller created scope
// they would not work anymore
app.directive("menu", function() {
return {
restrict: "E",
// no need to isolate scope here, *scope:true* creates a new scope
// which inherits from the current scope
scope: true,
// with controllerAs (introduced in angular 1.2), you can skip
// polluting the scope injection.
controllerAs: "ctrl",
controller: function( MenuService ) {
this.list = MenuService.getMenu();
this.changeSub = function ( menuItem ) { MenuService.select( menuItem ); };
},
template: "<div ng-repeat='menu in ctrl.list'><button ng-click='ctrl.changeSub(menu)'>{{menu.name}}</button></div>"
};
});
app.directive("submenu", function() {
return {
restrict: "E",
scope: true,
controllerAs: "ctrl",
controller: function( MenuService ) {
this.sublist = MenuService.getSubmenu();
},
template: "<span ng-repeat='menu in ctrl.sublist'>{{menu.name}} | </span>aa"
};
});
And here is the updated HTML file, just to show both directives work now not directly inserted in the $rootScope
<div ng-app="app">
<div ng-controller="main">
<menu></menu>
<h1>Hello World!</h1>
<div class="main-content">
<submenu></submenu>
</div>
</div>
</div>
Hope it helps!
Try this Code:
function MyCtrl ($scope) {
$scope.subMenu = []; // default is false
$scope.toggleSubMenu = function (index) {
$scope.subMenu[index] = !$scope.subMenu[index];
};
}
HTML
<ul>
<li ng-class="{active: subMenu[0]}"> Name1
<ul>
<li>test</li>
<li>test</li>
<li>test</li>
</ul>
</li>
<li ng-class="{active: subMenu[1]}"> Name2
<ul>
<li>bar</li>
<li>bar</li>
<li>bar</li>
</ul>
</li>
</ul>
Also Check this

Resources