Is PrimeNG's p-pickList changing source and target? - javascript

I have an Angular app with PrimeNG. I am using the PickList component like this:
<p-pickList
[source]="source"
[target]="target"
>
<ng-template
let-item
pTemplate="item">
{{item | json}}
</ng-template>
</p-pickList>
<h2>source</h2>
<ul>
<li *ngFor="let s of source">{{s|json}}</li>
</ul>
<h2>target</h2>
<ul>
<li *ngFor="let t of target">{{t|json}}</li>
</ul>
The component itself is straightforward:
#Component({
selector: 'app-hello',
templateUrl: './hello.component.html',
styleUrls: ['./hello.component.css'],
})
export class HelloComponent {
source: string[];
target: string[] = [];
constructor() {
this.source = [
"foo",
"bar",
"baz",
];
}
}
I do not use two-way-binding here, so how does PrimeNG updates source and target property?
On my main project source and target are #Input() properties and thus I don't want some sub component to fiddle with it.
How is it possible that PrimeNG changes those values?

You could replicate the values of source and target in your HelloComponent, then use these copies for the underlying PrimeNG PickList widget. This allows you to pass updates to the HelloComponent that will be communicated down to the PrimeNG widget, but changes to those arrays within the PrimeNG widget won't impact the original input array.
I believe in your original code, the original array that's being passed as an input to HelloComponent, then passed into the PrimeNG widget, is being passed by a "copy of a reference."
private _sourceOptions: string[];
#Input set sourceOptions(options: string[]) {
// Ensure we're not passing a reference to the array down because it may still
// be changed. Create a new array with the same items. This can also help with
// firing change detection in the PrimeNG widget.
this._sourceOptions = options.slice(0);
}
get sourceOptions(): string[] {
return this._sourceOptions;
}
<!-- Component template file -->
<p-pickList [source]="sourceOptions" [target]="target">
<ng-template let-item pTemplate="item">
{{item | json}}
</ng-template>
</p-pickList>

Odds are that within the component for primeNG there is an OnChange listener, and just generally speaking when something changes within one #Input it does trigger an onChange event.
As the Angular doc says (https://angular.io/api/core/OnChanges) anytime a data bound property changes it fires onChange. In this case here, target is a databound property.
Also, what do you mean by changing values? If you select foo it turns into foobar? Under the hood primeNG is not manipulating the data you passed it, it probably has its own internal store of how it displays the data for its picker list component.

Related

Is there a way I can dynamically set the formControlName on an <input> element in angular?

I'm working on writing a component intended to simplify/homogenize the way our forms look and interact. The code looks something like this:
Example Usage
...
<my-form-input labelKey = "email" controlName="emailAddress" [form]="userForm">
<input myInput class="form-control" type="email" formControlName="emailAddress" />
</my-form-input>
...
You can see that "emailAddress" is passed to MyFormInputComponent as the controlName and is passed a second time to the FormControlName directive on the <input> element. I'd like to only pass this once so that my end user doesn't have to do this.
Is there a good way I can go about this, or is this just a constraint I should accept (if yes, an explanation of why this constraint exists would be welcome)? Code is shown below.
I've tried two approaches:
Setting a #HostBinding("attr.formControlName") annotation in the MyInput component. I can manipulate an attribute called formcontrolname on the element this way, but it doesn't trigger the directive that Angular Forms needs to properly register the control with the group.
Ask the user to supply formControlName to the <input> element and read the value off of this for the rest of the component. This might work, but I'd have to access the DOM directly through an ElementRef, which is not recommended. The recommended route for interacting with DOM -- Renderer -- doesn't seem to expose any ability to read attributes either.
my-form-input.component.ts
#Component({
selector: 'my-form-input',
templateUrl: './my-form-input.component.html',
styleUrls: ['./my-form-input.component.scss']
})
export class MyFormInputComponent implements OnInit, AfterContentInit {
#Input()
labelKey: string;
#Input()
controlName: string;
#Input()
form: FormGroup;
#ContentChild(MyInputDirective)
input: MyInputDirective;
ngAfterContentInit(): void {
this.initInput();
}
/**
* Updates the input we project into the template
*/
private initInput() {
this.input.updatePlaceholder(this.labelKey);
// I'd like to somehow edit the FormControlName directive applied to the input here
}
}
my-form-input.component.html
<label>{{ labelKey | translate }}</label>
<ng-content></ng-content>
<my-input-error [control]="form.controls[controlName]" [name]="labelKey | translate" />
my-input.directive.ts
#Directive({
selector: '[myInput]'
})
export class myInputDirective implements OnInit {
private placeholderKey = "";
#HostBinding("placeholder")
private placeholder: string;
updatePlaceholder(placeholderKey: string) {
this.placeholderKey = placeholderKey;
this.placeholder = this.translateService.instant(this.placeholderKey);
}
constructor(private translateService: TranslateService) {
}
}
my-form-error.component.ts
// Not shown since not relevant.
I'm still not sure the exact explanation, but some reasoning alludes to where I might have strayed.
I assumed that my component owned the elements that it was projecting, but I actually think this isn't true. Your opportunity to set attributes/directives is in the template. This means you are better off including any elements that you want to own/control into the template rather then just projecting them.
In this case, that leads you to making separate components for specific form controls (like <input>, <textarea>, etc). This is what I've ended up doing. This is better from a design endpoint anyway -- one component to wrap all possible form-controls was never going to happen. Either project a form control that duplicates some properties, like I do in the question, or create specific components. Or both (just make your most common form controls into components, wrap the one-off problems in your projecting component).
Here are some blogs that helped me find my way:
https://medium.com/#vadimkorr/implementing-nested-custom-controls-in-angular-5-c115c68e6b88
https://blog.angularindepth.com/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms-93b9eee9ee83

Angular - mat-grid-list not displaying children passed by <ng-content>

I'm using angular material design components and want to create a custom grid-list component. The component would adapt the number of grid-list-columns based on its size. The components template would look like this:
<mat-grid-list [cols]="adaptiveCols">
<ng-content></ng-content>
</mat-grid-list>
And the component will be used like this:
<my-grid>
<mat-grid-tile>tile_content</mat-grid-tile>
<mat-grid-tile>tile_content</mat-grid-tile>
<mat-grid-tile>tile_content</mat-grid-tile>
</my-grid>
So you would think this would display a <mat-grid-list> with 3 <mat-grid-tile>s in them, and the resulting DOM will contain those elements. But you don't see the tiles nor their content.
Adding more tiles to the grid-list and using a fixed rowHeight doesn't affect the height-attribute of the grid-list (from what I could figure out the grid-list calculates its height based on the number and col-/rowspan of tiles). That makes you think the grid-list doesn't "see" the <mat-grid-tile>-children. Looking at the source code of the grid-list and setting a breakpoint on _layoutTiles() confirmed this, in this method this._tiles is empty.
My guess is the angular ContentChildren-annotation (here) doesn't find the tile-children in the first code snippet because it only works on the first level of DOM, and that first level we only have the <ng-content>.
The desired result is clear, I want to be able to use a custom grid-list component, give it some children and use its features. But for some reason, angular doesn't want me to succeed, or I still have the stuff to learn.
I'm using angular 6.1 and installed the material-components using the official docs.
mat-grid-list uses content projection in it's own implementation and so it needs to be able to query it's projected content. Trying to add another level of content projection makes this not possible since the mat-grid-tile's are not actually being projected by the the list, but by your component.
So short answer: this isn't gona work.
There is likely a way to achieve your goal but content projection isn't the way. If you clarify your intentions a bit, I may be able to help. You likley need to use templates though.
Here's the simple case: write a wrapper around mat-grid-list that accepts an array of data as input that you can iterate over with ngFor to create the tiles
#Component({
selector: 'my-grid',
templateUrl: './my-grid.html'
})
export class MyGrid {
#Input() data: string[];
}
<mat-grid-list>
<mat-grid-tile *ngFor="let d of data">{{d}}</mat-grid-tile>
</mat-grid-list>
This provides a reusable wrapper around mat-grid-list that abstracts a bit of it away for you. however, this is no good if your grid content is more complex than a string (likely) or requires custom templates on every usage. In this case, you need a somewhat more refined approach, using templates and directives:
#Directive({
selector: 'gridTemplate'
})
export class GridTemplate {
#Input() key: string;
constructor(private elementRef: ElementRef, public template: TemplateRef<any>) {}
}
This directive will identify your custom templates then, in your grid list, you can use these templates to populate the tiles
#Component({
selector: 'my-grid',
templateUrl: './my-grid.html'
})
export class MyGrid implements AfterContentInit {
//structure the data so it can be assigned to a template by key
#Input() data: {templateKey:string, data: any}[];
//use content children to find projected templates
#ContentChildren(GridTemplate)
gridTemplates: QueryList<GridTemplate>;
gridTemplateMap: {[key:string]: TemplateRef<any>} = {};
ngAfterContentInit() {
//store queried templates in map for use in template
this.gridTemplates.forEach(t => this.gridTemplateMap[t.key] = t.template);
}
}
<ng-template #defaultGridTemp let-d="data">{{d}}</ng-template>
<mat-grid-list>
<mat-grid-tile *ngFor="let d of data">
<!-- here we use the template map to place the correct templates or use a default -->
<!-- we also feed the data property as context into the template -->
<ng-container [ngTemplateOutlet]="(gridTemplateMap[d.templateKey] || defaultGridTemp)"
[ngTemplateOutletContext]="{ data: d.data }" ></ng-container>
</mat-grid-tile>
</mat-grid-list>
Then your usage of your component is like this:
#Component({...})
export class MyComponent {
data = [
{ templateKey:'MyKey1', data: {message: 'Projected Message', number: 1} },
{ templateKey:'MyKey2', data: {whatever: 'Message', youWant: 'to place'} }
];
}
<my-grid [data]="data">
<ng-template gridTemplate key="MyKey1" let-d="data">
<div>{{d.message}}</div>
<div>{{d.number}}</div>
<div>Any other arbitrary content</div>
</ng-template>
<ng-template gridTemplate key="MyKey2" let-d="data">
<div>{{d.whatever}}</div>
<div>{{d.youWant}}</div>
<div>Any other arbitrary content</div>
</ng-template>
</my-grid>
with this approach, you can add whatever reusable logic to mat-grid-list you want inside your grid component but still maintain the flexibility of providing custom templates to your grid. The drawback is that you have to take the extra steps to structure your data to take advantage of this approach.

Angular4 Template

I have written a simple code to demonstrate ngTemplate
<div> <ng-container [ngTemplateOutlet] ="template1"> </ng-container></div>
and here are the template
<ng-template #template1> This is 1st template </ng-template>
<ng-template #template2>This is 2nd template </ng-template>
It is working as expected, but my problem is I am not able to pass dynamic value to [ngTemplateOutlet]
I want to achieve something like this
<div> <ng-container [ngTemplateOutlet] ="selectTemplate"></ng-container> </div>
Which will either select template1 or template2.
Whenever I am passing some dynamic value which I have defined in my typescript file it gives is error
ERROR TypeError: templateRef.createEmbeddedView is not a function
My typescript file contains this code
if(some condition) {
this.selectTemplate = "template1";
} else {
this.selectTemplate = "template2";
}
In your first example, you're binding ngTemplateOutlet to the value of the expression template1, which is the first ngTemplate element. So far so good.
In your second example, you're binding ngTemplateOutlet to the value of the expression selectTemplate, which is not a template element - you've set it to be a string!
To fix this, you first need to add the template references to your component as fields using the ViewChild annotation:
#ViewChild('template1') template1;
#ViewChild('template2') template2;
Then set selectTemplate to refer to the element itself rather than its name:
if (some condition) {
this.selectTemplate = this.template1;
}
else {
this.selectTemplate = this.template2;
}
Note that the ViewChild variables will not be set until the component's view has been initialized - you can use the ngViewAfterInithook to wait for that to happen.
update Angular 5
ngOutletContext was renamed to ngTemplateOutletContext
See also https://github.com/angular/angular/blob/master/CHANGELOG.md#500-beta5-2017-08-29
original
You can pass a context
<ng-container [ngTemplateOutlet]="template1; context: {foo: 'foo', bar: 'bar', $implicit: 'implit' }"
In the template you can use it like
<ng-template #template1 let-foo1="foo" let-foo2="bar" let-item>
This is 1st template
<div>foo1: {{foo1}}</div>
<div>foo2: {{foo2}}</div>
<div>foo1: {{item}}</div>
</ng-template>
I hope the names I used are not too confusing. I tried to make the distinction between
the name as the value is passed to the context
the name as it is used inside the template
$implicit which doesn't require ="somename" in the template variable definintion (let-item)

How to use templateRef?

I am trying to find a way to dynamically construct a template in Angular2. I was thinking templateRef might provide a way to do this. But I could be wrong.
I found an example of templateRef being used here.
I was looking at templateRef in this example. I noticed the syntax is [ng-for-template] I also tried [ngForTemplate] cause I know this has changed recently.
So at the moment I have this:
import {Component, TemplateRef} from 'angular2/core';
#Component({
selector : 'body',
template : `
<template [ngForTemplate]="container">
<div class="container"></div>
</template>
`
})
export class App
{
#ContentChild(TemplateRef) container;
constructor() {}
ngAfterContentInit()
{
console.log(this);
}
}
This example throws an error:
Can't bind to 'ngForTemplate' since it isn't a known native property
So firstly I am wondering. What is the right way to do this? The docs don't provide any examples.
Secondly, is there a good way I can add new template logic to my template or dynamically construct a template? The structure of the application can be a very large amount of different structural combinations. So if possible I would like to see if there is a way I can do this without having a huge template with a bunch of different ngIf and ngSwitch statements..
My question is really the first part about templateRef. But any help or suggestions on the second part is appreciated.
Creating your own template directive it's not difficult, you have to understand two main things
TemplateRef contains what's inside your <template> tag
ViewContainerRef as commented by Gunter, holds the template's view and will let you to embed what's inside the template into the view itself.
I will use an example I have when I tried to solve this issue, my approach is not the best for that, but it will work for explaining how it works.
I want to clarify too that you can use any attribute for your templates, even if they're already used by builtin directives (obviously this is not a good idea, but you can do it).
Consider my approach for ngIfIn (my poor approach)
<template [ngIfValue]="'make'" [ngIfIn]="obj">
This will print
</template>
<template [ngIfValue]="'notExistingValue'" [ngIfIn]="obj">
This won't print
</template>
We have here two templates using two inputs each ngIfIn and ngIfValue, so I need my directive to grab the template by these two inputs and get their values too, so it would look like this
#Directive({
selector : '[ngIfIn][ngIfValue]',
inputs : ['ngIfIn', 'ngIfValue']
})
First I need to inject the two classes I mentioned above
constructor(private _vr: ViewContainerRef, private _tr: TemplateRef) {}
I also need to cache the values I'm passing through the inputs
_value: any;
_obj: any;
// Value passed through <template [ngIfValue]="'...'">
set ngIfValue(value: any) {
this._value = value;
}
// Value passed through <template [ngIfIn]="...">
set ngIfIn(obj: any) {
this._obj = obj;
}
In my case I depend on these two values, I could have my logic in ngOnInit but that would run once and wouldn't listen for changes in any of the inputs, so I put the logic in ngOnChanges. Remember that ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed (copy and paste from the docs).
Now I basically copy & paste NgIf logic (not so complex, but similar)
// ngOnChanges so this gets re-evaluated when one of the inputs change its value
ngOnChanges(changes) {
if(this._value in this._obj) {
// If the condition is true, we embed our template content (TemplateRef) into the view
this._vr.createEmbeddedView(this._tr);
} else {
// If the condition is false we remove the content of the view
this._vr.clear();
}
}
As you see it's not that complicated : Grab a TemplateRef, grab a ViewContainerRef, do some logic and embed the TemplateRef in the view using ViewContainerRef.
Hopefully I made myself clear and I made how to use them clear enough also. Here's a plnkr with the example I explained.
ngForTemplate is only supported with ngFor
<template [ngFor] [ngForOf]="..." [ngForTemplate]="container"
or
<div *ngFor="..." [ngForTemplate]="container"
not on a plain template. It is an #Input() on the NgFor directive
Another way to use TemplateRef
If you have a reference to ViewContainerRef you can use it to "stamp" the template
constructor(private _viewContainer: ViewContainerRef) { }
ngOnInit() {
this.childView = this._viewContainer.createEmbeddedView(this.templ);
this.childView.setLocal('data', this.data);
}

How do you access dom selectors using angular 2

I have a simple script that would add a class to a dom selector.
showParent() {
console.log('show parent');
$("seasonsGrid").toggleClass("grid-layout");
}
How does angular allow you to target these dom nodes whether they are tags or named elements?
in this case $("seasonsGrid") is a reference to
#Component({ selector: 'seasonsGrid', ...
but would be useful to know how to dom traverse using tags and named el's.
what would the ng2 equivalent be for this
var seasonsGrid = $(seasonsGrid);
You could take use of ElementRef/Renderer to get component element
import {Component, View, ElementRef} from 'angular2/angular2';
import {NgClass} from 'angular2/common'; //import when you wanted to use ngClass
import {Renderer} from 'angular2/core';
#Component({ selector: 'seasonsGrid', ...})
export class MyComponent {
let el: any;
let componentElement:any;
constructor(private elementRef: ElementRef, private renderer: Renderer){
//el = elementRef.nativeElement;
componentElement = renderer.selectRootElement(); //will have component root element
}
ngAfterViewInit() {
console.log(el); //nativeElement to process
console.log(componentElement); //nativeElement to process
}
}
But in your case you can think of to have ngClass directive to have it in a place like [ngClass]="{className: expression}"
I must need to do some more homework but from the looks of it angular 2 is by far more convoluted if all that is required just to initialize the states of a toggle class on an element. jquery is 1 line
Angular 2 is a data-driven reactive framework. Instead of thinking about selectors and DOM manipulation code, I encourage you to embrace Angular-think, which is quite different from jQuery-think.
If you want to change a class in a data-driven framework, you first declaratively bind some data to that element's class property in a component template. Then, to change the class, you simply change the bound data. As others have already mentioned, NgClass is the way to do that.
Similarly, component logic should not manipulate the DOM (i.e., it shouldn't call things like toggleClass()), rather it should change data or emit an event (up to its parent).
When Angular change detection runs, it will notice the data changes and update the DOM (or native UI) or propagate the data change to a parent component (for an emitted event) or to a child component (for an input data property change).
Angular-think is all about data-driven changes. And Angular 2 pushes us further down that road than Angular 1 did. (That's a good thing.)
This probably won't answer the question as I'm very new to NG2 (like three days), but here's a few things I've found in the documentation:
<div [class.isStopped] = "isStopped"></div>
<div [style.color] = "isStopped ? 'red' : 'blue'"></div>
<button [attr.aria-label] = "ok"></button>
<div [ngClass] = "{selected:isSelected}"></div>
ngClass is a Directive Property, just like ngStyle and ngModel. For the most part, they just took out the dash.
While we're on Property Bindings:
<img [src] = "vehicle.imageUrl" />
<vehicle-detail [vehicle] = "currentVehicle"></vehicle-detail>
You tie the component (code) to the template so in this case it wouldn't be necessary to get the element in code and change it, just change the Component (view model).

Categories

Resources