why getting the focus trigs databinding in angular 5? - data-binding

Developing a multiculture Angular5 app I've structured a class which contains a dictionary (custom ts class) to hold the translations. When the user changes the culture, anything related to it must change. It works but.... too much.
Since I use to console.log everytime the method which gets the right sentence according to the selected culture is called, I noticied that if I just click on a textbox, Angular updates everything and, incomprehensibly, if I click on nothing soon after just to leave the focus, again angular updates everything! I know there must be something related with ChangeDetectionStrategy but I tried to solve without any success.
When the app grows up, if the browser has to reload everything everytime... what a problem! The app looks like this:
a MainController.ts holds anything can be useful to the components so it is passed in every component constructor.
MainController looks like this:
#Injectable()
export class MainController extends BaseClass {
private currentCulture: string;
private platformLocalizedSentencies: KeyedCollection<LocalizedString>;
the class LocalizedString looks like this:
#Injectable()
export class LocalizedString extends BaseClass {
public DefaultText: string;
public IdTranslationIndex: number;
public Translations: KeyedCollection<string>;
constructor() {
super();
this.Translations = new KeyedCollection<string>();
}
public GetTranslation(culture: string) {
console.log('getting translation for ' + this.DefaultText + ' in culture ' + culture);
if (!this.Translations.ContainsKey(culture)) {
return '*' + this.DefaultText;
} else {
return this.Translations.Item(culture);
}
}
}
Now, there's a component (culture-selector.component.ts) which shows flags (changeDetection: ChangeDetectionStrategy.OnPush) and when the user select a flag this happens:
onCultureClick(menuItem: string) {
console.log(menuItem + ' clicked.');
this.mainController.CurrentCulture = menuItem; // this must be the thing which unleash the databinding on the other components I think and I hope
this.updateSelectedCultureUI(); // doesn't do anything special, just sets the right flag and culture name on the top of the control
this.mainController.trace(TraceType.Info, 'Culture ' + this.cultureName + ' clicked');
}
and the component where I noticied the hell wild by the databinding, the login component:
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css'],
})
export class LoginComponent extends BaseClass implements OnInit, AfterViewInit {
constructor(public mainController: MainController) {
super();
}
ngOnInit() {
this.usernamePlaceholder = this.mainController.PlatformLocalizedSentencies.Item('Username');
Now, "usernamePlaceholder" is used into the html like this:
<mat-form-field style="width: 100%;">
<input matInput [placeholder]="usernamePlaceholder.GetTranslation(mainController.CurrentCulture)" [(ngModel)]="loginInput.username" (keypress)="eventHandler($event.keyCode, 'txtUsername')" #txtUsername>
</mat-form-field>
in the following picture you can see into the console-window what happens if I just click into the username textbox.... I cleared the console before clicking. What is going on? I spent two days trying to undestand... sorry it's my first angular App.. I should have started with something easier :)
You can see username translation is get by ClassLibray.ts while the other sentencies by mainController.ts... this is because I tried different ways but same result... in mainController the code is almost the same:
.... and the method to retrieve the correct string
public GetPlatformSentence(key: string) {
console.log('getting translation for ' + key + ' in culture ' + this.CurrentCulture);
if (!this.PlatformLocalizedSentencies.ContainsKey(key)) {
return '[NOTRANSLATION]';
}
if (!this.PlatformLocalizedSentencies.Item(key).Translations.ContainsKey(this.CurrentCulture)) {
return '*' + this.PlatformLocalizedSentencies.Item(key).DefaultText;
} else {
return this.PlatformLocalizedSentencies.Item(key).Translations.Item(this.CurrentCulture);
}
}
Thank you anyway
here is what happens

Ok guys, I've tried anything without any success. Angular binding is... too pedantic :-)
The best result I was able to achieve was the refresh at first user-interaction. So I had to choose between refreshing everything everytime (which was too much) or refreshing as soon as the user clicked somewhere forcing the binding refresh (that was what I've been able to get playing with ChangeDetectionStrategy settings and some additional code).
So I solved (just for multicuture immediate binding) using a messageService. Every component subscribes and asks to a common maicontroller to update anything related on culture when it changes. Of course I needed to write e little bit more code but the result in terms of user experience, responsiveness and CPU load is very very nice. Also the executed code is the same for every component so it is not so bad.
Of course it isn't what I expected but, it works better for me.

Related

angular2 data binding between service and component properties

I need some clarification on binding between service and component properties and data binding in angular2
assume i have a service(singleton) and a component
export class Service {
name = "Luke";
object = {id:1};
getName(){return this.name};
getObject(){return this.object};
}
export class Component implements OnInit{
name:string;
object:any;
constructor(private _service:Service){}
ngOnInit():any{
//Is this 2 way binding?
this.name = this._service.name;
this.object = this._service.object;
//Is this copying?
this.name = this._service.getName();
this.object = this._service.getObject();
}
}
If you update elements by reference (if you update something into the object property), you will see the updates in the view:
export class Service {
(...)
updateObject() {
this.object.id = 2;
}
}
If you update elements by value (if you update something into the name property), you won't see the updates in the view:
export class Service {
(...)
updateName() {
this.name = 'Luke1';
}
}
See this plunkr: https://plnkr.co/edit/w7bS0fAVjOc3utnpD39b?p=preview.
Angular binding only works for bindings declared in the view (HTML).
If you want properties in your component being updated when values in a service change, you need to take care of it yourself.
Observables make this easy. See detect change of nested property for component input for an example.
If you want properties in a component updates as soon as a value in change in a service changes:
Import DoCheck from #angular/core and your service into the
component.
Call the service functions affecting the component property in ngDoCheck(){...}
The component view will be updated automatically as soon as any changes
Something like this in your component:
ngDoCheck() {
this.qty = this.cartService.getTotalQtyInCart();
}

Bidirectional data binding on a component input property

I am trying to make something work on angular2 and I am unable to find something about this behavior.
I have an application that implements a custom component like this one :
import {Component,Input} from 'angular2/core'
#Component({
selector:'my-comp',
template:`<input type="text" style="text-align:center; [(ngModel)]="inputText"> <p>{{inputText}}</p>`
})
export class MyComp{
#Input() inputText : string;
}
And I am trying to do a bidirectional databinding on my inputText variable from my component like this:
<my-comp [(inputText)]="testString"></my-comp>
Where the testString is a variable defined in the MyApp.ts which contains a string. I want my testString variable to be modified when my inputText is modified by the user.
Here is a Plunker with a simple sample code : https://plnkr.co/edit/zQiCQ3hxSSjCmhWJMJph?p=preview
Is there a way to make this works simply ? Do I have to implements an Angular2 class on my custom components and overload functions in order to make this works like an ngModel ? Do i necessarily have to create a inputTextChanged variable of EventEmitter type that emit my data when it's changed and do something like this :
<my-comp [inputText]="testString" (inputTextChanged)="testString = $event;"></my-comp>
Thank you in advance.
This is explained in the Template Syntax doc, in the Two-Way Binding with NgModel section:
<input [(ngModel)]="currentHero.firstName">
Internally, Angular maps the term, ngModel, to an ngModel input property and an ngModelChange output property. That’s a specific example of a more general pattern in which it matches [(x)] to an x input property for Property Binding and an xChange output property for Event Binding.
We can write our own two-way binding directive/component that follows this pattern if we're ever in the mood to do so.
Note also that [(x)] is just syntactic sugar for a property binding and an event binding:
[x]="someParentProperty" (xChange)="someParentProperty=$event"
In your case, you want
<my-comp [(inputText)]="testString"></my-comp>
so your component must have an inputText input property and an inputTextChange output property (which is an EventEmitter).
export class MyComp {
#Input() inputText: string;
#Output() inputTextChange: EventEmitter<string> = new EventEmitter();
}
To notify the parent of changes, whenever your component changes the value of inputText, emit an event:
inputTextChange.emit(newValue);
In your scenario, the MyComp component binds input property inputText using the [(x)] format to ngModel, so you used event binding (ngModelChange) to be notified of changes, and in that event handler you notified the parent component of the change.
In other scenarios where ngModel isn't used, the important thing is to emit() an event whenever the value of property inputText changes in the MyComp component.
I'll combine #pixelbits and #Günter Zöchbauer answers and comments to make a clear answer on my question if someone in the future is searching for this.
To make bidirectional data binding works on custom variables you need to creates your component based on the following.
MyComp.ts file :
import {Component,Input,Output,EventEmitter} from 'angular2/core'
#Component({
selector:'my-comp',
templateUrl:`<input type="text" style="text-align:center;"
[ngModel]="inputText" (ngModelChange)="inputText=$event;inputTextChange.emit($event);">`
})
export class MyComp{
#Input() inputText : string;
#Output() inputTextChange = new EventEmitter();
}
MyApp.ts file:
import {Component} from 'angular2/core'
import {MyComp} from './MyComp'
#Component({
selector:'my-app',
templateUrl:`<h1>Bidirectionnal Binding test </h1>
<my-comp [(inputText)]="testString"></my-comp><p>
<b>My Test String :</b> {{testString}}</p>`,
directives:[MyComp]
})
export class MyApp{
testString : string;
constructor(){
this.testString = "This is a test string";
}
}
There the bidirectional data binding to the inputText variable works correctly.
You can comment the answer for a more beautiful or simpler way to implement this code.
Your Plunker already contains the EventEmitter. The #Output() annotation is missing. To change the value call inputTextChanged.emit(newValue) (this also changes the value on inputText)
What I do is use a property, so when I change the data the change is emitted automatically
private _data: AnyType;
#Input() get data(): AnyType{
return this._data;
}
set data(value: AnyType) {
this._data = value;
this.dataChange.emit(this._data);
}
#Output() dataChange: EventEmitter<AnyType> = new EventEmitter();
In html you will bind the property using [(data)]
<comp [(data)]="getData()"></comp>

flex: How to respond to change of data inside a component

I've created a custom component based on Image component. I want to teach it to respond to change of binded variable. E.g. if main class has a variable balance, I want the component to change image in case balance = 100 one image, in case balance = 50 to another.
Can somebody help to understand how to do that
I am using flex3 so don't see propertyWatcner there. Also I am using component2 from main mxml file this way
<MyComp:MyIcon left="15" top="20" width="60" height="60"
id="tower" price="100" accountState="{accountMoney}"
click="drawBuildingShadow(event)" />
And inside component MyIcon I want to be able to react changes of binded accountState variable.
Without any code to go by (please include a sample of both components if you can), there are a number of ways you could approach this.
You could add a ChangeWatcher which is bound to the variable in question, and invokes a method when the property changes. This method could change the image, based on whatever conditions apply to the property.
It would look something like this:
Component 1:
[Bindable]
public var yourVariable:Number;
Component 2:
private var propertyWatcher:ChangeWatcher;
//In some initialization method -- this is JUST an example method for how to create the property watcher
public function init(): void {
...
propertyWatcher = ChangeWatcher.watch(component1, "yourVariable", onVariableUpdate);
}
//Define this as a new method to handle when the property changes
private function onVariableUpdate(event:PropertyChangeEvent):void {
if(event.newValue == 50) {
yourImage.source = newSource;
}
else if(event.newValue == 100) {
yourImage.source = otherSource;
}
}
Obviously, this is very truncated and shorthand, but hopefully it will help get you started.
Edit: ChangeWatchers do exist in Flex 3, but it sounds like you should go in a different direction. Since the code snippet you posted is a bit small, I'm going to make a few assumptions on how you might do this :)
As alxx mentioned in his comment, you can change the property accountState in your component from an actual property, to a setter/getter. This will allow you to do more extensive processing when accountState gets updated.
It should look something like this:
MyComp:
//Inside your script tag:
private var _accountState:Number;
[Bindable]
public function get accountState():Number {
return _accountState;
}
public function set accountState(state:Number):void {
_accountState = state;
switch(state) {
case 50:
yourIcon.source = "blahblahblah";
break;
case 100:
yourIcon.source = "blahblahblah2";
break;
//And so on
}
}
This won't change the code you posted: it should still work as you've written it. I haven't tested this, so it's possible I'm missing something, but hopefully this will help :)

Flex: Special-casing an item in a list or menu?

I've found it's often useful to special case the first item in a drop-down menu (ie, an instance of Menu). For example, if I want to pick a color from the list provided by a web service:
<mx:PopUpMenuButton id="colorSelelector"
dataProvider="{colorsService.lastResult}" />
I might also want a special-case, which is "enter a new color", allowing the user to enter the RGB values for a new color which isn't in the list. For example:
var newColor = { label: "Enter a new color", rgb: null };
Then used with:
<mx:PopUpMenuButton id="colorSelelector"
dataProvider="{colorsService.lastResult}"
lastOption="{newColor}" />
So, apart from changing the list I get back from the service, is there any better way to do this?
(and just a preemptive comment: this is a simplification… I'm not actually trying to make a color-picking-list)
When you bind to the dataProvider, call a function that adds your special case. For instance:
<mx:PopUpMenuButton id="colorSelector"
dataProvider="{addSpecialCases(colorsService.lastResult)}"/>
So, apart from changing the list I get
back from the service, is there any
better way to do this?
This approach is going to be the cleanest, without extending HTTPService, which would work well (but is really just altering your result ;) ):
package
{
import mx.rpc.http.HTTPService;
public class MyHTTPService extends HTTPService
{
public var appendToResult:Object;
public function MyHTTPService(rootURL:String=null, destination:String=null)
{
super(rootURL, destination);
}
[Bindable("resultForBinding")]
override public function get lastResult():Object
{
//I know what my type is, Array as an example
var myResult:Array = operation.lastResult;
myResult.push( this.appendToResult )
return myResult;
}
}
}

Why is 'textField' not instantiated when I subclass TextArea in Flex?

I'm experimenting with TextArea and inheritance to implement some additional functionality on the protected textField property.
Unfortunately, when I create a new instance of the subclass, this property is set to null. I'm probably misunderstanding the way super() works, but I thought it would have been instantiated after the constructor finished.
Here's a small snippet of code which extends TextArea:
public final class ExtTextArea extends TextArea {
public function ExtTextArea() {
super();
}
public function testTextField():void {
if (textField == null)
Alert.show("null!");
}
}
}
The invoking code is simple:
var extTextArea:ExtTextArea = new ExtTextArea();
extTextArea.testTextField();
The Alert in ExtTestArea appears every time I run this code.
Why is this? Is there something more I need to do to access the textField property?
Since textField is "the internal UITextField that renders the text of this TextArea" I believe it will remain null until you add it to the display via .addChild(...). I ran a quick test to verify that once I've added it to the display, it is no longer null. You might want to add an event handler to the "creation complete" event and adjust it at that point (I think).
The Flex SDK comes with source code, so you can take a peek and see when this field is initialized. It is not initialized in the constrcutor, but you will see that a new TextField instantiated by createChildren(), which is called when the component is added to a layout container.

Resources