简体   繁体   中英

Changing Component Property from Directive in Angular2

I have an Angular 1 app that works with a simple contentEditable directive, which can be used like this in templates:

<span contenteditable="true"  ng-model="model.property" placeholder="Something">

Editing the element would fire $setViewValue(element.html() and it worked as expected.

I would like to make something in Angular2 with a similarly succinct template syntax. Ideally, I would like the template to look like this:

<span contentEditable="true" [(myProperty)]="name"></span>

where 'name' is a property on the component and have the directive update the component when changed. I feel like I'm close with this ( Plunker Link ):

//our root app component
import {Component, Input, Output Directive, ElementRef, Renderer, OnInit} from 'angular2/core'

@Directive({
    selector: '[contentEditable]',
    host: {
        '(blur)': 'update($event)'
    }
})

export class contentEditableDirective implements OnInit {
    @Input() myProperty;
    constructor(private el: ElementRef, private renderer: Renderer){}

    update(event){
      this.myProperty = this.el.nativeElement.innerText;
    }
    ngOnInit(){
        this.el.nativeElement.innerText =  this.myProperty; 
    }
}

This idea works if I pass an object like {name: "someName"} but if just pass a property it seems like it's passing the value, but not the reference and so the binding doesn't flow back to the component. Is there a way to do this that will still allow a template syntax that isn't verbose but still allows easy reuse of the directive.

The directive doesn't know about its parent name property. You can though emit an event from the directive and catch it in the parent. Check this example

@Directive({
    selector: '[contentEditable]',
    host: {
        '(input)': 'update($event)' // I changed it to input to see the changes immediatly
    }
})
export class contentEditableDirective implements OnInit {

// Output that will emit outside the directive
@Output() updateProperty: EventEmitter<any> = new EventEmitter();

// When 'update' is called we emit the value
update(event){
  this.updateProperty.emit(this.el.nativeElement.innerText);
}

Now that our directive is emitting correctly, we have to catch the value in the component. For brevity only the template

<div contentEditable="true" [myProperty]="name" (updateProperty)="name = $event"></div>

updateProperty is the @Output from the directive. When it gets triggered we catch it and the value we emied will be assigned to $event . After that we assign $event to our property name and you got your app working.

Here's your plnkr working. I hope it helps.

Update

Thank to this answer I saw that it is possible what you asked for.

You can match the Output to what is called when the syntax [()] is desugared. If you have a syntax like [(myProperty)]="expr" it is desugared to [myProperty]="expr" (myPropertyChange)="expr = $event"

So changing the original answer to as follows

@Output() myPropertyChange: EventEmitter<any> = new EventEmitter();
update(event){
  this.myPropertyChange.emit(this.el.nativeElement.innerText);
}

It will give you this template, which is what you asked from the beginning.

<div contentEditable="true" [(myProperty)]="name"></div>

Here's the plnkr updated to the real correct answer.

I found this very smooth solution that worked for my case (adding a readonly role to existing UI) using the @Host decorator for injecting the component you want to set property of in the Directive. In my case I have an abstract class with readonly property that is then extended by all custom components.

@Directive({
  selector: 'authCheck'
})
export class ComponentReadonlyDirective implements OnInit {

  constructor(private authService: AuthorizationService,
              @Host() private baseComponent: BaseComponent) {
  }

  ngOnInit() {
    if (!this.authorizationService.canEdit()) {
      this.baseComponent.readonly = true;
    }
  }
}

Where in the selector part I put directly the selectors of my custom components that implement the BaseComponent (eg my-comp). This is because I want the directive to be automatically applied to all instances of my-comp. I have an additional param that can turn the directive off.

If this is a single component to use the directive - put it on the place of BaseComponent.

If multiple components will use the same directive - you'll need to specify what will be injected on the @Host parameter by specifying a provider in the extending class:

@Component({
  selector: 'my-comp',
  ...
  providers: [{
      provide: BaseComponent, useExisting: forwardRef(() => MyComp)
  }]
})
export class MyComp extends BaseComponent { ... }

Source: https://github.com/angular/angular/issues/13776

If you simply want to change the value using pure JavaScript and do not want to go towards the [(model]) route, then this is for you.

const input = this.el.nativeElement.querySelector('#myElement');
input.value = 'My Programmatic Value';
input.dispatchEvent(new Event('input'));

Issue - https://github.com/text-mask/text-mask/issues/696

Solution - https://github.com/text-mask/text-mask/issues/696#issuecomment-354887412

Hope this helps someone.

Cheers!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM