I'm building a transaction entry form to better understand Stimulus after the Odin Project lesson (https://www.theodinproject.com/lessons/ruby-on-rails-stimulus). 1. When the user is finished entering the amount, the next button is clicked and the numpad is hidden (including the 'next' button). 2. If the user would like to edit the amount, a click on the 'amount_box' toggles the hidden state of the numpad.
I've completed the first part, but getting an error on the second part.
JS (toggle_controller.js)
export default class extends Controller {
static classes = [ "change" ]
toggle() {
this.element.classList.toggle(this.changeClass)
}
}
HTML
<div id="amount_container" >
<div id="amount_box">
<div>$999,999.99</div>
</div>
<div id="numpad" data-controller="toggle"
data-toggle-change-class='hidden'>
<div>1</div>
<div>2</div>
<div>3</div>
...
<div data-action="click->toggle#toggle">Next</div>
</div>
</div>
This hides the id="numpad". But once hidden it can't be clicked to un-hide. In that case I would click the id="amount_box". So I moved the data-controller attribute to id='amount_container', which contains both the element being clicked and being toggled.
The updated HTML below throws an error "Error: Missing attribute "data-toggle-change-class" ... element: div#amount_container". It wants to see the data-toggle-change-class on the same element which has data-controller="toggle" but putting it on div#amount_container would just hide the whole thing which is my whole problem in the first place.
<div id="amount_container" data-controller="toggle">
<div id="amount_box">
<div>$999,999.99</div>
</div>
<div id="numpad" data-toggle-change-class='hidden'>
<div>1</div>
<div>2</div>
<div>3</div>
...
<div data-action="click->toggle#toggle">Next</div>
</div>
</div>
How can I click on "Next" or "div#amount_box, and hide/unhide div#numpad?
Currently, your toggle_controller.js toggles the classlist of this.element
export default class extends Controller {
static classes = [ "change" ]
toggle() {
this.element.classList.toggle(this.changeClass)
}
}
this.element refers to the element on which the data-controller attribute is placed. By moving the data-controller from div id="numpad" to <div id="amount_container">, this.element changes scope.
The solution is to define a stimulus target attribute to specify which element to toggle.
export default class extends Controller {
static targets = [ "numpad" ]
static classes = [ "change" ]
toggle() {
this.numpadTarget.classList.toggle(this.changeClass)
}
}
<div id="amount_container" data-controller="toggle" data-toggle-change-class='hidden'>
<div id="amount_box">
<div>$999,999.99</div>
</div>
<div id="numpad" data-toggle-target="numpad">
<div>1</div>
<div>2</div>
<div>3</div>
...
<div data-action="click->toggle#toggle">Next</div>
</div>
</div>
Related
I'm looking for the best approach to override the Bootstrap modal javascript close function. I have modal that pops up with a form that changes items on the page. The form doesn't close the modal when it is submitted. It stays on the form to allow users to create/delete as many tags as they like, rendering a turbo_stream.update partial instead of leaving the modal.
The issue is that once a user closes the modal after making their changes, the tag data on the page is stale and doesn't reflect the changes they made with the form displayed in the modal.
I'd like the bootstrap modal close function to either reload the page, or better yet render another turbo_stream.update partial to correct the tag information.
Calling the modal/form in view:
<%= link_to add_to_deck_tag_path, class: link_class, data: { turbo_frame: 'remote_modal' } do %><i class="bi bi-tag-fill"></i>Add Tag<% end %>
Remote modal code:
<%= turbo_frame_tag "remote_modal" do %>
<div class="modal fade" id=modalLive" tabindex="-1" data-controller="remote_modal">
<div class="modal-dialog">
<div class="modal-content<%= modal_css %>">
<div class="modal-header">
<h5 class="modal-title" id="modalLiveLabel"><%= title %></h5>
<button type="button" class="btn-close<%= button_close_css %>" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="remote_modal_body" class="modal-body">
<%= yield %>
</div>
</div>
</div>
</div>
<% end %>
The javascript controller:
import { Controller } from "#hotwired/stimulus"
import { Modal } from "bootstrap"
export default class extends Controller {
connect() {
this.modal = new Modal(this.element)
this.modal.show()
}
}
Ok, so this works to reload the page. I wasn't putting the addEventListener in the right place plus a few syntax issues.
I'm still working on rendering just the turbo-stream.update partial to avoid reloading the whole page.
import { Controller } from "#hotwired/stimulus"
import { Modal } from "bootstrap"
export default class extends Controller {
connect() {
this.modal = new Modal(this.element)
this.modal.show()
this.element.addEventListener('hidden.bs.modal', (event) => {
location.reload();
})
}
}
Issue
Toggling a modal with Javascript from another modal creates a conflict with the .modal-backdrop. Solutions I have thought of require me to make some functions to count .modal-backdrop but it breaks Bootstrap's native modal-dismiss behavior. I think maybe there could be better way that incorporates Bootstrap's native CSS (fade, etc.) without additional functions?
Here is my current working example
Ultimate Goal
Create a modal that can toggle another modal while retaining Bootstrap's default CSS. Also eliminate .modal-backdrop from creating more than one instance.
Background
I am creating a "parent" form that can be built dynamically (with JQuery) based on user input. Two sections of the form, "Add System" and "Add Circuit", will require an additional "child" form that will be placed inside a modal, outside the scope of the parent form (This helps avoid nesting one form inside the parent form). The reason I want to separate the user inputs from the parent and child forms is because the child form inputs may have different JQuery validation rules than the parent form (i.e., the data is not required to create a CSD, but if you want to add a system/circuit then I want to make sure you give me all the necessary data to create those objects). If a user wants to Add a system or a circuit:
they click the "Add System/Circuit" button where the 1st modal appears.
The user is prompted with a form to search for an existing one or create a new one.
If the user decides to "Add New System/Circuit", then a different modal appears.
The user is prompted with a form to enter the relevant data.
Here is my javascript function creating the modals. I am generating the modal dynamically based on the option the user chooses. One thing to note, since I am creating these modals in the same function, I want to make sure my function deletes the existing modal content too. (That's why I added the line
if (modalWrap !== null) {
modalWrap.remove()
}
at the beginning of my function)
var modalWrap = null
function create_dynamic_csd_modal(modal_type, section_type) {
if (modalWrap !== null) {
modalWrap.remove()
}
modalWrap = document.createElement("div")
if (modal_type == "new") {
if (section_type == "system") {
var modalContent = `
<div class="modal-header">
<h1 class="modal-title">Add New System</h1>
</div>
<div class="modal-body" style="height: 50vh;">
[...]
</div>
`
} else if (section_type == "circuit") {
var modalContent = `
<div class="modal-header">
<h1 class="modal-title">Add New Circuit</h1>
</div>
<div class="modal-body" style="height: 50vh;">
[...]
</div>
`
}
modalWrap.innerHTML = `
<div class="modal fade csd-add-new-${section_type}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-xl">
<div class="modal-content">
<form
action='/ajax/csd_add_new_${section_type}/'
method='POST'
class="csd-add-new-${section_type}"
novalidate
>
${modalContent}
<div class="modal-footer">
<button
class="btn btn-warning cancel"
type="reset"
data-bs-dismiss="modal"
>
Cancel
</button>
<button
class="btn btn-secondary previous"
type="button"
>
Previous
</button>
<button
class="btn btn-danger"
type="button"
>
Add
</button>
</div>
</form>
</div>
</div>
</div>
`
} else {
if (section_type == "system") {
var modalContent = `
<div class="modal-body mt-4">
[...]
</div>
`
} else if (section_type == "circuit") {
var modalContent = `
<div class="modal-body mt-4">
[...]
</div>
`
}
modalWrap.innerHTML = `
<div class="modal fade csd-add-existing-${section_type}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<form
action='/ajax/csd_add_existing_${section_type}/'
method='POST'
class="csd-add-existing-${section_type}"
novalidate
>
${modalContent}
<div class="modal-footer">
<button
class="btn btn-warning cancel"
type="reset"
data-bs-dismiss="modal"
>
Cancel
</button>
<button
class="btn btn-danger"
type="submit"
>
Add
</button>
</div>
</form>
</div>
</div>
</div>
`
}
document.body.append(modalWrap)
var modal = new bootstrap.Modal(modalWrap.querySelector(".modal"))
modal.show()
if ($(".modal-backdrop").length >= 1) {
$(".modal-backdrop").not(":first").remove()
}
}
$(document).on(
"click",
"form.create-csd-series button.add-system, form.create-csd-series button.add-circuit, form.csd-add-existing-system button.add-system, form.csd-add-existing-circuit button.add-circuit",
function(e) {
var button = this
var modal_type = null
if (button.classList.contains("add-system")) {
var section_type = "system"
} else if (button.classList.contains("add-circuit")) {
var section_type = "circuit"
}
if (button.closest(".modal") !== null) {
modal_type = "new"
}
create_dynamic_csd_modal(modal_type, section_type)
},
)
The idea is once the data inside the "child" form is entered and the user clicks "Add" in the modal - the data from this form will populate the parent form in a consolidated view. All the inputs' attributes (like name='some_input_name', id='some_input_id', etc.) from the child form will be transposed into the parent form that match validation rules for it. The user could then edit/delete that data before submitting the parent form to the server. (something like this).
I couldn't find any documentation on how to get the target of dropped element using cdk drag drop.
below is the sample code dragging item from box1 div to Shoppingbasket div. here im expecting target should be box2 div. but I'm receiving Shoppingbasket div.here is the
stackblitz example
HTML
<div cdkDropListGroup>
<div class="example-container">
<h2>Available items</h2>
<div cdkDropList [cdkDropListData]="items" class="example-list" cdkDropListSortingDisabled
(cdkDropListDropped)="drop($event)">
<div id="box1" class="example-box" *ngFor="let item of items" cdkDrag>{{item}}</div>
</div>
</div>
<div class="example-container2">
<h2>Shopping basket</h2>
<div id="Shoppingbasket" cdkDropList [cdkDropListData]="basket" class="example-list"
(cdkDropListDropped)="drop($event)">
<div id="box2" class="example-box1" cdkDragHandle></div>
</div>
</div>
</div>
TS
drop($event){
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex,event.currentIndex);
} else {
transferArrayItem(event.previousContainer.data,event.container.data,event.previousIndex);
}
}
There's no event.target in the cdk drag and drop events but you can manually add the attribute to the event object when needed like this :
event.target = event.container.element.nativeElement;
I have this code to show info about a certain product registered in my database.
Button that triggers my Details Controller:
#Html.ActionLink("Details", "Details", new { id = prod.ID }, new { #class = "btn btn-danger" }) |
Controller code
public ActionResult Details(int? id)
{
PRODUCTS pRODUCTS = db.PRODUCTS.Find(id);
if (pRODUCTS == null)
{
return HttpNotFound();
}
return View(pRODUCTOS);
}
I want to display that "Details" view in a modal popup. I tried to create my modal in the same view where i have my "Details" button, but i can't pass my product id to the controller and it only shows me an empty view.
To show data inside a modal popup, you need an action method which returns HTML markup needed for the modal popup. So the first step is to make an action method which returns a partial view result.
public ActionResult Details(int id)
{
var product = db.PRODUCTS.Find(id);
if (product== null)
{
return HttpNotFound();
}
return View(product);
}
In the above simple example, I am querying the entity and passing the entity object directly to my view using the PartialView method. If you have a view model for your view, please use that.
Now in the Details.cshtml view, we will write code to return the HTML markup needed for the modal popup. Since we are passing the PRODUCTS object to the view, we will make sure our view is also strongly typed to that type.
#model YourNamespaceHere.PRODUCTS
<div id="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"> Details</h5>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<h2>Hello from #Model.Name</h2>
</div>
<div class="modal-footer">
<button type="button" class="btn" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
In the above example, I am simply printing the Name property value of PRODUCTS object in the modal body. You may update it to render other properties as needed.
Now we will make some changes to our HTML markup being rendered for the product. We need the click event on the details link to open the modal dialog. So let's first give some attributes to the link element, which we can later use to wireup the click event. Here I am going to add an additional CSS class to the links, modal-link.
#Html.ActionLink("Details", "Details", new { id = prod.ID },
new { #class = "btn modal-link" })
Now let's write some JavaScript code to listen to the click event on the link element with the modal-link CSS class, read the the href attribute value of the element and make an Ajax call to that URL and render the response of that call to build the modal dialog.
$(function () {
$('body').on('click', 'a.modal-link', function (e) {
e.preventDefault();
$("#modal").remove();
// Get the Details action URL
var url = $(this).attr("href");
//Make the Ajax call and render modal when response is available
$.get(url, function (data) {
$(data).modal();
});
});
});
I am using CSS class as the selector here, you can use any other selector as you wish
i think you should use jquery ajax to call action on click your button or action link and get your model as json result and return it then first clean your modal data and fill it with new result and at the end show your modal......
I've been trying to use backbonejs and jqm together.
I can render the main page alright. The page has a list that the user can tap on. The item selected should show a detail page with info on the list item selected. The detail page is a backbone view with a template that's rendered in the item's view object.
The detail's view .render() produces the html ok and I set the html of the div tag of the main page to the rendered item's detail markup. It looks like this:
podClicked: function (event) {
console.log("PodListItemView: got click from:" + event.target.innerHTML + " id:" + (this.model.get("id") ? this.model.get("id") : "no id assigned") + "\n\t CID:" + this.model.cid);
var detailView = new PodDetailView({ model: this.model });
detailView.render();
},
The detail view's render looks like this:
render: function () {
this.$el.html(this.template({ podId: this.model.get("podId"), isAbout_Name: this.model.get("isAbout_Name"), happenedOn: this.model.get("happenedOn") }));
var appPageHtml = $(app.el).html($(this.el));
$.mobile.changePage(""); // <-- vague stab in the dark to try to get JQM to do something. I've also tried $.mobile.changePage(appPageHtml).
console.log("PodDetailView: render");
return this;
}
I can see that the detail's view has been rendered on the page by checking Chrome's dev tools html editor but it's not displaying on the page. All I see is a blank page.
I've tried $.mobile.changePage() but, without an URL it throws an error.
How do I get JQM to apply it's class tags to the rendered html?
the HTML and templates look like this:
<!-- Main Page -->
<div id="lessa-app" class="meditator-image" data-role="page"></div>
<!-- The rest are templates processed through underscore -->
<script id="app-main-template" type="text/template">
<div data-role="header">
<h1>#ViewBag.Title</h1>
</div>
<!-- /header -->
<div id="main-content" data-role="content">
<div id="pod-list" data-theme="a">
<ul data-role="listview" >
</ul>
</div>
</div>
<div id="main-footer" data-role='footer'>
<div id="newPod" class="ez-icon-plus"></div>
</div>
</script>
<script id="poditem-template" type="text/template">
<span class="pod-listitem"><%= isAbout_Name %></span> <span class='pod-listitem ui-li-aside'><%= happenedOn %></span> <span class='pod-listitem ui-li-count'>5</span>
</script>
<script id="page-pod-detail-template" type="text/template">
<div data-role="header">
<h1>Pod Details</h1>
</div>
<div data-role="content">
<div id='podDetailForm'>
<fieldset data-role="fieldcontain">
<legend>PodDto</legend>
<label for="happenedOn">This was on:</label>
<input type="date" name="name" id="happenedOn" value="<%= happenedOn %>" />
</fieldset>
</div>
<button id="backToList" data-inline="false">Back to list</button>
</div>
<div data-role='footer'></div>
</script>
Thanks in advance for any advice... is this even doable?
I've finally found a way to do this. My original code has several impediments to the success of this process.
The first thing to do is to intercept jquerymobile's (v.1.2.0) changePage event like this:
(I've adapted the outline from jqm's docs and left in the helpful comments: see http://jquerymobile.com/demos/1.2.0/docs/pages/page-dynamic.html
)
$(document).bind("pagebeforechange", function (e, data) {
// We only want to handle changePage() calls where the caller is
// asking us to load a page by URL.
if (typeof data.toPage === "string") {
// We are being asked to load a page by URL, but we only
// want to handle URLs that request the data for a specific
// category.
var u = $.mobile.path.parseUrl(data.toPage),
re = /^#/;
// don't intercept urls to the main page allow them to be managed by JQM
if (u.hash != "#lessa-app" && u.hash.search(re) !== -1) {
// We're being asked to display the items for a specific category.
// Call our internal method that builds the content for the category
// on the fly based on our in-memory category data structure.
showItemDetail(u, data.options); // <--- handle backbone view.render calls in this function
// Make sure to tell changePage() we've handled this call so it doesn't
// have to do anything.
e.preventDefault();
}
}
});
The changePage() call is made in the item's list backbone view events declaration which passes to the podClicked method as follows:
var PodListItemView = Backbone.View.extend({
tagName: 'li', // name of (orphan) root tag in this.el
attributes: { 'class': 'pod-listitem' },
// Caches the templates for the view
listTemplate: _.template($('#poditem-template').html()),
events: {
"click .pod-listitem": "podClicked"
},
initialize: function () {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
render: function () {
this.$el.html(this.listTemplate({ podId: this.model.get("podId"), isAbout_Name: this.model.get("isAbout_Name"), happenedOn: this.model.get("happenedOn") }));
return this;
},
podClicked: function (event) {
$.mobile.changePage("#pod-detail-page?CID='" + this.model.cid + "'");
},
clear: function () {
this.model.clear();
}
});
In the 'showItemDetail' function the query portion of the url is parsed for the CID of the item's backbone model. Again I've adapted the code provided in the jquerymobile.com's link shown above.
Qestion: I have still figuring out whether it's better to have the code in showItemDetail() be inside the view's render() method. Having a defined function seems to detract from backbone's architecture model. On the other hand, having the render() function know about calling JQM changePage seems to violate the principle of 'separation of concerns'. Can anyone provide some insight and guidance?
// the passed url looks like #pod-detail-page?CID='c2'
function showItemDetail(urlObj, options) {
// Get the object that represents the item selected from the url
var pageSelector = urlObj.hash.replace(/\?.*$/, "");
var podCid = urlObj.hash.replace(/^.*\?CID=/, "").replace(/'/g, "");
var $page = $(pageSelector),
// Get the header for the page.
$header = $page.children(":jqmData(role=header)"),
// Get the content area element for the page.
$content = $page.children(":jqmData(role=content)");
// The markup we are going to inject into the content area of the page.
// retrieve the selected pod from the podList by Cid
var selectedPod = podList.getByCid(podCid);
// Find the h1 element in our header and inject the name of the item into it
var headerText = selectedPod.get("isAbout_Name");
$header.html("h1").html(headerText);
// Inject the item info into the content element
var view = new PodDetailView({ model: selectedPod });
var viewElHtml = view.render().$el.html();
$content.html(viewElHtml);
$page.page();
// Enhance the listview we just injected.
var fieldContain = $content.find(":jqmData(role=listview)");
fieldContain.listview();
// We don't want the data-url of the page we just modified
// to be the url that shows up in the browser's location field,
// so set the dataUrl option to the URL for the category
// we just loaded.
options.dataUrl = urlObj.href;
// Now call changePage() and tell it to switch to
// the page we just modified.
$.mobile.changePage($page, options);
}
So the above provides the event plumbing.
The other problem I had was that the page was not set up correctly. It's better to put the page framework in the main html and not put it in an underscore template to be rendered at a later time. I presume that avoids issues where the html is not present when jqm takes over.
<!-- Main Page -->
<div id="lessa-app" data-role="page">
<div data-role="header">
<h1></h1>
</div>
<!-- /header -->
<div id="main-content" data-role="content">
<div id="pod-list" data-theme="a">
<ul data-role="listview">
</ul>
</div>
</div>
<div id="main-footer" data-role='footer'>
<div id="main-newPod" class="ez-icon-plus"></div>
</div>
</div>
<!-- detail page -->
<div id="pod-detail-page" data-role="page">
<div data-role="header">
<h1></h1>
</div>
<div id="detail-content" data-role="content">
<div id="pod-detail" data-theme="a">
</div>
</div>
<div id="detail-footer" data-role='footer'>
back
</div>
</div>