Stimulus js Cross Controller events context - ruby-on-rails-7

I have two, independent, stimulus controllers. One that manages a table and one that is used to trigger a new row for the table.
I have a button on the page that calls the turbo-table-new-row#show function, which does dispatch the event and the turbo-table#show function is called, but I don't seem to have access to the turbo-table's 'this', so I'm unable to access the targets, values, etc...
If I move the button into the turbo-table's scope, I don't need the second controller, and everything works. However, from the UI perspective, this isn't workable.
How do I get access to the receiving controller's 'this' after receiving the event?
<div data-controller="turbo-table-new-row turbo-table"
data-action="turbo-table-new-row:show->turbo-table#display">
<button data-action="click->turbo-table-new-row#show">
</div>
// turbo-table-new-row-controller
show(e) {
this.dispatch("show", { detail: { url: e.params.url} })
}
// turbo-table-controller
show(e) {
console.log("[turbo_table] - turbo-table-new-row->show event")
console.log(e.detail.url)
// I don't have access to the turbo-table-contoller 'this'
this.hasPanelTarget ...
}

It should be possible to dispatch an event from one controller and read it in another controller when not in the same DOM element.
When you dispatch an event from JavaScript, it will bubble up the DOM tree through to the window. You can listen to global events with the #window action descriptor to catch any events that have bubbled up outside of the controller's DOM tree.
See
https://stimulus.hotwired.dev/reference/actions#global-events
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters
You may need to be careful to check that it is the 'right' event you want, but as a basic set up you need to add the #window on your data action.
Example
Here is a working end to end example, not turbo-links, where the table controller is not a parent of your add row button (it is a sibling). Using the window event listener approach we can listen to events outside of the DOM tree.
Once the event is received in your tables controller, the class method should have access to that controller's this without issue.
If you want to access the original trigger button's target element you can do this via event.target.
<main>
<table data-controller="table" data-action="table-action:add#window->table#show">
<thead>
<tr>
<th data-table-target="status"></th>
</tr>
</thead>
<tr data-table-target="row">
<td>Item 1</td>
</tr>
<tr data-table-target="row">
<td>Item 2</td>
</tr>
<tr data-table-target="row">
<td>Item 3</td>
</tr>
<tr data-table-target="row">
<td>Item 4</td>
</tr>
</table>
<div>
<button data-controller="table-action" data-action="table-action#add"
data-table-action-url-param="https://path.to.tables/">
Add row
</button>
</div>
</main>
import { Controller } from '#hotwired/stimulus';
class TableController extends Controller {
static targets = ['row', 'status'];
show({ detail: { url } = {}, target }) {
console.log('event.target - the button that triggered the click', event.target);
if (url) {
const rowCount = this.rowTargets.length;
this.statusTarget.innerText = `Request from: ${url}, there are ${rowCount} rows.`;
}
}
}
export default TableController;
import { Controller } from '#hotwired/stimulus';
class TableActionController extends Controller {
add({ params }) {
this.dispatch('add', {
detail: { ...params },
bubbles: true,
cancelable: false,
});
}
}
export default TableActionController;

From v3.2.0 you can use Outlets API for cross-controller communication and coordination as an alternative to dispatching custom events on controller elements.
See
https://stimulus.hotwired.dev/reference/outlets

Related

inside single view create form as well as display data

I am creating a view page where I need to display data and as well as a form to insert data. To insert data I have bootstrap modal. How can I bind my view page so that I can have data to display in the page as well as create form to insert data. I mean how can I bind my view to display data?
public ActionResult GetFirm()
{
return View(db.FirmModels.ToList());
}
My view page
#model models.FirmModel
// code for bootstrap modal
// code for data table
<table id="tblFirmData">
<thead>
<tr>
<th>Edit/Print</th>
<th style="visibility:hidden;">#Html.DisplayNameFor(model => model.FirmId)</th>
<th>NAME</th>
<th>CONTACT</th>
</tr>
</thead>
<tbody>
#foreach(var item in models)
{
int status = item.FirmRegistrationStatus;
}
</tbody>
</table>
When I do foreach(var item in models) getting error 'models' is a namespace but is used like a variable and when I do #foreach(var item in Model) I am getting error foreach statement cannot operate on variables of type 'FirmModel' because 'FirmModel' does not contain a public instance definition for 'GetEnumerator'.
How to solve this problem, shall I need to modify my GetFirm return method or need to change in view page?
Because of you pass a list to the view define you view model as:
#model IEnumerable<models.FirmModel>
The IEnumerable interface is implementing the GetEnumerator() method used to iterate through the collection.
Or:
#model IList<models.FirmModel>
The IList interface inherits the IEnumerable.
And correspondingly:
#foreach(var item in Model)
{
....
}

How to reduce the amount of data transferred with Blazor server

I have a blazor component that fetch data from a service and then render data into a table.
This is my scenario:
The service returns a list of objects, each object contains a large set of properties and an object hierarchy
The user interface must only display a small amount of this data, therefore only some #myObject.MyProperty properties are displayed
#page "/fetchdata"
#inject WeatherForecastService ForecastService
#if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
#foreach (var forecast in forecasts)
{
<tr>
<td>#forecast.Date.ToShortDateString()</td>
<td>#forecast.TemperatureC</td>
<td>#forecast.TemperatureF</td>
<td>#forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
#code {
private List<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = ForecastService.List;
ForecastService.MyEvent += _Event;
}
private void _Event(object sender, EventArgs e)
{
InvokeAsync(StateHasChanged);
}
}
So in this case WeatherForecast is a large object, but only a small set of its properties are displayed.
Is this scenario already optimized by the Blazor or does the server always "serve" the entire object to the client?
It is effectively 'optimized' to send only the changes.
https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-3.1#comparison-to-server-rendered-ui
From the Blazor docs (from Blazor Server section):
A UI update in Blazor is triggered by:
User interaction, such as selecting a button. App triggers, such as a
timer. The graph is rerendered, and a UI diff (difference) is
calculated. This diff is the smallest set of DOM edits required to
update the UI on the client. The diff is sent to the client in a
binary format and applied by the browser.

How can I respond to a right-click in an HTML table "cell" (td) in a Meteor project?

Assuming the following:
I have an HTML page that loads a template.
The template name is scheduleTable.
The template desrcribes an HTMLTable, with (of course) various trs and tds, with a particular td that uses a CSS class named "tddetails1"
If I want to respond with JavaScript to a right-click on that "tddetails1" element, I would think I would need code something like this in the js file:
Template.scheduleTable.events({
"right-click .tddetails1": function (event) {
// do something
});
But what event can I use? There is no "right-click" event, and I don't want to capture "regular" (left) clicks. Am I doomed to use an html button in each td, and respond to their click events?
UPDATE
Yes, Christian's answer was "spot on"; this works:
HTML
<template name="tblExpenseDescription">
<table width="75%" border="1">
<tr>
<td width="200px"><strong>Description of Expense</strong></td>
<td class="rc1" >Date 1: <input type="date" id="date1" name="date1"/>
</td>
. . .
JavaScript
if (Meteor.isClient) {
Template.tblExpenseDescription.events({
"mousedown .rc1": function (event) {
if (event.button == 2) {
console.log('date1 was right-clicked');
}
}
});
}
Just use the mousedown event handler and check that event.button == 2:
Template.scheduleTable.events({
"mousedown .tddetails1": function (event) {
if (event.button == 2) {
// this code will run on right-click
// do something
}
}
});
EDIT:
To also prevent the context menu from popping up add:
"contextmenu .tddetails1": function (event) {
return false;
}
into your event handler object.

Delete Field Spring MVC

I am using SimpleFormController with a result page looking like this:
<tr>
<td>Name: </td>
<td>${product.name}</td>
</tr>
<tr>
<td>Text: </td>
<td>${product.text}</td>
</tr>
A user can enter a name and some text. I'm trying to implement a delete functionality for each entry (there should be a link next to each entry). I'm having trouble with understanding, if it can be done in the same Controller as for the input or not (am new to Spring) and how. The onSubmit method helps to display data that was added, do I need to implement an extra delete method? If yes, how can I "map" it to my delete link in my jsp?
I suppose you are not wanting to put a delete link even when the user is just entering the name!
Delete links should normally appear when you are displaying data, not creating them.
Here is how you can create a delete link according to associated ids.
<tr>
<td>Name: </td>
<td>${product.name}</td>
<td>delete</td>
</tr>
and this should be in your controller:
#Controller
public class ProductController{
#RequestMapping("/delete/{id}")
public String deleteProduct(#PathVariable("id")Integer id) {
// get product by id
// delete that product
// save database
// or do as you wish
return "redirect:/index";
}
}
Hope that helps :)

Odd behavior in javascript

I have a table like so
<table>
<tr id="trRow1" runat="server" style="display: none">
<td>First Name:</td>
<td><asp:Label id="lblFirstName" runat="server"></asp:Label></td>
</tr>
<tr>
<td>Last Name:</td>
<td><asp:Label id="lblLastName" runat="server"></asp:Label></td>
</tr>
</table>
As you can see, initially the first row is not being displayed. When the user clicks a certain radio button on the page an asynchronous postback occurs, and at that time I set the style of trRow1 to "inline". Nothing fancy; nothing new. It works just great.
Or at least up until I try to do the following in a javascript function.
function Test() {
var obj = trRow1.getElementsByTagName("select");
alert(obj.length);
}
At the point I call Test(), I get an error that says "Microsoft JScript runtime error: 'trRow1' is undefined."
My guess is it has something to do with the fact that I'm messing with setting the Display style using AJAX, and for whatever reason the DOM can't find trRow1 even after I set it's display to "inline".
Can anyone throw me a bone on this one? I'm stuck.
The object trDegree is not defined, by your naming conventions looks like trDegree is a table row element, I think that you're trying to do something like this:
function WTF() {
var trDegree = document.getElementById('trDegree'); // Locate the element
var obj = trDegree.getElementsByTagName("select");
alert(obj.length);
}
Further Reference:
element.getElementsByTagName
I don't see any variable for trDegree in your sample. You would need to have trDegree loaded before calling getElementsByTagName.
For example:
function WTF() {
var trDegree = document.getElementById('trDegree');
var obj = trDegree.getElementsByTagName("select");
alert(obj.length);
}
or you could just load the tags from the document level. I'm not sure if this is the effect you want though.
function WTF() {
var obj = document.getElementsByTagName("select");
alert(obj.length);
}
The solution is in the first answer. U must get the element before using it adding this line:
var trRow1 = document.getElementById('trRow1');

Resources