Tabbed navigation in Vue.Js - javascript

I'm trying to create a basic tabbed navigation system with Vue. I'm capturing user's navigation choice and calling a function which changes a certain data. That data is eventually going to determine which tab should be active. So far I have this:
HTML
<div id="app">
<div id="nav">
Home
Info
</div>
<div class="content">
<div class="boxes" id="home" v-bind:class="choice">
</div>
<div class="boxes" id="info" v-bind:class="choice">
</div>
</div>
</div>
Vue
new Vue({
el: '#app',
data: {
choice: 'homeActive' // Home is chosen by default
},
methods: {
makeActive: function(val) {
this.choice = val;
}
}
});
At the moment, if the user clicks on Home link, both home and info divs get homeActive class, this is because I couldn't manage to return two statements with my methods with this basic logic:
enable tab1 & disable tab2 || enable tab 2 & disable tab1
I'm trying different approaches but I can affect only a single state with called methods due to binding my content to a single class.
Any suggestions on how I can get this working with Vue?

The way I generally solve this is by adding another function isActiveTab like so...
new Vue({
el: '#app',
data: {
choice: 'homeActive' // Home is chosen by default
},
methods: {
makeActive: function(val) {
this.choice = val;
},
isActiveTab: function(val) {
return this.choice === val;
}
}
});
Then in your view...
<div id="app">
<div id="nav">
Home
Info
</div>
<div class="content">
<div class="boxes" id="home" v-show="isActiveTab('homeActive')">
</div>
<div class="boxes" id="info" v-show="isActiveTab('infoActive')">
</div>
</div>
</div>

Related

Sharing reactive localStorage state between multiple vue apps on the same page

I'm creating a dashboard with Laravel and VueJS, I created a button that allows to enlarge or reduce my sidebar in a Sidebar.vue component here is my components:
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="head-aside">
<div class="app-logo">
<i class="bx bxl-trip-advisor"></i>
</div>
<span class="app-name">CONTROLPANEL</span>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu()">
<span class="boxicons"><i class="bx bx-chevrons-right"></i></span>
</button>
</div>
</aside>
</template>
<script>
export default {
data() {
return {
is_expanded: ref(localStorage.getItem('is_expanded') === 'true'),
}
},
methods: {
ToggleMenu() {
this.is_expanded = !this.is_expanded
localStorage.setItem('is_expanded', this.is_expanded)
},
},
}
</script>
The problem that arises is that in another component I created a navbar with a fixed width, what I would like to do is that when my sidebar changes size I would like my navbar to also change, in the other component I just have a template with a nav and the import of my sidebar.
You say you are using laravel with vue. It's not clear how the vue components are integrated, but I'm going to assume that laravel injects individual vue components that you would like to communicate. (as opposed to a vue based SPA that communicates with laravel using API only)
The two components are not aware of each other. Even though they both have access to the same localStorage, they don't know when the values there are updated.
There are several ways you can deal with this, here's one way
create a reactive object outside of the components to manage the shared state
import { ref, computed } from 'vue'
const isExpandedRef = ref(localStorage.getItem('is_expanded') === 'true');
window.IS_EXPANDED = computed({
get(){
return isExpandedRef.value
},
set(value){
isExpandedRef.value = !!value;
localStorage.setItem('is_expanded', isExpandedRef.value);
}
})
the ref is required so that when isExpandedRef.value changes, the computed getter triggers notifications to it's listeners.
If you load this before any of the components, you will create a computed(reactive) variable available to any script on the page (frames and shadow dom aside)
then you can use in your components like this.
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="head-aside">
<div class="app-logo">
<i class="bx bxl-trip-advisor"></i>
</div>
<span class="app-name">CONTROLPANEL</span>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="is_expanded = !is_expanded">
<span class="boxicons"><i class="bx bx-chevrons-right"></i></span>
</button>
</div>
</aside>
</template>
<script>
export default {
data() {
return {
is_expanded: window.IS_EXPANDED,
}
},
}
</script>
because is_expanded is a computed with with getters and setters, you can set the value straight from the template, or use this.is_expanded = !this.is_expanded in methods.
As stated already, this relies on using the global window object. This solution is proposed for its simplicity. There are some drawbacks to using the window object, and a more robust solution would rely on injecting such shared state instead of relying on window, but it comes with more overhead.
This code works fine
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="head-aside">
<div class="app-logo">
<i class="bx bxl-trip-advisor"></i>
</div>
<span class="app-name">CONTROLPANEL</span>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu()">
<span class="boxicons">
<i class="bx bx-chevrons-right"></i>click me
</span>
</button>
</div>
</aside>
</template>
<script>
export default {
data() {
return {
is_expanded: localStorage.getItem("is_expanded") === "true",
};
},
methods: {
ToggleMenu() {
this.is_expanded = !this.is_expanded;
localStorage.setItem("is_expanded", this.is_expanded);
},
},
};
</script>
Here is a GIF showing the result in action: https://share.cleanshot.com/TtKsUj
Make local storage reactive by using watcher and setting its deep property to true
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
or if you are using vue2 then use event bus

Vue.js select closest div with certain classname

I have the following HTML markup:
<div id="app">
<div class="image">
<div class="overlay">
<p>Some overlay text</p>
</div>
<img src="https://placeimg.com/640/480/any" class="img-fluid">
</div>
<div class="info">
<h6 class="name">Title here</h6>
<p class="meta">Meta here</p>
</div>
<div class="info-button" #mouseover="addParentClass" #mouseout="removeParentClass">
Mouse over here!
</div>
What I would like to do is whenever someone hovers over the div with class "info-button" certain classes get added to the image above and the overlay.
I have got it working with the following Vue.js markup:
let vm = new Vue({
el: "#app",
data:{
isHovering: false
},
methods: {
addParentClass (event) {
event.target.parentElement.children[0].children[1].classList.add('active')
event.target.parentElement.children[0].children[0].classList.add('overlay-active')
},
removeParentClass (event) {
event.target.parentElement.children[0].children[1].classList.remove('active')
event.target.parentElement.children[0].children[0].classList.remove('overlay-active')
},
},
})
However it seems like a lot of redundant JS. I have tried to get it working with:
event.target.parent.closest('.overlay'.).classList.add('overlay-active')
And a lot of similar parent/children/closest selectors, however I can not seem to get the result I want. How can I get the "closest" selector to work here?
Here's a codepen with a very rough working example: Link to codepen
Edit: I want to point out that i want to use this in a loop, so I will have mulitple images and I want to make the overlay only appear on the current image.
VueJS is reactive. This means the data should drive the DOM. You should not play with the DOM yourself.
Add an active property to data;
let vm = new Vue({
el: "#app",
data:{
isHovering: false,
active: false
},
methods: {
addParentClass (event) {
this.active = true;
},
removeParentClass (event) {
this.active = false; },
},
})
And make the DOM reactive by;
<div class="overlay" :class="{'overlay-active': active}" >
<p>Some overlay text</p>
</div>
Here is the updated codepen
https://codepen.io/samialtundag/pen/Jeqooq

Retain scroll position when a button is toggled

I am working on an angularJS application which has a page where I display around 30 items using ng-repeat. In front of each item, there is a toggle button (enabled/disabled). With the current code that I have, I can toggle these items. But the problem is if I scroll down and toggle lets say item 25, then automatically it scrolls to the top of the page. If I now scroll down, I can see that the toggle actually took place.
So the requirement now is to make sure that the scroll position is retained after the toggle button is clicked.
Please see below the code that I have.
HTML
<div id="eventTypes" class="panel-body">
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
<div ng-if="spinner" class="spinner">
<div class="spinner-container container1">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container2">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container3">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
</div>
</div>
JS
(function () {
'use strict';
angular.module('myApp').controller('itemsController', function ($scope, itemsService) {
var serviceError = function (errorMsg) {
console.log(errorMsg);
$scope.turnOffSpinner();
};
$scope.items = [];
$scope.item = {};
$scope.spinner = true;
$scope.toggleEnabled = function (item) {
$scope.turnOnSpinner();
itemsService.toggleEnabled(item)
.then(function () {
$scope.loaditems();
});
};
$scope.loaditems = function () {
itemsService.getitems().then(function (response) {
$scope.items = response.data;
}, serviceError);
$scope.turnOffSpinner();
};
$scope.turnOnSpinner = function () {
$scope.spinner = true;
};
$scope.turnOffSpinner = function () {
$scope.spinner = false;
};
$scope.loaditems();
});
}());
How this works right now is, once I click the toggle button, a spinner is enabled. Meanwhile the controller will call the itemService.toggleEnabled() method which does an ajax call to the server to just change the status of the item(enabled to disabled or vice-versa) in the backend. On successful change of the status and when the ajax call returns, the $scope.loadItems() method is called in the controller. This method will then do another ajax call to fetch the items (now with the updated status of the item that was toggled). The spinner is disabled and the data is then displayed on the UI.
When all of this is done, the page is scrolled to the top. This is annoying when I want to toggle an item which is way down in the list.
I want the page to be present at the same position when I clicked the toggle button of the corresponding item and not scrolling up to the top.
I am new to AngularJS and any help in this regard would be really helpful.
It looks like your spinner scheme is what's causing you problems:
...
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
...
<div ng-if="spinner" class="spinner">
...
Whenever you click your button, you are removing every single element in your ng-repeat from the DOM when you $scope.turnOnSpinner(). That's why it appears to jump to the top. It's not really jumping, there just aren't enough DOM elements to fill up the page, making the page so short that the scrollbar disappears (even if it's only for a second). Then when the spinner is done, your ng-repeat fills up the page with DOM elements again, resulting in your scroll position being lost.
So basically what you are trying to fix is a symptom of a less than ideal loading spinner implementation.
ng-if is a "brutal" way of hiding things in Angular. It's mostly meant to hide things for a longer period of time than "softer" directives like ng-show/ng-hide. One solution to your problem is to use ng-disabled on each one of your buttons to prevent the user from interacting with it while the spinner is active, rather than doing a hard removal of each element:
Before:
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
After:
<div ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
ng-disabled="spinner"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
Another solution, which I really like and use myself is this Angular module: https://github.com/darthwade/angular-loading
You can attach it to any element in the page and it will put a loading spinner over it and prevent you from interacting with it until your ajax or whatever is done.
If you don't like either of those, try putting your ng-repeat into a container that you can use to prevent interaction with your elements when the spinner is up:
<div class="container" ng-class="{'you-cant-touch-this': spinner}">
<div ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
</div>
Now you can style it in some way to prevent interaction without having to remove all those items from the DOM:
.you-cant-touch-this {
pointer-events: none;
}

Navigation problems with pager.js

Im trying to implement pager.js in my knockout SPA.
It is working, sort of, however i am experiencing some strange behaviour, even though I have followed the official guide..
My page structure look like this.
<div data-bind="page: { id='start', title='index'}">
</div>
<div data-bind="page: { id='mainPage1', title='mainPage1'}">
</div>
//implement deep nav??
<div data-bind="page: { id='mainPage2', title='mainPage2'}">
//Grid page
<div data-bind="page: { id='start', title='grid'}">
//Error here - page dont exist? | URL output: mainPage2/detail
<a data-bind="page-href: '../detail' ">go to detail page.</a>
</div>
//Detail page
<div data-bind="page: { id='detail', title='detail'}">
</div>
</div>
Also when i type in the url I can navigate to mainPage2 by this url:
/mainPage1/dsjak/adsPae1/madaadsnPage1/mainPage2
Aslong as the last part of the url is valid it will navigate to that part of the page, is this intended?
Not sure if this can have anything to do with my Back end routing but it looks like this..
routes.MapRoute(
name: "Default",
url: "{*catchall}",
defaults: new { controller = "Home", action = "Index" }
);
I am also using require.js..
Your syntax is all wrong
e.g. id='start' change to id:'start'
it should be
<div data-bind="page: { id:'start', role: 'start', title:'index'}">start</div>
<div data-bind="page: { id:'mainPage1', title:'mainPage1'}">page 1</div>
<div data-bind="page: { id:'mainPage2', title:'mainPage2'}">
page2
<div data-bind="page: { id:'start', title:'grid'}">
//Error here - page dont exist? | URL output: mainPage2/detail
<a data-bind="page-href: '../detail' ">go to detail page.</a>
</div>
//Detail page
<div data-bind="page: { id:'detail', title='detail'}"></div>
</div>

Create custom follow button for my application

I would like a directive that dynamically knows if I'm following the user in my App.
I have a resource to get the currentUser
this.following = function () {
var defer = $q.defer();
var user = $cookies.getObject('currentUser');
UserResource.get({id: user.id}).$promise.then(
function (data) {
defer.resolve(data.following);
});
return defer.promise;
};
This is in one of my services. It returns all users that I'm following.
When instantiating my controller I fetch the users I follow within my app:
UserService.following().then(
function (data) {
$scope.following = data;
});
I would like to move that into a directive so that I can easily reuse it somewhere else in my app.
This is the HTML I am using right now (and it's not really beautiful) :
<div class="item" ng-repeat="user in users">
<div class="right floated content">
<div ng-show="isFollowing(user)" class="ui animated flip button" tabindex="0"
ng-click='unFollow(user)'>
<div class='visible content'>
Following
</div>
<div class="hidden content">
Unfollow
</div>
</div>
<div ng-hide="isFollowing(user)" ng-click="follow(user)" class="ui button">Follow</div>
</div>
</div>
But instead something like :
<div class="item" ng-repeat="user in users">
<um-follow-button um-user="user"></um-follow-button>
</div>
then depending if I'm following the user or not then render one of the two options.
I don't know if I will have to use a controller in my directive.
I have looked at : https://gist.github.com/jhdavids8/6265398
But it looks like a mess.

Categories