MrTomAsh MrTomAsh - 1 month ago 6
Javascript Question

Use AngularJS 2 Component like a function in the element attribute

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!!

Answer

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

Comments