I am currently writing a class for an Angular 2 component that is using Input/Output decorators and a setter like so:
export class ItemDetails {
// Assign our `item` to a locally scoped property
#Input('item') _item: Item;
originalName: string;
selectedItem: Item;
// Allow the user to save/delete an item or cancel the
// operation. Flow events up from here.
#Output() saved = new EventEmitter();
#Output() cancelled = new EventEmitter();
// Perform additional logic on every update via ES6 setter
// Create a copy of `_item` and assign it to `this.selectedItem`
// which we will use to bind our form to
set _item(value: Item) {
if (value) this.originalName = value.name;
this.selectedItem = Object.assign({}, value);
}
}
I am pretty sure unless I missed something that this code should be fine, yet I get the error:
error TS2300: Duplicate identifier '_item'
Any insight as to why this is would be very much appreciated :)
To accomplish what I was trying to do, this revised class works fine:
export class ItemDetails {
#Input('item') set _item(value: Item) {
if (value) this.originalName = value.name;
this.selectedItem = Object.assign({}, value);
}
originalName: string;
selectedItem: Item;
#Output() saved = new EventEmitter();
#Output() cancelled = new EventEmitter();
}
A setter doesn't attach onto an existing property, it is its own class member - you can't define _item and then name a setter the same thing.
Related
I have my app.component with a list of objects
class Hero {
alias: string;
constructor(public firstName: string,
public lastName: string) {
}
}
class AppComponent {
...
heroes: Hero[] = [
new Hero("foo", "bar")
];
...
onHeroChange($event: Hero, index: number): void {
this.heroes[index] = $event;
}
<div *ngFor="let hero of heroes; let index=index">
<hero [hero]="hero" (heroChange)="onHeroChange($event, index)"></hero>
</div>
The HeroComponent is
export class HeroComponent {
#Input()
set hero(newValue: Hero) {
this._hero = newValue;
this.showAlias = !!newValue.alias;
}
get hero(): Hero {
return this._hero;
}
#Output() heroChange: EventEmitter<Hero> = new EventEmitter<Hero>();
showAlias: boolean = !1;
private _hero: Hero;
//
#HostListener('click')
onHeroClick(): void {
this.hero.alias = `alias_${+new Date()}`;
console.info('HERO TO EMIT', this.hero);
this.heroChange.emit(this.hero);
}
}
My problem is that even by assigning the changed hero in app.component, the set hero inside hero.component is not called, so showAlias in the example is not updated and I don't see the alias in the hero.component.
Do I need to force the ngFor by assigning the entire array?
Maybe a workaround could be removing the object from the array and then inserting again?
Sounds like useless computation though.
Note: this is just an example, it's not what I'm really working on, so something like
Update the showAlias prop in the onHeroClick method
or
Assign hero in the hero.component
unfortunately don't solve the issue. I need the changes to be on the outside because other stuff happens.
Could be another option changing the detection to onPush and marking for check manually?
Blitz ==> https://stackblitz.com/edit/angular-ivy-kpn3ds
You're not setting a new hero, you're just modifying a property on the existing one:
this.hero.alias = `alias_${+new Date()}`;
That doesn't fire the setter. Change the line like this:
this.hero = {...this.hero, alias: `alias_${+new Date()}`};
Is there a good way to use getter / setter pattern for array properties?
For example:
export class User {
private _name: string;
set name(value: string) {
this._name = value;
}
get name(): string {
return this._name;
}
private _roles = new Array<string>();
set roles(value: Array<string>) {
this._roles = value;
}
get roles(): Array<string> {
return this._roles;
}
constructor() {
}
}
While changing user.name fires the setter method, adding or removing items from roles does not.
Now i think i understand why it does not fire the setter, because adding items to the array does not change the pointer but merely adds to the already allocated space (correct me if i'm wrong).
How can we get the desired getter / setter behaviour on array properties?
As you said doing something like user.roles.push('my-role') will merely mutate the existing array. Instead of giving direct access to the array through the roles-setter, you could add methods like addRole and removeRole. Then you can implement whatever logic you need when adding or removing to the roles array, keeping it totally private.
I've a class as:
class Field{
constructor(name) {
this.name= name
this.otherAttr = null
}
changeName(newName) {
this.name = newName
}
}
const f = new Field("Charanjit")
f.setName("Singh") // It shoukd reflect in observer
f.name = "Rahul" // It should also reflect in observer
How to make f object observable such that any changes in f's attributes, update the observer components.
Currently, I'm getting error: https://github.com/mobxjs/mobx/issues/1932 if I use:
#observable(f)
>>> It shows Error: https://github.com/mobxjs/mobx/issues/1932
Looking at MobX documentation, It would be probably a good approach doing something like that:
import { observable, action, decorate } from "mobx";
class Field {
name = '';
otherAttr = null;
changeName(name) {
this.name = name;
}
}
decorate(Field, {
name: observable,
otherAttr: observable,
changeName: action
})
Mark properties as observables with the decorate utility will do what are you looking for.
Please go through the docs: https://mobx.js.org/best/decorators.html
I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer
I have a backbone mobile application that is tied to a Rails web service. All models of other objects are loaded and displayed correctly, but this particular one does not. The issue I am having is that Item gets created, but does not load the attributes. I have confirmed correct json output of the rails api. The difference between this collection and all other collections of this application is that this collection has a parentID that must be used in the url to load the correct items (category_id).
Now... the funny thing is that if I remove the {category_id: options.attributes[0].category_id} argument to the constructor call of ItemCollectionView and hard code a category_id directly into the url, it works! (however I need to assign this dynamically based on the category of the parent view.
Here are my classes:
export class ItemCollectionView extends Backbone.View {
public template: string;
public collection: itemsImport.Items;
constructor(options?: Backbone.ViewOptions){
this.collection = new itemsImport.Items({category_id: options.attributes[0].category_id});
super(options);
}
...
public addOne(item: itemImport.Item): void{
var view: itemItemImport.ItemItemView = new itemItemImport.ItemItemView({el: this.el, model: item});
//Breakpoint right here shows that item.attributes.id = undefined
view.render();
}
}
export class Items extends Backbone.Collection {
public url: string;
constructor(attributes?: any, options?: any){
this.url = 'http://localhost:3000/categories/' + attributes.category_id + '/items';
this.model = itemImport.Item;
super(attributes, options);
}
}
In my debugging I can confirm that:
options.attributes[0].category_id == 1;
and
this.url == 'http://localhost:3000/categories/1/items';
and the response from that url is:
[{"id":1,"category_id":1,"title":"Item 1","description":null,"active":true,"comment":null,"extra":null,"deleted":"0","url":"http://localhost:3000/categories/1/items/1.json"},{"id":2,"category_id":1,"title":"Item 2","description":null,"active":true,"comment":null,"extra":null,"deleted":"0","url":"http://localhost:3000/categories/1/items/2.json"}]
which you can see is a correct json response.
So my question is: What am I doing wrong? || What is the correct way to dynamically pass variables into collections to set the correct url at runtime?
Thank you for your help
Define url as function like in second case here
var Notes = Backbone.Collection.extend({
url: function() {
return this.document.url() + '/notes';
}
});
And check network tab if you really load correct url.
The other answer selected will work if using javascript, but if using TypeScript the compiler will complain that url must be a property, not a method. The solution I found (which will work in javascript as well) is to set the url argument on the fetch() method of the collection like below
export class ItemCollectionView extends Backbone.View {
public template: string;
public collection: itemsImport.Items;
constructor(options?: Backbone.ViewOptions){
this.collection = new itemsImport.Items({category_id: options.attributes[0].category_id});
super(options);
}
public initialize(options?:any): void {
this.listenTo(this.collection, 'add', this.addOne);
this.listenTo(this.collection, 'reset', this.addAll);
this.collection.fetch({url: this.collection.seturl(options.attributes[0].category_id)});
}
...
}
I hope this helps future users looking for this functionality in Backbone
It is possible to define a dynamic url property to your Backbone model in your Typescript code. This would be better than using the url parameter in all your calls to fetch().
You could either do it using an ECMAScript 5 getter:
export class Items extends Backbone.Collection {
get url(): string {
return '/categories/' + this.get('category_id') + '/items';
}
constructor(attributes?: any, options?: any){
this.model = itemImport.Item;
super(attributes, options);
}
}
or by setting it directly on the prototype:
export class Items extends Backbone.Collection {
constructor(attributes?: any, options?: any){
this.model = itemImport.Item;
super(attributes, options);
}
}
Items.prototype.url = function() {
return '/categories/' + this.get('category_id') + '/items';
};
The second solution will work on all browser.