How to hide bootstrap offcanvas in vue3 when router-link is clicked? - javascript

Good day, I would like to hide bootstrap-5/Offcanvas when I click on link inside. Here is Offcanvas:
//Button activate Offcanvas
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDarkNavbar"
aria-controls="offcanvasDarkNavbar">
<span class="navbar-toggler-icon"></span>
</button>
//Offcanvas
<div class="offcanvas offcanvas-end text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar"
aria-labelledby="offcanvasDarkNavbarLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">Menu</h5>
</div>
<div class="offcanvas-body">
<ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
<li class="nav-item">
<router-link to="/about" class="nav-link link-info align-middle px-0">
<i class="fs-4 bi-house"></i> <span class="ms-1 d-none d-sm-inline">Home</span>
</router-link>
</li>
</ul>
</div>
</div>
In official docs they say:
You can create an offcanvas instance with the constructor, for example:
var myOffcanvas = document.getElementById('myOffcanvas') OR
var bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas)
and then use method "hide". I tried to use in router-link #click="hideThisCanvas" and then
methods: {
hideThisCanvas(){
let myOffcanvas = document.getElementById('offcanvasDarkNavbar')
myOffcanvas.hide();
}
}
but it gives the error myOffcanvas.hide is not a function. Please, help!

you have to create an offcanvas instance with the constructor, for example:
methods: {
hideThisCanvas(){
let myOffcanvas = document.getElementById('offcanvasDarkNavbar')
let bsOffcanvas = bootstrap.Offcanvas.getInstance(myOffcanvas);
bsOffcanvas.hide();
}
}
OR
methods: {
hideThisCanvas(){
let myOffcanvas = document.getElementById('offcanvasDarkNavbar')
let bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas);
bsOffcanvas.hide();
}
}
Because using traditional document selectors is not ideal for modern JS framework, you can create a ref for your offCanvas like this.
<div ref="offCanvas" class="offcanvas offcanvas-start" tabindex="-1" id="example_canvas">
Then access it in your vuejs function the way you should
example
let myOffcanvas = this.$refs.offCanvas;
your final method should be something like this
methods: {
hideThisCanvas(){
let myOffcanvas = this.$refs.offCanvas;
let bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas);
bsOffcanvas.hide();
}
}
I do not know so much about vuejs but I hope you get concept now and it helps you.

Related

Updating module on instant change when list is updated

Sorry for the long post, but I tried explaining things in as much detail as possible.
So as I dive deeper into JavaScript and start learning more and more about AJAX requests and other components, I've stumbled across something that I can't seem to figure out.
So below, I will explain what I'm doing and what I would like to do, and see if someone has some guidance for me.
So here is my Vue.js app:
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
data: function() {
return {
list: {},
}
},
created() {
this.fetchApplicationsMenu();
},
methods: {
fetchApplicationsMenu() {
var self = this;
wp.apiRequest( {
path: 'fh/v1/menus/applications',
method: 'GET',
}).then(menu => self.list = menu.data);
},
changed() {
const selected = this.$data.list.selected;
function get_ids(list, field) {
const output = [];
for (let i=0; i < list.length ; ++i)
output.push(list[i][field]);
return output;
}
const result = get_ids(selected, "id");
wp.apiRequest( {
path: 'fh/v1/menus/applications',
method: 'PUT',
data: {
ids: result,
},
}).then((post) => {
return post;
},
(error) => {
console.log(error);
});
},
add(x) {
this.$data.list.selected.push(...this.$data.list.available.splice(x, 1));
this.changed();
},
remove(x) {
this.$data.list.available.push(...this.$data.list.selected.splice(x, 1));
this.changed();
},
},
});
Then here is the HTML portion that I'm using to render the two columns:
<div class="column is-half-desktop is-full-mobile buttons">
<nav class="level is-mobile mb-0">
<div class="level-left">
<div class="level-item is-size-5 has-text-left">Selected</div>
</div>
<div class="level-right">
<div class="level-item">
<i class="fas fa-sort-alpha-up is-clickable"></i>
</div>
</div>
</nav>
<hr class="mt-1 mb-3">
<draggable class="list-group"
v-model="list.selected"
v-bind="dragOptions"
:list="list.selected"
:move="onMove"
#change="changed">
<button class="button is-fullwidth is-flex list-group-item o365_app_handle level is-mobile" v-for="(app, index) in list.selected" :key="app.id">
<div class="level-left">
<span class="icon" aria-hidden="true">
<img :src="app.icon_url" />
</span>
<span>{{app.name}}</span>
</div>
<div class="level-right">
<span class="icon has-text-danger is-clickable" #click="remove(index)">
<i class="fas fa-times"></i>
</span>
</div>
</button>
</draggable>
</div>
<div class="column is-half-desktop is-full-mobile buttons">
<div class="is-size-5 has-text-left">Available</div>
<hr class="mt-1 mb-3">
<draggable class="list-group"
v-model="list.available"
v-bind="dragOptions"
:list="list.available"
:move="onMove">
<button class="button is-fullwidth is-flex list-group-item o365_app_handle level is-mobile" v-for="(app, index) in list.available" :key="app.id">
<div class="level-left">
<span class="icon" aria-hidden="true">
<img :src="app.icon_url" />
</span>
<span>{{app.name}}</span>
</div>
<div class="level-right">
<span class="icon has-text-primary is-clickable" #click="add(index)">
<i class="fas fa-plus"></i>
</span>
</div>
</button>
</draggable>
</div>
That outputs the following items, and all works great. See the video display below of each component working as needed. This all works great! I'm calling the changed() method on add and remove which grabs all the IDs and stores them in the DB via an endpoint.
The Problem:
Now I have the following dropdown menu, which depends on the fh/v1/menus/applications endpoint to pull in all the items as shown below:
As you can see below, when I open the dropdown, it has three apps, when I open the cog wheel and remove one of the apps and it saves it but the dropdown doesn't get automatically updated, I have to refresh the page and then I will see the updates.
Does anyone know how to fetch the new items without a refresh?
Here is the HTML and the JS for the dropdown piece:
HTML: As you can see in there, I have data-source="applications" which pulls in the items inside the init_menu as shown in the JS.
<div class="dropdown-menu" id="dropdown-o365" role="menu">
<div class="dropdown-content">
<div class="container is-fluid px-4 pb-4">
<?php if ($application = Applications::init()): ?>
<div class="columns">
<div class="dropdown-item column is-full has-text-centered is-size-6">
<div class="level is-mobile">
<div class="level-left">
<?= $application->get_name() ?>
</div>
<div class="level-right">
<a class="navbar-item modal-element icon" id="o365-apps-cogwheel" data-target="o365-modal-edit" aria-haspopup="true">
<i class="fa fa-cog"></i>
</a>
</div>
</div>
</div>
</div>
<div class="columns is-multiline" data-source="applications"></div>
<?php else: ?>
<div class="columns">
<div class="column is-full">
No applications present.
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
Then here is the JavaScript. I initilize the method inside DOMContentLoaded using init_menu('applications');:
function init_menu(paths)
{
paths.forEach(path => {
const target = document.querySelector('[data-source=' + path + ']');
if (target) {
wp.api.loadPromise.done(function () {
const Menus = wp.api.models.Post.extend({
url: wpApiSettings.root + 'fh/v1/menus/' + path,
});
const menus = new Menus();
menus.fetch().then(posts => {
// This returns the data object.
const data = posts.data;
let post_list;
// Check if it's an array and see if selected is empty otherwise show available.
if (Array.isArray(data.selected) && data.selected.length !== 0) {
post_list = data.selected;
} else {
post_list = data.available;
}
post_list.forEach(function (post) {
switch(path) {
case 'applications':
target.appendChild(create_apps_dom_tree(post));
break;
default:
console.log('Path route is invalid.');
break;
}
})
})
})
}
});
}
function create_apps_dom_tree(post) {
const {
icon_url,
url,
name,
} = post
const container = document.createElement('div');
container.className = 'column is-one-third is-flex py-0';
const anchor = document.createElement('a');
anchor.href = url;
anchor.className = 'dropdown-item px-2 is-flex is-align-items-center';
const figure = document.createElement('figure');
figure.className = 'image is-32x32 is-flex';
const img = document.createElement('img');
img.src = icon_url;
const span = document.createElement('span');
span.className = 'pl-2';
span.textContent = name;
figure.appendChild(img);
anchor.append(figure, span);
container.appendChild(anchor);
return container;
}
If anyone has some guidance or an answer on how to pull in live data from the database on the fly, that would be amazing.
Basically, I need my data-source: to automatically grab the items when my vue/db request is sent so I don't have to refresh the page.
Inside my Vue app, I have the following method:
fetchApplicationsMenu() {
var self = this;
wp.apiRequest( {
path: 'fh/v1/menus/applications',
method: 'GET',
}).then(menu => self.list = menu.data);
},
which calls a GET request and then stores the data inside the return { list: {} }.
A quick fix might be to just invoke init_menu() from the component's beforeDestroy() hook, called when the dialog closes. You might choose to do it from changed() instead if the dropdown is still accessible with this dialog open.
new Vue({
// option 1:
beforeDestroy() {
init_menu('applications');
},
// option 2:
methods: {
changed() {
init_menu('applications');
}
}
})
Alternative: You already know what the final application list is in changed(), so you could update the dropdown with the new list from that method.
function update_menu(path, post_list) {
const target = document.querySelector('[data-source=' + path + ']');
// remove all existing children
Array.from(target.childNodes).forEach(x => x.remove());
post_list.forEach(post => target.appendChild(create_apps_dom_tree(post)))
}
new Vue({
methods: {
changed() {
update_menu('applications', this.$data.available);
}
}
})

TypeError: menu is not a function

I have this error that I can't find why it is not being called properly menu is a function yet it says it is not. error (TypeError: menu is not a function) I tried to move it before the HTML that calls it but that did not work.
<script>
var dropdown = document.querySelector("nav .dropdown");
var menu = document.querySelector("nav div .menu");
function menu()
{
if(dropdown.style.display === "none")
{
dropdown.style.display = "grid";
}
else
{
dropdown.style.display = "none";
}
} </script>
<input class="button" type="button" onclick="location.href='Controller/logout.php';" value="logout" /> <nav>
<div class="content">
<div class="links">
Home
Portfolio
Reviews
</div>
<i class="material-icons menu" onclick="menu()">menu</i>
</div>
<div class="dropdown">
Home
Portfolio
Reviews
</div> </nav>
That probably returns an error, since the naming issues.
Menu is not a function because you first initialize it with var and then redefine it with function.
Try renaming either of variables (or even remove the first one, since it's never used)
it's not working because you declare "menu" as html class and JS function.
just rename your function at both place.
<i class="material-icons menu" onclick="menu()">menu</i>
to
<i class="material-icons menu" onclick="menuFunc()">menu</i>
also
function menu()
to
function menuFunc()

Vue Object not updating with axios POST request

<template>
<div>
<div class="dropdown d-inline-block ml-2">
<button type="button" class="btn btn-sm btn-dual" id="page-header-notifications-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-on:click="MarkAllAsRead($event)">
<i class="si si-bell"></i>
<span class="badge badge-danger badge-pill" v-if="CountUnread(notifications)">{{ CountUnread(notifications) }}</span>
</button>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right p-0 border-1 font-size-sm" aria-labelledby="page-header-notifications-dropdown">
<div class="p-2 bg-primary text-center">
<h5 class="dropdown-header text-uppercase text-white">Notifications</h5>
</div>
<ul class="nav-items mb-0" style="overflow-y:scroll; height:500px;">
<!-- notifications should have been updated by axios but they never appear -->
<li v-for="notification of notifications">
<div v-if="notification.length">
<notification v-bind:notification="notification[0]"></notification>
</div>
<div v-else>
<notification v-bind:notification="notification"></notification>
</div>
</li>
<div class="p-2 border-top" v-if="hasMore">
<a class="btn btn-sm btn-light btn-block text-center" href="javascript:void(0)" v-on:click="loadMoreNotifications($event)">
<i class="fa fa-fw fa-arrow-down mr-1"></i> Load More..
</a>
</div>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
props:[
'user'
],
data: function() {
return{
notificationsIndex:0,
notifications:[],
hasMore:1
}
},
methods: {
loadNotifications: function(){
var data = {
index: this.notificationsIndex
};
axios.post('/notifications/getAll',data).then( response => {
if(response.data){
if(this.notifications.length==0){
this.notifications=response.data;
console.log(this.notifications);//data fetched here successfully
}
else{
this.notifications.push.apply(this.notifications,response.data);
}
}
else{
this.hasMore = 0;
}
});
console.log(this.notifications);//couldn't find data, just observer object
},
loadMoreNotifications: function(event){
event.preventDefault();
this.notificationsIndex++;
this.loadNotifications();
},
CountUnread: function(notifications){
var count=0;
for(var i=0;i<notifications.length;i++){
if(notifications[i].read_at == null){
count++;
}
}
return count;
},
HasNotification: function(notification){
list = this.notifications.filter(item => {
return item.id == notification.id;
});
return list.length
}
},
created: function(){
this.loadNotifications();
window.Echo.private('App.User.' + this.user.id)
.notification((notification) => {
if(this.HasNotification){
this.notifications.unshift(notification);
}
});
}
}
</script>
And notifications object has nothing in html template as well.
Note: This same code works fine on all pages where I have this only instance of vue. On another page (localhost/horizon)(using laravel horizon package and updating it's layout.blade.php ) where there are two instances of vue, one for notifications and other of Laravel/Horizon, this request returns nothing.
The problem here as #cbaconnier mentioned is that you're using an async function and not waiting for the result.
The console.log() that's failing is due to the fact that it's executed before the post request is retrieved (that's why you're receiving an observer).
The same happens in your created method.
try taking all the code that's dependant on the received notifications into the .then() callback of the axios call.
The problem here was, I was using a bootstrap shipped with app theme I was using and when I installed Horizon and updated Horizon layout.blade.php file according to my customisations, it broke this piece of code because it was using some other bootstrap version and once I removed the bootstrap from the horizon require('bootstrap'); in one of it's app.js file, everything worked fine.

how to make Intersection Observer to replicate bootstrap scroll spy behavior

I'm building a blazor application where I should keep java script code to minimal. So I'm using only bootstrap css in my parallax page. I have made scroll spy like behavior work with window.onscroll since it cannot be done with c# as of now; but it gives poor performance.
After googling, I recently came across IntersectionObserver API. so I thought of making my existing window.onscroll logic to work with IntersectionObserver API. However I'm not able to achieve it.
Here is what I'm trying to do. In my parallax page with fixed top nav bar and I would like to apply active css class to a.nav-items when the user scroll reaches the respective section he wants to view.
Here is the HTML:
<body data-spy="scroll" data-target=".site-nav" data-offset="55">
<header id="page-hero" class="site-header d-flex flex-column align-content-between">
<nav class="site-nav family-sans navbar navbar-expand-md navbar-dark fixed-top">
<div class="container-fluid">
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#myTogglerNav" aria-controls="myTogglerNav"
aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand text-uppercase" href="#page-hero">
<i class="fas fa-cube mr-2"></i> Layout Components</a>
<div class="collapse navbar-collapse" id="myTogglerNav">
<div class="navbar-nav ml-auto font-weight-regular text-uppercase">
<a class="nav-item nav-link" href="#page-hero">home</a>
<a class="nav-item nav-link" href="#page-multicolumn">columns</a>
<a class="nav-item nav-link" href="#page-media">media</a>
<a class="nav-item nav-link" href="#page-photogrid">grid</a>
<a class="nav-item nav-link" href="#page-carousel">carousel</a>
<a class="nav-item nav-link" href="#page-nested">nested</a>
<a class="nav-item nav-link" href="#page-icons">icons</a>
<a class="nav-item nav-link" href="#page-floater">floater</a>
<a class="nav-item nav-link" href="#page-cards">cards</a>
</div>
</div>
</div>
</nav>
<section class="layout-hero jumbotron jumbotron-fluid d-flex align-items-center mt-5 text-reverse">
<div class="container text-center">
......
......
......
<article id="page-multicolumn" class="page-section vertical-padding">
<header class="page-section-header container">
.....
.....
.....
<article id="page-media" class="page-section vertical-padding">
<header class="page-section-header container text-center">
....
I have article tag with id matching href of a in top .nav-item. I have only pasted few of the html as the skeleton is same for all article tags.
Here is my existing window.onscroll:
var sections = {};
document.querySelectorAll(".page-section,.site-header").forEach(section => sections[section.id] = section.offsetTop);
window.onscroll = function () {
document.querySelector('header nav').classList.toggle('inbody', document.documentElement.scrollTop > 380);
document.querySelector('#page-media .layout-animation').style['visibility'] = 'hidden';
var scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
Object.keys(sections).forEach(key => {
if (sections[key] <= scrollPosition + 55) {
document.querySelectorAll('a.nav-item').forEach(a => a.classList.remove('active'));
document.querySelector('a[href="#' + key + '"]').classList.add('active');
if (key === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
}
});
};
The above code works. But I would like to use the right API.
Here is my IntersectionObserver:
if (window.IntersectionObserver) {
let observer = new IntersectionObserver(
(entries, observer) => {
console.log(entries);
entries.forEach(entry => {
/* Here's where we deal with every intersection */
document.querySelectorAll('a.nav-item').forEach(a => a.classList.remove('active'));
if (entry.isIntersecting) {
document.querySelector('header nav').classList.toggle('inbody', entry.target.id !== 'page-hero');
document.querySelector('#page-media .layout-animation').style['visibility'] = 'hidden';
document.querySelector('a[href="#' + entry.target.id + '"]').classList.add('active');
if (entry.target.id === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
}
});
}, { root: document.querySelector('.site-nav'), rootMargin: "0px", threshold: 0 });
document.querySelectorAll('.page-section,.site-header').forEach(section => observer.observe(section));
}
But my intersection observer is not working. I tired using console.log(entry) to see what happens. All the entries gets logged only on page load and not working after that. Please assist on where I'm wrong.
The reason the code here doesn't work is that it's running at the wrong time. The call to document.querySelectorAll('.page-section,.site-header') needs to be executed after those elements have been rendered into the DOM, otherwise it won't match anything.
Here is how I made it to work, I tweaked the function little bit as shown below,
function highlightMenu() {
const sections = document.querySelectorAll('.page-section,.site-header');
const config = {
rootMargin: '-55px 0px -85%'
};
let observer = new IntersectionObserver(function
(entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) {
intersectionHandler(entry);
}
});
}, config);
sections.forEach(section => observer.observe(section));
}
function intersectionHandler(entry) {
const id = entry.target.id;
document.querySelector('header nav').classList.toggle('inbody', id !== 'page-hero');
if (id === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
const currentlyActive = document.querySelector('nav a.nav-item.active');
const shouldBeActive = document.querySelector('nav a.nav-item[href="#' + id + '"]');
if (currentlyActive) {
currentlyActive.classList.remove('active');
}
if (shouldBeActive) {
shouldBeActive.classList.add('active');
}
}
and called from inside my component OnAfterRenderAsync using JS interop in blazor,
#code{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("highlightMenu");
}
}
}

Bind paired data in object to another element outside ng-repeat - angular

I have this array of objects that I need to work with:
$scope.pdfs = [
{ "pdf_title": "Corporate Hire", "attached_file": "http://file1.jpg"},
{ "pdf_title": "Wedding Hire", "attached_file": "http://file2.jpg"},
{ "pdf_title": "Filming Hire", "attached_file": "http://file3.jpg"}
];
The pdf_file value is ng-repeated in li's.
What I want to do is if that li is clicked, to push its paired to another div, say the src for an href.
Here are my workings, but not quite correct:
Controller function:
$scope.bindWithFile = function(value) {
var currentValue = $scope.corpResult = value;
// pdfs support
var pdfs = $scope.pdfs;
for (var i = pdfs.length - 1; i >= 0; i--) {
if (currentValue == hasOwnProperty(key[pdfs])) {
value[pdfs] = $scope.corpLinkHref;
}
};
Markup:
<div class="w-12" ng-controller="corpHireController">
<div class="c-6-set">
<ul>
<li ng-repeat="pdf in pdfs" class="col-7 link link-inherit" ng-click="bindWithFile(pdf.pdf_title)">{{::pdf.pdf_title}}</li>
</ul>
</div>
<div class="c-6-set">
<div class="w-12">
<i class="fs-4 col-7 icon icon-pdf"></i>
</div>
<span class="col-7 h4" ng-bind="corpResult"></span>
<button ng-href="{{::corpLinkHref}}" class="button green2-button smaller-letters full-width">Download</button>
</div>
</div>
What is needed:
Clicking on the titles on the left, binds the pdf_title under the pdf icon and binds the attached_file to the button's href
Instead of passing the title of the selected pdf, why not passing the whole object. This way you don't have to performance any find or search function.
Markup:
<div class="w-12" ng-controller="corpHireController">
<div class="c-6-set">
<ul>
<li ng-repeat="pdf in pdfs" class="col-7 link link-inherit"
ng-click="bindWithFile(pdf)">
{{::pdf.pdf_title}}
</li>
</ul>
</div>
<div class="c-6-set">
<div class="w-12">
<i class="fs-4 col-7 icon icon-pdf"></i>
</div>
<span class="col-7 h4" ng-bind="corpResult"></span>
<button ng-href="{{::corpLinkHref}}"
class="button green2-button smaller-letters full-width">
Download
</button>
</div>
</div>
Controller
$scope.bindWithFile = function(selectedPdf) {
$scope.corpResult = selectedPdf.pdf_title;
$scope.corpLinkHref = selectedPdf.attached_file;
}

Categories

Resources