jQuery: Convert to a single tag - jquery-selectors

I want to convert the below HTML to single tag:
Before :
<div class="Parent1">
<div class="Child1">
Child1
</div>
<div class="Child2">
Child2
</div>
</div>
<div class="Parent2">
<div class="Child1">
Child1
</div>
<div class="Child2">
Child2
</div>
</div>
After :
<div class="Parent1">
<button>Child1</button>
<button>Child2</button>
</div>
<div class="Parent2">
<button>Child1</button>
<button>Child2</button>
</div>
i tried with wrapping/unwrapping. But it does not work. This I want to be generic.

Try this (demo):
$('div[class^="Parent"]').each(function(index, parent) {
var html = [];
$('a', parent).each(function(index, child) {
html.push('<button>'+$(child).html()+'</button>');
});
$(parent).html(html.join(''));
});
It assumes that there is a pattern to the parent divs but that that isn't some common class name.

One way to do it:
$("a").unwrap().wrap("<button>").parent().text(function () {
return $(this).text();
});
http://jsfiddle.net/Tomalak/Gct7H/
Another one in the same spirit:
$("a").unwrap().replaceWith(function () {
return $("<button>", {text: $(this).text()});
});
PS: In reality you would want to select something more specific than just $("a"). Maybe $("#container a") or $("a.someClass"), depending on your markup.

Related

How to generate empty div for modal window when page is rendered first time?

Are you able to suggest how to generate an empty 'div' tag for a modal window when the page is rendered the first time?
'div' content will be fetched with ajax request.
I would like to avoid fetching modal window content twice - first when the page is rendered and then when I click on the link to show modal window.
I do not use wicket modal window implementation.
I have a base ModalBorder class.
ModalBorder.java
public class ModalBorder extends Border {
public ModalBorder(String id) {
super(id);
}
}
with ModalBorder.html markup
<wicket:border>
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<wicket:body/>
</div>
</div>
</wicket:border>
AboutAppModalPanel.java
public class AboutModalPanel extends Panel {
public AboutModalPanel(String id) {
super(id);
setOutputMarkupId(true);
var mb = new ModalBorder("modalBorder");
mb.setRenderBodyOnly(true);
add(mb);
}
}
AboutAppModalPanel.html
<wicket:panel>
<div wicket:id="modalBorder">
<article class="media">
<div class="media-content">
<div class="content">
<p>
<strong>
About application content
</strong>
</p>
</div>
</div>
</article>
</div>
</wicket:panel>
I would like to achieve the below output:
MainPage.html
Page is rendered first time (with empty div)
<html>
<body>
...
<div class="modal" id="aboutAppModalPanel2">
<!-- modal is empty and hidden (not-active) -->
</div>
</body>
</html>
and then requesting modal content via ajax request
<html>
<body>
...
<div class="modal is-active" id="aboutAppModalPanel2">
<!-- modal is filled in and SHOWN (added 'is-active' class) -->
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<article class="media">
<div class="media-content">
<div class="content">
<p><strong>About application content</strong>
...
</div>
</body>
</html>
but now I am facing this issue:
Page is rendered first time (with filled in div)
<html>
<body>
...
<div class="modal" id="aboutAppModalPanel2">
<!-- modal is filled in and HIDDEN (not-active) -->
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<article class="media">
<div class="media-content">
<div class="content">
<p><strong>About application content</strong>
...
</div>
</body>
</html>
and then requesting modal content via ajax request
<html>
<body>
...
<div class="modal is-active" id="aboutAppModalPanel2">
<!-- modal is filled in and SHOWN (added 'is-active' class) -->
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<article class="media">
<div class="media-content">
<div class="content">
<p><strong>About application content</strong>
...
</div>
</body>
</html>
Are you able to suggest a solution?
I use Wicket 9.
Add an empty WebMarkupContainer instead of your panel to the page, making sure you call setOutputMarkupId(true) so it can be refreshed in an ajax call. Then replace it with your panel in the Link's onClick(AjaxRequestTarget target) method.
thanks for a tip. I needed to change my implementation but now it works.
// In page constructor:
// Instead of empty WebMarkupContainer you suggested an empty panel with setOutputMarkupId(true)
add(new AjaxEmptyPanel("aboutApp"));
add(showAboutAppLink());
// and AjaxLink:
private AjaxLink<String> showAboutAppLink() {
return new AjaxLink<>("showAboutApp", new ResourceModel("SHOW")) {
private static final long serialVersionUID = -3035458028030059416L;
#Override
public void onClick(AjaxRequestTarget ajaxRequestTarget) {
var aboutApp = new AboutAppModalPanel("aboutApp");
aboutApp.setOutputMarkupId(true);
getPage().replace(aboutApp);
ajaxRequestTarget.add(aboutApp);
}
};
}

How to implement validation rules for elements from a partial view

We are developing a .net core 3.1 MVC application (actual with MVVMC).
We have implemented custom validation attributes. And want them to be checked on server and on client side.
The view is a standard view, however the user has the possibility to add multiple partial views to the standard view (via a button).
In the partial view we were not able to use the 'asp-for' tag helper within the input fields, because one can have several times the same item added, and we need to be able to create a list in the ViewModel out of those additional partial views selected. That's why we tried to build the tags by ourselfes.
Once a partial view has been requested via ajax we are calling the validator again. Server validation works, client validation only works for those inputs, which are on the main view, not for the inputs on the partial views. However, after refreshing or when the server sends the user back after a validation error, client validation starts working (because site is completely refreshed and jquery validation takes input fields of partial views into account).
Please find the code below:
Implementation of validation attribute
public class AllowedExtensionsAttribute: ValidationAttribute, IClientModelValidator
{
private readonly List<string> _extensions = new List<string>();
public AllowedExtensionsAttribute(string extensions)
{
_extensions.Add(extensions);
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (value is IFormFile file)
{
var extension = Path.GetExtension(file.FileName);
if (!_extensions.Contains(extension.ToLower()))
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-extension", ErrorMessage);
}
}
MainViewModel
[AllowedExtensions(".pdf", ErrorMessage = "This data type is not allowed!")]
public IFormFile PdfDocumentOne { get; set; }
PartialViewModel
[AllowedExtensions(".pdf", ErrorMessage = "This data type is not allowed!")]
public IFormFile PdfDocumentTwo { get; set; }
Main View
<form method="post" asp-controller="Home" asp-action="MainView" enctype="multipart/form-data">
<div class="form-group top-buffer">
<div class="row">
<div class="col-2">
<label asp-for="PdfDocumentOne" class="control-label"></label>
</div>
<div class="col-3">
<input asp-for="PdfDocumentOne" class="form-control-file" accept="application/pdf" />
<span asp-validation-for="PdfDocumentOne" class="text-danger"></span>
</div>
</div>
</div>
<div id="container">
<div id="containerFull" class="form-group">
<div class="row">
<div class="col-2">
<label class="control-label">...</label>
</div>
<div class="col-10">
<div id="containerPartialView">
</div>
</div>
</div>
<div class="row">
<div class="col-2">
</div>
<div class="col-3">
<button id="AddPartialView" type="button" class="form-control">...</button>
</div>
</div>
</div>
</div>
...
<div class="form-group top-buffer">
<div class="row">
<div class="col-2">
<input type="submit" value="Submit" class="form-control" id="checkBtn" />
</div>
</div>
</div>
</form>
Partial View
<input id="Lists[#Model.ViewId].ViewId" name="Lists[#Model.ViewId].ViewId" class="partialViewModel" type="hidden" value="#Model.ViewId" />
<div id="Lists[#Model.ViewId].MainContainer" class="partialView">
<div class="form-group">
<div>
<div class="col-2">
<label asp-for="PdfDocumentTwo" class="control-label"></label>
</div>
<div class="col-3">
<input name="Lists[#Model.ViewId].PdfDocumentTwo" id="Lists[#Model.ViewId].PdfDocumentTwo " type="file" class="form-control-file" accept="application/pdf"
data-val="true" data-val-extension="This data type is not allowed!"/>
<span class="text-danger field-validation-valid" data-valmsg-for="Lists[#Model.ViewId].PdfDocumentTwo" data-valmsg-replace="true"></span>
</div>
</div>
</div>
...
</div>
Javascript
function AddPartialView() {
var i = $(".partialView").length;
$.ajax({
url: '/Home/AddPartialView?index=' + i,
success: function (data) {
$('#containerPartialView').append(data);
$().rules('remove','extension');
jQuery.validator.unobtrusive.adapters.addBool("extension");
},
error: function (a, b, c) {
console.log(a, b, c);
}
});
}
$('#AddPartialView').click(function () {
AddPartialView();
});
jQuery.validator.addMethod("extension",
function (value, element, param) {
var extension = value.split('.').pop().toLowerCase();
if ($.inArray(extension, ['pdf']) == -1) {
return false;
}
return true;
});
jQuery.validator.unobtrusive.adapters.addBool("extension");
HomeController
[HttpPost]
public IActionResult MainView(MainViewModel vm)
{
if (vm == null)
{
return RedirectToAction("Index");
}
DatabaseHelper.GetMainViewModel(vm);
if (!ModelState.IsValid)
{
#ViewData["StatusMessageNegative"] = "The entered data is not valid. Please scroll down to correct your data.";
return View(vm);
}
return RedirectToAction("UploadDocument", new { Id = vm.Id});
}
[HttpGet]
public ActionResult AddPartialView(int index)
{
PartialViewModel pvm = DatabaseHelper.GetPartialViewModel(index);
return PartialView("Partial", pvm);
}
After searching in the internet we found the following argument (data-ajax="true"). However, we don't know where to place it or how to use it correctly.
Have you tried re-apply validation after loading your partial view?
$.ajax({
url: '/Home/AddPartialView?index=' + i,
success: function (data) {
$('#containerPartialView').append(data);
$().rules('remove','extension');
jQuery.validator.unobtrusive.adapters.addBool("extension");
},
error: function (a, b, c) {
console.log(a, b, c);
}
}).then(function () {
$("form").each(function () { $.data($(this)[0], 'validator', false); });
$.validator.unobtrusive.parse("form");
});

How to select the closest element with a specific tag in chai protractor

If the html looks as follow:
<form>
<div>
<field-component>
<div class='field'></div>
</field-component>
</div>
<div>
<div class='icon'></div>
</div>
</form>
I am able to get the field using fieldElement = element(by.css('.field')), but how can I get the closest element with the icon class?
try this:
var form = element(by.css('form'));
return form.element(by.css('.field')).isPresent().then((present) => {
if (present) {
return form.element(by.css('.icon')).click(); #click or whatever on this element
}
});

Template not reactive in meteor on adding new data?

This are the event listeners for my two templates,specified in events.js
// event listeners on the addSiteForm template
Template.addCommentForm.events({
// this runs when they click the add button... you need to compete it
'click .js-add-comment':function(event){
var comment_text = $('#comment_input').val();// get the form value using jquery...
var user = 'anonymous person';
// the 'this' variable contains
// the data that this template is displaying
// which is the Website item
var site = this;
if (Meteor.user()){
user = Meteor.user().emails[0].address
}
var comment = {"text":comment_text,
"siteId":site._id,
"createdOn":new Date(),
"createdBy":user};// create a simple object to insert to the collectoin
Comments.insert(comment);
console.log("events start");
console.log("totla coomets are "+Comments.find({}).count()+"\n");
console.log("commenst in site "+Comments.find({siteId:site._id}).count()+"\n");
console.log("site id is "+site._id);
console.log("events end");
return false;
}
});
// event listeners on the addSiteForm template
Template.addSiteForm.events({
// this runs when they click the add button... you need to compete it
'click .js-add-site':function(event){
var url = $('#url_input').val();// get the form value using jquery...
var user = 'anonymous person';
if (Meteor.user()){
user = Meteor.user().emails[0].address
}
var site = {"url":url,
"createdOn":new Date(),
"createdBy":user};// create a simple object to insert to the collectoin
Websites.insert(site);
return false;
}
});
The router function is specified in router.js
Router.configure({
layoutTemplate: 'ApplicationLayout'
});
// the main route. showing the list of sites.
Router.route('/', function () {
this.render('siteList');
});
// this route is for the discussion page for a site
Router.route('/discussSite/:_id', function () {
var siteId = this.params._id;
site = Websites.findOne({_id:siteId});
this.render('discussSite', {data:site});
});
the main.js contain the collections
// shared code
Websites = new Mongo.Collection("websites");
Comments = new Mongo.Collection("comments");
helpers is
// this helper gets the data from the collection for the site-list Template
Template.siteList.helpers({
'all_websites':function(){
return Websites.find({});
},
'safer_email':function(email){
if (email.indexOf('#')!=-1){// we have an email
return email.split('#')[0];
}
else{// probably anonymouse.
return email;
}
}
});
Template.discussSite.helpers({
'comments':function(siteId){
//console.log("helper comments "+this.site);
// complete the code here so that it reruns
console.log(this.params.site._id);
return Comments.find({siteId:siteId});
// all the comments with a siteId equal to siteId.
}
});
the html file is in main.html.here it first displays the websites available in the webpage.On clicking this the user is routed to a new page.Here the user can add comments for the website
<head>
<title>week_4_peer_assessment</title>
</head>
<body>
</body>
<!-- this is the template that iron:router renders every time -->
<template name="ApplicationLayout">
<div class="container">
Home
{{>loginButtons}}
<h1>SiteAce - discuss your favourite websites</h1>
<!-- iron router will select what to render in place of yield-->
{{> yield }}
</div>
</template>
<template name="discussSite">
<h3>Discussing: {{url}} </h3>
{{> addCommentForm}}
<!-- write some code here that iterates through the comments and displays
the comment text and the author -->
<!-- clue - you have already written the 'comments' helper function -->
<ul>
{{#each comments}}
<li>{{text}} (added by {{safer_email createdBy}})
</li>
{{/each}}
</ul>
</template>
<template name="addCommentForm">
<div class="row">
<div class="col-lg-6">
<div class="input-group">
<input type="text" id="comment_input" class="form-control" placeholder="Enter your comment">
<input type="hidden" id="site_id" class="form-control" placeholder="Enter your comment">
<span class="input-group-btn">
<button class="btn btn-default js-add-comment" type="submit">Add!</button>
</span>
</div><!-- /input-group -->
</div><!-- /.col-lg-6 -->
</div>
</template>
<template name="siteList">
Fill in the form and click submit to add a site:
{{>addSiteForm}}
<h3>Sites you have added:</h3>
<ul>
{{#each all_websites}}
<li>{{url}} (added by {{safer_email createdBy}})
<br/>discuss
<br/>visit site
</li>
{{/each}}
</ul>
</template>
<template name="addSiteForm">
<div class="row">
<div class="col-lg-6">
<div class="input-group">
<input type="text" id="url_input" class="form-control" placeholder="Enter website URL...">
<span class="input-group-btn">
<button class="btn btn-default js-add-site" type="submit">Add!</button>
</span>
</div><!-- /input-group -->
</div><!-- /.col-lg-6 -->
</div>
</template>
The starup.js contains the start up code run by the server
Meteor.startup(function(){
if (!Websites.findOne()){// nothing in the database yet
var site = {"url":"http://www.google.com",
"createdOn":new Date(),
"createdBy":"Michael"};// create a simple object to insert to the collectoin
Websites.insert(site);
site = {"url":"http://www.yeeking.net",
"createdOn":new Date(),
"createdBy":"Janet"};// create a simple object to insert to the collectoin
Websites.insert(site);
site = {"url":"http://www.coursera.org",
"createdOn":new Date(),
"createdBy":"Jose"};// create a simple object to insert to the collectoin
Websites.insert(site);
}
});
This is what your template should be: (I removed safer_email. It was erroring. Not sure if it was something else you were working on.)
<template name="discussSite">
<h3>Discussing: {{url}} </h3>
{{> addCommentForm}}
<!-- write some code here that iterates through the comments and displays
the comment text and the author -->
<!-- clue - you have already written the 'comments' helper function -->
<ul>
{{#each comments}}
<li>{{text}} (added by {{createdBy}})</li>
{{/each}}
</ul>
</template>
helper:
your helper function is designed to take in siteId as an input, yet you are not passing one to it.
this._id or Router.current().params._id will get you the siteId that you are looking for. You'd used this.site._id which used to error out as this.site was not valid.You can further see what the this object contains by doing a console.log(this). That would have helped you sort this out faster.
'comments':function(){
console.log("site id in helper is ",this._id); // or Router.current().params._id;
console.log(Comments.find({siteId:this._id}).fetch());
return Comments.find({siteId:this._id});
}

Open bootstrap modal with vue.js 2.0

Does anyone know how to open a bootstrap modal with vue 2.0? Before vue.js I would simply open the modal by using jQuery: $('#myModal').modal('show');
However, is there a proper way I should do this in Vue?
Thank you.
My code is based on the Michael Tranchida's answer.
Bootstrap 3 html:
<div id="app">
<div v-if="showModal">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" #click="showModal=false">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">Modal title</h4>
</div>
<div class="modal-body">
modal body
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
<button id="show-modal" #click="showModal = true">Show Modal</button>
</div>
Bootstrap 4 html:
<div id="app">
<div v-if="showModal">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true" #click="showModal = false">×</span>
</button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" #click="showModal = false">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
<button #click="showModal = true">Click</button>
</div>
js:
new Vue({
el: '#app',
data: {
showModal: false
}
})
css:
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: table;
transition: opacity .3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
And in jsfiddle
Tried to write a code that using VueJS transitions to operate native Bootsrap animations.
HTML:
<div id="exampleModal">
<!-- Button trigger modal-->
<button class="btn btn-primary m-5" type="button" #click="showModal = !showModal">Launch demo modal</button>
<!-- Modal-->
<transition #enter="startTransitionModal" #after-enter="endTransitionModal" #before-leave="endTransitionModal" #after-leave="startTransitionModal">
<div class="modal fade" v-if="showModal" ref="modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button class="close" type="button" #click="showModal = !showModal"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">...</div>
<div class="modal-footer">
<button class="btn btn-secondary" #click="showModal = !showModal">Close</button>
<button class="btn btn-primary" type="button">Save changes</button>
</div>
</div>
</div>
</div>
</transition>
<div class="modal-backdrop fade d-none" ref="backdrop"></div>
</div>
Vue.JS:
var vm = new Vue({
el: "#exampleModal",
data: {
showModal: false,
},
methods: {
startTransitionModal() {
vm.$refs.backdrop.classList.toggle("d-block");
vm.$refs.modal.classList.toggle("d-block");
},
endTransitionModal() {
vm.$refs.backdrop.classList.toggle("show");
vm.$refs.modal.classList.toggle("show");
}
}
});
Example on Codepen if you are not familiar with Pug click View compiled HTML on a dropdown window in HTML section.
This is the basic example of how Modals works in Bootstrap. I'll appreciate if anyone will adopt it for general purposes.
Have a great code 🦀!
I did an amalgam of the Vue.js Modal example and the Bootstrap 3.* live demo.
Basically, I used the Vue.js modal example but replaced (sorta) the Vue.js "html" part with the bootstrap modal html markup, save one thing (I think). I had to strip the outer div from the bootstrap 3, then it just worked, voila!
So the relevant code is regarding bootstrap. Just strip the outer div from the bootstrap markup and it should work. So...
ugh, a site for developers and i can't easily paste in code? This has been a serious continuing problem for me. Am i the only one? Based on history, I'm prolly an idiot and there's an easy way to paste in code, please advise. Every time i try, it's a horrible hack of formatting, at best.
i'll provide a sample jsfiddle of how i did it if requested.
Using the $nextTick() function worked for me. It just waits until Vue has updated the DOM and then shows the modal:
HTML
<div v-if="is_modal_visible" id="modal" class="modal fade">...</div>
JS
{
data: {
isModalVisible: false,
},
methods: {
showModal() {
this.isModalVisible = true;
this.$nextTick(() => {
$('#modal').modal('show');
});
}
},
}
Here's the Vue way to open a Bootstrap modal..
Bootstrap 5 (2022)
Now that Bootstrap 5 no longer requires jQuery, it's easy to use the Bootstrap modal component modularly. You can simply use the data-bs attributes, or create a Vue wrapper component like this...
<bs-modal id="theModal">
<button class="btn btn-info" slot="trigger"> Bootstrap modal </button>
<div slot="target" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
</bs-modal>
const { Modal } = bootstrap
const modal = Vue.component('bsModal', {
template: `
<div>
<slot name="trigger"></slot>
<slot name="target"></slot>
</div>
`,
mounted() {
var trigger = this.$slots['trigger'][0].elm
var target = this.$slots['target'][0].elm
trigger.addEventListener('click',()=>{
var theModal = new Modal(target, {})
theModal.show()
})
},
})
Bootstrap 5 Modal in Vue Demo
Bootstrap 4
Bootstrap 4 JS components require jQuery, but it's not necessary (or desirable) to use jQuery in Vue components. Instead manipulate the DOM using Vue...
Launch modal
<div :class="modalClasses" class="fade" id="reject" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Modal</h4>
<button type="button" class="close" #click="toggle()">×</button>
</div>
<div class="modal-body"> ... </div>
</div>
</div>
</div>
var vm = new Vue({
el: '#app',
data () {
return {
modalClasses: ['modal','fade'],
}
},
methods: {
toggle() {
document.body.className += ' modal-open'
let modalClasses = this.modalClasses
if (modalClasses.indexOf('d-block') > -1) {
modalClasses.pop()
modalClasses.pop()
//hide backdrop
let backdrop = document.querySelector('.modal-backdrop')
document.body.removeChild(backdrop)
}
else {
modalClasses.push('d-block')
modalClasses.push('show')
//show backdrop
let backdrop = document.createElement('div')
backdrop.classList = "modal-backdrop fade show"
document.body.appendChild(backdrop)
}
}
}
})
Bootstrap 4 Vue Modal Demo
My priority was to keep using Bootstrap code, since they made the effort to make the modal work, fixin' the scrollbars and all. I found existing proposals try to mimic that, but they go only part of the way. I didn't even want to leave it to chance: I just wanted to use actual bootstrap code.
Additionally, I wanted to have a procedural interface, e.g. calling dialog.show(gimme plenty of parameters here), not just toggling a variable somewhere (even if that variable could be a complex object).
I also wanted to have Vue's reactivity and component rendering for the actual dialog contents.
The problem to solve was that Vue refuses to cooperate if it finds component's DOM to have been manipulated externally. So, basically, I moved the outer div declaring the modal itself, out of the component and registered the component such that I also gain procedural access to the dialogs.
Code like this is possible:
window.mydialog.yesNo('Question', 'Do you like this dialog?')
On to the solution.
main.html (basically just the outer div wrapping our component):
<div class="modal fade" id="df-modal-handler" tabindex="-1" role="dialog" aria-hidden="true">
<df-modal-handler/>
</div>
component-template.html (the rest of the modal):
<script type="text/x-template" id="df-modal-handler-template">
<div :class="'modal-dialog ' + sizeClass" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body" v-html="body"/>
<div class="modal-footer">
<button type="button" v-for="button in buttons" :class="button.classes" v-bind="button.arias"
#click.stop="buttonClick(button, callback)">{{ button.text }}
</button>
</div>
</div>
</div>
</script>
component-def.js - contains logic for showing & manipulating the dialog, also supports dialog stacks in case you make a mistake and invoke two dialogs in sequence:
Vue.component('df-modal-handler', {
template: `#df-modal-handler-template`,
props: {},
data() {
return {
dialogs: [],
initialEventAssignDone: false,
};
},
computed: {
bootstrapDialog() { return document.querySelector('#df-modal-handler'); },
currentDialog() { return this.dialogs.length ? this.dialogs[this.dialogs.length - 1] : null; },
sizeClass() {
let dlg = this.currentDialog;
if (!dlg) return 'modal-sm';
if (dlg.large || ['large', 'lg', 'modal-lg'].includes(dlg.size)) return 'modal-lg';
else if (dlg.small || ['small', 'sm', 'modal-sm'].includes(dlg.size)) return 'modal-sm';
return '';
},
title() { return this.currentDialog ? this.currentDialog.title : 'No dialogs to show!'; },
body() { return this.currentDialog ? this.currentDialog.body : 'No dialogs have been invoked'; },
callback() { return this.currentDialog ? this.currentDialog.callback : null; },
buttons() {
const self = this;
let res = this.currentDialog && this.currentDialog.buttons ? this.currentDialog.buttons : [{close: 'default'}];
return res.map(value => {
if (value.close == 'default') value = {
text: 'Close',
classes: 'btn btn-secondary',
data_return: 'close'
};
else if (value.yes == 'default') value = {
text: 'Yes',
classes: 'btn btn-primary',
data_return: 'yes'
};
else if (value.no == 'default') value = {
text: 'No',
classes: 'btn btn-secondary',
data_return: 'no'
};
value.arias = value.arias || {};
let clss = (value.classes || '').split(' ');
if (clss.indexOf('btn') == -1) clss.push('btn');
value.classes = clss.join(' ');
return value;
});
},
},
created() {
// make our API available
window.mydialog = this;
},
methods: {
show: function show() {
const self = this;
if (!self.initialEventAssignDone) {
// created is too soon. if we try to do this there, the dialog won't even show.
self.initialEventAssignDone = true;
$(self.bootstrapDialog).on('hide.bs.modal', function (event) {
let callback = null;
if (self.dialogs.length) callback = self.dialogs.pop().callback;
if (self.dialogs.length) event.preventDefault();
if (callback && callback.df_called !== true) callback(null);
});
}
$(self.bootstrapDialog).modal('show');
},
hide: function hide() {
$(this.bootstrapDialog).modal('hide');
},
buttonClick(button, callback) {
if (callback) { callback(button.data_return); callback.df_called = true; }
else console.log(button);
this.hide();
},
yesNo(title, question, callback) {
this.dialogs.push({
title: title, body: question, buttons: [{yes: 'default'}, {no: 'default'}], callback: callback
});
this.show();
},
},
});
Do note that this solution creates one single dialog instance in the DOM and re-uses that for all your dialog needs. There are no transitions (yet), so the UX isn't too great when there are multiple active dialogs. It's bad practice anyway, but I wanted it covered because you never know...
Dialog body is actually a v-html, so just instantiate your component with some parameters to have it draw the body itself.
I create button with params for modal and simply trigger click()
document.getElementById('modalOpenBtn').click()
<a id="modalOpenBtn" data-toggle="modal" data-target="#Modal">open modal</a>
<div class="modal" id="Modal" tabindex="-1" role="dialog" aria-labelledby="orderSubmitModalLabel" aria-hidden="true">...</div>
From https://getbootstrap.com/docs/4.0/getting-started/javascript/#programmatic-api
$('#myModal').modal('show')
You can do this from a Vue method and it works just fine.
modal doc
Vue.component('modal', {
template: '#modal-template'
})
// start app
new Vue({
el: '#app',
data: {
showModal: false
}
})
<script type="text/x-template" id="modal-template">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
default body
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" #click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</script>
<!-- app -->
<div id="app">
<button id="show-modal" #click="showModal = true">Show Modal</button>
<!-- use the modal component, pass in the prop -->
<modal v-if="showModal" #close="showModal = false">
<!--
you can use custom content here to overwrite
default content
-->
<h3 slot="header">custom header</h3>
</modal>
</div>