Hi guys!
I'm learning AngularJS 2 for a while now and now creating my own app based on Laravel 5 REST API. Anyway - that isn't very important atm.
What is important is that I want to provide the translation for the whole application and I found an issue that is hard to solve for me.
So - from the beginning... I'm created my ResourcesService that's translating the string:
getTranslation ( key: string, replace: Array<TranslationReplace> = null, locale: string = null, fallback: boolean = null ): Observable<Resource> {
var params = "key=" + key +
( replace ? "&replace=" + JSON.stringify(replace) : '') +
( locale ? "&locale=" + locale : '') +
( fallback ? "&fallback=" + fallback : '');
var headers = new Headers({'Content-Type':'application/x-www-form-urlencoded'});
return this.http.post(this.apiUrl + 'getTranslation', params, {headers: headers})
.map(this.extractData)
.startWith({ name: 'Loading...', value: 'Translating...' })
.catch(this.handleError);
}
And I created a TranslateComponent that's providing the translation, here's the whole component:
import {Component, Input, Injectable, OnInit, OnChanges, SimpleChange} from "#angular/core";
import {ResourcesService} from "../services/resources.service";
import {TranslationReplace} from "../models/TranslationReplace";
#Component({
selector: 'translate',
template: `{{translation}}`
})
#Injectable()
export class TranslateComponent implements OnInit, OnChanges {
#Input() ref: string;
#Input() replace: Array<TranslationReplace>;
#Input() locale: string;
#Input() fallback: boolean;
private translation: string;
constructor(private resourcesService: ResourcesService) {}
ngOnInit() : void {
this.getTranslation();
}
ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
for (let propName in changes) {
if(propName == 'replace') {
this.getTranslation();
}
}
}
private getTranslation(): void {
this.resourcesService.getTranslation(this.ref, this.replace, this.locale, this.fallback).forEach(translation => this.translation = translation.value );
}
}
All is working just perfect and to call for the translation I have to simply call the selector like that:
<translate [ref]="'string.to_translate'"></translate>
But...
Now I'd like to use the translation in the attribute.
So I found the ugly way to achieve it by creating the reference of the translation and the call it in the attribute. But it's very nasty...
First of all I need to add this bit to my template:
<translate [ref]="'string.to_translate'" style="display:none;" #myStringTranslation></translate>
And next in my element call it and ask for the property by the reference:
<input type="text" [(ngModel)]="input" #input="ngModel [placeholder]="myStringTranslation.translation">
And I really don't like the idea.
What I'm looking for is to call it somehow, I don't know... emit it? And make it looks better. Don't create extra elements.
So my question is:
Can I do it better? Can I somehow call the translation directly from the attribute without the reference?
** ----- UPDATE ----- **
Ok, I learn my lesson :) Thanks to Meir for showing me the right direction and also the Angular.io site for the tutorials.
So finally I added a TranslateDirective to my application:
import {Directive, Input, ElementRef, OnChanges, OnInit, SimpleChange, Renderer} from "#angular/core";
import {TranslationReplace} from "../models/TranslationReplace";
import {ResourcesService} from "../services/resources.service";
#Directive({
selector: '[translate]'
})
export class TranslateDirective implements OnInit, OnChanges {
#Input('translate') ref: string;
#Input('translateReplace') replace: Array<TranslationReplace>;
#Input('translateLocale') locale: string;
#Input('translateFallback') fallback: boolean;
#Input('translateAttr') attr: string;
private translation: string;
constructor(
private elRef: ElementRef,
private renderer: Renderer,
private resourcesService: ResourcesService
) {}
ngOnInit():void {
this.getTranslation();
}
ngOnChanges(changes: {[propKey: string]: SimpleChange}):void {
for (let propName in changes) {
if(propName == 'replace') {
this.getTranslation();
}
}
}
private getTranslation(): void {
if(this.attr)
this.resourcesService.getTranslation(this.ref, this.replace, this.locale, this.fallback).forEach(translation =>
{
this.translation = translation.value;
this.renderer.setElementAttribute(this.elRef.nativeElement,this.attr,this.translation);
});
}
}
And now can easily add the translations to the attributes like that:
<input type="text" [(ngModel)]="input" #input="ngModel [translate]="'string.to_translate'" [translateAttr]="'placeholder'">
Thanks for your help!!
You can turn it into an attribute directive:
#Directive({
selector: 'translate'
})
export class TranslateDirectiev {
#Input() translate: string;
constructor(private elRef: ElementRef){}
ngOnChanges(changes: SimpleChanges): void {
if(this.translate){
var translatedText: string = translateSvc.translate(this.translate);
this.renderer.setElementProperty(this.elementRef.nativeElement, 'innerHTML', translatedText);
}
}
}
This is a simple example without the service injection. Also, for input fields you might need to have a different approach and update the value attribute and not the innerHtml
Related
I am getting many errors at the dev tools console when adding a service into my component but the code still working but I want to get rid of from these errors
This's the service:
getPagesData(pageSlug: string): Observable<any[]> {
return this._http.get<any[]>(`${environment.apiUrl}wp/v2/pages/?slug=${pageSlug}`);
}
This is the component:
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
#Component({
selector: 'app-membership',
templateUrl: './membership.page.html',
styleUrls: ['./membership.page.scss'],
})
export class MembershipPage implements OnInit {
public pageContent: any = {};
public content: string;
constructor(
private _data: DataService
) { }
ngOnInit() {
this._data.getPagesData('memberships')
.subscribe(
page => this.pageContent = page[0]
)
}
getContent(): string {
return this.pageContent.content.rendered.replace(/\[(.+?)\]/g, "");
}
}
What cause the errors is the getContent() method! it says that is the .rendered is an undefined property but it doses defined on the API!
I have searched on that problem and most of the solutions I found it's about using the symbol ? at HTML template but I can't use that in the component itself.
If you are calling getContent() in the HTML/template, you can most likely avoid this error by either:
Making pageContent initially null and using *ngIf to only display the content once it has asynchronously resolved:
Component:
public pageContent: any = null;
Template:
<div *ngIf="pageContent">{{getContent()}}</div>
Or you could instead RxJS operators such as map() and the async pipe:
Component:
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
#Component({
selector: 'app-membership',
templateUrl: './membership.page.html',
styleUrls: ['./membership.page.scss'],
})
export class MembershipPage implements OnInit {
public pageContent: Observable<string>;
public content: string;
constructor(private _data: DataService) { }
ngOnInit() {
this.pageContent = this._data.getPagesData('memberships')
.pipe(
map(page => page[0].content.rendered.replace(/\[(.+?)\]/g, ""))
);
}
}
Template:
<div>{{pageContent | async}}</div>
That being said, you should probably have additional checks to ensure each sub-property is available prior to accessing it, but usually this type of error is because you are attempting to access the contents before they have resolved.
Hopefully that helps!
Yes, you cannot use ? Elvis (Safe navigation) operator in the component itself because it is designed for view part only.
But you can add some check in the component too to avoid such errors like -
getContent(): string {
const dataToReturn = this.pageContent && this.pageContent.content && this.pageContent.content.rendered.replace(/\[(.+?)\]/g, "");
return dataToReturn
}
.rendered is an undefined property
Also, This error may produce you have defined pageContent = {} so on {} neither content nor rendered exist , may be that is also the reason to exist such errors.
Angular recommend to strongly typecast your data before use.
I'm developing an Angular 2 SPA. My application is composed by:
One component
One directive
I've builded one directive that format text input using onfocus and onblur events. On focus event remove dots to text value, on blur event add thousand dots to text value.
Following component's code:
<div>
<input id="input" [(ngModel)]="numero" InputNumber />
</div>
Following component's TypeScript code:
import { Component } from '#angular/core';
#Component({
selector: 'counter',
templateUrl: './counter.component.html'
})
export class CounterComponent {
numero: number;
public incrementCounter() {
}
ngOnInit() {
this.numero = 100100100;
}
}
Following directive's TypeScript code:
import { Directive, HostListener, ElementRef, OnInit } from "#angular/core";
#Directive({ selector: "[InputNumber]" })
export class InputNumber implements OnInit, OnChanges {
private el: HTMLInputElement;
constructor(private elementRef: ElementRef) {
this.el = this.elementRef.nativeElement;
}
ngOnInit(): void {
// this.el.value is empty
console.log("Init " + this.el.value);
this.el.value = this.numberWithCommas(this.el.value);
}
ngOnChanges(changes: any): void {
// OnChanging value this code is not executed...
console.log("Change " + this.el.value);
this.el.value = this.numberWithCommas(this.el.value);
}
#HostListener("focus", ["$event.target.value"])
onFocus(value: string) {
this.el.value = this.replaceAll(value, ".", "");
}
#HostListener("blur", ["$event.target.value"])
onBlur(value: string) {
this.el.value = this.numberWithCommas(value);
}
private numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
private escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
private replaceAll(str, find, replace) {
return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
}
}
The following code works except that I need lost focus for show my number like "100.100.100". How can I perform this action on init data loading?
I add one example at this link: Plnkr example
Thanks
You can do this by using a Pipe which takes a boolean parameter that represents your focus/no focus action.
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({name: 'dots'})
export class DotsPipe implements PipeTransform {
transform(value: number, hasFocus:boolean): any {
if(hasFocus){
return value.toString().replace(/\./g,'');
}else{
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
}
}
Then you have to apply the Pipe on your [ngModel] and use Angular events (focus) and (focusout) to change your variable.
<input [ngModel]="numero | dots : hasFocus" (focus)="hasFocus=true" (focusout)="hasFocus=false" (ngModelChange)="numero=$event" />
I think that your directive should implement ControlValueAccessor interface https://angular.io/docs/ts/latest/api/forms/index/ControlValueAccessor-interface.html
It is needed for writing model in your directive. ControlValueAccessor interface has writeValue(value: any) method that will be initially called.
So your writeValue method will be something like this:
private onChange: (_: any) => {};
...
writeValue(val) {
const editedValue = this.numberWithCommas(val);
this._onChange(val);
}
registerOnChange(fn: any) : void {
this._onChange = fn;
}
In Angular 4 to dynamically create a component you can use ngComponentOutlet directive: https://angular.io/docs/ts/latest/api/common/index/NgComponentOutlet-directive.html
something like this:
Dynamic component
#Component({
selector: 'dynamic-component',
template: `
Dynamic component
`
})
export class DynamicComponent {
#Input() info: any;
}
App
#Component({
selector: 'my-app',
template: `
App<br>
<ng-container *ngComponentOutlet="component"></ng-container>
`
})
export class AppComponent {
this.component=DynamicComponent;
}
How do I pass #Input() info: any; information in this template <ng-container *ngComponentOutlet="component"></ng-container> ?
Such a feature was discussed in the pull request for ngComponentOutlet but was dropped for now.
Even the componentRef shown currently in https://angular.io/docs/ts/latest/api/common/index/NgComponentOutlet-directive.html is not public and therefore not available https://github.com/angular/angular/blob/3ef73c2b1945340ca6bd21f1790260c88698ae26/modules/%40angular/common/src/directives/ng_component_outlet.ts#L78
I'd suggest you create your own directive derived from https://github.com/angular/angular/blob/3ef73c2b1945340ca6bd21f1790260c88698ae26/modules/%40angular/common/src/directives/ng_component_outlet.ts#L72
and assign values to inputs like shown in Angular 2 dynamic tabs with user-click chosen components
this.compRef.instance.someProperty = 'someValue';
With the help of the post of #Günter Zöchbauer I solved a similar problem this way - I hope you can adapt it somehow.
First I defined some interfaces:
// all dynamically loaded components should implement this guy
export interface IDynamicComponent { Context: object; }
// data from parent to dynLoadedComponent
export interface IDynamicComponentData {
component: any;
context?: object;
caller?: any;
}
then I implemented them inside of the dynamically loaded component
dynamicLoadedComponentA.ts
// ...
export class DynamicLoadedComponentA implements IDynamicComponent {
// ...
// data from parent
public Context: object;
// ...
After that I built a new component which is responsible for the magic. Important here is that I had to register all dyn. loaded components as entryComponents.
dynamic.component.ts
#Component({
selector: 'ngc-dynamic-component',
template: ´<ng-template #dynamicContainer></ng-template>´,
entryComponents: [ DynamicLoadedComponentA ]
})
export class DynamicComponent implements OnInit, OnDestroy, OnChanges {
#ViewChild('dynamicContainer', { read: ViewContainerRef }) public dynamicContainer: ViewContainerRef;
#Input() public componentData: IDynamicComponentData;
private componentRef: ComponentRef<any>;
private componentInstance: IDynamicComponent;
constructor(private resolver: ComponentFactoryResolver) { }
public ngOnInit() {
this.createComponent();
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['componentData']) {
this.createComponent();
}
}
public ngOnDestroy() {
if (this.componentInstance) {
this.componentInstance = null;
}
if (this.componentRef) {
this.componentRef.destroy();
}
}
private createComponent() {
this.dynamicContainer.clear();
if (this.componentData && this.componentData.component) {
const factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(this.componentData.component);
this.componentRef = this.dynamicContainer.createComponent(factory);
this.componentInstance = this.componentRef.instance as IDynamicComponent;
// fill context data
Object.assign(this.componentInstance.Context, this.componentData.context || {});
// register output events
// this.componentRef.instance.outputTrigger.subscribe(event => console.log(event));
}
}
}
here the usage of this shiny new stuff:
app.html
<!-- [...] -->
<div>
<ngc-dynamic-component [componentData]="_settingsData"></ngc-dynamic-component>
</div>
<!-- [...] -->
app.ts
// ...
private _settingsData: IDynamicComponent = {
component: DynamicLoadedComponentA,
context: { SomeValue: 42 },
caller: this
};
// ...
I think for now you can use
https://www.npmjs.com/package/ng-dynamic-component
It is made specifically for this issue
So, What I am trying to do seems like it would be trivial. And it probably is. But I can't figure it out. My question is:How can I pass a variable from #Input to a service in an Angular2 component? (Code has been simplified)
My component is as follows:
import { Component, Input } from '#angular/core';
import { CMSService } from '../cms.service';
#Component({
selector: 'cmstext',
templateUrl: './cmstext.component.html',
styleUrls: ['./cmstext.component.css']
})
export class CMSTextComponent {
constructor(private cms: CMSService) { }
#Input() id : string;
content = this.cms.getContent(this.id); // this.id is NULL so content is NULL
}
And then my service:
import { Injectable } from '#angular/core';
#Injectable()
export class CMSService {
constructor() { }
getContent(textId:string) : string {
this.text = textId; // textId is NULL so this.text returns NULL
return this.text;
}
}
My component template:
<p>id: {{id}}</p>
<p>Content: {{content}}</p>
When <cmstext id="4"></cmstext> is added to another component template the output is:
id: 4
content:
I'm just diving into Angular2 any help or suggestions would be greatly appreciated!
Just make it a setter and put the code there:
#Input()
set id(value : string) {
this.content = this.cms.getContent(value);
}
As pointed out by #Kris Hollenbeck,ngOnInit() was the answer. My final code looked like this. The component now passed the variable to the service.
import { Component, Input, OnInit } from '#angular/core';
import { CMSService } from '../cms.service';
#Component({
selector: 'cmstext',
templateUrl: './cmstext.component.html',
styleUrls: ['./cmstext.component.css']
})
export class CMSTextComponent implements OnInit {
public content : string;
#Input() id : string;
constructor(private cms: CMSService) { }
ngOnInit() {
this.content = this.cms.getContent(this.id);
}
}
This assigned the data from the service to the variable "content" and the id passed from the element attribute to the variable "id". Both variables were then accessible to the template!
I have a component that all it does is render , its something like this:
#Component({
selector: 'my-comp',
host: ???,
template: `
<ng-content></ng-content>
`
})
export default class MyComp {
#Input() title: string;
public isChanged: boolean;
}
The component has a isChanged property and I want to apply styling on the host element based on that isChanged property. Is this even possible?
You use the class and style prefix for this. Here is a sample:
#Component({
selector: 'my-comp',
host: {
'[class.className]': 'isChanged'
},
template: `
<ng-content></ng-content>
`
})
export default class MyComp {
#Input() title: string;
public isChanged: boolean;
}
See the Günter's answer for more details:
ngClass in host property of component decorator does not work
Solution using #HostBinder
The accepted solution is using the host metadata property which goes against the rules of TSLint:
TSLint: Use #HostBinding or #HostListener rather than the host
metadata property (https://angular.io/styleguide#style-06-03)
The same can be achieved using #HostBinding instead:
import { Component, HostBinding, Input } from '#angular/core';
#Component({
selector: 'my-comp',
template: `
<ng-content></ng-content>
`
})
export default class MyComp {
#Input() title: string;
public isChanged: boolean;
#HostBinding('class.className') get className() { return this.isChanged; }
}
Not sure what you're trying to do but something like this should suffice where you use ngAfterViewInit and ElementRef:
import {AfterViewInit, ElementRef} from '#angular/core';
#Component({
selector: 'my-comp',
host: ???,
template: `
<ng-content></ng-content>
`
})
export default class MyComp implements AfterViewInit {
#Input() title: string;
public isChanged: boolean;
constructor(private _ref: ElementRef) {}
ngAfterViewInit() {
var host = this._ref.nativeElement;
if (this.isChanged) {
host.style.width = '200px';
}
}
}
If you want to do some checking for isChanged every time it changes you could implement ngDoCheck instead/as well:
ngDoCheck() {
if (this.isChanged !== this.previousIsChanged) {
var host = this._ref.nativeElement;
if (this.isChanged) {
host.style.width = '200px';
}
}
}
I think you want to let your component fire an event that can be catched by the host (and possibly pass some data with it).
To do that you would have an #output property like:
#Output() isChanged: EventEmitter<any> = new EventEmitter()
then in your code you could do:
this.isChanged.emit(some value to pass)
And catch it like:
(isChanged)="doSomething($event)"