Angular nested draggable rows and columns - html

Resources I've looked at:
https://github.com/valor-software/ng2-dragula
Issues describing similar problems:
https://github.com/valor-software/ng2-dragula/issues/309
https://github.com/valor-software/ng2-dragula/issues/663
I'm using ng2-dragula and I'm trying to have nested draggable items but I cannot for the life of me figure out how to do it despite looking at examples.
This one allows columns to be dragged from one row to another just fine:
<div>
<row *ngFor="let row of rows" dragula="columns" [dragulaModel]="row?.columns">
<column *ngFor="let column of row?.columns"></column>
</row>
</div>
However I also need the rows themselves to be draggable so I did this:
<div dragula="rows" [dragulaModel]="rows">
<row *ngFor="let row of rows" dragula="columns" [dragulaModel]="row?.columns">
<column *ngFor="let column of row?.columns"></column>
</row>
</div>
This allows the rows to be draggable but now the columns doesn't work, if I try to drag a column into another row I get the error:
ERROR DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
The columns also cannot be re-ordered within the same row.
It appears as if dragging a column causes the row to be dragged instead of the column.
What am I doing wrong here?

I made an example for you. Let me know if this answers your question. Setting dragula options for the row "bag" allows you to pass your own invalid handler. So, dont let the parent container drag if you are attempting to drag something with the class name "column" for example.
https://embed.plnkr.co/Y4Gy0nHuGKpaYQObXLWG/
In your constructor, or whenever you set your dragula options for the parent dragula:
constructor(dragulaService: DragulaService) {
dragulaService.setOptions('bag', {
invalid(el, handle) {
// If the selected element className is column,
// dont allow the row to be dragged. This will allow
// the column to be dragged separate from the row.
return (el.className == "column");
}
});
}
HTML:
<div [dragula]="'bag'" [dragulaModel]="rows">
<div class="row" *ngFor="let row of rows">
{{row.value}}
<div [dragula]="'bag2'" [dragulaModel]="row.columns">
<div class="column" *ngFor="let column of row.columns">
{{column.value}}
</div>
</div>
</div>
</div>

Try this:
<div dragula="myBag" [dragulaModel]="rows" class="iAmContainer">
<row *ngFor="let row of rows" dragula="myBag" [dragulaModel]="row?.columns" class="iAmRow">
<column *ngFor="let column of row?.columns" class="iAmCol"></column>
</row>
</div>
I set dragula="myBag" to both row and column and set classes on elements.
Now in ngOnInit use code similar to this:
const bag: any = this.dragulaService.find('myBag');
if (bag !== undefined) this.dragulaService.destroy('myBag');
this.dragulaService.setOptions('myBag', {
revertOnSpill: true,
accepts: function (el, target, source, sibling) {
return !el.contains(target) &&
((el.className == 'iAmCol' && target.className == 'iAmRow') ||
(el.className == 'iAmRow' && target.className == 'iAmContainer'));
},
});
I used the className of the element and target to determine if they are compatible.

Related

Scoping Issue in Table when trying to access a specific row in a dilaog box (Vue.js, Element-ui)

I have created a table using el-table (Element-ui) in Vue.js. I want to access a specific row in the table when clicked on a button in that row, but the catch here is that after clicking on the button, a dialog box should open up and then access that specific row. When I try to access the row outside of the dialog box using scope.row, it works perfectly fine but it does not work properly when accessed inside teh dialog box, instead it runs in a loop till the end of the table.
Please find the code below:
<el-table-column prop="count"
label="Total">
<template slot-scope="scope">
<!-- {{fetchData(scope.row)}} When scope.row is accessed here, it works perfectly-->
<el-button type="text" #click="dialogVisible = true">{{scope.row.count}}</el-button>
<el-dialog
:visible.sync="dialogVisible"
:before-close="handleClose">
<!--I want to access the speicfic row clicked here, but it ends up looping through the table and doesnt send that specific row only. -->
{{fetchData(scope.row)}}
</el-dialog>
</template>
</el-table-column>
Can someone please suggest some solution to this issue in the above code? I am stuck on this for while. Would appreciate it.
Thank you.
This is a table... So fetchData will be called for each row as your code sits now.
But if you attach fetchData on the button instead, it will work. But then you would have to add a variable to the mix, or use a computed property. Anyways, I don't like calling functions in template, handle that logic in script or using computed properties. So here's what I'd do:
data() {
return {
chosenRow: null
}
},
methods: {
fetchData(row) {
this.chosenRow = row;
}
}
Template:
<template slot-scope="scope">
<el-button type="text" #click="fetchData(scope.row); dialogVisible = true">
{{ scope.row.follower_count }}
</el-button>
<el-dialog :visible.sync="dialogVisible">
{{ chosenRow }}
</el-dialog>
</template>
or just assign the row in template...
#click="chosenRow = scope.row; dialogVisible = true"

Conditionally Display Element in Angular 7

I have a <select> element in my Angular component that, based on the selected item, returns the identifier of that item.
I want to know how I would be able to conditionally display a <div> when the identifier is either not undefinded or 0.
I have the following code
<div *ngIf="this.id != undefined || this.id != null"></div>
However, it still display's the element even though the condition should theoretically be satisfied as, at the point of the <select> not having a value, should be undefined.
Are there any suggestions that would conditionally display an element using the *ngIf directive based on an id returned from a <select>?
It depends on how the select is set up. For eg., if it's of the following form
<select #sel (change)="change(sel.value)" [(ngModel)]="id">
<option [ngValue]="undefined">Undefined</option>
<option *ngFor="let option of options" [ngValue]="option">{{ option }}</option>
</select>
with options = [ 0, 1, 2, 3, 4, 5 ];
And if id is declared as id: any; in the controller, then explicit checks to undefined and null isn't required. You could do null check just with the following
<div *ngIf="id && id !== 0; else undefinedBlock">
ID is defined: {{ id }}
</div>
<ng-template #undefinedBlock>
ID is undefined
</ng-template>
Use ng-template when you want to show something depending on some condition. See example
Instead of:
<div *ngIf="isDisplayed">Item 1</div>
<div *ngIf="!isDisplayed">Item 2</div>
You can do this:
<div *ngIf="isDisplayed; else showItem2">Item 1</div>
<ng-template #showItem2>
Item 2
</ng-template>
I want to know how I would be able to conditionally display a <div>
when the identifier is either undefinded or 0.
First of all make sure this is not undefined in all cases. then you can use sth like this :
<div *ngIf="this.id"></div>
Look at this fiddle which you can manually set id to null or remove it (e.g undefined)

How to display saved filters in headers with PrimeNG?

I am able to save the filters locally using stateStorage="local" stateKey="myKey". So when the user leaves the component and returns, the data is still filtered based upon whatever filters they have set.
The problem is, the user has no idea what they are filtering on anymore, as the filter headers do not show them anything anymore, just the default label. I can access the filters via local storage, and delete them using localStorage.removeItem("myKey");, but I cannot for the life of me figure out how to get this filter information to display in the filter headers. We are not using lazyLoad, as suggested in another answer. You'd think this would be built in any time a filter is saved because not knowing what you are filtering on seems like a major flaw.
For more clarity, I have attached the primeFaces documentation below. If you select 'Red' 'White' and 'Green' from the multiselect dropdown, it will display your selected filter in the header (Red, White, Green) above. I need this information to display anytime the user enters the component if they have filters saved (both with text input, and with the dropdowns).
https://www.primefaces.org/primeng/#/table/filter
I am using multi-select dropdown filters, text input, as well as calendar filters. Here is a snippet of the html, which includes examples of these three filter types:
<th *ngFor="let col of columns" [ngSwitch]="col.field">
<input *ngSwitchCase="'userID'" pInputText type="text" size="12" placeholder="contains" (input)="table.filter($event.target.value, col.field, col.filterMatchMode)" [value]="table.filters['userID'] ? table.filters['userID'].value : ''">
<div class="ui-g ui-fluid">
<p-calendar #calendar1 class="ui-fluid" *ngSwitchCase="'myDate'" [monthNavigator]="true" [showOnFocus]="false" [yearNavigator]="true" yearRange="2010:2060" [showIcon]="true"
[showOtherMonths]="false" [showButtonBar]="true" [appendTo]="attach" [style]="{'overflow': 'visible'}"
[(ngModel)]="calendar1Filter"
(ngModelChange)="table.filter($event, 'myDate', calendar1Option)"
(onSelect)="table.filter($event, 'myDate', calendar1Option)">
<p-footer>
<div class="ui-grid-row">
<div class="ui-grid-col-3"><label style="font-weight: bold; color: #337ab7">Mode:</label></div>
<div class="ui-grid-col-6"><p-dropdown [options]="calendar1Options" [style]="{'width':'60px', 'padding-top': '0px'}" [(ngModel)]="calendar1Option" (onChange)="onChangeModCalOpt(calendar1, 1)" ></p-dropdown> </div>
</div>
</p-footer>
</p-calendar>
</div>
<div class="ui-fluid">
<p-multiSelect *ngSwitchCase="'myDropdown'" appendTo="body" [options]="dropdownOptions" pInputText type="text" size="12" [style]="{'width':'100%'}" defaultLabel="All" [(ngModel)]="myDropdownFilter" (onChange)="table.filter($event.value, col.field, 'in')"></p-multiSelect>
</div>
</th>
I did this almost a year ago; it involved some tricky code, as you'll see below, because the table element was beneath an *ngIf directive. I'm not sure if your case is the same, but if it is, here's what I had to to do get it to work. In the example, I have a fooTable that has a custom filter on the status column.
foo.component.ts:
import { ChangeDetectorRef, Component, ViewChild } from "#angular/core";
#Component({
selector : 'foo',
templateUrl : 'foo.component.html'
})
export class FooComponent {
// members /////////////////////////////////////////////////////////////////////////////////////////////////////////
showFooTable: boolean = true;
statusFilter: string[] = [];
// constructor(s) //////////////////////////////////////////////////////////////////////////////////////////////////
constructor(private cd: ChangeDetectorRef) { }
// getters and setters /////////////////////////////////////////////////////////////////////////////////////////////
/**
* Due to the Angular lifecycle, we have to do some tricky things here to pull the filters from the session,
* if they are present. The workarounds done in this function are as follows:
*
* 1) Wait until the element is accessible. This is not until the *ngIf is rendered, which is the second call to
* this function. The first call is simply 'undefined', which is why we check for that and ignore it.
* 2) Check and see if the filters for this object are even part of the template. If not, just skip this step.
* 3) If we find the filters in the session, then change this object's filters model to equal it and call the change
* detection manually to prevent Angular from throwing an ExpressionChangedAfterItHasBeenCheckedError error
* #param fooTable the reference to the foo table template
*/
#ViewChild('fooTable') set fooTable(fooTable: any) {
if(fooTable != undefined) {
let filters = fooTable.filters['status'];
if (filters != undefined && filters.value != undefined) {
this.statusFilter = filters.value;
}
this.cd.detectChanges();
}
}
}
foo.component.html:
<ng-container *ngIf="showFooTable">
<div id="filters">
<p-checkbox [(ngModel)]="statusFilter" value="up" (onChange)="fooTable.filter(statusFilter, 'status', 'in')"></p-checkbox> Up
<p-checkbox [(ngModel)]="statusFilter" value="down" (onChange)="fooTable.filter(statusFilter, 'status', 'in')"></p-checkbox> Down
<p-checkbox [(ngModel)]="statusFilter" value="unknown" (onChange)="fooTable.filter(statusFilter, 'status', 'in')"></p-checkbox> Unknown
</div>
<p-table #fooTable
stateStorage="session"
stateKey="foo-table-state">
</p-table>
</ng-container>
Turbo tables filters can be access like so. table.filters['myColumn']?.value. You will need to set the input values in the header, [value]="table.filters[col.field]?.value"
...
<tr>
<th *ngFor="let col of columns" [ngSwitch]="col.field" class="ui-fluid">
<input pInputText
type="text"
(input)="table.filter($event.target.value, col.field, col.filterMatchMode)"
[value]="table.filters[col.field]?.value">
</th>
</tr>
...
https://www.primefaces.org/primeng/#/table/state
Just figured this out. What I ended up doing was attaching the default label to a model like so:
<p-multiSelect [defaultLabel]="table.filters[col.field]?.value || 'All'">
If the table has the filter value in its state, it will populate the label else default to 'All'.

Displaying hidden Div element at specific index using Angular Material table

I have a mat-table which has the following functionality: -
(1) Add / Remove rows
(2) Add data into a row using various controls (combo-box, text box, etc).
One of the controls (Addition Information) is a text box that when a ? is entered displays a hidden 'div' element that will eventually be used to hold a list of data.
The issue I have is that if I add say 3 rows and enter a ? into the 3rd row the hidden 'div' display on all 3 rows.
I need a way to somehow index each row added to the table and only display the 'div' element of that row.
Unfortunately my knowledge of HTML is limited and I am fairly new to Angular as well.
I have created a StackBlitz solution demoing my issue. demo
HERE'S A WORKING STACKBLITZ I created an array expandedCols : boolean[] = []; that keeps track of the state (expanded or not) of your div, when you add a row, I also add one to that array with default value false, when you put ? I just change the value at index i to true.
<ng-container matColumnDef="additionalCode" class="parent" >
<th mat-header-cell *matHeaderCellDef>Additional Code</th>
<td mat-cell *matCellDef="let element; let i = index" class="parent" >
<mat-form-field class="type">
<input matInput (keyup)="toggleLookup($event, i)" autocomplete="off" (keydown.ArrowDown)="onDown()" placeholder="Enter ?">
</mat-form-field>
<div *ngIf="expandedCols[i] == true" class="child">Yah it expanded
<button (click)="expanded = false">Close</button>
</div>
</td>
</ng-container>
TS:
addRow() {
this.doAddRow();
this.expanded = false;
this.expandedCols.push(false);
}
toggleLookup(event: any, i): void {
if (event.target.value !== '?') {
return;
}
event.target.value = '';
this.expanded = true;
this.expandedCols[i] = true;
}
You should also pay attention to removing rows, do splice, you get the idea.
The proper way to solve your issue is to use row local state - so put your attribute inside column collection (new column is hidden). This way you will have an opportunity to manage expanded property of specific row. Here is your updated stackblitz.

ng directives for adding new row once using angular js

I have table of textboxes. When i try to add the text ion last row I should get new blank row of textboxes added on new row. Currently ng-Blur is there. And I am getting new blank row added on lost focus. But i want it on 1st key press.
If I use ng-keypress or ng-change then on entering each new character new row is added which is not right. Or can I restrict the event to once.
Please let me know what can be done?
following is my HTML code
<div class="col-md-5">
<input placeholder="New Key" ng-model="propertyKey" ng-blur="addNewRow()" class="form-control" type="text" id="key">
</div>
One approach would be to pass the current row index into your ng-keypress handler and only add a new row if the index is exactly one less than the row count.
So your component/directive template would look something like this:
<div ng-repeat="row in $ctrl.rows">
<input placeholder="{{ row.id }}" ng-keypress="$ctrl.handleKeypress($index)" />
</div>
where you pass the $index of the ng-repeat instance to your keypress handler, and that handler would look something like this (assuming this.rows is your data array):
this.handleKeypress = (rowIndex) => {
if (rowIndex === this.rows.length - 1) {
this.rows.push({ id: `row${rowIndex+1}` });
}
};
So once you've added one extra row beyond the current one you're in, no more will be added.
Here's a plunk showing this approach:
http://plnkr.co/edit/4HbYg4S7M67Vpib6dI0E?p=preview