Issues Building Datatable Within ng-bootstrap Modal - javascript

I have an Angular 7.2.15 project that I have installed ng-bootstrap (https://ng-bootstrap.github.io/#/components/modal/examples) on with great success until now when I realize I need to change the contents of the modal dialog.
The dialog will simply display the result of an API call which I know is coming back fine from Chrome Network views, essentially for each record that comes back, we need to display the "name" attribute in the datatable as a link (eventually, that link will load a saved query by its name) and an icon to delete it.
Here are the two important snippets from the search-bar.component.html in question:
<ng-template #contentOpen let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Open Query</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
Select from the following list of saved queries:
<table #dataTable class="table">
<thead>
<tr class="table-header">
<td class="max-width-80">Search Name</td>
<td class="max-width-20">Delete</td>
</thead>
<tbody class="table-body">
</tbody>
</table>
</div>
</ng-template>
...
<div class="col-md-1">
<button class="SearchGrayButton" (click)="openModal(contentOpen)">Open Search <span class="fa fa-folder-open-o"></span></button>
</div>
Then, in our search-bar.component.ts - we successfully get the SearchHistory from our webservice from the filteringService, but rendering the results poses the issue. If I take the datatable out from the ng-template #contentOpen section above, it will render the datatable fine (albeit not in the modal as we would like it to be). So within the modal, it does not render at all, but outside of it, the table will render without issue.
openModal(content) {
this.GetSearchHistory();
this.modalService.open(content, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => {
this.closeResult = `Closed with: ${result}`;
}, (reason) => {
this.closeResult = `Dismissed`;
});
}
/* Obtains all of the search history for a user and renders table in
the modal to display them */
GetSearchHistory() {
this.filteringService.getSearchHistory().subscribe(
resp => {
console.log(resp, 'res');
let r: WebServiceResponse;
r = resp as WebServiceResponse;
this.searchHistory = r.data;
this.buildTableForSearchHistory();
},
error => { console.log(error, 'error'); }
);
}
buildTableForSearchHistory() {
const options = {
sDom: 't',
renderer: 'bootstrap',
destroy: true,
data: this.searchHistory,
columns: [
{ data: 'name',
className: 'dt-center max-width-10'
}
],
order: [0, 'desc'],
createdRow( row, data, dataIndex ) {
},
drawCallback: () => {
}
};
this.dataTable = $(this.table.nativeElement);
this.dataTable.DataTable(options);
}
As a test, I also set up a mock "refresh" button of sorts within the modal that would trigger the getSearchHistory() we see above and build the table after we know the modal is in focus, and this also does not resolve the issue. The console complains about the following line, which makes sense as I think it's having trouble finding the table in question to render to:
this.dataTable = $(this.table.nativeElement);
I don't know if it's needed beyond context especially for how simple it is, but a sample of the web service's JSON response in question:
{"status":null,"code":null,"messages":[],"data":[{"id":1,"userId":null,"name":"manual test name","queryText":null,"filtersJson":null},{"id":2,"userId":null,"name":"testname","queryText":null,"filtersJson":null},{"id":3,"userId":null,"name":"testname","queryText":null,"filtersJson":null}]}
Also noteworthy is we don't necessarily have to use DataTables here, as requirements really only are to display all the names as links and perhaps have the queryText and filtersJson as metadata since we'll need them for later. I just thought it might be a "nice to have" if we allow them to sort the results.
Does anyone have any thoughts or ways to help resolve this?

Realized binding in this manner resolves it since there was no need for the DataTables usage:
<div class="modal-body">
Select from the following list of saved queries:
<ul>
<li *ngFor="let s of searchHistory">{{s.name}}</li>
</ul>
</div>

Related

Vuejs How to have many <a> tags and hashtags within <router-link> or #click function

enter image description here
i want to have a post card like twitter post card, if clicked on any part of the post card goes to post page, but when clicked on hashtags or links goes to the hashtag or link
example below
1.
<div #click="gotoPostPage" class="text-weight-light text-justify">
{{ feed.content }}
check google if you need more information then follow #benny(a tag) or follow these hashtags
#google #finda, #checks, now when i click within this post take me to post page
</div>
now this hits the gotoPostPage function even if link or a tag is clicked
using this also
2.
<router-link :to="`/post/${ feed.id }`">
{{ feed.content }}
check google if you need more information then follow #benny(a tag) or follow
these hashtags
#google #finda, #checks, now when i click within this post take me to post page
</router-link>
goes to even when check google is clicked
Please how can i resolve this, your help is highly appreciated,
Thanks
Add #click.stop to your links. E.g:
<a href="https://google.com" #click.stop>check google</a>
This will stop propagation of click events to the parent DOM elements.
Note: #click.stop is shorthand for #click="e => e.stopPropagation()".
Docs here.
Update, based on data shown in comment:
I would avoid storing HTML id database. That's a really bad pattern. You're supposed to detach the data from the UI layer. You have to allow the UI layer to change, based on device and medium. I'd try to store that data as strings holding a hashtag name and, where the name is different than the hastag, as on object, containing both.
Something like this:
const { createApp, reactive, computed, onMounted, toRefs } = Vue;
createApp({
setup() {
const state = reactive({
items: [],
links: computed(() => state.items.map(
item => ({
name: item.name || `#${item}`,
url: item.url || `/hashtag/${item}`
})
))
});
onMounted(async() => {
/* replace this promise with an axios call to server which returns
an array, similar to the one i'm passing here:
(see axios call example below)
*/
const data = await new Promise(resolve => {
setTimeout(() => {
resolve([
'one',
'two',
'three',
'печаль',
'грусть',
{
name: '#mens',
url: '/hashtag/fıstıklıbaklava'
},
'чайная',
'джаз'
]);
});
})
// example axios call:
/*
const data = await axios
.get('http://endpoint-returning-data')
.then(({data}) => data);
*/
state.items = data;
console.log('See difference betwen items (received from server) \r\n\and links (same data, mapped for template):\r\n', {...state}, '\r\nMakes sense?');
})
return {
...toRefs(state),
log: () => console.log('You clicked on app!')
}
}
}).mount('#app')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="app">
<div v-for="link in links" :key="link.url" #click="log">
<a :href="link.url"
v-text="link.name"
#click.stop></a>
</div>
</div>
As you can see, it produces the HTML from the data. The template also applies the stopPropagation(), in the v-for.

Svelte App can read firebase database, but not add

Hello stackoverflow community.
I am making a simple database application using Svelte, Materialize CSS, and firebase.
I am able to read from the firebase database, so I know it is not a credentials problem, however upon using the "addLink" function below, the database does not update. The alerts show the date, and newLink string correctly as well. Code in context is below as well.
function addLink() {
alert(date.getTime());
db.collection("links").add({ link: newLink, id: date.getTime() });
}
<script>
import GetForm from "../layout/GetForm.svelte";
import TranslateButton from "../layout/TranslateButton.svelte";
import { db } from "../firebase.js";
let arrList = [];
let newLink = "";
let date = new Date();
db.collection("links")
.orderBy("id", "asc")
.onSnapshot(snapData => {
arrList = snapData.docs;
});
function addLink() {
alert(date.getTime());
db.collection("links").add({ link: newLink, id: date.getTime() });
}
</script>
<div class="container right-align" style="margin-top: 30px;">
<TranslateButton />
</div>
<div class="container" style="margin-top: 150px;">
<div class="row">
<div class="center-align">
<div class="row">
<form class="col s12">
<div class="row">
<div class="input-field col s10">
<textarea
id="textarea1"
class="materialize-textarea"
bind:value={newLink} />
<label for="textarea1">Put your URL here.</label>
</div>
<button
class="btn waves-effect waves-light col s2"
on:click={addLink}>
Send
<i class="material-icons right">arrow_forward</i>
</button>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="center-align">
<GetForm />
</div>
</div>
<ul>
{#each arrList as listItem}
<li>{listItem.data().link}</li>
{/each}
</ul>
</div>
As per the official documentation Set a Document indicates, you need to use the method set() to add documents to your collection. One example of an insert in the database is the following - the documentation is in Node.js, but I believe it should help you as a starting point.
let data = {
link: 'Link you want',
id: 'Id you want'
};
let setDoc = db.collection('link').doc('Id').set(data);
This is just a simple example of adding a document to a collection, but it should help you understading the logic for your.
Besides that, in the below articles, you should get more information on how to perform queries on Firestore.
Get started with Cloud Firestore
Adding Data
Let me know if the information helped you!
Adding a new entry in Firestore DB
Which tutorial are you following for firebase? That's not how you add data to firebase. Replace add() with set()
For Automatically generated ID
Pass your nothing as an argument to the doc()
// For automatically generated id
db.collection('links')
.doc()
.set({ link: newLink })
.then(() => { // fetch the doc again and show its data
ref.get().then(doc => {
console.log(doc.data()) // prints {link: newLink, id: 1321415}
})
})
For using your own id
Pass your id as an argument to the doc()
// Or you could set your own id, which is in your case,
// Pass your `id` as argument for `doc()`
db.collection("links")
.doc(date.getTime())
.set({
link: newLink
})
Add data to Cloud Firestore

Vue with VCalendar component: Keep data in sync with another Vue instance

I'm programming a page that displays a list of meetings in a table. It's also possible to edit and delete meetings. Now I'd like to offer an alternative view using VCalendar.
Data is received from the server on page load and stored in a JS variable. Both the Vue instance containing the table and the VCalendar component share this data. If I edit a table cell, the changes are reflected in the component. But when I delete a date in the table view, it remains in the calendar.
This is the relevant HTML (edit: Added some attributes to the td):
<calendar-component></calendar-component>
<table id='meetings-table'>
<tr v-for='meeting in meetings' :key='date.id'>
<td contenteditable #blur='handleInput($event,meeting,"name")>
#{{ meeting.name }}
</td>
<td>
<input type='checkbox' v-model='selected'
:value='meeting.id'>
</td>
</tr>
</table>
<div>
<button v-if='selected.length' #click='deleteMeetings'>
Delete selected rows
</button>
</div>
My JS (edit: Added handleInput method):
let table = new Vue({
el:'#meetings-table',
data: {
selected: [],
meetings: window.meetings,
},
methods: {
/**
* Deletes selected meetings.
*/
deleteMeetings: function () {
let requests = [];
// Make a single request and store it
for (let id of this.selected) {
requests.push(axios.delete('/termine/' + id)
.then(response => {
// Remove meetings
this.meetings = this.meetings.filter(t => t.id != id);
// Remove id from list of selected meetings
this.selected = this.selected.filter(elem => elem != id);
}));
}
const axiosArray = axios.all(requests);
},
/**
* Handles edits in table cells.
*/
handleInput: function($event, meeting, field) {
const newValue = $event.target.textContent;
// Update value in $data
meeting[field] = newValue;
// AJAX request follows, but is not necessary for this example to work
}
}
});
The relevant parts of the component:
<template>
<v-calendar :attributes='attributes'>
<div
slot='meeting-row'
slot-scope='{ customData }'>
<!-- Popover content omitted -->
</div>
</v-calendar>
</template>
<script>
let meetings = window.meetings;
export default {
data() {
return {
incId: meetings.length,
editId: 0,
meetings,
};
},
computed: {
attributes() {
return [
// Today attribute
{
// ...
},
// Meeting attributes
...this.meetings.map(meeting => ({
key: meeting.id,
dates: new Date('2018,11,31'),// moment(meeting.slot.date, 'DD.MM.YY').format('YYYY, MM, DD'), //meeting.dates,
customData: meeting,
order: meeting.id,
dot: {
backgroundColor: '#ff8080',
},
popover: {
// Matches slot from above
slot: 'meeting-row',
}
}))
];
}
}
};
</script>
This is what happens:
I load the page containing only a single meeting. The meeting is
shown both in the table and the calendar component. Vue devtools show
it in both meetings arrays (in the component as well as in the other
Vue instance). Using the console, I can also see it in
window.meetings.
After clicking the delete button (triggering the deleteMeetings method in my JS), the meeting is gone from the table, but remains in
the calendar, in the component's meetings array and in
window.meetings.
What do I have to change to keep the meetings arrays in sync even when deleting a meeting in the table? Note that I haven't yet implemented any methods for the calendar component.
Calendar, and table components should share a single state: currently selected meetings. From what I understand, right now you have that state in 2 separate places: table Vue instance, and a calendar-component, which is a child of some other Vue instance.
It may look like you're sharing the state already (with window.meetings), but it's not the case: you only initialize the same set of meetings when the components are created. And then changes in one component are not reflected in another component.
What you can try to do is to have meetings stored in the 'main' Vue app on your page, pass them as props to table and calendar components, and then trigger events from table and calendar components, when meetings array is modified. You should also define the event hanlders in the 'main' Vue app, and listen on components. A rough sketch of the solution:
<div id="app">
<table-component
:meetings="meetings"
#meetingUpdated="handleMeetingUpdate"
#meetingDeleted="handleMeetingDeletion"
></table-component>
<calendar-component
:meetings="meetings"
#meetingUpdate="handleMeetingUpdate"
#meetingDeleted="handleMeetingDeletion"
></calendar-component>
</div>
let app = new Vue({
el:'#app',
data: {
meetings: []
},
methods: {
handleMeetingUpdate(event) {
//
},
handleMeetingDeletion(event) {
//
},
}
//
});
I hope the above is enough to point you in the right direction. If not, please let me know, and I'll do my best to help you with this further.

Dynamically compiling and mounting elements with VueJS

The Issue
I've created a light-weight wrapper around jQuery DataTables for VueJS like so:
<template>
<table ref="table" class="display table table-striped" cellspacing="0" width="100%">
<thead>
<tr>
<th v-for="(column, index) in columns">
{{ column.name }}
</th>
</tr>
</thead>
</table>
</template>
<script>
export default {
props: ['columns', 'url'],
mounted: function () {
$(this.$refs.table).dataTable({
ajax: this.url,
columns: this.columns
});
// Add any elements created by DataTable
this.$compile(this.$refs.table);
}
}
</script>
I'm utilizing the data table like so:
<data-table
:columns="
[
{
name: 'County',
data: 'location.county',
},
{
name: 'Acres',
data: 'details.lot_size',
},
{
name: 'Price',
data: 'details.price',
className: 'text-xs-right',
},
{
name: 'Actions',
data: null,
render: (row) => {
return "\
<a #click='editProperty' class='btn btn-warning'><i class='fa fa-pencil'></i> Edit</a>\
";
}
},
]
"
url="/api/properties"
></data-table>
Note the "render" method for the Actions column. This function runs just fine and renders the button as expected, however the #click handler is not functional.
Looking around I've found two links which were not helpful:
Issue 254 on the VueJS GitHub repo provides a solution for VueJS 1.0 (using this.$compile) however this was removed in VueJS 2.0
A blog post by Will Vincent discusses how to make the DataTable re-render when local data changes dynamically, but doesn't provide a solution for attaching handlers to the rendered elements
Minimum Viable Solution
If the rendered element can't be compiled and mounted, that would alright so long as I could run methods of the DataTable component on-click. Perhaps something like:
render: (row) => {
return "\
<a onclick='Vue.$refs.MyComponent.methods.whatever();' />\
";
}
Is there any such way to call methods from outside of the Vue context?
This meets your minimum viable solution.
In your columns definition:
render: function(data, type, row, meta) {
return `<span class="edit-placeholder">Edit</span>`
}
And in your DataTable component:
methods:{
editProperty(data){
console.log(data)
}
},
mounted: function() {
const table = $(this.$refs.table).dataTable({
ajax: this.url,
columns: this.columns
});
const self = this
$('tbody', this.$refs.table).on( 'click', '.edit-placeholder', function(){
const cell = table.api().cell( $(this).closest("td") );
self.editProperty(cell.data())
});
}
Example (uses a different API, but the same idea).
This is using jQuery, but you're already using jQuery so it doesn't feel that terrible.
I played some games trying to get a component to mount in the render function of the data table with some success, but I'm not familiar enough with the DataTable API to make it work completely. The biggest issue was the DataTable API expects the render function to return a string, which is... limiting. The API also very irritatingly doesn't give you a reference to the cell you are currently in, which seems obvious. Otherwise you could do something like
render(columnData){
const container = document.createElement("div")
new EditComponent({data: {columnData}).$mount(container)
return container
}
Also, the render function is called with multiple modes. I was able to render a component into the cell, but had to play a lot of games with the mode, etc. This is an attempt, but it has several issues. I'm linking it to give you an idea what I was trying. Maybe you will have more success.
Finally, you can mount a component onto a placeholder rendered by DataTable. Consider this component.
const Edit = Vue.extend({
template: `<a #click='editProperty' class='btn btn-warning'><i class='fa fa-pencil'></i> Edit</a>`,
methods:{
editProperty(){
console.log(this.data.name)
this.$emit("edit-property")
}
}
});
In your mounted method you could do this:
mounted: function() {
const table = $(this.$refs.table).dataTable({
ajax: this.url,
columns: this.columns
});
table.on("draw.dt", function(){
$(".edit-placeholder").each(function(i, el){
const data = table.api().cell( $(this).closest("td") ).data();
new Edit({data:{data}}).$mount(el)
})
})
}
This will render a Vue on top of each placeholder, and re-render it when it is drawn. Here is an example of that.

Angular: Keep model data in sync with database

i'm making my first weapons with Angular. I cannot understand the best way to handle data modifications in model and store it in the database, e.g:
In controller:
$scope.items = [
{ id: 1, status: 0, data: 'Foo Item' }
{ id: 2, status: 0, data: 'Foooo' }
{ id: 3, status: 1, data: 'OooItem' }
];
In view:
<tr ng-repeat="item in items | orderBy:'-id'">
<td>{{item.id}}</td>
<td>{{item.data}}</td>
<td>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default" ng-click="status(item.id, 1)">Acept</button>
<button type="button" class="btn btn-default" ng-click="status(item.id, 2)">Reject</button>
</div>
</td>
</tr>
What should I do to update the item status to 1 or 2? Make a call to the server, and then retrieve the new model? Update the model data with JS, and make the call? Is any way to do it automatic? Angular provide some method to access to the current "clicked" item to update the prop (status in this case)?
Hope I was clear.
Thanks.
EDIT: So, based on Dr Jones comment, i write this using underscore.
function status(id, status) {
$http.put().then()....
//after success response, update the model
item = _.find($scope.items, function (rw) {
return rw.id == id
});
item.status = status;
}
This is a valid and correct way to this?
The answer is that it's really up to you. As with all async programming you have the option to 'auto-sync' or sync as a result of a user event (e.g. hitting a save button). It really depends on how your app is designed.
Frameworks like firebase (angular-fire lib) have handy built in auto-sync functionality, alternatively the REST Post/Put request is a more traditional design pattern.

Categories

Resources