3

I'm working on a form and I'm using Angular Reactive Forms. I need to have multi-select in my form, I did get it done by pressing ctrl and selecting multiple options. I would like to be able to use multiselect without pressing the ctrl key. Would be possible?

Form Component:

this.productForm = new FormGroup({
  'items': new FormControl(this.product.items, Validators.required),
});

Template:

<div class="form-group">
  <select multiple="multiple" class="form-control" id="items" formControlName="items" required>
    <option *ngFor="let item of itemList" [value]="item.id">{{item.name}}</option>
  </select>
</div>

UPDATE:

I tried the greg95000's solution, but it's only a partially working solution.

<div class="form-group">
  <select multiple="multiple" class="form-control" id="items" formControlName="items" required>
    <option (mousedown)="onMouseDown($event)" (mousemove)="$event.preventDefault()" *ngFor="let item of items" [value]="item.id">{{item.name}}</option>
  </select>
</div>


public onMouseDown(event: MouseEvent) {
  event.preventDefault();
  event.target['selected'] = !event.target['selected'];
}

THIS SOLUTION BREAKS THE DATA BINDING

Multiselect view

Fabio
  • 1,272
  • 3
  • 21
  • 41

2 Answers2

0

I have a solution that works. Because i copy the array angular knows there is a change so the select gets updated. this is my select:

 <select class="form-control" multiple name="positionTypes" [(ngModel)]="freelancer.positionTypes" #select1>
  <option *ngFor="let positionType of positionTypes" [value]="positionType" (mousedown)="false" 
   (click)="positionTypMouseDown($event)">{{'PositionType.'+positionType | translate}}</option> 
 </select>

and this is the function in my component:

positionTypMouseDown( event: any ) {
    event.stopPropagation();
    let scrollTop = 0;
    if ( event.target.parentNode ) {
        scrollTop = event.target.parentNode.scrollTop;
    }
    const stringValue = event.target.value.split( '\'' )[1];
    const index = this.freelancer.positionTypes.indexOf( stringValue, 0 );
    if ( index > -1 ) {
        this.freelancer.positionTypes.splice( index, 1 );
    } else {
        this.freelancer.positionTypes.push( stringValue );
    }
    // to make angular aware there is something new
    const tmp = this.freelancer.positionTypes;
    this.freelancer.positionTypes = [];
    for ( let i = 0; i < tmp.length; i++ ) {
        this.freelancer.positionTypes[i] = tmp[i];
    }
    setTimeout(( function() { event.target.parentNode.scrollTop = scrollTop; } ), 0 );
    setTimeout(( function() { event.target.parentNode.focus(); } ), 0 );
    return false;
}

i updated my answer now it also works fine in chrome. the important part is for focus and scroll to not focus on option but on the select (which is event.target.parentNode

tibi
  • 657
  • 1
  • 10
  • 22
-2

I don't know if this can help but maybe you can use JQuery to do that. Please refer to this answer:

https://stackoverflow.com/a/27056015/9220387

Here is the code in JQuery:

$("select").mousedown(function(e){
   e.preventDefault();

   var select = this;
   var scroll = select .scrollTop;

   e.target.selected = !e.target.selected;

   setTimeout(function(){select.scrollTop = scroll;}, 0);

   $(select ).focus();
}).mousemove(function(e){e.preventDefault()});

in full Javascript:

element.onmousedown= function(event) {
    //this == event.target
    event.preventDefault();
    var scroll_offset= this.parentElement.scrollTop;
    this.selected= !this.selected;
    this.parentElement.scrollTop= scroll_offset;
}
element.onmousemove= function(event) {
    event.preventDefault();
}

Look at the parent element(the select box) and record the vertical scroll offset before selecting/deselecting the option. Then reset it manually once you have performed the action.

The reasoning behind preventing default behavior for the mousemove event is because if you don't prevent it and you happen to click an option while moving the mouse, all options will become de-selected.

UPDATE

You can try this solution, I don't know if there is a better solution but mine works. Change your onMouseDown method to :

  public onMouseDown(event: MouseEvent, item) {
    event.preventDefault();
    event.target['selected'] = !event.target['selected'];
    if(event.target['selected']) {
      this.productForm.value.items.push(item.id);
    } else {
      let index: number = -1;
      index = this.productForm.value.items.indexOf(item.id);
      if(index > -1) {
          this.productForm.value.items.splice(index);
      }
    }
  }

I don't think that you can have a fully and native binded data with this kind of selector (because of the preventDefault). Of course you have to change your html to this :

<div class="form-group">
    <select multiple="multiple" class="form-control" id="items" formControlName="items" required>
      <option (mousedown)="onMouseDown($event, item)" (mousemove)="$event.preventDefault()" *ngFor="let item of itemList" [value]="item.id">{{item.name}}</option>
    </select>
</div>
greg95000
  • 74
  • 5
  • I would like to use just Angular/Typescript or just plain JavaScript without any additional libraries like jQuery. – Fabio Jan 18 '18 at 10:21
  • 1
    Then you can use this answer : https://stackoverflow.com/a/30110103/9220387 It's plain Javascript – greg95000 Jan 18 '18 at 10:23
  • @greg95000 I already tried using your suggested solution and it's not working properly. Please, see my update – Fabio Jan 18 '18 at 11:09
  • @greg95000 I tested your solution and it wasn't working properly. Anyway, now I fixed the small bug by changing `this.productForm.value.items.splice(index)` to `this.productForm.value.items.splice(index, 1)` so in this way only one element will be removed. I just accepted your answer, Thanks – Fabio Jan 23 '18 at 21:08
  • @greg95000 The only issue that I'm still having is related to the scroll offset. I tried using `let scrollOffset = event.target['parentElement']['scrollTop'];`, then `event.target['parentElement']['scrollTop'] = scrollOffset;`, but this doesn't solve the issue – Fabio Jan 23 '18 at 21:16