11
votes

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.

3
I'll add that I can do this if I add 2 inputs to the directive and pass both the object and property name as separate inputs. My template would then look like this: <myElement contentEditable="true" [myobject]="anobject" property="somevalue"> Then I can manipulate myobject[property] from the directive. Would prefer a cleaner way though. - Mark

3 Answers

11
votes

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.

4
votes

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 (e.g. 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

2
votes

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!