Angular cdkDropList drag element restriction - javascript

I am currently using Angular 2 and a drag and drop module from https://material.angular.io/cdk/drag-drop/overview. I have made the drag and drop features work. I have two different types of class objects that i desire to be limited to their own types of drag and drop lists.
This could very likely be solved with grouping the lists but since I am using recursion other issues came up...
Currently I am having every lists inside the same group, meaning that anything can be dragged and dropped in every list (cdkDropListGroup, is positioned in a component before the recursion part is performed).
I am trying to make the lists restricted to only accept either Element or Attribute (but not both), but I have no idea of how to do this...
I have the following:
Classes:
export class Attribute {
name: string;
type: string;
}
export class Element {
id: number;
name: string;
elements: Element[]
attributes: Attribute[];
}
HTML:
<div >
Elements
<div
cdkDropList
[cdkDropListData]="elements"
class="example-list"
(cdkDropListDropped)="drop($event)"
[cdkDropListEnterPredicate]="isElement">
<div type="button" text-align="right" class="btn btnNotInline" (click)="addNewElement()">
<img src="assets/img/IconPlus.png" class="elementListIcon"></div>
<div *ngFor="let element of elements" class="example-box" cdkDrag>
<mat-list>
<mat-list-item>
<mat-form-field appearance="standard dense" class="example-container">
<input matInput placeholder="{{element.name}}">
</mat-form-field>
</mat-list-item>
<mat-list-item>
<div
cdkDropList
[cdkDropListData]="attributes"
class="cdk-drag-list-attributes"
(cdkDropListDropped)="drop($event)"
[cdkDropListEnterPredicate]="isAttribute">
<div type="button" text-align="right" class="btn btnNotInline" (click)="addNewAttribute()">
<img src="assets/img/IconPlusPurple.png" class="elementListIcon"></div>
<div *ngFor="let attribute of attributes" class="example-container" cdkDrag>
<p class="mat-input-element-attribute">
<input matInput placeholder="{{attribute.name}}">
<input matInput placeholder="{{attribute.type}}">
</p>
</div>
</div>
</mat-list-item>
<mat-list-item>
<app-listboardelement [attributes]="element.attributes" [elements]="element.elements"></app-listboardelement>
</mat-list-item>
</mat-list>
</div>
The ts. method being called (the attribute looks alike)
isElement(drag : CdkDrag){
console.log("check " + (drag instanceof Element) + typeof drag + " , "+ typeof drag.data + ", "+ drag.data + " , " +(drag.data instanceof Element));
return (drag.data instanceof Element);
}
from the output I simply gets: "check false object , undefined, undefined , false"
From this I have tried to compare the dragged object with a class.. but I didn't have any luck.
Is there any way I can limit dragged object to certain lists dynamically? I know about [cdkDropListConnectedTo] but this gave me issues with the occuring recursion and the bindings. Any guidance would be appreciated
EDIT:
Added image for presentation of how it is displayed - but does not work properly;

You can always check the drag-n-drop 'origin to destination' containers and take action accordingly, something like :
drop(event: CdkDragDrop<string[]>) {
// same container (just reorder items)
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
// from first list to second list
if (event.previousContainer.id === 'cdk-drop-list-0' && event.container.id === 'cdk-drop-list-1') {
// do something
}
// from second list to first list
if (event.previousContainer.id === 'cdk-drop-list-1' && event.container.id === 'cdk-drop-list-0') {
// do something
}
}
}
Hope this helps!

The ts. method being called (the attribute looks alike)
isElement(drag : CdkDrag){
console.log("check " + (drag instanceof Element) + typeof drag + " , "+ typeof drag.data + ", "+ drag.data + " , " +(drag.data instanceof Element));
return (drag.data instanceof Element);
}
drag.data does not work, because you didn't assign any data to your cdkDrag via [cdkDragData]
<div *ngFor="let element of elements" class="example-box" cdkDrag [cdkDragData]="element">
...
</div>
To your question... You can either create two sets of cdkDropList arrays for elements and attributes and connect the list groups with the [cdkDropListConnectedTo] binding or connect all lists together in one array and allow the drop with the [cdkDropListEnterPredicate]="isElement" you already mentioned.
To solve the problem regarding recursion, you need to do some additional steps and check if the current drop container is the right one.
I've a detailed description in my question regarding nesting.
Angular Nested Drag and Drop / CDK Material cdkDropListGroup cdkDropList nested

Related

How can i access each element in a ngFor with a function?

I created a simple note app which for loops through an array of objects which holds note data. I have a button which opens up the note for edit by returning true when clicked returning false when clicked again.
The problem is when clicked all the notes open up in edit mode because the boolean variable is shared.
Question is: "How can i access that specific note where i clicked the edit button?"
HTML:
<div class="view-notes" *ngFor="let note of notes; index as i">
<div class="tools">
<i class="fa fa-edit" (click)="openNote(i)"></i>
<i (click)="deleteNote(i)" class="fa fa-trash"></i>
</div>
<input [disabled]="!onEdit()" type="text" [(ngModel)]="note.title" #title>
<p *ngIf="!onEdit()">{{note.date | noteDate}}</p>
<textarea type="text" *ngIf="onEdit()" name="body" id="" [(ngModel)]="note.note"></textarea>
</div>
<h1 class="error" *ngIf="noNotes()">No notes to be displayed.</h1>
The functions:
openNote(i: number) {
if (this.count == 0) {
// open
this.edit = true; this.count++;
} else {
//save
this._noteService.updateNote(i, this.notes[i].title, this.notes[i].note);
//close
this.count--;
this.edit = false;
}
}
onEdit() {
return this.edit;
}
In your title, and in your own words, you're asking:
How can i access each element in a ngFor with a function?
and
"How can i access that specific note where i clicked the edit button?"
To answer this question directly -- you can access the scoped variable that's created implicitly within the loop.
This is to say, the *ngFor="let note of notes" creates a note variable scoped to your to each iteration of the loop.
You're already doing this where your ngModel binding is in your <input> and <textarea> for the note title/text respectively.
You can also pass that variable to functions, just as you do with bindings.
So, you could use the note variable to pass to a function, which will be called using whichever note is clicked. e.g. openNote(note)
// HTML
<div class="view-notes" *ngFor="let note of notes">
<div class="tools">
<i class="fa fa-edit" (click)="openNote(note)"></i> // passes the current note you clicked
...
// TS
openNote(note: any) {
// do something with this note, here
}
That's your question answered (at least what your question is directly asking from the title).
However, your question appears to be asking more than one thing, namely to do with showing/hiding specific notes (i.e. the ones that were clicked). Please try and keep your questions focused, or at least asking the same question in your post as what the title says :)
I'll answer what I think you're asking, looking at the problem you've described in your question; which I think would be:
"How can I show just the note I wish to edit; and save/close it when I edit click again, or edit a different note?"
Regarding the show/hide of specific notes; as already pointed out, you're just showing/hiding all notes based on a single boolean (this.edit returned by onEdit()) variable, which will have the same effect on all your notes (showing/hiding them all at the same time).
Seeing as you have access to each note inside your notes array within your *ngFor loop, you could keep a record of which note is currently displayed, using a property on your component:
export class SomeComponent {
currentlyShownNote: any; // use this to store the reference of the note currently open
// rest of component code...
}
Then, you can simply check in your HTML if the currentlyShownNote is this particular one, and show/hide based on this check:
<textarea type="text" *ngIf="currentlyShownNote === note" ...>
Then, create a showHideNote function in your component to set which note is being shown when you click it:
// HTML
<div class="view-notes" *ngFor="let note of notes; index as i">
<div class="tools">
<i class="fa fa-edit" (click)="showHideNote(note)"></i>
...
// TS
showHideNote(note: any) {
// if there is a currently shown note, save it
if (this.currentlyShownNote) {
const currentNote = this.currentlyShownNote;
this._noteService.updateNote(this.notes.indexOf(currentNote), currentNote.title, currentNote.note);
}
if (this.currentlyShownNote == note) {
this.currentlyShownNote = null;
} else {
this.currentlyShownNote = note;
}
}
Or, rather than using the reference to each note variable, you could simply use the index (index as i) in the array to track which note is shown (similar to how you're deleting the notes):
// HTML
<div class="view-notes" *ngFor="let note of notes; index as i">
<div class="tools">
<i class="fa fa-edit" (click)="showHideNote(i)"></i>
<i (click)="deleteNote(i)" class="fa fa-trash"></i>
</div>
<input [disabled]="shownNoteIndex !== i" type="text" [(ngModel)]="note.title" #title>
<p *ngIf="shownNoteIndex !== i">{{note.date | noteDate}}</p>
<textarea type="text" *ngIf="shownNoteIndex === i" name="body" id="" [(ngModel)]="note.note"></textarea>
</div>
// TS
shownNoteIndex?: number; // property to hold the currently shown note index
showHideNote(noteIndex: number) {
// if there is a currently shown note, save it
if (this.shownNoteIndex >= 0) {
const i = this.shownNoteIndex;
this._noteService.updateNote(i, notes[i].title, notes[i].note);
}
if (this.shownNoteIndex == noteIndex) {
this.shownNoteIndex = null;
} else {
this.shownNoteIndex = noteIndex;
}
}
BONUS: For coming back round full circle, you can create another function in your component to make your shownNoteIndex === i and shownNoteIndex !== i (or even your currentlyShowNote === note) checks even more succinct:
// HTML
<div class="view-notes" *ngFor="let note of notes; index as i">
<div class="tools">
<i class="fa fa-edit" (click)="showHideNote(i)"></i>
<i (click)="deleteNote(i)" class="fa fa-trash"></i>
</div>
<input [disabled]="!isNoteShown(i)" type="text" [(ngModel)]="note.title" #title>
<p *ngIf="!isNoteShown(i)">{{note.date | noteDate}}</p>
<textarea type="text" *ngIf="isNoteShown(i)" name="body" id="" [(ngModel)]="note.note"></textarea>
</div>
// TS
// if you're using the note index
isNoteShown(noteIndex: number) {
return this.shownNoteIndex === noteIndex;
}
// or
// if you're using the note reference
isNoteShown(note: any) {
return this.currentlyShownNote === note;
}
Your edit flag should be a number representing which note should be edited. Then you can check if the item in the list matches the edit number and display edit for just that one.
try calling the function with doSomething($event) in the html
and define that function in ts file as doSomething(event){}

How would I add an "active" class to the rendered images based on object properties?

I have the following JSON file - https://ddragon.leagueoflegends.com/cdn/9.14.1/data/en_US/runesReforged.json
As you can see, there are 5 main rune categories and each category has 4 slots which contain 3 runes each ( except the first one which contains 4 runes ). My code below iterates over the 4 slots and render the images of all runes per slot. My end result looks like this:
All good so far, except that I want to to add an active class to the runes that are selected by the user. All selected runes can be found as properties inside a data property called match.
data(){
return {
match: null
}
},
methods: {
primaryPerks(){
let perksTree = Object.entries(this.$store.state.summonerRunes).find(([key,value]) => value.id === this.match.mainParticipant.stats.perkPrimaryStyle);
return perksTree[1]
}
}
It's null because the object is assigned after a GET request. In that object there are properties called perk0, perk1, perk2 and perk3 which are the ids of the runes. I have to somehow insert a check in my iteration that renders the images and add an "active" class to the rune images if their id is equal to any of the perk properties. The problem is that I'm not sure how to implement that check.
<div class="runes">
<div v-for='runes in this.primaryPerks().slots' class="perks">
<div v-for='rune in runes.runes' class="perk">
<img class='inactive' :src="'https://ddragon.leagueoflegends.com/cdn/img/' + rune.icon" alt="">
</div>
</div>
</div>
So at some point match object looks something like this?
data(){
return {
match: {
perk0: 8112,
perk1: 8113,
perk2: 8115,
perk3: 8122,
}
}
},
If so, than this is how to conditionaly attach a class.
<div class="runes">
<div v-for='runes in this.primaryPerks().slots' class="perks">
<div v-for='(rune,index) in runes.runes' class="perk">
<img :class="{inactive: Object.values(match).find(rune.id)}" :src="'https://ddragon.leagueoflegends.com/cdn/img/' + rune.icon" alt="">
</div>
</div>
</div>
I would suggest you make the method for rune checking which returns true or false and takes id as an argument.
checkRune(id) {
return Object.values(match).find(id)
}
then your class should look like :class="{inactive: checkRune(rune.id)}

Remove the selected option from select box

I am making angular application with angular form.
Here i have given a form with input fields first name and last name which will always showing..
After that i am having children which will be displayed upon clicking the add button and the children will get removed on click remove button.
As of now everything works fine.
Here i am making patching of data to the inputs on click option from select box.. The neccessary inputs gets patched..
HTML:
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div *ngFor="let question of questions" class="form-row">
<ng-container *ngIf="question.children">
<div [formArrayName]="question.key">
<div *ngFor="let item of form.get(question.key).controls; let i=index" [formGroupName]="i">
<div *ngFor="let item of question.children">
<app-question [question]="item" [form]="form.get(question.key).at(i)"></app-question>
</div>
</div>
<select multiple (change)="changeEvent($event)">
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
</div>
</ng-container>
<ng-container *ngIf="!question.children">
<app-question [question]="question" [form]="form"></app-question>
</ng-container>
</div>
<div class="form-row">
<!-- <button type="submit" [disabled]="!form.valid">Save</button> -->
</div>
</form> <br>
<!-- Need to have add and remove button.. <br><br> -->
<button (click)="addControls('myArray')"> Add </button>
<button (click)="removeControls('myArray')"> Remove </button><br/><br/>
<pre>
{{form?.value|json}}
</pre>
</div>
TS:
changeEvent(e) {
if (e.target.value == 1) {
let personOneChild = [
{ property_name : "Property one" },
{ property_name : "Property two" },
]
for (let i = 0; i < personOneChild.length; i++) {
this.addControls('myArray')
}
this.form.patchValue({
'myArray': personOneChild
});
}
if (e.target.value == 2) {
let personTwoChild = [
{ property_name : "Property three" },
{ property_name : "Property four" },
{ property_name : "Property five" },
]
for (let i = 0; i < personTwoChild.length; i++) {
this.addControls('myArray')
}
this.form.patchValue({
'myArray': personTwoChild
});
}
}
addControls(control: string) {
let question: any = this.questions.find(q => q.key == control);
let children = question ? question.children : null;
if (children)
(this.form.get(control) as FormArray).push(this.qcs.toFormGroup(children))
}
removeControls(control: string) {
let array = this.form.get(control) as FormArray;
array.removeAt(array.length - 1);
}
Clear working stackblitz: https://stackblitz.com/edit/angular-x4a5b6-fnclvf
You can work around in the above link that if you select the person one option then the value named property one and property two gets binded to the inputs and in select box the property one is highlighted as selected..
The thing i am in need is actually from here,
I am having a remove button, you can see in demo.. If i click the remove button, one at last will be got removed and again click the last gets removed..
Here i am having two property one and two, if i remove both the inputs with remove button, the the highlighted value person one in select box needs to get not highlighted.
This is actually my requirement.. If i remove either one property then it should be still in highlighted state.. Whereas completely removing the both properties it should not be highlighted..
Hope you got my point of explanation.. If any needed i am ready to provide.
Note: I use ng-select for it as i am unable implement that library, i am making it with html 5 select box.. In ng-select library it will be like adding and removing the option.. Any solution with ng-select library also appreciable..
Kindly help me to achieve the result please..
Real time i am having in application like this:
Selected three templates and each has one property with one,two,three respectively:
If choose a dropdown then the property values for the respective will get added as children.
Here you can see i have deleted the property name three for which the parent is template three and the template three still shows in select box even though i removed its children
Firstly, get a reference to the select, like so:
HTML:
<select multiple (change)="changeEvent($event)" #mySelect>
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
TS:
import { ViewChild } from '#angular/core';
// ...
#ViewChild('mySelect') select;
Then, in your remove function, check if all elements have been removed, and if they have, set the value of the select to null
if (array.length === 0) {
this.select.nativeElement.value = null;
}
Here is a fork of the StackBlitz

Dragula and Angular 5 - update object location value by dropping

I have a bag with several divs where each div has a list of items that all have the same value for location, so the "Misc" div has items that all have "location: misc", the "Armor" div has items that all have "location: armor", etc.
I can sort the items into their respective divs but I want to be able to drag an item to another div and then change the item's location value accordingly, but I have no idea how to go about this.
I tried this solution but I must not be understanding it correctly.
Code Snippet - this just consoles 'undefined'
HTML:
<div
[dragula]='"bag-equipment"'
[dragulaModel]="equipmentBagOfHolding"
[attr.data-id]="bag-equipment"
>
<mat-card
*ngFor="let item of equipmentBagOfHolding"
>
{{ item.name }}
</mat-card>
</div>
<div
[dragula]='"bag-equipment"'
[dragulaModel]="equipmentArmor"
[attr.data-id]="bag-equipment"
>
<mat-card
*ngFor="let item of equipmentArmor"
>
{{ item.name }}
</mat-card>
</div>
TS:
dragulaService.drop.subscribe(value => {
const [bagName, e, el] = value;
console.log('id is:', e.dataset.id);
});
I realized that I can get the origin and destination with:
this.dragula.drop.subscribe(value => {
//Bag
console.log(value[0]);
// What is moved
console.log(value[1]);
// Destination
console.log(value[2]);
// Origin
console.log(value[3]);
});
And find the id, of the destination for example, with:
console.log(value[2]['id']);

how to make autocomplete in angularjs (filter list not display )

I am trying to make autocomplete in angular js .But when I type anything on text field it not reflect on view In other word it not give filter list after typing on text field .I have station name and station code .I need to filter my list with filter code. here is my code
http://codepen.io/anon/pen/xGxKKE
When I type "B" it should display the list which have station code started from "B" . could you please tell me where i am doing wrong ?
<ion-content>
<div class="list">
<label class="item item-input">
<span class="input-label">StationName</span>
<input type="text" ng-model="station.stationCode" class="bdr">
</label>
</div>
<div class="list">
<li class="item" ng-repeat="station in data.data">{{station.stationName+"-("+station.stationCode+")"}}</li>
</div>
</ion-content>
You weren't actually filtering the list:
http://codepen.io/anon/pen/rVNBOO
Added:
$scope.startsWith = function (actual, expected) {
var lowerStr = (actual + "").toLowerCase();
return lowerStr.indexOf(expected.toLowerCase()) === 0;
};
and changed ng-repeat:
ng-repeat="station in data.data | filter:station.stationCode:startsWith"
Edit:
The code in startsWith takes the actual value (the stationName in your case) and checks to see if it finds the searched characters at the beginning of the string. If indexOf "your searched string" is === 0 - then the string starts with those characters.
<li class="item" ng-repeat="station in data.data | filter:station.stationCode">
This will allow the list to filter based on the content in textbox.
Note: this will work as Contains filter rather than StartsWith.

Categories

Resources