KnockoutJs + DataTables Server side binding + ButtonEvent - datatables

I have a KnockoutJs and DataTables project. All data is coming server-side.
My Question is this:
In one column we have a button with a data-bind="click: someFunction"
Obviously this is not getting called because the page is already bound before the grid is loaded. Fair enough. Inside the table definition I override the "fnDrawCallback" function and add this code
self.addButtonEventsToGridRow(self, $("#tableDomId #buttonId"));
and the function looks like this
self.addButtonEventsToGridRow = function (model, element) {
for (var i = 0; i < element.length; i++) {
ko.cleanNode(element[i]);
ko.applyBindings(model, element[i]);
}
};
I have to do this for every button. (Obviously that in itself is ugly)
"<button type='button' id='buttonId' class='btn btn-primary btn-xs' data-bind='click: someFunction'>ButtonText</button>"
However, now the event DOES fire. I can prove this
self.someFunction = function(data) {
console.log(data);
}
Here is the problem. The "data" object passed in seems to be the entire viewmodel, not the actual row data.
I feel my solution in itself is a failure, but I cannot find a proper way to make this work.
I am open to either modifying my solution, or if someone can point me to the proper way this is supposed to work I would greatly appreciate it.
FYI, here is my UGLY workaround for this issue inside the "render" overdide for the particular column. Note if I
"<button type='button' id='buttonId' class='btn btn-primary btn-xs' data-bind='click: function(){ return someFunction(" + fulldata + ");}'>ButtonText</button>" +

Related

For each results in v-for loop how can I nest another v-for loop using a parameter from the results of the first loop

Using a v-for loop in Vue js. I am looping through the readingTasks data object which correctly produces two results from the data below.
readingTasks:Array[2]
0:Object
enabled:true
newunit:-1
task:"The part 3 guide"
unit:-1
unit_task_id:27
url:"#"
1:Object
enabled:true
newunit:-1
task:"The part 3 training units"
unit:-1
unit_task_id:28
url:"#"
The bit I am unsure about is how for each result, how do I run another Axios database call that shows if the reading Task is complete or not. For example for the first record, the complete status should be true (unit_task_id:27) and the second record should be false.
userTasks:Array[1]
0:Object
complete:true
enabled:true
newunit:-1
task:"The part 3 guide"
unit:-1
unit_task_id:27
unit_task_user_id:21
<ul>
<li v-for="task in readingTasks">
{{task.task}}
//trying to call a function that does an Axios call passing in parameters from readingTasks
{{getUserTaskByUnit(task.unit, task.unit_task_id)}}
<template v-for="usertask in userTasks">
{{usertask.complete}}
</template>
</li>
</ul>
//javascript if its useful
data: {
readingTasks: [],
userTasks: []
},
mounted() {
this.lastUnit();
},
methods: {
//functons
lastUnit: function() {
this.tasks();
},
tasks: function() {
var self = this;
var unit = this.unit;
axios.get("/WebService/units.asmx/GetTasks?unit=" + unit).then(function(response) {
self.readingTasks = response.data;
})
.catch(function(error) {
console.log(error);
})
.then(function() {
});
},
getUserTaskByUnit: function(unit, unitTaskId) {
var self = this;
axios.get("/WebService/units.asmx/GetUserTasks?unit=" + unit + "&unitTaskId=" + unitTaskId).then(function(response) {
self.userTasks = response.data;
})
.catch(function(error) {
console.log(error);
})
.then(function() {});
}
This code seems close to doing the correct thing, however {{usertask.complete}} flickers between true and false for both sets of results. Like it is stuck in a loop.
I would expect the first result to show True here and the second result to show False.
The part 3 guide - true
The part 3 training units - false
There are a few problems here.
The template has a dependency on userTasks, so every time userTasks changes it will cause the component to re-render, running the template again.
Every time the template runs it calls getUserTaskByUnit for both tasks. That will, asynchronously, update userTasks. When userTasks is updated it will trigger a re-render, which will call getUserTaskByUnit again, going round and round in an infinite loop.
Worse than just being an infinite loop, each time it renders it will trigger two requests, each of which will trigger another re-rendering. The number of requests will balloon exponentially.
When those requests do return you're then storing them in userTasks. But both responses are being stored in exactly the same place, so you'll only ever see the results of one request in the UI.
The first thing you'll need is a better data structure for storing the responses in getUserTaskByUnit. The simplest place to store them would be on the tasks in readingTask. That might look something like this:
// Note the whole task is now being passed to getUserTaskByUnit
getUserTaskByUnit: function(task) {
var self = this;
axios.get("/WebService/units.asmx/GetUserTasks?unit=" + task.unit + "&unitTaskId=" + task.unit_task_id).then(function(response) {
task.userTasks = response.data;
})
...
}
The call to getUserTaskByUnit needs moving out of the template. Moving it into the tasks method seems as good a place as any. There are also a few changes required to get it to work with the new version of getUserTaskByUnit:
tasks: function() {
var self = this;
var unit = this.unit;
axios.get("/WebService/units.asmx/GetTasks?unit=" + unit).then(function(response) {
var readingTasks = response.data;
// Pre-populate userTasks so it will be reactive
readingTasks.forEach(function(task) {
task.userTasks = [];
});
// This must come after userTasks is pre-populated
self.readingTasks = readingTasks;
readingTasks.forEach(function(task) {
// Passing task to getUserTaskByUnit, not unit and unit_task_id
self.getUserTaskByUnit(task);
});
})
...
Then within the template we'd need to loop over task.userTasks:
<ul>
<li v-for="task in readingTasks">
{{task.task}}
<template v-for="usertask in task.userTasks">
{{usertask.complete}}
</template>
</li>
</ul>
There are alternative data structures we could use depending on what other requirements you have. For example, you could retain a separate userTasks object to hold the userTasks but for that to work it would need to be a nested data structure rather than just an array. You'd need to key it by unit and then unitTaskId. The result in the template would be something like this:
<ul>
<li v-for="task in readingTasks">
{{task.task}}
<template v-for="usertask in userTasks[task.unit][task.unit_task_id]">
{{usertask.complete}}
</template>
</li>
</ul>
Much like with the earlier solution you would need to pre-populate the userTasks with empty values when readingTasks first loads to ensure the values are reactive and also to avoid the template blowing up at the undefined entries. Alternatively you could use $set and suitable v-if checks respectively.
This is all quite fiddly. It may be that you can simplify it a little based on your knowledge of the system. For example, it may be possible to form compound string keys for userTasks rather than using two levels of nesting. Or it might be that unit is a prop that can be considered constant and doesn't need including in that data structure.
Your userTasks is a view property and gets overwritten upon every call to getUserTaskByUnit (i.e. for each item in readingTasks). What you instead want is a nested structure. You should call getUserTaskByUnit in a loop as soon as readingTasks got loaded, i.e. after the line self.readingTasks = response.data;, and store the response as a property for every readingTask object.

Event handling after HTML injection with Vue.js

Vue is not registering event handler for HTML injected objects. How do I do this manually or what is a better way to work around my problem?
Specifically, I send a query to my server to find a token in text and return the context (surrounding text) of that token as it exists in unstructured natural language. The server also goes through the context and finds a list of those words that also happen to be in my token set.
When I render to my page I want all of these found tokens in the list to be clickable so that I can send the text of that token as a new search query. The big problem I am having is my issue does not conform to a template. The clickable text varies in number and positioning.
An example of what I am talking about is that my return may look like:
{
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
And the resulting output I am looking for is the sentence looks something like this in psuedocode:
When in the Course of <a #click='search("human events")'>human events</a>, it becomes necessary for <a #click='search("one people")'>one people</a> to dissolve the <a #click='search("political bands")'>political bands</a> which have connected
This is what I have tried so far though the click handler is not registered and the function never gets called:
<v-flex xs10 v-html="addlink(context.context, context.chunks)"></v-flex>
and in my methods section:
addlink: function(words, matchterms){
for(var index in matchterms){
var regquery = matchterms[index].replace(this.regEscape, '\\$&');
var query = matchterms[index];
var regEx = new RegExp(regquery, "ig");
words = words.replace(regEx, '<a href=\'#\' v-on:click.prevent=\'doSearch("'+ query +'")\'>' + query + '</a>');
}
return words;
}
As I said, this does not work and I know why. This is just showing that because of the nature of the problem is seems like regex is the correct solution but that gets me into a v-html injection situation. Is there something I can do in Vue to register the event handlers or can some one tell me a better way to load this data so I keep my links inline with the sentence and make them functional as well?
I've already posted one answer but I've just realised that there's a totally different approach that might work depending on your circumstances.
You could use event delegation. So rather than putting click listeners on each <a> you could put a single listener on the wrapper element. Within the listener you could then check whether the clicked element was an <a> (using event.target) and act accordingly.
Here's one way you could approach it:
<template>
<div>
<template v-for="segment in textSegments">
<a v-if="segment.link" href="#" #click.prevent="search(segment.text)">
{{ segment.text }}
</a>
<template v-else>
{{ segment.text }}
</template>
</template>
</div>
</template>
<script>
export default {
data () {
return {
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
},
computed: {
textSegments () {
const chunks = this.chunks
// This needs escaping correctly
const re = new RegExp('(' + chunks.join('|') + ')', 'gi')
// The filter removes empty strings
const segments = this.context.split(re).filter(text => text)
return segments.map(segment => {
return {
link: segment.match(re),
text: segment
}
})
}
},
methods: {
search (chunk) {
console.log(chunk)
}
}
}
</script>
I've parsed the context text into an array of segments that can then be handled cleanly using Vue's template syntax.
I've used a single RegExp and split, which will not discard matches if you wrap them in a capture group, (...).
Going back to your original example, v-html only supports native HTML, not Vue template syntax. So you can add events using onclick attributes but not #click or v-on:click. However, using onclick wouldn't provide easy access to your search method, which is scoped to your component.

VuesJS, generate randomkey in v-for loop

Good evening,
My problem is this: I have a loop that displays simple divs.
I have a method that specifies the dimensiosn of my div (mandatory in my case). However, when I call the method a second time by changing the sizes of the divs, it does not work because there is no re-render.
To overcome this, I generate a guid on my: key of v-for with a variable such as:
<div v-for="task in tasks" :key="task.id + guid()">...blabla...</div>
Is it possible to generate this code directly during the loop to avoid concatenation?
<div v-for="(task, maVar=guid()) in tasks" :key="maVar">...blabla...</div>
PS : code for guid() method :
guid() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16))
}
Thanks
You could create a computed property that returns an array of task with a guid added, or if you want to leave tasks untouched, return an object containing each task plus a guid,
computed: {
tasksWithGuid: function() {
return this.tasks.map(task => { return {task, key: task.id + guid() } })
}
}
<div v-for="taskWithGuid in tasksWithGuid" :key="taskWithGuid.key">
{{taskWithGuid.task.someProperty}}
</div>
There is a simpler, more concise technique shown below. It avoids polluting the iterated object with a redundant property. It can be used when there is no unique property in the objects you iterate over.
First in your viewmodel add the method to generate a random number (e.g. with Lodash random)
var random = require('lodash.random');
methods: {
random() {
return random(1000);
}
}
Then in your template reveal the index in v-for and randomize it in v-bind:key with your random() method from the viewmodel by concatenation.
<div v-for="(task, index) in tasks" v-bind:key="index + random()">
// Some markup
</div>
This is as clean as easy.
However note this approach would force redrawing each item in the list instead of replacing only items that differ. This will reset previously drawn state (if any) for unchanged items.
I do like this
function randomKey() {
return (new Date()).getTime() + Math.floor(Math.random() * 10000).toString()
}

Aurelia: Deleting array elements when changed to empty value

I have an array of strings bound to input elements:
<div repeat.for="i of messages.length">
<input type="text" value.bind="$parent.messages[i]">
</div>
I need to delete an element when the input content is deleted, without using dirty-checking.
This sounds easy - just delete the element which has empty value from the input.delegate handler, unfortunately this does not work due to an Aurelia bug #527. Here's a gist that tries this approach: https://gist.run/?id=e49828b236d979450ce54f0006d0fa0a
I tried to work around the bug by using queueTask to postpone deleting the array element, to no avail. And since the devs closed the bug because according to them it is a duplicate of a completely unrelated issue I guess it is not getting fixed anytime soon.
I am out of ideas how to implement this, so any suggestions are welcome.
Absolutely no need for any kind of dirty checking here! :)
Here's a working demo for your scenario: https://gist.run/?id=20d92afa1dd360614147fd381931cb17
$parent isn't needed anymore. It was related to pre-1.0 Aurelia versions.
If you use a variable instead of array indexes, you can leverage two-way data-binding provided by the input.
<template>
<div repeat.for="msg of messages">
<input type="text" value.bind="msg" input.delegate="onMessageChanged(msg, $index)">
</div>
</template>
So, your onChange event could be simplified like this:
msg holds the actual value of your current input.
i index will be used for deletion.
export class App {
messages = ['Alpha','Bravo','Charlie','Delta','Echo'];
onMessageChanged(msg, i){
if (!msg || msg.length === 0) {
this.messages.splice(i, 1);
}
}
}
There was a related question about a similar problem. This answer might give you more details about the main idea.
Ok, so the solution to this is not to use the buggy (in this case) aurelia 2-way binding, but to use 1-way binding and set the value from the input.delegate handler:
https://gist.run/?id=2323c09ec9da989eed21534f177bf5a8
The #marton answer seems to work at first sight, but it actually disables 2-way binding, so any changes to the inputs are not copied to the array. But it gave me an important hint how to solve the issue.
The equivalent of this html code:
<div repeat.for="msg of messages">
<input type="text" value.bind="msg">
</div>
is this:
for (let msg of messages) {
msg = 'something else'; // note: this does not change the contents of the array
}
See issue #444 for more details
Hence, this forces one-way binding. To fix this in the #marton solution, we only have to change the value from the input.delegate handler:
onMessageChanged(msg, i){
if (!msg || msg.length === 0) {
this.messages.splice(i, 1);//delete the element
}
else {
this.messages[i] = msg;//change the value
}
}

jquery .live on form submit not picking up dynamically added inputs

when my ajaxupload script finishes it adds a read-only input w/ the value of the image's URL.
it is a long script, but i think this is the relevant part that fires on successful completion:
var location = '<div id="'+ID+'_location" class="img_location">' + '<input name="'+ID+'" class="location regular-text" type="text" size="50" readonly="readonly" value="'+response+'" />';
$(container).append(location).show(); //create readonly input
$(container) is defined just as the parent div of the upload button. that part seems to work... the image is uploaded, it is saved properly, and the input w/ the image's location is added to to the DOM. but i've discovered a bug that if I click my SAVE button (which triggers my ajax save function) then this new input is NOT captured.
here is my save function:
$('form#childoptions').live('submit', function(e) {
e.preventDefault();
var values = $(this).serialize();
alert(values);
var data = {
action: 'save_function',
type : 'save',
_nonce: '<?php echo $nonce; ?>',
formdata: values
};
$.post(ajaxurl, data, function(response) {
//alert(response);
if(response == 1) {
show_message(1);
t = setTimeout('fade_message()', 2000);
} else {
show_message(99);
t = setTimeout('fade_message()', 2000);
}
});
//return false;
});
only the new input is not captured. the rest works properly. there is also no problem if i refresh in between as I presume the input is part of the DOM. which is why i thought to use .live. i thought i had solved the issue twice- 1. i wasn't using a "name" on the dynamic input and 2. i wasn't using .live on the form. but now i am doing both and not getting anywhere.
all help is much appreciated. let me know if there is more information I can provide.
It appears that your using live on the whole form, not on inputs. So the live event binding would try to pickup new forms with id childoptions. This won't work. You'd be better off using bind() instead. Have you tried:
$('form#childoptions').bind('submit', function(e) {…}
I'm curious if this will fix your issue.