I'm using jQuery dataTables to display a table. I need to be able to pass a row selection event on to my Aura component that handles the selection and performs some operations on the data from that row.
In the initialize() function:
initialize: function()
{
$("#mytable tbody").click(function(event)
{
$(mytable.fnSettings().aoData).each(function ()
{
$(this.nTr).removeClass('row_selected');
});
$(event.target.parentNode).addClass('row_selected');
});
mytable = $('#mytable').dataTable();
},
I set up the click handler for the row selection, but how do I get a reference to the enclosing component so I can sandbox.emit() function to issue messages? I can put a reference to the component into the Closure, but that essentially makes this component a singleton and I could never have two instances of the component on the page at the same time.
Is there a standard way, using jQuery selectors or some other method, that I can retrieve a reference to the enclosing component from inside the click() handler?
Edit: I should never try to write code until I have had 32oz of caffine. You can pass a reference to the current component via the click() method itself. Like so:
$("#mytable tbody").click(this, function(event)
{
$(mytable.fnSettings().aoData).each(function ()
{
$(this.nTr).removeClass('row_selected');
});
$(event.target.parentNode).addClass('row_selected');
event.data.sandbox.emit('mychannel', {data: 'stuff'});
});
If I understand your question correctly, you could try something like this
initialize: function () {
var that = this;
$("#mytable tbody").click(function(event) {
//have acces to component as 'that'
});
}
what I used for events is view inside component configuration:
View: {
events: {
'click a[data-question-edit-id]': function (e) {
var button = $(e.currentTarget),
id = button.attr('data-question-edit-id'),
examId = this.component.examModel.get('id');
this.sandbox.router.navigate('/exams/' + examId + '/questions/' + id + '/edit', {trigger: true});
},
'click a[data-question-delete-id]': function (e) {
var button = $(e.currentTarget),
id = button.attr('data-question-delete-id');
this.component.showDeleteConfirmation(id);
}
}
}
If you'll find be helpful, here is my repo of aura project I'm working on:
https://github.com/lyubomyr-rudko/aura-test-project
Related
I am currently using angular to build an application that uses buttons within Datatables to manipulate data. The current buttons I have available are "View" "Mark as completed", "Mark as incomplete", which upon clicking trigger specific functions. The relevant functionality is being achieved by the following:
/**
* Hides ID and adds onclick functionality
*/
ngAfterViewInit() {
const table = $('.display').DataTable({
responsive: true,
bRetrieve: true
});
table.column(0).visible(false);
this.clickListener(table, this.route);
}
/**
* Listens to row click
*/
clickListener(table: any, route: any) {
const self = this;
let rowData;
$('.display').on('click', 'tbody tr td .view', function(e) {
rowData = self.checkIfRowsHidden(table, this, e);
self.router.navigate([route + '/detail/' + rowData[0]]);
});
$('.display').on('click', 'tbody tr td .done', function(e) {
rowData = self.checkIfRowsHidden(table, this, e);
self.executePutData(rowData[0], 'Completed', self);
});
$('.display').on('click', 'tbody tr td .undone', function(e) {
rowData = self.checkIfRowsHidden(table, this, e);
self.executePutData(rowData[0], 'In Progress', self);
});
}
As you can see I am using var self = this in order to create a reference to the global scope. Please also note I am also using this within the function checkIfRowsHidden to capture the local scope. The function uses the local scope like this:
checkIfRowsHidden(table, scope, event) {
event.stopImmediatePropagation();
if (table.responsive.hasHidden()) {
const parentRow = $(scope).closest("tr").prev()[0];
return table.row( parentRow ).data();
} else {
return table.row($(scope).closest('tr')).data();
}
}
I recently introduced myself to using binding instead of self as a more structured and possibly memory efficient way. I tried to use it in my clickListener function by modifying it to the following:
clickListener(table: any, route: any) {
let rowData;
$('.display').on('click', 'tbody tr td .view', function(e) {
rowData = this.checkIfRowsHidden(table, this, e);
this.router.navigate([route + '/detail/' + rowData[0]]);
}.bind(this));
/*rest of the code
.
.
*/
Unfortunately, since I had been using multiple scopes, the this from the local and global scopes were being treated as the same, creating errors while retrieving data from the tables. Is there a way to bind global scope to only a specific items? or do I have to resort to var self=this?
You can use arrow functions to preserve global scope. Instead of using anonymous functions, you can switch to arrow functions, they automatically provide that:
$('.display').on('click', 'tbody tr td .view', (e) => {
rowData = self.checkIfRowsHidden(table, this, e);
self.router.navigate([route + '/detail/' + rowData[0]]);
});
This way, you don't need to bind the scope, instead this refers to the Angular component class.
I am showing a form on addDetail buttton inside this element. How can I bind my data to this cell and send it to the server using the toJSon() method?
// Create a custom view for that element that displays an HTML div above it.
// -------------------------------------------------------------------------
joint.shapes.html.ElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element">',
'<button class="delete">x</button>',
'<label></label>',
'<span></span>', '<br/>',
'<input type="text" name="name" placeholder="name"/>',
'<button class="addDetail">+</button>',
'</div>'
].join(''),
initialize: function () {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
// Prevent paper from handling pointerdown.
this.$box.find('input').on('mousedown click', function (evt) {
evt.stopPropagation();
});
// This is an example of reacting on the input change and storing the input data in the cell model.
this.$box.find('input').on('change', _.bind(function (evt) {
alert($(evt.target).val());
this.model.set('input', $(evt.target).val());
}, this));
this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model));
this.$box.find('.addDetail').on('click', _.bind(function (evt) {
addActionDetail();
})
);
// Update the box position whenever the underlying model changes.
this.model.on('change', this.updateBox, this);
// Remove the box when the model gets removed from the graph.
this.model.on('remove', this.removeBox, this);
this.updateBox();
},
render: function () {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
updateBox: function () {
// Set the position and dimension of the box so that it covers the JointJS element.
var bbox = this.model.getBBox();
// Example of updating the HTML with a data stored in the cell model.
this.$box.find('label').text(this.model.get('label'));
this.$box.find('span').text(this.model.get('select'));
this.$box.css({
width: bbox.width,
height: bbox.height,
left: bbox.x,
top: bbox.y,
transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)'
});
},
removeBox: function (evt) {
this.$box.remove();
}
});
}
In order to save some data on your element you must follow this steps:
Add some elementData propery to the shape model.
Each time the user click on addDetail inside your element you must have the element id, extract the elementData out of it, and then to render the form (you can achieve this by adding custom event listener to your paper)
When clicking the submit form, add add some custom trigger event.
Listen to that triggered event on your graph and try look for the specific cell by the ModelId and update it.
Here is the basic idea example:
1.your shape model:
joint.shapes.myShapes = joint.shapes.myShapes || {};
joint.shapes.myShapes.Element = joint.shapes.basic.Generic.extend({
//basically the defaults function doesn't needed, because the set function adds that automatically.
defaults: _.defaultsDeep({
elementData: null,
}, joint.shapes.basic.Generic.prototype.defaults),
getElementData: function () {
return this.get("elementData");
},
setElementData: function (elementData) {
this.set("elementData", elementData);
},
});
2.On your paper init, add your custom event listener function,
notice that you must have the ModelId to be remembered:
paper.on('addDetail:click', function (cell) {
var elementData = cell.model.getElementData();
elementData.ModelId = cell.model.id;
formRender(elementData);
});
3.trigger some custom event on your submit and the object to be updated within the element model:
function formSubmit() {
graph.trigger('custom:update', newElementData);
}
4.Add some custom event listener to your graph, add call the setElementData by the ModelId:
graph.on('custom:update', function (elementData) {
var cell = graph.getCell(elementData.ModelId);
cell.setElementData(elementData);
}, this);
Now you can send it to the server using the toJSon() method.
I'm trying to transform my code into a more plugin type of code, so everything will be separated, in case I change class names in the future.
For some reason, in my code, I get Cannot read property 'dropdown' of undefined.
My guess is, the function Navigation.bindEvents() runs before I set the config, so It can't find it... But I don't know how to solve it.
Here's my Navigation.js file:
let Navigation = {
config: {},
init(config) {
this.config = config;
this.bindEvents();
},
bindEvents() {
$(this.config.trigger).on('click', this.toggleNavigation);
$(document).on('click', this.hideAllDropdowns);
},
toggleNavigation(event) {
// Store the current visible state
var visible = $(this).siblings(this.config.trigger).hasClass('visible');
// Hide all the drop downs
this.hideAllDropdowns();
// If the stored state is visible, hide it... Vice-versa.
$(this).siblings(this.config.content).toggleClass('visible', !visible);
event.preventDefault();
event.stopPropagation();
},
hideAllDropdowns() {
$(this.config.dropdown + ' ' + this.config.content).removeClass('visible');
}
}
export default Navigation;
And here's my app.js file which I run all the init functions.
window.$ = window.jQuery = require('jquery');
import Navigation from './layout/navigation.js';
Navigation.init({
dropdown: '.dropdown',
trigger: '.dropdown-trigger',
content: '.dropdown-content'
});
I guess you got problem with the scope $(document).on('click', this.hideAllDropdowns);
Let's try
bindEvents() {
$(this.config.trigger).on('click', this.toggleNavigation);
$(document).on('click', this.hideAllDropdowns.bind(this));
},
UPDATE:
bindEvents() {
$(this.config.trigger).bind('click', {self:this}, this.toggleNavigation);
$(document).on('click', this.hideAllDropdowns.bind(this));
},
And replace all this.config by event.data.self inside toggleNavigation function
this in the context of toggleNavigation refers to the clicked element.
That is why you can do $(this).siblings(...) to get the sibling elements.
You need to have a reference to the Navigation object. Perhaps you can use the on syntax that allows you to pass extra data $(this.config.trigger).on('click', this, this.toggleNavigation);
Then rewrite the handler
toggleNavigation(event) {
//get the navigation reference
var nav = event.data;
// Store the current visible state
var visible = $(this).siblings(nav.config.trigger).hasClass('visible');
// Hide all the drop downs
nav.hideAllDropdowns();
// If the stored state is visible, hide it... Vice-versa.
$(this).siblings(nav.config.content).toggleClass('visible', !visible);
event.preventDefault();
event.stopPropagation();
},
The behavior of this is one of the hardest things to understand in JavaScript. Here this is obviously dynamic, which means that its value depends on where your method has been called...
let module = {
config() {
console.log(`config(): 'this' is 'module' ---> ${Object.is(this, module)}`);
console.log(`config(): 'this' is 'document' ---> ${Object.is(this, document)}`);
},
init() {
console.log(`init(): 'this' is 'module' ---> ${Object.is(this, module)}`);
console.log(`init(): 'this' is 'document' ---> ${Object.is(this, document)}`);
module.config();
}
};
$(document).ready(module.init);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
I'm learning about Session and reactive data sources in Meteor JS. They work great for setting global UI states. However, I can't figure out how to scope them to a specific instance of a template.
Here's what I'm trying to do
I have multiple contenteditable elements on a page. Below each is an "Edit" button. When the user clicks on the Edit button, it should focus on the element and also show "Save" and "Cancel" buttons.
If the user clicks "Cancel", then any changes are eliminated, and the template instance should rerender with the original content.
Here's the code I have so far
// Helper
Template.form.helpers({
editState: function() {
return Session.get("editState");
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get("editState")) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", null);
},
});
// Template
<template name="form">
<div class="{{editState}}">
<p class="form-field" contenteditable>
{{descriptionText}}
</p>
</div>
Edit
Save
Cancel
</template>
// CSS
.edit-btn
.cancel-btn,
.save-btn {
display: inline-block;
}
.cancel-btn,
.save-btn {
display: none;
}
.is-editing .cancel-btn,
.is-editing .save-btn {
display: inline-block;
}
The problem
If I have more than one instance of the Form template, then .form-field gets focused for each one, instead of just the one being edited. How do I make so that only the one being edited gets focused?
You can render a template with data, which is basically just an object passed to it when inserted in to a page.
The data could simply be the key to use in the Session for editState.
eg, render the template with Template.form({editStateKey:'editState-topForm'})
you could make a handlebars helper eg,
Handlebars.registerHelper('formWithOptions',
function(editStateKey){
return Template.form({editStateKey:editStateKey})
});
then insert it in your template with
{{{formWithOptions 'editState-topForm'}}} (note the triple {, })
Next, change references from Session.x('editState') to Session.x(this.editStateKey)/ Session.x(this.data.editStateKey)
Template.form.helpers({
editState: function() {
return Session.get(this.editStateKey);
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get(this.data.editStateKey)) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, null);
},
});
Note: if you are using iron-router it has additional api's for passing data to templates.
Note2: In meteor 1.0 there is supposed to be better support for writing your own widgets. Which should allow better control over this sort of thing.
As a matter of policy I avoid Session in almost all cases. I feel their global scope leads to bad habits and lack of good discipline regarding separation-of-concerns as your application grows. Also because of their global scope, Session can lead to trouble when rendering multiple instances of a template. For those reasons I feel other approaches are more scalable.
Alternative approaches
1 addClass/removeClass
Instead of setting a state then reacting to it elsewhere, can you perform the needed action directly. Here classes display and hide blocks as needed:
'click .js-edit-action': function(event, t) {
var $this = $(event.currentTarget),
container = $this.parents('.phenom-comment');
// open and focus
container.addClass('editing');
container.find('textarea').focus();
},
'click .js-confirm-delete-action': function(event, t) {
CardComments.remove(this._id);
},
2 ReactiveVar scoped to template instance
if (Meteor.isClient) {
Template.hello.created = function () {
// counter starts at 0
this.counter = new ReactiveVar(0);
};
Template.hello.helpers({
counter: function () {
return Template.instance().counter.get();
}
});
Template.hello.events({
'click button': function (event, template) {
// increment the counter when button is clicked
template.counter.set(template.counter.get() + 1);
}
});
}
http://meteorcapture.com/a-look-at-local-template-state/
3 Iron-Router's state variables
Get
Router.route('/posts/:_id', {name: 'post'});
PostController = RouteController.extend({
action: function () {
// set the reactive state variable "postId" with a value
// of the id from our url
this.state.set('postId', this.params._id);
this.render();
}
});
Set
Template.Post.helpers({
postId: function () {
var controller = Iron.controller();
// reactively return the value of postId
return controller.state.get('postId');
}
});
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md#setting-reactive-state-variables
4 Collection data
Another approach is to simply state by updating data in your collection. Sometimes this makes perfect sense.
5 update the data context
Session is often the worse choice in my opinion. Also I don't personally use #3 as I feel like being less tied to iron-router is better incase we ever want to switch to another router package such as "Flow".
At row level I catch the event and try to add an extra parameter
onRowClick: function(e){
console.log("Event in row");
e.model = "test";
console.log(e.model) // prints 'test'
}
In main view I catch the same event again
onRowClick: function(e){
console.log("Event in main view");
console.log(e.model) //prints undefined
}
Console:
>Event in row
>test
>Event in main view
>undefined
How can I append an attribute to the event?
The answer is that you don't catch the same event, but rather two (initially) identical events. Changing the first does not change the latter.
If you want to pass data between those events, you would need to store that data elsewhere (e.g. a closure, or if you don't care about the scope save it in the window object).
There are 2 ways that I know of to pass data to a jQuery event. One with with e.data, you can add any properties to e.data like this.
http://www.barneyb.com/barneyblog/2009/04/10/jquery-bind-data/
the other way is to use closures such as:
function myFunc() {
var model = 'test';
var x = {
onRowClick: function(e){
console.log("Event in row");
console.log(model) // prints 'test'
}
}
}
instead of catching the rowClick event in the main view, i suggest you catch it in the row view, and pass it through the backbone event system...
your parentview can bind to it's rows to catch a click.
there are two ways to do this,
trigger a custom event on your row's model, and let the parent bind to every model in the collection, though that seems like a hack and a performance hit.
i suggest doing it with an event aggregator:
var App = {
events: _.extend({}, Backbone.Events);
};
var myGeneralView = Backbone.Views.extend({
initialize: function() {
_.bindAll(this, "catchMyCustomEvent";
/*
and here you bind to that event on the event aggregator and
tell it to execute your custom made function when it is triggered.
You can name it any way you want, you can namespace
custom events with a prefix and a ':'.
*/
App.events.bind('rowView:rowClicked');
},
catchMyCustomEvent: function (model, e) {
alert("this is the model that was clicked: " + model.get("myproperty"));
}
// other methods you will probably have here...
});
var myRowView = Backbone.Views.extend({
tagName: "li",
className: "document-row",
events: {
"click" : "myRowClicked"
},
initialize: function() {
_.bindAll(this, "myRowClicked");
},
myRowClicked: function (e) {
/*
You pass your model and your event to the event aggregator
*/
App.events.trigger('rowView:rowClicked', this.model, e);
}
});