How to make ngIf work after transclusion? - angularjs

I have a list component where I want to define custom columns inside. These columns get transcluded into the row of the components template. Unfortunately I can't use ngIf in this context.
Here is my $postLink function of the myList component:
const template = $templateCache.get('myList.tpl.html');
const jqTemplate = angular.element(template);
const row = angular.element(jqTemplate.children()[0]);
$transclude(clone => {
row.append(clone);
$element.html(jqTemplate.html());
});
$compile($element.contents())($scope);
Here is a plnkr of the minimal sample: http://plnkr.co/edit/C9Rvs8NiTYsV3pwoPF6a
Is that because of the terminal property? Can someone entlighten me why ngIf does not work like expect it to?

I think trying to perform the operation in postLink phase is too late, since it is first being applied to child elements.
Compile phase seems to be more appropriate. In there things are simpler, you don't even need to use transclusion or clone-linking function. Scope is applied at a later state.
I provide a solution using directive since in cases like this I find component syntax more confusing.
app.directive('myList', function($templateCache){
return {
bindings: {
list: '='
},
transclude: false,
compile: function(tElement) {
const template = $templateCache.get('myList.tpl.html');
const jqTemplate = angular.element(template);
var elemChildren = tElement.children('div');
jqTemplate.children('.row').append(elemChildren);
tElement.append(jqTemplate);
return {};
}
}
});
http://plnkr.co/edit/MrLJmnMKxO8PVPkzE8KK?p=preview

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;

AngularJS binding template variable

I have to renderize the template dynamically depending the layout sended (for now we have original and alternative).
In the beggining I was traking manually in the html. Like this:
<component layout="original"></component>
Component template:
template: ($element, $attrs) => {
let process = 'original';
if ($attrs.layout) {
process = $attrs.layout;
}
return require(`./templates/${process}.html`);
}
But now I have to compile according the variable. For example:
<component layout="{{vm.templateType}}"></component>
But when I acess the $attrs in the template the Angular is not compiled and the result is the string like this:"{{vm.templateType}}".
There is a way to force the template compilation before run the template function?
This is a problem in AngularJS you can check the discuss here: #2895 and #13526.
To solve it I had to use the $compile in the controller like this:
this.$onChanges = function (obj) {
const layout = require(`./templates/${obj.layout.currentValue}.html`);
$element.append($compile(layout)($scope));
};
And remove the template attr of the component.

angular 1.5 component / default value for # binding

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
}

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>

Why doesn't scope pass through properly to nested directives?

In this example plunker, http://plnkr.co/edit/k2MGtyFnPwctChihf3M7, the nested directives compile fine when calculating the DOM layout, but error when the directive tries to reference a variable to bind to and says the variable is undefined. Why does this happen? The data model I am using is a single model for many nested directives so I want all nested directives to be able to edit the top level model.
I havn'et got a clue as to what you're trying to do. However, your comment 'so I want all nested directives to be able to edit the top level model' indicates you want your directive to have scope of your controller. Use
transclude = true
in your directive so that your directives can have access to your the parent scope.
http://docs.angularjs.org/guide/directive#creating-a-directive-that-wraps-other-elements
I don't know why you are doing it this way exactly, it seems like there should be a better way, but here goes a stab at getting your code working. First you create an isolated scope, so the scopes don't inherit or have access to anything but what is passed in the data attribute. Note that you can have your controller set dumbdata = ... and say <div data="dumbdata" and you will only have a data property on your isolated scope with the values from dumbdata from the parent in the data property. I usually try to use different names for the attribute and the data I'm passing to avoid confusion.
app.directive('project', function($compile) {
return {
template: '<div data="data"></div>',
replace: true,
scope: {
data: '=' // problem
},
Next, when you compile you are passing variables as scopes. You need to use real angular scopes. One way is to set scope: true on your directive definition, that will create a new child scope, but it will inherit from the parent.
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
scope: true, // new child scope inherits from parent
compile: function compile(tEle, tAttr) {
A better way is probably to create the new child scope yourself with scope.$new(), and then you can add new child properties to pass for the descendants, avoiding the problem of passing values as scopes and still letting you have access to the individual values you're looping over (plunk):
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
compile: function compile(tEle, tAttr) {
return function postLink(scope,ele,attrs) {
angular.forEach(scope.outer.middles, function(v, i) {
var x = angular.element('<div middle></div>');
var s = scope.$new(); // new child scope
s.middle = v; // value to be used by child directive
var y = $compile(x)(s); // compile using real angular scope
ele.append(y);
});
};
}
};
return r;
});

Resources