I am trying to learn about Angular components but am having trouble getting them to work in a nested component configuration. Basically what I want to do is to have a parent component that periodically updates a value. I then want the inner child component to bind to the value in the parent and trigger a call to $onChanges when the value changes.
I made a jsFiddle demonstrating what I'm trying to accomplish. The parent component seems to be working and displaying the updated value, but for some reason the child component doesn't render at all. Here is the code I am using to accomplish this:
let app = angular.module('app', []);
class ParentController {
constructor($interval) {
this.value = 0;
$interval(() => this.value++, 1000);
}
}
let ParentComponent = {
controller: ParentController,
template: `<div>parent value: {{$ctrl.value}}</div>`
};
app.component('parent', ParentComponent);
class ChildController {
$onChanges(changesObj) {
console.log(changesObj);
}
}
let ChildComponent = {
bindings: {
value: '='
},
controller: ChildController,
require: {
parent: '^^parent'
},
template: `<div>child value: {{$ctrl.value}}</div>`
};
app.component('child', ChildComponent);
And the HTML:
<div ng-app="app">
<parent>
<child value="$ctrl.parent.value"></child>
</parent>
</div>
Am I doing something wrong or is what I'm trying to accomplish not possible?
The thing about components is that anything inside of them typically gets replaced unless you use the 'ng-transclude' directive.
To get your child element to appear, you need the following changes:
let ParentComponent = {
controller: ParentController,
transclude: true,
template: `<div>parent value: {{$ctrl.value}}</div><div ng-transclude></div>`
};
To clarify, you add transclude: true to the component object as well as an element with the ng-transclude directive within the template. Whatever element has the ng-transclude directive will have its contents replaced with whatever is in the <parent> tag.
This will only get your child component to render, you still have another error in your HTML being that you're trying to use
<child value="$ctrl.parent.value"></child>
Which, in the JavaScript, is equivalent to $scope.$ctrl.parent.value which is undefined.
Just change your HTML to:
<div ng-app="app">
<parent>
<child></child>
</parent>
</div>
And the child component to:
let ChildComponent = {
controller: ChildController,
require: {
parent: '^^parent'
},
template: `<div>child value: {{$ctrl.parent.value}}</div>`
};
And then you're all good!
EDIT (from comment):
In order to trigger a update call whenever the parent value changes, you would need to replace your ChildController with the following:
let ChildController = function (scope) {
scope.$watch('$ctrl.parent.value', function (newValue, oldValue) {
console.log(newValue);
});
};
This adds a watch on the child controller that will call the function each time the parent.value changes.
i have made som modifications to your code.
there is a different way of achieving what you want to do.
here is my code I will try to explain how it should be done.
let app = angular.module('app', []);
class ParentController {
constructor($interval) {
this.value = 0;
$interval(() => this.value++, 1000);
}
}
let ParentComponent = {
controller: ParentController,
//here is the important edit. loading child component inside the parent component
template: `<div>
parent value: {{$ctrl.value}}
<child value="$ctrl.value"></child>
</div>`
};
app.component('parent', ParentComponent);
class ChildController {
$onChanges(changesObj) {
if(changesObj.hasOwnProperty('value')){
this.value=changesObj.value.currentValue //this referes to current scope.
}
}
}
let ChildComponent = {
bindings: {
value: '<'
},
controller: ChildController,
require: {
//parent: '^^parent' no need for this. we will use $onChanges
},
template: `<div>child value: {{$ctrl.value}}</div>`
};
app.component('child', ChildComponent);
<div ng-app="app"><parent></parent></div>
things I have done in this code sample :
I have included component inside the parent component template
because what you want to do is load child inside parent. so added child inside the parent template.
Removed Require in child component definition because it would make your child component dependent on the parent component. there is a different use case where you must use the parent. it's best you use bindings and $onChanges to update data in a child from a parent,
added some validations to the $onChanges data.
if(changesObj.hasOwnProperty('value')){
this.value=changesObj.value.currentValue
}
you need to understand that the change obj is a simple change Object. it has multiple properties like is first change(), current value, OldValue etc.
you must always validate what you want from here.
also i am setting it to this.value which is the current scope.
we are not directly using the parent scope or binding scope variable. i am creating a new scope object which you will use to display the data. it will be populated by the binding object.
overall this is happening
1.parent interval updates parents scope
2.this.value is passed to child as Bindings
3.$onChanges validates bindings and assigns it to local scope this.value
4.child template renders this.value in the UI
Related
Is there any way to specify the default value for an # binding of a component.
I've seen instruction on how to do it with directive: How to set a default value in an Angular Directive Scope?
But component does not support the compile function.
So, I have component like this:
{
name: 'myPad',
bindings : {layout: '#'}
}
I want to free users of my component from having to specify the value of the 'layout' attribute. So..., this:
<my-pad>...</my-pad>
instead of this:
<my-pad layout="column">...</my-pad>
And... this 'layout' attribute is supposed to be consumed by angular-material JS that 'm using, so it needs to be bound before the DOM is rendered (so the material JS can pick it up & add the corresponding classes to the element).
update, some screenshots to clarify the situation:
Component definition:
{
name : 'workspacePad',
config : {
templateUrl: 'src/workspace/components/pad/template.html',
controller : controller,
bindings : {
actions: '<', actionTriggered: '&', workspaces: '<', title: '#',
flex: '#', layout: '#'
},
transclude: {
'workspaceContent': '?workspaceContent'
}
}
}
Component usage:
<workspace-pad flex layout="column" title="Survey List" actions="$ctrl.actions"
action-triggered="$ctrl.performAction(action)">
<workspace-content>
<div flex style="padding-left: 20px; padding-right: 20px; ">
<p>test test</p>
</div>
</workspace-content>
</workspace-pad>
I want to make that "flex" and "layout" in the second screenshot (usage) optionals.
UPDATE
My "solution" to have this in the constructor of my component:
this.$postLink = function() {
$element.attr("flex", "100");
$element.attr("layout", "column");
$element.addClass("layout-column");
$element.addClass("flex-100");
}
I wish I didn't have to write those last 2 lines (addClass)... but well, since we don't have link and compile in component.... I think I should be happy with it for now.
First of there is great documentation for components Angularjs Components`. Also what you are doing I have done before and you can make it optional by either using it or checking it in the controller itself.
For example you keep the binding there, but in your controller you have something like.
var self = this;
// self.layout will be the value set by the binding.
self.$onInit = function() {
// here you can do a check for your self.layout and set a value if there is none
self.layout = self.layout || 'default value';
}
This should do the trick. If not there are other lifecycle hooks. But I have done this with my components and even used it in $onChanges which runs before $onInit and you can actually do a check for isFirstChange() in the $onChanges function, which I am pretty sure will only run once on the load. But have not tested that myself.
There other Lifecycle hooks you can take a look at.
Edit
That is interesting, since I have used it in this way before. You could be facing some other issue. Although here is an idea. What if you set the value saved to a var in the parent controller and pass it to the component with '<' instead of '#'. This way you are passing by reference instead of value and you could set a watch on something and change the var if there is nothing set for that var making it a default.
With angularjs components '#' are not watched by the component but with '<' any changes in the parent to this component will pass down to the component and be seen because of '<'. If you were to change '#' in the parent controller your component would not see this change because it is not apart of the onChanges object, only the '<' values are.
To set the value if the bound value is not set ask if the value is undefined or null in $onInit().
const ctrl = this;
ctrl.$onInit = $onInit;
function $onInit() {
if (angular.isUndefined(ctrl.layout) || ctrl.layout=== null)
ctrl.layout = 'column';
}
This works even if the value for layout would be false.
Defining the binding vars in constructor will just initiate the vars with your desired default values and after initialization the values are update with the binding.
//ES6
constructor(){
this.layout = 'column';
}
$onInit() {
// nothing here
}
you can use
$onChanges({layout}) {
if (! layout) { return; }
this.setupLayout(); ---> or do whatever you want to do
}
I'm in the process of eliminating the "scope soup" architecture of a legacy Angular 1.5 app following this guide: http://teropa.info/blog/2015/10/18/refactoring-angular-apps-to-components.html#replace-external-reference-with-bound-input
I'm trying to remove the reference to $rootscope.taskui, so I tried to add a binding to the component. Unfortunately, taskui is now undefined. The "component" is an Angular 1.5 component (which is just a normal directive under the hood). Am I doing something wrong?
If you replace "this.taskui" with "$rootscope.taskui" (correctly injected), method prints the taskui object just fine.
export default {
bindings: {
taskui: '='
},
controller,
templateUrl: "component.html"
};
Here's the controller code:
class Controller {
constructor() {
this.method = () => console.log(this.taskui)
}
}
The problem was a misunderstanding of angularjs scope. When using isolated scope, it is not enough to just bind a variable. You must also pass the value as an attribute. See solution #3 here: https://stackoverflow.com/a/17900556/555493
The code (using the original example) should be:
// component
export default {
bindings: {
taskui: '='
},
controller,
templateUrl: "component.html"
};
// parent template
<component taskui="taskui"></component>
am trying to pass a value from a parent-component to his nested child-component in angular 1.5
The value can be updated from the parents, but child cannot edit it, just show it. So is a one-way binding '<' right ?
And i cannot pass the child component right in the parent component declaration, because the parent component would have other uses too.
The point is my parent-component have common data stored, but them
children gonna use it in different ways.
And the parent-component gonna be used multiples times, with different
children, thats why i cannot pass the children inside parent
declaration. I need to bind the info, for auto updates purposes, when
parents updates the data, must be reflected by the children
HTML
<parent-component ng-transclude>
<child-component name="$ctrl.characters.arya"></child-component>
<child-component name="$ctrl.characters.john"></child-component>
</parent-component>
JS
// Parent Component declaration
// /////////////////////////
(function() {
'use strict';
angular
.module('app')
.component("parentComponent", {
transclude: true,
controller: "ParentComponentController",
template:
'<div class="parent-c"></div>'
});
})();
// Parent Component Controller
// /////////////////////////
(function() {
'use strict';
angular
.module('app')
.controller('ParentComponentController', ParentComponentController);
function ParentComponentController() {
var $ctrl = this;
$ctrl.characters = {};
$ctrl.characters.arya = "Arya Stark";
$ctrl.characters.john = "John Snow";
}
})();
//CHILD Component declaration
// /////////////////////////
(function() {
'use strict';
angular
.module('app')
.component("childComponent", {
bindings: {
name: '<'
},
controller: "ChildComponentController",
template:
'<div class="child-c"' +
'<h3>Im a child Component</h3>' +
'<p><strong>Name: </strong>{{$ctrl.name}}</p>' +
'</div>'
});
})();
// CHILD Component Controller
// /////////////////////////
(function() {
'use strict';
angular
.module('app')
.controller('ChildComponentController', ChildComponentController);
function ChildComponentController() {
var $ctrl = this;
}
})();
Check the WORKING SAMPLE on plunkr
The require attribute is for components communication, but i'm trying to use it with no success :(, need a piece of light here.
you have to use : <child-component name="$parent.$ctrl.characters.arya"></child-component>to pass a value from a parent-component to his nested child-component
There are different issues with your code:
function ParentComponentController() {
var $ctrl = this;
$ctrl.characters = {};
$ctrl.characters.arya = "Arya Stark";
$ctrl.characters.john = "John Snow";
}
You don't need to define a local variable for this since not changing context anywhere.
controller: "ParentComponentController",
Don't pass a string to this property, pass a reference:
controller: ParentComponentController,
Then if you want to pass name through the parent controller with require in the child component:
require: { parent: '^^parentComponent' },
Now that you have the parent controller bound to child you can use it with:
{{$ctrl.parent.characters.arya}}
in the template.
http://plnkr.co/edit/3PRgQSGdBEIDKuUSyDLY?p=preview
If you need to pass the name as an attribute to your child components, you have to put the child components inside the parent's template so you can call $ctrl.
http://plnkr.co/edit/1H7OlwbumkNuKKrbu4Vr?p=preview
I am building a simple view:
<tabulation tabulation-data="vm.tabs"></tabulation>
<div ng-switch="vm.activeTab.id">
<account-details ng-switch-when="details"></account-details>
<account-history ng-switch-when="history"></account-history>
<account-summary ng-switch-when="summary"></account-summary>
<account-dashboard ng-switch-when="dashboard"></account-dashboard>
</div>
Essentially, as I have it working now, tabulation will $emit an event to the parent account controller, which will update the vm.activeTab property to toggle through the different tab content.
A colleague of mine told me it may be more elegant to use bindings (&) on the tabulation component, which will use a function passed by the parent account component...
Unfortunately, I don't seam to understand how it functions:
Parent account controller:
function PocDemoContainerController($scope) {
var vm = this;
vm.tabs = [{
label: 'Details',
id: 'details'
},
{
label: 'History',
id: 'history'
},
{
label: 'Summary',
id: 'summary'
},
{
label: 'Dashboard',
id: 'dashboard'
}];
vm.activeTab = vm.tabs[0];
// this is the function that I want to pass to the tabulate component
vm.onClickTab = function (tab) {
vm.activeTab = tab;
};
...
}
Tabulate component html:
<tabulation tabulation-data="vm.tabs" on-click-tab="vm.onClickTab(tab)">
<div class="tabulation">
<nav class="container">
<button class="tabulation__mobile-toggle"
ng-class="{'tabulation__mobile-toggle--is-open': vm.mobileTabulationIsOpen}"
ng-click="vm.toggleMobileTabulation()">{{vm.activeTab.label}}</button>
<ul class="tabulation__container"
ng-class="{'tabulation__container--is-open': vm.mobileTabulationIsOpen}">
<li class="tabulation__item"
ng-repeat="tab in vm.tabs"
ng-class="{'tabulation--is-active': vm.isTabActive(tab)}">
<a id={{tab.id}}
class="tabulation__link"
ng-click="vm.onClick(tab)">{{tab.label}}</a>
</li>
</ul>
</nav>
</div>
</tabulation>
Tabulate controller:
...
module.exports = {
template: require('./tabulation.html'),
controller: TabulationController,
controllerAs: 'vm',
bindings: {
tabulationData: '<',
onClickTab: '&' // this should send data up, right?
}
};
Tabulation controller:
function TabulationController($scope) {
var vm = this;
...
vm.onClick = function (tab) {
vm.onClickTab(tab); // This is the function from the parent I want to call
};
...
}
TabulationController.$inject = [
'$scope'
];
module.exports = TabulationController;
So, the tabulation controller can see and call vm.onClickTab but the parameter value that is being passed is not passed to the parent account component controller...
How do I achieve this? (is it even possible that way?)
Alright! I finally found out how to do it, and it was not at all intuitive (the angular docs don't tell you this).
Based on the example above, my tabulation needs to tell the parent component which tab is now active so that it can update the view.
Here is how:
Declare a function on your parent component:
vm.onClickTab = function (tab) { ... };
Put the function on your child component.
<tabulation on-click-tab="vm.onClickTab(tab)"
IMPORTANT: If you are passing an argument, use the same name as the one you will define in your child component.
Inside your child component, declare a new function, and it is that function that should call the parent's callback.
vm.onClick = function (tab) { ... };
Now, here comes the part that is not mentioned anywhere: You have to call the parent's callback function using an object, with a property that uses the same name defined when passing that callback to the child component:
.
function TabulationController($scope) {
...
vm.onClick = function (tab) {
// The onClickTab() function requires an object with a property
// of "tab" as it was defined above.
vm.onClickTab({tab: tab});
};
...
}
Now, when vm.onClick() is called, it calls the parent's callback with an object, passing it the argument, and the parent can now use that data to update the view.
Looking at your Tabulate component html I wonder what is the tab parameter you are sending. Is it a local property of your controller? it seems not because there is no vm prefix to it (or any other name you've defined). Your code seems legit, it is the parameter origin that is not clear, therefore undefined.
Give us a hint on its origin for further analysis.
I had a similar problem and found this solution. I'm not sure if it's the best one but it works.
In the parent controller
I call my component
<contact button-action="vm.select(targetContact)"/>
And define my function
function select(contact) {...}
In my contact component
I define the binding:
bindings: { buttonAction: '&' }
And call the function
<button type="button" ng-click="$ctrl.buttonAction()">Click me</button>
When I click on my component button, select function is called passing the targetContact
Angular 1.5 components easily allow creating a call back to the parent from the component. Is there a way i can call a function in a component from a function in parent's controller ?
Lets say my component is called task-runner and below is the HTML for it in the parent container.
<task-runner taskcategogyid=5></task-runner>
<button type="button" ng-click="doSomethingInParent()">ParentToChildButton</button>
The plunkr is here. I want that when ParentToChildButton is clicked, the function doSomethingInParent() calls the remotefunc in component.
A few different ways:
Pass an object as an attribute with two-way binding (scope:{myattr:'='}) to the task-item-header directive which the directive could then add a function to for the parent controller to call.
Set an attribute that has either one-way binding (scope:{myattr:'#'}) on it and then attrs.$observe changes to it to trigger the action, or two-way binding (scope:{myattr:'='}) and then $scope.$watch changes to it to trigger the action.
Have the directive raise an event (scope:{raiseLoaded:'&onLoaded'}) that passes an object that represents a remote control object with a method on it that triggers the action you want. To raise the event, you'd call something like raiseLoaded({remoteControl: remoteControlObj}) within the directive, and then to listen to the event, you'd use <task-item-header on-loaded="setRemote(remoteControl)"> assuming you have a setRemote() method on your parent controller.
Update I just realized your question was for a newer version of AngularJS, so I'm not sure if my answer still applies. I'll leave it here for now, but if you find it is not helpful I can delete it.
I needed something like this previously so I thought I would share how I solved this problem.
Similar to the OP, I needed to freely trigger methods in child components from a parent component. I wanted to be able to trigger this method in the parent freely/separately without the use of the $onChanges lifecycle hook.
Instead I created a notification-registration mechanism to allow a child component to 'register' a method with the parent when it is loaded. This method can then be freely triggered by the parent outside of the $onChanges cycle.
I created a codepen to demonstrate this. It can be easily extended to handle different types of notifications from the parent that aren't related to the data changes.
Index.html
<div ng-app="tester">
<parent></parent>
</div>
Script.js
angular.module('tester', []);
angular.module('tester').component('parent', {
controller: parentController,
template: `
<div class="tester-style">
<button ng-click="$ctrl.notifyChild()">Notify child</button>
<child parent-to-child-notification-registration="$ctrl.childComponentNotificationRegistration(handler)">
</div>
`
});
function parentController() {
let childComponentEventHandler = null;
this.$onInit = function() {
this.value = 0;
};
this.childComponentNotificationRegistration = function(handler) {
childComponentEventHandler = handler;
console.log('Child component registered.');
};
this.notifyChild = function() {
if (childComponentEventHandler) {
childComponentEventHandler(this.value++);
}
};
}
angular.module('tester').component('child', {
bindings: {
parentToChildNotificationRegistration: '&',
},
controller: childController,
template: `
<div class="tester-style">
<h4>Child Component</h4>
</div>
`
});
function childController() {
this.$onInit = function() {
this.parentToChildNotificationRegistration({
handler: this.processParentNotification
});
};
this.processParentNotification= function(parentValue) {
console.log('Parent triggered child notification handler!!!');
console.log('Value passed to handler:', parentValue);
};
};
}
Also for something similar to #adam0101's #3 answer see codepen.