Expose element of parent component to child - angularjs

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>`
};
});

Related

How to pass data between sibling components without using $scope?

I am making a component that contains 3 child components in this way:
<header-component>
<side-component>
<main-component>
The main component contains list of heroes.
The header component contains two buttons that are suppose to switch the view on the main component to list or grid view.
The problem I have now is passing data from the header-component to the main component. So when I click grid button the view on the main content should change to grid view , same for the row view.
How can the data be passed between child components in angular 1.5 ?
Component approach
I would suggest you to align with Angular 2 component approach and use inputs/outputs approach. If you do so, you will be able to easily migrate to Angular 2, because components will be conceptually identical (with difference only in syntax). So here is the way you do it.
So we basically want header and main components to share piece of state with header to be able to change it. There are several approaches we can use to make it work, but the simplest is to make use of intermediate parent controller property. So let's assume parent controller (or component) defines this view property you want to be used by both header (can read and modify) and main (can read) components.
Header component: input and output.
Here is how simple header component could look like:
.component('headerComponent', {
template: `
<h3>Header component</h3>
<a ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
<a ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
`,
controller: function() {
this.setView = function(view) {
this.view = view
this.onViewChange({$event: {view: view}})
}
},
bindings: {
view: '<',
onViewChange: '&'
}
})
The most important part here is bindings. With view: '<' we specify that header component will be able to read outer something and bind it as view property of the own controller. With onViewChange: '&' components defined outputs: the channel for notifying/updating outer world with whatever it needs. Header component will push some data through this channel, but it doesn't know what parent component will do with it, and it should not care.
So it means that header controller can be used something like
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
Main component: input.
Main component is simpler, it only needs to define input it accepts:
.component('mainComponent', {
template: `
<h4>Main component</h4>
Main view: {{ $ctrl.view }}
`,
bindings: {
view: '<'
}
})
Parent view
And finally it all wired together:
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>
Take a look and play with simple demo.
angular.module('demo', [])
.controller('RootController', function() {
this.view = 'table'
})
.component('headerComponent', {
template: `
<h3>Header component</h3>
<a class="btn btn-default btn-sm" ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
<a class="btn btn-default btn-sm" ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
`,
controller: function() {
this.setView = function(view) {
this.view = view
this.onViewChange({$event: {view: view}})
}
},
bindings: {
view: '<',
onViewChange: '&'
}
})
.component('mainComponent', {
template: `
<h4>Main component</h4>
Main view: {{ $ctrl.view }}
`,
bindings: {
view: '<'
}
})
<script src="https://code.angularjs.org/1.5.0/angular.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />
<div class="container" ng-app="demo" ng-controller="RootController as root">
<pre>Root view: {{ root.view }}</pre>
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>
</div>
Demo: http://plnkr.co/edit/ODuY5Mp9HhbqA31G4w3t?p=info
Here is a blog post I wrote covering component-based design in details: http://dfsq.info/site/read/angular-components-communication
Although the parent component approach (passing down data via attributes) is a perfect valid and yet good implementation, we can achieve the same thing in a simpler way using a store factory.
Basically, data is hold by the Store, which is referenced in both components scope, enabling reactive updates of the UI when the state changes.
Example:
angular
.module('YourApp')
// declare the "Store" or whatever name that make sense
// for you to call it (Model, State, etc.)
.factory('Store', () => {
// hold a local copy of the state, setting its defaults
const state = {
data: {
heroes: [],
viewType: 'grid'
}
};
// expose basic getter and setter methods
return {
get() {
return state.data;
},
set(data) {
Object.assign(state.data, data);
},
};
});
Then, in your components you should have something like:
angular
.module('YourApp')
.component('headerComponent', {
// inject the Store dependency
controller(Store) {
// get the store reference and bind it to the scope:
// now, every change made to the store data will
// automatically update your component UI
this.state = Store.get();
// ... your code
},
template: `
<div ng-show="$ctrl.state.viewType === 'grid'">...</div>
<div ng-show="$ctrl.state.viewType === 'row'">...</div>
...
`
})
.component('mainComponent', {
// same here, we need to inject the Store
controller(Store) {
// callback for the switch view button
this.switchViewType = (type) => {
// change the Store data:
// no need to notify or anything
Store.set({ viewType: type });
};
// ... your code
},
template: `
<button ng-click="$ctrl.switchViewType('grid')">Switch to grid</button>
<button ng-click="$ctrl.switchViewType('row')">Switch to row</button>
...
`
If you want to see a working example, check out this CodePen.
Doing so you can also enable the communication between 2 or N components. You just only have to:
inject the store dependency
make sure you link the store data to your component scope
like in the example above (<header-component>).
In the real world, a typical application needs to manage a lot of data so make more sense to logically split the data domains in some way. Following the same approach you can add more Store factories. For example, to manage the current logged user information plus an external resource (i.e. catalog) you can build a UserStore plus a CatalogStore -- alternatively UserModel and CatalogModel; those entities would also be good places to centralize things like communication with the back-end, add custom business logic, etc. Data management will be then sole responsibility of the Store factories.
Keep in mind that we're mutating the store data. Whilst this approach is dead simple and clear, it might not scale well because will produce side effects. If you want something more advanced (immutability, pure functions, single state tree, etc.) check out Redux, or if you finally want to switch to Angular 2 take a look at ngrx/store.
Hope this helps! :)
You don't have to do it the Angular 2 way because just in case
you would migrate sometimes... Do it if it make sense for you to do it.
Use custom events to achieve this.
you can pass message across your application using event dispatchers $emit(name, args); or $broadcast(name, args);
And you can listen for this events using method $on(name, listener);
Hope it helps
Ref:
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$emit
Example:
you can notify change like below from your header-component
$rootScope.$emit("menu-changed", "list");
And you can listen for the change in your main-component directive like
$rootScope.$on("menu-changed", function(evt, arg){
console.log(arg);
});

Add event handler to DOM element that is created after a $resource get (AngularJS)

I'm having a hard time enabling a Bootstrap's popover component to my dom elements.
I'm working with AngularJS and on my template, I am using the ng-repeat directive to create a gallery.
<div ng-repeat="phone in phones" >
<a class="thumb" href="#/phones/{{phone.id}}">
<img class="img-responsive phone_image" ng-src="{{phone.image_path}}" data-content="{{phone.text}}" rel="popover" data-placement="right" data-trigger="hover">
</a>
</div>
On my template controller, I'm fetching the phones data from a third party API and than injecting it to the scopes variable "phones", like so:
phoneControllers.controller('PhoneListCtrl', ['$scope', 'Phones',
function ($scope, Cards) {
// Phones is the service that queries the phone data to the API
Phones.query(function(data){
// Got a response, add received to the phones variable
$scope.phones = data;
// for each .card_image element,give it the popover property
$('.phone_image').popover({
'trigger': 'hover'
});
});
}]
);
My problem lies with the $('.phone_image').popover segment. My thought was that by doing it inside the query's callback function it would work, since that's when the ".phone_image" elements are created. However it doesn't.
I seem to be failing to understand exactly in what scope should I assign the .popover property. I know it works because if I do it on the developer tools console, after all page content has been loaded, it works properly. I just don't know where to call it in my code to begin with.
Thanks in advance
It's happening because you are manipulating the DOM inside a controller. You should not do this, as the documentation says:
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic. Putting any presentation logic into Controllers significantly affects its testability. Angular has databinding for most cases and directives to encapsulate manual DOM manipulation.
In other words, when you use an Angular controller, you're just delegating the DOM manipulation to Angular through $scope databinding.
If you would like to manipulate the DOM, you should rely on directives. In your case, you can create a phonePopover directive like this:
angular
.module('phone', [])
.directive('phonePopover', function() {
return {
restrict: 'A',
replace: false,
scope: {
phoneText: '=phonePopover'
},
link: function(scope, element, attr) {
element.popover({
'trigger': 'hover',
'placement': 'right',
'content': scope.phoneText
});
}
});
And apply it to your element as following:
<img data-phone-popover="{{phone.text}}" class="img-responsive phone_image" ng-src="{{phone.image_path}}">

Elegant burger menu directive

I have started to develop a burger module, consisting essentially in 2 parts :
a "burger-opener" button which opens the menu, most probably an attribute directive including a click event listener, dom and css agnostic
a "burger menu" element, most probably a directive benefiting from transclusion, letting the client decide what the menu contains for the sake of reusability. This basically provides a close button at the top of it, before the ng-transclude element.
There must be a tight relationship between those 2 elements in terms of functionality, i.e the button element will call "open" into the burger menu element.
The thing is, I have a constraint which is that the button and the menu do not have to be contained within each other. For example, one must be able to use the module like so
<ul burger-menu>
<li>Save</li>
<li>Load</li>
</ul>
<section id="container">
<a href="" burger-opener class="burgerOpen"><a>
</section>
This constraint seems to be auto-excluding directive to directive communication using the "require" syntax because this angularjs functionality supposes directives are self-contained. So unless I create a top level DOM controller containing my 2 elements... I'm stuck.
I have been using a brute force approach, that is to use a broadcast from the rootscope for the button to send the "open" message to the menu directive. It works like a charm but I am not satisfied with it.
One other approach would be to set an even on the button but I would take this as a failure for some weird reason. I'm probably wrong but I'm quite sure there is a more elegant way to connect those two elements using the AngularJS paradigm without using broadcast nor events.
Do you know it ? I guess basically I am asking how components such as ui bootstrap modal service actually work.
Here is what I came up to. This seems quick and reusable enough to me, let me know if you can create something better !
Basically, the burgerMenu directive shares its parent scope (scope:false or nothing, it's false by default) and sets an api within it using the 'controller as' syntax. Thus the button whose role is to open the menu has a clear click handler with burgerCtrl.openBurger().
Here is the burgerMenu directive :
angular.module('app')
.directive("burgerMenu", [function () {
return {
scope: false,
controller: function () {
var self = this;
this.openBurger = function () {
self.isOpen = true;
};
this.closeBurger = function () {
self.isOpen = false;
};
this.isOpen = false;
},
controllerAs: 'burgerCtrl',
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'js/app/burgerMenu/_burger.tpl.html'
}
}
]);
The template :
<section class="nav_bar" ng-class="{open:burgerCtrl.isOpen}">
<div class="nav_content" ng-show="burgerCtrl.isOpen">
<h1 ng-click="burgerCtrl.closeBurger();">X</h1>
<ng-transclude></ng-transclude>
</div>
</section>
Css (main idea) :
.nav_bar { position:fixed; }
.nav_bar.open { width: 240px; }
Usage :
<section id="header">
<div class="burger" ng-click="burgerCtrl.openBurger()"></div>
<h1>App title</h1>
</section>
<section data-burger-menu>
<ul id="menu">
<li>Save</li>
<li>Share</li>
<li>Load n°1</li>
</ul>
</section>

parent directive erasing child ng-repeat

I am learning angularjs through the process of taking an existing site that was built primarily with JQuery and trying to "angularize" it. I am having trouble reproducing the same functionality in angular.
Please see the following plunker.
http://plnkr.co/edit/n4cbcRviuzNsieVvr4Im?p=preview
I have a ul element with an angularjs directive called "scroller" as seen below.
<ul class="dropdown-menu-list scroller" scroller style="height: 250px">
<li data-ng-repeat="n in notifications">
<a href="#">
<span class="label label-success"><i class="icon-plus"></i></span>
{{n.summary}}
<span class="time">{{n.time}}</span>
</a>
</li>
</ul>
The scroller directive looks like this:
.directive('scroller', function () {
return {
priority: 0,
restrict: 'A',
scope: {
done: '&',
progress: '&'
},
link: function (scope, element, attrs) {
$('.scroller').each(function () {
var height;
if ($(this).attr("data-height")) {
height = $(this).attr("data-height");
} else {
height = $(this).css('height');
}
$(this).slimScroll({
size: '7px',
color: '#a1b2bd',
height: height,
disableFadeOut: true
});
});
}
};
What i want to happen is that the ng-repeat executes on the notifications array in the controller, producing a collection of li elements that exceed 250px therefore a slimscrollbar would be added. What actually happens is the result of the ng-repeat is not included in the final DOM. I believe the call in the parent scroller directive of $(this).slimScroll() is called after the ng-repeat executes and replaces the DOM. If i remove the scroller attribute, the li elements show up.
I am sure there is a strategy for this and am hoping the community can educate me on a better approach or alternate approach. thoughts? again the plunker is here.
http://plnkr.co/edit/n4cbcRviuzNsieVvr4Im?p=preview
Thanks,
Dan
The issue is actually your directive scope. You are using an explicit object as the scope, which means you are isolating the scope, which means the directive scope isn't inheriting from its parent anymore. So notifications from the parent controller is no longer reachable from the directive scope (and therefore any elements inside of its element).
If you remove this from your directive it should work:
scope: {
done: '&',
progress: '&'
}
I notice that you aren't using those attributes anyway so it shouldn't break any other functionality.
Look at the API docs http://docs.angularjs.org/guide/directive and look for isolate scope for more details.
An alternative to what you're trying to do would just be something like this
scope.$watch(attr.done, function(val) { //do something when the value changes })
Since I don't know your use case I can't say what the best solution would be.

How to create this custom control with AngularJS directive?

I'm a bit new to AngularJS and am trying to write a custom select control based on Zurb Foundation's custom select(see here: http://foundation.zurb.com/docs/components/custom-forms.html)
I know I need to use a directive for this but am not sure how to accomplish this.
It's going to have to be reusable and allow for the iterating of whatever array is passed in to it. A callback when the user selects the item from the dropdown list is probably needed.
Here is the markup for the custom Foundation dropdown list:
<select name="selectedUIC" style="display:none;"></select>
<div class="custom dropdown medium" style="background-color:red;">
Please select item
<ul ng-repeat="uic in uics">
<li class="custom-select" ng-click="selectUIC(uic.Name)">{{uic.Name}}</li>
</ul>
</div>
This works for now. I am able to populate the control from this page's Ctrl. However, as you can see, I'd have to do this every time I wanted to use a custom dropdown control.
Any ideas as to how I can turn this baby into a reusable directive?
Thanks for any help!
Chris
If you want to make your directives reusable not just on the same page, but across multiple AngularJS apps, then it's pretty handy to set them up in their own module and import that module as a dependency in your app.
I took Cuong Vo's plnkr above (so initial credit goes to him) and separated it out with this approach. Now this means that if you want to create a new directive, simply add it to reusableDirectives.js and all apps that already have ['reusableDirectives'] as a dependency, will be able to use that new directive without needing to add any extra js to that particular app.
I also moved the markup for the directive into it's own html template, as it's much easy to read, edit and maintain than having it directly inside the directive as a string.
Plnkr Demo
html
<zurb-select data-label="{{'Select an option'}}" data-options="names"
data-change-callback="callback(value)"></zurb-select>
app.js
// Add reusableDirectives as a dependency in your app
angular.module('angularjs-starter', ['reusableDirectives'])
.controller('MainCtrl', ['$scope', function($scope) {
$scope.names = [{name: 'Gavin'}, {name: 'Joseph'}, {name: 'Ken'}];
$scope.callback = function(name) {
alert(name);
};
}]);
reusableDirectives.js
angular.module('reusableDirectives', [])
.directive('zurbSelect', [function(){
return {
scope: {
label: '#', // optional
changeCallback: '&',
options: '='
},
restrict: 'E',
replace: true, // optional
templateUrl: 'zurb-select.html',
link: function(scope, element, attr) { }
};
}]);
zurb-select.html
<div class="row">
<div class="large-12 columns">
<label>{{label || 'Please select'}}</label>
<select data-ng-model="zurbOptions.name" data-ng-change="changeCallback({value: zurbOptions.name})"
data-ng-options="o.name as o.name for o in options">
</select>
</div>
</div>
Is something like this what you're looking for?
http://plnkr.co/edit/wUHmLP
In the above example you can pass in two attribute parameters to your custom zurbSelect directive. Options is a list of select option objects with a name attribute and clickCallback is the function available on the controller's scope that you want the directive to invoke when a user clicks on a section.
Notice there's no code in the link function (this is where the logic for your directive would generally go). All we're doing is wrapping a template so that it's reusable and accepts some parameters.
We created an isolated scope so the directive doesn't need to depend on parent scopes. We binded the isolated scope to the attribute parameters passed in. The '&' means bind to the expression on the parent scope calling this (in our case the callback function available in our controller) and the '=' means create a two way binding between the options attribute so when it changes in the outter scope, the change is reflected here and vice versa.
We're also restricting the usage of this directive to only elements (). You can set this to class, attributes, etc..
For more details the AngularJs directives guide is really good:
http://docs.angularjs.org/guide/directive
Hope this helps.

Resources