angular 1.5 component / default value for # binding - angularjs

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
}

Related

angularjs component controller not passing initial binding value to directive in template (summernote)

I have this rather simple angularjs component that has an optional height binding:
"use strict";
angular
.module("app")
.component("myComp", getComp());
function getComp() {
return {
bindings: getBindings(),
template: getTemplate(),
require: "ngModel",
controller,
};
}
function getBindings() {
return {
height: "<?",
noExternalFonts: "<?",
ngModel: "=",
}
}
function getTemplate() {
return `<summernote height="$ctrl.height" ng-model="$ctrl.ngModel" config="$ctrl.options"></summernote>`
}
function controller(chSummernoteService) {
const ctrl = this;
ctrl.chSummernoteService = chSummernoteService;
ctrl.$onInit = onInit.bind(ctrl);
}
function onInit() {
const ctrl = this;
ctrl.options = ctrl.chSummernoteService.getOptions({
noExternalFonts: ctrl.noExternalFonts,
});
}
The problem is, when I call it, the height is being ignored by summernote, the directive in the template, even tho the other two attributes, ng-model and options work fine.
<my-comp height="400" ng-model="$ctrl.someOtherCtrl></my-comp>
I also checked replated the $ctrl.height in the template with a hard coded number and that DOES work.
Is that some angularjs quirk I'm unaware of? (I'm new to angularjs, coming from React)
Or is that a bug in the summernote directive maybe?
Thanks in advance for any help!
I checked out the angular-summernote src and it appears to be bugged. Their controller is improperly written such that it reads the raw string value of the height attribute no matter what, so it's literally interpreted as "$ctrl.height". It's also written in such a way that it'll still read the raw value even if you try to force it in between interpolation bindings, i.e. height="{{ $ctrl.height }}" won't work either.
Luckily, the config attribute is being parsed properly, and according to the documentation the height can be specified as a property within that instead. So remove the height attribute from your element and within your onInit function, add the height property to your config object:
ctrl.options.height = ctrl.height;

confused by adaptation of AngularJs component-based modal to TypeScript [duplicate]

This question already has answers here:
TypeScript AngularJS component modal - this.$modalInstance.dismiss is not a function?
(1 answer)
When to use the AngularJS `$onInit` Life-Cycle Hook
(1 answer)
Closed 3 years ago.
When you create a AngularJS component in Javascript, and display as a modal using ui-bootstrap, you pass a binding that the modal instance can use to dismiss or close itself, like this:
app.component("fringeEdit", {
controller: "FringeEditController",
templateUrl: "/template/fringeEditTemplate.html",
bindings: {
close: '&', <---
dismiss: '&' . <---
}
});
In the javascript version from the Angular-UI Bootstrap Modal Directive, that makes the $modal.close() and $modal.dismiss() methods magically available to the modal controller function so that modal can close itself:
let FringeEditController = function() {
var $ctrl = this;
$ctrl.ok = function () {
$ctrl.close({$value: $ctrl.selected.item}); <==
};
$ctrl.cancel = function () {
$ctrl.dismiss({$value: 'cancel'}); <==
};
}
Once you register modal controller, the parent controller can open the modal like this:
$ctrl.openComponentModal = function () {
var modalInstance = $uibModal.open({
animation: $ctrl.animationsEnabled,
component: 'fringeEdit'
}
});
That all makes sense in Javascript -- but in TypeScript, I keep running into this problem: where can I find these bindings?
They don't seem to magically insert themselves into the controller function like they do in the javascript examples, where suddenly there is a $ctrl.close(...) function available. Here I use a class for my controller, and I define the close and dismiss functions although I don't implement them (hoping that somehow they will magically fall into the instantiated controller like they do in JS) but that never happens. :( Once I show the modal, and trigger the dismissMe function, I just get the console.log message dismiss() is undefined.
I thought maybe I could "find them" somehow and assign to the function vars "dismiss" and "close", but I don't know where to find the reference to these function bindings. All a bit of a mystery, can someone give me some guidance?
class FringeEditController {
dismiss: ((params: object) => any ) | undefined;
close: ((params: object) => any ) | undefined;
dismissMe() : void {
if (this.dismiss===undefined) {
console.log("dismiss() is undefined!")
} else {
this.dismiss({$value: "dismissed"};
}
}
...implementation
}
The example given here in Binary Horizon Blog looked promising (although a bit painful) but his code doesn't actually show how to get the function binding into component either.
Ok, this was actually due to a problem with the component template, not the Typescript code. Inside the template itself, I had ng-controller="fringeEditController" (Yes, dumb) This resulted in having a second controller instantiated and then bound to the modal itself. This second controller instance did not have the bindings that were specified in the $uibModal.open function
By removing the ng-controller directive from the template, the controller created by the $uibModal.open function was properly bound to the modal, and the close function was properly bound.
interesting things I learned along the way: The bindings remain undefined in the controller's constructor. They get bound sometime between initialization and $onInit where they are present.

Cannot bind to rootscope in Angular 1.5 component

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>

Using transcludeFn in angular components

Is it true, that I cannot customize transclusion in angular components (angular 1.5)? The task I want to solve is passing a template to a component using transclusion and make it able to use "in-the-component" variables. Like this:
<my-items-component items="$ctrl.items">
<div>{{::item.description}}</div>
</my-items-component>
Where item supposed to be put into my-items-component documentation, and used to customize the item presentation inside the component.
I was able to do this with directives, using transcludeFn function, but it seems there are no arguments passed to $postLink component hook.
So, should I use a directive for this or there's another approach?
To use tansclusion in AngularJS 1.5 components you need first to enable tarnsclusion in your component by using transclude: true, then use <ng-transclude></ng-transclude> in your component template.
I have created a sample pen as an example http://codepen.io/fadihania/pen/bwpdPq
I've found an answer.
Access $scope of Component within Transclusion in AngularJS 1.5
This worked with my problem. My example:
<my-custom-component>
<input model="$parent.$ctrl.name">
</my-custom-component>
Then in my component now I have "name". I hope this helped you.
there are 2 ways of solving your problem
to place all our html inside component template definition
app.component('myItemComponent', new myItemComponentConfig());
function myItemComponentConfig() {
this.controller = componentController;
this.template = '<div>{{::item.description}}</div>',
this.bindings = {
this.bindings = {
items:'<'
}
};
this.require = {};
}
use it like this :
<my-items-component items="$ctrl.items"></my-items-component>
2.use Ng-transclude to load child HTML of a component
app.component('myItemComponent', new myItemComponentConfig());
function myItemComponentConfig() {
this.controller = componentController;
this.template = '<div></div>',
this.bindings = {
items:'<'
};
this.require = {};
this.transclude:true;
}
use it like this :
<my-items-component items="$ctrl.items">
<div>{{::item.description}}</div>
</my-items-component>

Angular 1.5 Component Bindings: Check if Callback is Present

I've got a simple contactList component, which has 2 bindings: contacts and onRemove.
contacts is just an array of contacts to display
onRemove is a callback function
app
.component('contactList', {
template:
`<div ng-repeat="c in $ctrl.contacts">
{{c.name}}
<div ng-click="$ctrl.onRemove({contact: c})">Remove</div>
</div>`,
bindings: {
contacts: '<',
onRemove: '&'
},
controller: function() {}
})
I use this component multiple times within my app. And the onRemove callback can behave differently, depending on where the contactList component is getting used. Example:
contactMainView (= component) displays a search bar and the resulting list of contacts using the contactList component. onRemove will delete the contact from the database.
groupMembersView displays all contacts belonging to the given group using the contactList component. Here it shouldn't be possible to remove a contact, though onRemove will not be set.
Ideas:
First I thought, that I could use an ng-if="$ctrl.onRemove" but that doesn't work, because onRemove is never undefined within contactList component. console.log(this.onRemove) always prints: function(locals) { return parentGet(scope, locals); }
Of course I could use another showRemove: '#' binding, but that doesn't look DRY to me.
Do you have any idea or some better solution to get things done?
The simplest way would to check if the attribute is defined. In your controller inject $attrs and then you can do:
if( $attrs.onRemove ) { //Do something }
Using the & binding angular will wrap the function in order to keep references to the original $scope of the passed method, even if is not defined.
Execute the function onRemove on component allow to get if a function was passed in parameter. So you can use ng-if="$ctrl.onRemove()"
component('contactList', {
template:
`<div ng-repeat="c in $ctrl.contacts">
{{c.name}}
<div ng-click="$ctrl.onRemove()({contact: c})" ng-if="$ctrl.onRemove()">Remove</div>
</div>`,
bindings: {
contacts: '<',
onRemove: '&'
},
controller: function() {
console.log(this.onRemove);
console.log(this.onRemove());
}
})
another option is to define the callback as optional by adding a question mark in the binding definition:
onRemove: '&?'
text from the documentation: All 4 kinds of bindings (#, =, <, and &) can be made optional by adding ? to the expression. The marker must come after the mode and before the attribute name. See the Invalid Isolate Scope Definition error for definition examples.

Resources