How to get DOM nodes class list using Renderer2 - dom

Since Angular 4.0.0, Renderer2 should be used to access DOM nodes. I can’t find a way to access the class List of a DOM node. I can easily add or remove a class but I can’t get a class List. Am I missing something?
import { Component, ElementRef, Renderer2 } from '#angular/core';
#Component({
selector: 'auto-form',
template: `<form formContainer class="test"></form>
`
})
export class AutoFormComponent {
constructor(private formContainer:Renderer2) {
}
}
Renderer2 Documentation

There is no way to get any information from the DOM using Renderer. The renderer is only one-way - from code to DOM.
https://angular.io/docs/ts/latest/api/core/index/Renderer2-class.html
Ok, it seems there are 3 exceptions:
selectRootElement
parentNode
nextSibling

Related

Capacitor v3 Motion api - using AccelListenerEvent properties

I am using Capacitor version 3 and I'm trying out the Motion Api.
In the documentation here, the AccelListenerEvent comes with some properties which I want to set but there are no examples of how to use this.
So the part I'm using is addListener(‘orientation’, …)
I basically want to set the interval.
I've added this:
import { Component } from '#angular/core';
import { PluginListenerHandle } from '#capacitor/core';
import { Motion, AccelListenerEvent } from '#capacitor/motion';
#Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
accelHandler: PluginListenerHandle;
accelListenerEvent: AccelListenerEvent;
constructor() {
this.accelListenerEvent.interval = 10;
}
But it doesn't like it in the constructor.
Does anyone have any ideas on how to set these properties?
You can access the properties this way:
Motion.addListener('accel', event => {
console.log('Interval:', event.interval);
});
Since this is an event, you can only receive values, not set them.

Wrapping a FormControl in Angular (2+)

I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup and blur events on the input, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange as part of implementing ControlValueAccessor).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges of that and then propagate the changes back to the parent (via the function passed into registerOnChange).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.
Edit:
I've added a helper for doing just this an angular utilities library I've started: s-ng-utils. Using that you can extend WrappedFormControlSuperclass and write:
#Component({
selector: 'my-wrapper',
template: '<input [formControl]="formControl">',
providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
// ...
}
See some more documentation here.
One solution is to get the #ViewChild() corresponding to the inner form components ControlValueAccessor, and delegating to it in your own component. For example:
#Component({
selector: 'my-wrapper',
template: '<input ngDefaultControl>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent),
multi: true,
},
],
})
export class MyWrapper implements ControlValueAccessor {
#ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;
writeValue(obj: any) {
this.valueAccessor.writeValue(obj);
}
registerOnChange(fn: any) {
this.valueAccessor.registerOnChange(fn);
}
registerOnTouched(fn: any) {
this.valueAccessor.registerOnTouched(fn);
}
setDisabledState(isDisabled: boolean) {
this.valueAccessor.setDisabledState(isDisabled);
}
}
The ngDefaultControl in the template above is to manually trigger angular to attach its normal DefaultValueAccessor to the input. This happens automatically if you use <input ngModel>, but we don't want the ngModel here, just the value accessor. You'll need to change DefaultValueAccessor above to whatever the value accessor is for the material dropdown - I'm not familiar with Material myself.
I'm a bit late to the party but here is what I did with wrapping a component which might accept formControlName, formControl, or ngModel
#Component({
selector: 'app-input',
template: '<input [formControl]="control">',
styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
constructor(#Optional() #Self() public ngControl: NgControl) {
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
}
control: FormControl;
// These are just to make Angular happy. Not needed since the control is passed to the child input
writeValue(obj: any): void { }
registerOnChange(fn: (_: any) => void): void { }
registerOnTouched(fn: any): void { }
ngOnInit() {
if (this.ngControl instanceof FormControlName) {
const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
if (formGroupDirective) {
this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
}
} else if (this.ngControl instanceof FormControlDirective) {
this.control = this.ngControl.control;
} else if (this.ngControl instanceof NgModel) {
this.control = this.ngControl.control;
this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
} else if (!this.ngControl) {
this.control = new FormControl();
}
}
}
Obviously, don't forget to unsubscribe from this.control.valueChanges
I have actually been wrapping my head around this for a while and I figured out a good solution that is very similar (or the same) as Eric's.
The thing he forgot to account for, is that you can't use the #ViewChild valueAccessor until the view has actually loaded (See #ViewChild docs)
Here is the solution: (I am giving you my example which is wrapping a core angular select directive with NgModel, since you are using a custom formControl, you will need to target that formControl's valueAccessor class)
#Component({
selector: 'my-country-select',
templateUrl: './country-select.component.html',
styleUrls: ['./country-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CountrySelectComponent,
multi: true
}]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
#ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;
private country: number;
private formControlChanged: any;
private formControlTouched: any;
public ngAfterViewInit(): void {
this.valueAccessor.registerOnChange(this.formControlChanged);
this.valueAccessor.registerOnTouched(this.formControlTouched);
}
public registerOnChange(fn: any): void {
this.formControlChanged = fn;
}
public registerOnTouched(fn: any): void {
this.formControlTouched = fn;
}
public writeValue(newCountryId: number): void {
this.country = newCountryId;
}
public setDisabledState(isDisabled: boolean): void {
this.valueAccessor.setDisabledState(isDisabled);
}
}
NgForm is providing an easy way to manage your forms without injecting any data in a HTML form. Input data must be injected at the component level not in a classic html tag.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>
Other way is to create a form component where all the data model is binded using ngModel ;)

Angular 2 - get a reference to the formGroup

In Angular 2 template driven forms, I can write
<form #form="ngForm">
[...]
</form>
ngForm is a FormGroup, as fare as I could understand. How do I grab that object in the corresponding component?
class FormComponent {
formGroup: FormGroup; // How do I have the form INJECTED AUTOMATICALLY by the framework? Something like a ViewChild, but it's not a view
}
It must be simple but the documentation only shows how to use the form in the template.
Thanks
EDIT: the type of what I want to grab is NgForm, not FormGroup!
You can access any template variable from the component using ViewChild decorator. In you case:
class FormComponent {
#ViewChild('form') formGroup;
ngOnInit() {
this.formGroup.statusChanges().subscribe(() => {
// Your code here!
});
console.log(this.formGroup); // Inspect the object for other available property and methods.
}
}

Angular2: Get dimensions of DOM-Element (without ElementRef)

I try to get dimensions of a DOM-Element (heigtht/width).
As for security reasons (see https://angular.io/docs/ts/latest/guide/security.html) I don't want to directly acccess the DOM.
Does anyone have some other Idea?
You could do it like this
import { Component } from '#angular/core';
#Component({
selector: 'my-selector',
template: '<div #myDiv (click)=getElementDimensions(myDiv)>Hello</div>'
})
export class AboutComponent {
constructor() { }
getElementDimensions(el: HTMLDivElement) {
console.log(el.getBoundingClientRect())
}
}
As I thought more about my Question:
Actually accessing the DOM-Element through ElementRef is no security threat in this case, as no (unsafe-)data is injected through it.
If you want to get around using ElementRef (eg. for server side rendering) and want to manipulate a DOM-Element I recommend to look at the Renderer class.

Angular2 Undefined Object attribute when using a callback function as Component #Input()

Im trying to bind a callback function to a directive, when the event is fired the attribute of the parent component are undefined
app.ts
import {Component} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {MyComponent} from './my-component';
#Component({
selector: 'my-app',
template: `
<button (click)="appOnClick('CLICK FROM APP')">BUTTOM OUTSIDE COMPONENT</button>
<br><br>
<my-component [onClick]="appOnClick"></my-component>`,
directives: [MyComponent]
})
export class MyApp {
public theBoundCallback: Function;
test:string = "THIS SHOULD HAVE A VALUE";
public ngOnInit(){
this.theBoundCallback = this.appOnClick.bind(this);
}
appOnClick(someText){
console.log(someText);
console.log(this.test);
}
}
bootstrap(MyApp);
my-component.ts
import {Component, Input} from 'angular2/core';
#Component({
selector: 'my-component',
template: `<button (click)="onClick('CLICK FROM COMPONENT')">BUTTOM INSIDE COMPONENT</button>`
})
export class MyComponent{
#Input() onClick: Function;
}
That will render two buttons:
BUTTOM OUTSIDE COMPONENT, this calls the appOnClick function direct from the app, when clicked the console shows:
- CLICK FROM APP
- THIS SHOULD HAVE A VALUE
BUTTOM INSIDE COMPONENT, this calls the appOnClick function via the #Input function in the component, when clicked the console shows:
- CLICK FROM APP
- undefined
I've created the example on Plunker
Is that a way to assign this correctly so I can work with my object attributes when the callback is trigger?
Updated plunkr
In order to pass appOnClick around this way, you need to declare it as a property like so:
export class MyApp {
...
appOnClick = (someText) => {
console.log(someText);
console.log(this.test);
}
}
instead of:
export class MyApp {
...
appOnClick(someText){
console.log(someText);
console.log(this.test);
}
}
I think that you forgot "(...)" when using the appOnClick method and use "[...]" instead of "(...)" when configuring the event handler:
<my-component (onClick)="appOnClick($event)"></my-component>`,
Moreover within your sub component you need to define a custom event with "#Output":
#Component({
selector: 'my-component',
template: `<button (click)="handleClick('CLICK FROM COMPONENT')">BUTTOM INSIDE COMPONENT</button>`
})
export class MyComponent{
#Output()
onClick:EventEmitter<string> = new EventEmitter();
handleClick(txt:string) {
this.onClick.emit(txt);
}
}