Im using Angular 1.5.3 with typescript.
I have a outer and a inner component where the outercomponents holds an array named arrayValue which I pass to the innercomponent via <binding:
class InnerComponent {
controller: Function = InnerController;
bindings: any = {
arrayValue : "<"
};
...
}
The InnerController uses the $onChanges method to track any changes from one-way bindings (e. g. arrayValue):
public $onChanges(changes: any){
this.onChangesCalledCounter++;
console.log(changes);
}
If I now change the arrayValue within the outer component using:
public switchArrayValue(): void {
if(this.arrayValue === this.fruits){
this.arrayValue = this.vegetables;
} else {
this.arrayValue = this.fruits;
}
}
The $onChanges within the innercomponent gets called. However, If I change the switchArrayValue method to perform a push instead of a reassignment of the array, the $onChanges method won't get called:
public switchArrayValue(): void {
this.arrayValue.push("hello, world!");
}
Can anyone tell me why the push don't trigger the $onChanges and maybe show a workaround for that?
Here is a plnkr (that I forked).
Related
It seems there is no way to watch changes in the parent component when using two-way data binding.
I have a custom input component for collecting a tag list. Two-way data binding is setup and working between this component and its parent.
// the parent component is just a form
// here is how I'm adding the child component
<input-tags formControlName="skillField" [(tags)]='skillTags' (ngModelChange)="skillTagUpdate($event)">
</input-tags>
In the parent component how do you watch the bound variable for changes? While it's always up to date (I've confirmed this) I cannot find any guidance on reacting to changes.
I've tried:
ngOnChanges(changes: SimpleChanges) {
if (changes['skillTags']) {
console.log(this.skillTags); // nothing
}
}
And
skillTagUpdate(event){
console.log(event); // nothing
}
UPDATE:
TWDB IMHO is not what it is advertised to be. Whenever I arrive at this place where TWDB seems to be a solution I rearchitect for a service and or observable communication instead.
When you implement a two way binding of your own, you have to implement an event Emitter. The syntax for that is mandatory.
this means that you have a hook to listen to if the value changes.
Here is a demo :
<hello [(name)]="name" (nameChange)="doSomething()"></hello>
_name: string;
#Output() nameChange = new EventEmitter();
set name(val) {
this._name = val;
this.nameChange.emit(this._name);
}
#Input()
get name() {
return this._name;
}
counter = 0;
ngOnInit() {
setInterval(() => {
this.name = this.name + ', ' + this.counter++;
}, 1000);
}
Stackblitz
From what I know, this seems the less annoying way to use it, and any two way binding will follow the same rule no matter what, i.e. it ends with the Change word !
Your implementation is actually not two-way databinding, the parent and child component are just sharing a reference on the same skillTags variable.
The syntax [(tags)]='skillTags' is syntaxic sugar for [tags]='skillTags' (tagsChange)='skillTags = $event'
You need to implement tagsChange in the child component like this: #Output('tagsChange') tagsChange = new EventEmitter<any>();, then any time you want to modify tags into the children component, dont do it directly, but use this.tagsChange.emit(newValue) instead.
At this point, you'll have real two-way databinding and the parent component is the unique owner of the variable (responsible for applying changes on it and broadcasting changes to the children).
Now in your parent component, if you want to do more than skillTags = $event (implicitly done with [(tags)]='skillTags'), then just add another listener with (tagsChange)='someFunction($event)'.
StackBlitz Demo
Don't know if this is what you're looking for, but have you tried using #Input()?
In child component
#Input() set variableName(value: valueType) {
console.log(value);
}
In parent component
<input-tags formControlName="skillField" [(tags)]='skillTags'
[variableName]="skillTagUpdate($event)"></input-tags>
The input function is called every time the object binded to the function is changed.
you could listen to the change:
<input-tags formControlName="skillField" [tags]='skillTags' (tagsChange)='skillTags=$event; skillTagUpdate();'></input-tags>
or use getter and setter:
get skillTags(): string {
return ...
}
set skillTags(value) {
variable = value;
}
another approach:
export class Test implements DoCheck {
differ: KeyValueDiffer<string, any>;
public skillTags: string[] = [];
ngDoCheck() {
const change = this.differ.diff(this.skillTags);
if (change) {
change.forEachChangedItem(item => {
doSomething();
});
}
}
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
}}
1.you can use output(eventemitter)
2.easiest solution is rxjs/subject. it can be observer and observable in same time
Usage:
1.Create Subject Property in service:
import { Subject } from 'rxjs';
export class AuthService {
loginAccures: Subject<boolean> = new Subject<boolean>();
}
2.When event happend in child page/component use :
logout(){
this.authService.loginAccures.next(false);
}
3.And subscribe to subject in parent page/component:
constructor(private authService: AuthService) {
this.authService.loginAccures.subscribe((isLoggedIn: boolean) => {this.isLoggedIn = isLoggedIn;})
}
Update
for two-way binding you can use viewchild to access to your child component items and properties
<input-tags #test></<input-tags>
and in ts file
#ViewChild('test') inputTagsComponent : InputTagsComponent;
save()
{
var childModel = this.inputTagsComponent.Model;
}
Hi All I am a beginner in javaScript and currently exploring JS Web-component and I got stuck due to some use cases
1 ) I want to pass a JS Object into my component like
<my-component data=obj ></my-component>
And require to use inside my component code Like
connectedCallback () {
console.log(this.data) // it should print {"name":"xyz" , "role" : "dev"}
}
2 ) I also need to pass some functions or maybe call back functions like.
function myFunction(e){
console.log(e)
}
<my-component click=myFunction ></my-component>
please try to add code snippet also in ans that will help me to learn more JS.
Thanks
You should pass large object by Javascript.
Via a custom element method:
let comp = document.querySelector( 'my-component' )
comp.myMethod( obj )
Or setting a property:
comp.data = obj
It is best to pass in complex data using a property and not an attribute.
myEl.data = {a:1,b:'two'};
The standard on events work fine on a custom element:
function myFunction(e){
alert(JSON.stringify(e.target.data));
e.target.data = {a:1,b:"two"};
}
class MyComponent extends HTMLElement {
constructor() {
super();
this._data = 0;
this.attachShadow({mode:'open'}).innerHTML="Click Me";
}
static get observedAttributes() {
return ['data'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
}
}
get data() {
return this._data;
}
set data(newVal) {
this._data = newVal;
}
}
customElements.define('my-component', MyComponent);
<my-component onclick="myFunction(event)"></my-component>
If your component dispatches a custom event then it is best to access it through code:
function specialEventHandler(evt) {
// do something
}
myEl.addEventListener('special-event;', specialEventHandler);
I did a Udemy course with Andreas Galster and the tutor passed in a JSON object via attribute.
As you can see it needs encodeURIComponent and decodeURIComponent as well to
attributeChangedCallback (name, oldValue, newValue) {
if (newValue && name === 'profile-data') {
this.profileData = JSON.parse(decodeURIComponent(newValue));
this.removeAttribute('profile-data');
}
this.render();
}
Pass in:
<profile-card profile-data=${encodeURIComponent(JSON.stringify(profile))}>
</profile-card>
The code worked fine for me.
Ad 1) You need to use JSON.stringify(obj)
Ad 2) As far as I know All attributes need to be defined as strings. You can pass the function that is global and inside component try to eval(fn)
I'm sure this is a rookie error so forgive me since I'm new (this week) to AngularJS.
I've got an input which is a checkbox like below and it's hooked up to an ng-change event.
<input type="checkbox" ng-model="vm.hasCondoKey" ng-change="vm.onKeyCheckboxChange()" />
That^ event fires and runs the function "onKeyCheckBoxChange" in the controller.
export class CondoKeyController {
public condoKey: AdditionalItemModel;
public additionalItems: AdditionalItemModel[];
public hasCondoKey: boolean = false;
static $inject = ["$scope","CondoKeyService"];
constructor($scope: any, CondoKeyService: ICondoKeyService) {
$scope.additionalItems.forEach((addOn: AdditionalItemModel, index: number, addOnArray: AdditionalItemModel[]) => {
if (addOn.typeId === Models.AdditionalItemType.CONDO_KEY && addOn.quantity > 0) {
this.condoKey = addOn;
this.hasCondoKey = true;
}
});
}
public onKeyCheckboxChange(): void {
console.log("Checkbox changed.");
if(this.hasCondoKey === true) {
this.condoKey.typeId = AdditionalItemType.CONDO_KEY;
this.condoKey.quantity = 1;
if(!this.addOnsContainCondoKey()) {
this.additionalItems.push(_this.condoKey);
}
} else {
this.condoKey.quantity = 0;
}
}
}
The "CondoKeyController" is nested in a parent which passes the array "additionalItems" to this controller via a directive. I can see these other variables in other functions and when constructing the controller so they make it into this controller fine.
My problem is that in the function "onKeyCheckBoxChange" I can access "this.hasCondoKey" but I cannot access any of the other values such as "this.condoKey" or "this.additionalItems".
I was thinking that this had to do with the scope and context of my function since it came from a checkbox event but then I reasoned that I should not have access to the "this.hasCondoKey" value. This value reads as "true" from my breakpoints so it's been changed from its initialization value.
Does anyone know why I can access some variables and not others? Also how do I access the other variables?
Angularjs 1.6.6
Typescript ~2.3.1
Add some debug helping log to the method:
public onKeyCheckboxChange(): void {
console.log("Checkbox changed.");
console.log(this.condoKey);
console.log(this.additionalItems);
// ...
}
Check the result. It could be possible that condoKey and additionalItems are undefined, meaning they are never set.
I found when I use the service within forEach,the parameter I send to the service beacome the last one of the circulation.
I thought it should be the closure cause this problem,so I try to use the anonymous function to solve it,but didn't work
Pass the obj in Component
this function is triggered in ngOnInit may cause the problem
when I put this code into a click event,it works fine
for (var y = 0; y < this.model.data.length;y++) {
if (this.model.data[i].to[x].id === this.model.data[y].id) {
let obj = {
selectedItem : this.model.data[i],
item : this.model.data[y]
};
(function(_obj,a) {
console.log ('obje in component:');
console.log (_obj.item.id) //each obj over here is correct now
a._drawLineService.drawLine(_obj);
a._dragLineService.dragLine(_obj.item);
})(obj,this)
}
}
Get the obj in directive
this.subscription = this._drawLineService.drawLine$
.subscribe(obj => {
console.log ('drawLine:')
console.log (obj.item.id) //each obj over here become the last one of array
});
I use the observable to pass the event and parameter from component to directive
My service
import {Injectable} from '#angular/core'
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
#Injectable()
export class DrawLineService {
private _drawLine = new BehaviorSubject<any>('');
drawLine$ = this._drawLine.asObservable();
drawLine(item) {
this._drawLine.next(item);
}
}
Console result:
How to solve this?
change var obj=... to let obj=...
let define variable to block scope while var define variable to function or global scope.
You are actually keep overwriting the obj variable with your loop.
Suppose I need to add unit tests for the following class from legacy code (that has no unit test for now). It is just a simple map or dictionary.
function Map(...) { ... }
Map.prototype.put = function (key, value) {
// associate the value with the key in this map
}
Map.prototype.get = function (key) {
// return the value to which the specified key is mapped, or undefined
// if this map contains no mapping for the key
}
Map.prototype.equals = function (obj) { ... }
// ... and more bound functions
It seems there is no way to test only one function at a time. You cannot test get() without calling put(), for example. How do I unit test this?
If there is a heavy dependancy between the methods you could stub or mock out all the other methods.. Have a look at jsMock for this.
Each method has a contract, explicit or implicit. Map.put() takes some kind of input and mutates something internal or external to Map. In order to test that function, your test needs access to what is mutated. If it is internal and not exposed externally, your test must either exist inside the Map class, the state must be exposed, or the mutateable state structure must be injected into the class in a way that external access remains possible:
ie:
/*Definition*/
function MockRepository() { /*implementation of the repository*/ }
function Map(repository) { /* store the repository */ }
Map.prototype.put = function() { /* mutate stuff in the repository */ }
/*Instantiation/Test*/
var mockRepository = new MockRepository(); /*mock repository has public methods to check state*/
var myMap = new Map(mockRepository);
myMap.put(/*whatever test input*/);
/* here use the mock repository to check that mutation of state occurred as expected based on ititial state of respository and input */
if you are working with data base, for the "get" method you can create DbScripts with inserts in your data base and then get those inserted items.
then you have to create DbScripts for deleting those added items.
for the "put" test you'll have to call the get method to check if it was inserted.
you just have to configure this in your test base class.
[TestFixtureSetUp]
public virtual void InitializeTestData()
{
TestConfiguration.ExecuteSqlFilesInFolder(this.DefaultScriptDirectory + "\\SetUp");
if (!string.IsNullOrEmpty(this.TestFixtureSpecificScriptDirectory))
{
TestConfiguration.ExecuteSqlFilesInFolder(this.TestFixtureSpecificScriptDirectory + "\\SetUp");
}
}
[TestFixtureTearDown]
public virtual void FinalizeTestData()
{
if (!string.IsNullOrEmpty(this.TestFixtureSpecificScriptDirectory))
{
TestConfiguration.ExecuteSqlFilesInFolder(this.TestFixtureSpecificScriptDirectory + "\\TearDown");
}
TestConfiguration.ExecuteSqlFilesInFolder(this.DefaultScriptDirectory + "\\TearDown");
}