vue chartjs not rendering when have a change in chartOption - vue.js

I'm building a chart in vuejs using composition API, chartjs#4.2, and vue-chartjs#5.2.0
however, I'm not managing to keep the options and data reactive, so I built a button that when active the zoom would be active, and when inactive it would be reset. But I'm not managing to keep this reactivity within the component<Bar>. In future I would make the grafico bars work as a filter in other visuals (just like Power BI). Could someone help me?
I have a js file that contains the opcoes and the dataset :
export const optionBar = {
responsive: true,
maintainAspectRatio: true,
plugins: {
datalabels: {
formatter: function(values) {
return new Intl.NumberFormat('pt-br',{ maximumFractionDigits: 2, minimumFractionDigits:2, }).format(values)
}
},
zoom: {
zoom: {
wheel: {
enabled: true,
},
drag: {
enabled: true,
backgroundColor: 'rgba(12, 173, 241, 0.10)'
},
pinch: {
enabled: true
},
mode: 'x'
}
}
}
}
export const dataSet = {
labels: [1,2,3,4],
datasets: [
{
label: `test`,
data: null,
},
],
}
I can see the chartOption change in the console log and template with {{ }} but my chart does not update accordingly. From what I saw in the document, according to the updated vers/ion (2022-23) the prop :options would be 'commending' any changes in options by containing the watch in vue-chartjs:
<template>
//Toggle Button that change the boolean value and active changes in chartOption
<button #click="ZoomAtivo" > {{zoomEnable}} </button> {{chartOptions.plugins }}
<div class="card ">
<div class="card-body">
<div class="card-header border-0 d-flex justify-content-between">
<h4 class="card-title mb-0">
Serie Temporal
</h4>
<div class="d-flex p-2">
<div
v-for="value in dateF" :key="value.id"
class="btn-group btn p-0 mx-1" role="group" aria-label="Basic radio toggle button group">
<input type="radio" class="btn-check" name="btnradio" :id="value.id_date_mode" :checked="value.check" />
<label class="btn btn-primary mb-0" :for="value.id_date_mode">
{{value.date_mode}}</label>
</div>
<p v-if="dateF == 1"> teste</p>
</div>
</div>
<Bar v-if="load" class="STChart" :data="datasets" :options="chartOptions"
:updateMode="'none'" />
</div>
</div>
</template>
<script>
import { Bar } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import moment from 'moment'
import { ref, reactive} from 'vue'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, zoomPlugin, ChartDataLabels)
import {optionBar,dataSet} from './options.js'
import { dateFilter } from '#/components/date-mode';
export default {
components: { Bar } ,
setup() {
console.log(Bar)
const chart = ref()
const zoomEnable = ref(false)
const data = ref([])
const dateF = reactive(dateFilter)
const chartOptions =reactive(optionBar)
const load = ref(false)
const data_mov = ref()
const sobra_perda = ref()
const datasets = reactive(dataSet)
const label = ref()
datasets.labels = data_mov
datasets.datasets[0].data = sobra_perda
datasets.datasets[0].label = `Sobra_Perda`
const zmm = reactive({
zoom: {
wheel: {
enabled: true,
},
drag: {
enabled: true,
backgroundColor: 'rgba(12, 173, 241, 0.10)'
},
pinch: {
enabled: true
},
mode: 'x'
}
})
const ZoomAtivo = () => {
zoomEnable.value = !zoomEnable.value
if(zoomEnable.value) {
chartOptions.plugins.zoom = zmm
console.log(chartOptions.plugins.zoom)
} else {
chartOptions.plugins.zoom=null
console.log(chartOptions.plugins.zoom)
}
}
async function DateChart(starDate, endDate, p_items) {
starDate = `01-01-2020`
endDate = `31-12-2022`
const url = `http://localhost:1800/dash/Date/${starDate}/${endDate}`
const respons = await fetch(url, {
method: 'POST',
body: JSON.stringify({
empresa: JSON.parse(localStorage.getItem('companiesGlobalFilter')),
items: p_items
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
},
});
data.value = await respons.json()
const title = Object.keys(data.value[0])
const _Sobra =data.value.map(v => v.sobra_perda)
const _date_movimento =data.value.map(x => {
let datee = new Date(x.date_movimento)
const data = moment(datee).utc()
datee = data.format('MM/YYYY')
return datee
})
data_mov.value = _date_movimento
sobra_perda.value = _Sobra
load.value = true
label.value = title
return data_mov
}
return {
chartOptions,
datasets,
load, data_mov,
sobra_perda,dateF,
zoomEnable, chart, ZoomAtivo
}
}
}
</script>
<style scoped>
#import './css/chartjs.css';
</style>

Related

Prop being passed but not updating the component

So I have a function called updateAll(data.items) which should take the array and randomly assign a new name, it appears the data.items is updating when I view in vue dev tools and is being passed to the component but not actually updating the browser
I've also added in my buildData.js this is being used for the same app but in react so was trying to keep it as general as I could
//HomeView.vue
<script setup>
import { reactive, watch } from "vue";
import Hero from '#/components/Hero.vue'
import FunctionButton from '#/components/FunctionButton.vue'
import ItemsContainer from '#/components/ItemContainer.vue';
import * as dataFunc from '#/data/buildData'
const data = reactive({
items: JSON.parse(localStorage.getItem('items')) || []
})
console.log(data.items)
watch(() => data.items,(newValue) => {
localStorage.setItem('items', JSON.stringify(newValue))
}, {deep: true});
</script>
<template>
<div class='w-2/3 mx-auto text-center'>
<Hero HeroText="The one stop shop to store recipes!" />
<main>
<h1 class="text-5xl font-extrabold dark:text-white">Recipe Manager</h1>
<div class='functions gap-4 grid grid-cols-2 md:grid-cols-3 mt-8 mx-auto w-fit'>
<FunctionButton name="Add new recipe" #click="() => data.items = [...data.items,...dataFunc.buildData(1)]"/>
<FunctionButton name="Add 100 recipes" #click="() => data.items = [...data.items,...dataFunc.buildData(100)]"/>
<FunctionButton name="Add 1000 recipes" #click="() => data.items = [...data.items,...dataFunc.buildData(1000)]"/>
<FunctionButton name="Delete all recipes" #click="() => data.items = dataFunc.deleteAll()"/>
<FunctionButton name="Edit all recipes" #click="() => data.items = dataFunc.updateAll(data.items)"/>
</div>
<ItemsContainer :items="data.items"/>
</main>
</div>
</template>
//ItemContainer.vue
<script setup>
import Item from '#/components/Item.vue';
const props = defineProps({
items: {type: Array, default: () => []},
})
</script>
<template>
<div
class='items-container w-full md:w-max bg-gray-800 dark:bg-gray-800 text-white dark:text-black mx-auto mt-12 p-4 space-y-4'>
<Item
v-for="item in items"
v-if="items.length > 0"
:key="item.id"
:meal="item"
/>
<p v-else class='text-white'>No items to display</p>
</div>
</template>
I tried to make this as generic as possible as trying to make a similar app on Vue and React so they could share this
import { v4 as uuidv4 } from 'uuid';
function create(){
const data = {
id: uuidv4(),
category: category[random(category.length)],
name: meals[random(meals.length)],
prepTime: randomTime(),
cookTime: randomTime(),
isFav: false
}
return data
}
function random(max){
return Math.round(Math.random() * 1000) % max;
}
const meals = [
"Roast Chicken", "Omelette", "Steak Pie", "Beef Stir Fry", "Fish And Chips", "Tomato And Prawn Risotto", "Pepperoni Pizza", "Cheesy Nachos", "Fajitas", "Baked Potato",
"Full English Breakfast", "Pancakes", "Chocolate Brownies", "Meatballs With Red Pepper Sauce", "Chicken Cesar Salad", "Beef Burger", "Chips","Macaroni Cheese", "Fillet Steak", "Chicken Wings",
"BBQ Ribs", "Tomato Soup", "Prawn Dim Sum", "Pork Gyozas", "Tomato Bruschetta", "Spring Rolls", "Beef Jerky", "Lasagne", "Spagetti Carbonara", "Salmon And Potatoes"
]
const category = [
"Breakfast", "Lunch", "Dinner"
]
function randomTime(){
return Math.round(Math.random() * 30)
}
function buildData(count){
const data = new Array(count);
for(let i = 0; i<count; i++){
data[i] = create();
}
return data;
}
function updateAll(currentArray){
return currentArray.map((item) => {
return{...item, name: meals[random(meals.length)], }
})
}
function updateOne(item){
return{...item, name: meals[random(meals.length)], }
}
function favouriteAll(currentArray = []){
return currentArray.map((item) => {
return {...item, isFav: true}
})
}
function deleteAll(){
const data = []
return data;
}
export {buildData, deleteAll, favouriteAll, updateAll, updateOne};
Check the console for errors.
If I take the functionality from HomeView.vue, your ItemContainer.vue and an Item.vue (which you didn't provided), then it works.
Check the working Vue SFC Playground
The code:
App.vue
<script setup>
import { ref, watch, reactive } from 'vue'
import ItemsContainer from './ItemContainer.vue';
const data = reactive({
items: []
})
console.log(data.items)
watch(() => data.items, (newValue) => {
alert(JSON.stringify(newValue))
}, {deep: true});
const addItem = () => {
data.items.push({ id: data.items.length, name: 'New Item'})
}
</script>
<template>
<ItemsContainer :items="data.items"/><br />
<button type="button" #click="addItem()">Add</button>
</template>
ItemContainer.vue is the same.
Item.vue
<script setup>
const props = defineProps({
meal: {
type: Object,
default: {undefined}
}
})
</script>
<template>
<div>
{{JSON.stringify(meal)}}<br />
</div>
</template>
UPDATE
I've updated the playground with your functions and it still does work well. The problem is somewhere else.

Watch, Compare & post updated form data to API using Axios in Vue 3

I need help to complete my code.
This is what have done.
I am fetching options from API, so I have defined the initial state as
empty.
Once I have a response from API, I update the state of options.
My form is displayed once I have a response from API.
Now using v-bind I am binding the form.
Where I need help.
I need to watch for the changes in form. If the values of form elements are different from the state of the API response, I would like to enable the submit button.
When the save button is clicked, I need to filter the options that were changed & submit that form data to my pinia action called updateOptions.
Note: API handles post data in this way. Example: enable_quick_view: true
Thank you in advance.
options.js pinia store
import { defineStore } from 'pinia'
import Axios from 'axios';
import axios from 'axios';
const BASE_API_URL = adfy_wp_locolizer.api_url;
export const useOptionsStore = defineStore({
id: 'Options',
state: () => ({
allData: {},
options: {
enable_quick_view: null, // boolean
quick_view_btn_label: "", // string
quick_view_btn_position: "", // string
},
newOptions: {}, // If required, holds the new options to be saved.
message: "", // Holds the message to be displayed to the user.
isLoading: true,
isSaving: false,
needSave: false,
errors: [],
}),
getters: {
// ⚡️ Return state of the options.
loading: (state) => {
return state.isLoading;
},
},
actions: {
// ⚡️ Use Axios to get options from api.
fetchOptions() {
Axios.get(BASE_API_URL + 'get_options')
.then(res => {
this.alldata = res.data.settings;
let settings = res.data.settings_values;
/*
* Set options state.
*/
this.options.enable_quick_view = JSON.parse(
settings.enable_quick_view
);
this.options.quick_view_btn_label =
settings.quick_view_btn_label;
this.options.quick_view_btn_position = settings.quick_view_btn_position;
/*
* End!
*/
this.isLoading = false;
})
.catch(err => {
this.errors = err;
console.log(err);
})
.finally(() => {
// Do nothing for now.
});
},
// ⚡️ Update options using Axios.
updateOptions() {
this.isSaving = true;
axios.post(BASE_API_URL + 'update_options', payload)
.then(res => {
this.needSave = false;
this.isSaving = false;
this.message = "Options saved successfully!";
})
.catch(err => {
this.errors = err;
console.log(err);
this.message = "Error saving options!";
})
}
},
});
Option.vue component
<script setup>
import { onMounted, watch } from "vue";
import { storeToRefs } from "pinia";
import { Check, Close } from "#element-plus/icons-vue";
import Loading from "../Loading.vue";
import { useOptionsStore } from "../../stores/options";
let store = useOptionsStore();
let { needSave, loading, options, newOptions } = storeToRefs(store);
watch(
options,
(state) => {
console.log(state);
// Assign the option to the newOptions.
},
{ deep: true, immediate: false }
);
onMounted(() => {
store.fetchOptions();
});
</script>
<template>
<Loading v-if="loading" />
<form
v-else
id="ui-settings-form"
class="ui-form"
#submit="store.updateOptions()"
>
<h3 class="option-box-title">General</h3>
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Enable quick view</p>
<p class="option-description">
Once enabled, it will be visible in product catalog.
</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-switch
v-model="options.enable_quick_view"
size="large"
inline-prompt
:active-icon="Check"
:inactive-icon="Close"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Button label</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-input
v-model="options.quick_view_btn_label"
size="large"
placeholder="Quick view"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<button type="submit" class="ui-button" :disabled="needSave == true">
Save
</button>
</form>
</template>
<style lang="css" scoped>
.el-checkbox {
--el-checkbox-font-weight: normal;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
</style>
In the watch function you can compare the new and old values. But you shuld change it to:
watch(options, (newValue, oldValue) => {
console.log(oldValue, newValue);
// compare objects
}, {deep: true, immediate: false};
Now you can compare the old with the new object. I think search on google can help you with that.
Hope this helps.

Cannot read properties of undefined (reading 'transition') - Swiper & VueJS & Ionic

I can't use the Swiper slideNext function in VueJS with Ionic.
I tried the demo codes from the official Swiper website, without success...
Here is my code:
<template>
<ion-page>
...
<ion-row class="mainRow">
<ion-col size="12">
<ion-row>
<div class="swiper">
<div
class="swiper-slide"
v-for="(question, index) in qst"
:key="`question_${index}`"
>
...
</ion-page>
</template>
<script>
setup() {
const swiperRef = ref(null);
const [slideSpace, setSlideSpace] = useState(0);
const swiper = new Swiper(".swiper", {
speed: 400,
slidesPerView: 1,
spaceBetween: slideSpace,
controller: {
control: swiperRef,
},
on: {
slideChange: function (e) {
setCurrentQuestion(e.activeIndex + 1);
},
},
});
const handleAnswerClick = (event, answer, question) => {
...
setTimeout(() => {
isCorrect && setScore((score) => score + 1);
event.target.setAttribute("color", "light");
swiper.slideNext(); // this line is problematic
checkIfComplete();
}, 1000);
};
The error :
it comes from the Swiper module

Multiple OpenLayers maps in Ionic Vue app

We're building an app using Ionic 6 / Vue 3 / Capacitor as our framework. On one of the pages we need to display a form which, among other inputs, contain 2 map components. The user can tap to pinpoint a geographical location in each of the maps. This is our map component:
<template>
<ion-item>
<ion-label position="stacked" color="tertiary" style="margin-bottom: 10px"
>{{ controlLabel }} (Tap to choose position)</ion-label
>
<div class="mapBox">
<div
:id="'map'+randomId"
class="map"
#click="getCoord($event)"
></div>
</div>
<div>
<ion-label color="tertiary" position="stacked">{{
$lang.Lengdegrad
}}</ion-label>
<ion-input
v-model="lon"
#change="setMarker($event)"
:controlIdLat="controlIdLat"
:data-value="valueLat"
/>
</div>
<div>
<ion-label color="tertiary" position="stacked">{{
$lang.Breddegrad
}}</ion-label>
<ion-input
v-model="lat"
#change="setMarker($event)"
:controlIdLon="controlIdLon"
:data-value="valueLon"
/>
</div>
</ion-item>
</template>
<script>
import { defineComponent } from "#vue/runtime-core";
import { IonInput, IonItem, IonLabel } from "#ionic/vue";
import Map from "ol/Map";
import View from "ol/View";
import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import { Style, Icon } from "ol/style";
import { OSM, Vector as VectorSource } from "ol/source";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { fromLonLat, toLonLat } from "ol/proj";
import XYZ from "ol/source/XYZ";
import "ol/ol.css";
import PinImg from "../../resources/icons8-pin-48.png";
import { Geolocation } from "#capacitor/geolocation";
export default defineComponent({
props: [
"controlLabel",
"controlIdLat",
"controlIdLon",
"lonLatFields",
"valueLat",
"valueLon",
"setStartMarker",
],
components: {
IonInput,
IonItem,
IonLabel,
},
data() {
return {
mainMap: null,
lat: null,
lon: null,
pinLayer: null,
pinFeat: null,
$lang: this.$lang,
isOnline: this.$isOnline,
isVisible: false,
randomId: Math.random().toString(36).substr(2, 5),
};
},
mounted() {
console.log("randomid",this.randomId)
this.source = new VectorSource();
setTimeout(async () => {
if (document.readyState === "loading") {
document.addEventListener(
"DOMContentLoaded",
await this.getLocation()
);
} else {
await this.getLocation();
}
this.myMap();
this.$nextTick(() => {
if (this.setStartMarker) {
this.setMarker();
}
view.setCenter(fromLonLat([this.lon, this.lat]));
});
}, 100);
},
methods: {
async getLocation() {
const position = await Geolocation.getCurrentPosition();
this.lat = position.coords.latitude.toFixed(3);
this.lon = position.coords.longitude.toFixed(3);
},
getCoord(event) {
let lonlat = toLonLat(this.mainMap.getEventCoordinate(event));
this.lat = lonlat[1].toFixed(3);
this.lon = lonlat[0].toFixed(3);
this.$nextTick(() => {
this.setMarker();
});
},
myMap() {
this.mainMap = new Map({
layers: [
new TileLayer({
source: new OSM(),
}),
new TileLayer({
source: new XYZ({
url: "https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}",
attributions:
'Kartverket',
}),
}),
(this.pinLayer = new VectorLayer({
source: new VectorSource({
features: [],
}),
style: new Style({
image: new Icon({
anchor: [0.5, 46],
anchorXUnits: "fraction",
anchorYUnits: "pixels",
src: PinImg,
}),
}),
})),
],
target: "map" + this.randomId,
view: view,
});
console.log("map", this.mainMap);
},
setMarker() {
let p = new Point(fromLonLat([this.lon, this.lat]));
console.log("setmarker", p);
if (!this.pinLayer.getSource().getFeatures().length) {
this.pinFeat = new Feature({
geometry: p,
});
this.pinLayer.getSource().addFeature(this.pinFeat);
} else {
this.pinFeat.setGeometry(p);
}
let vals = {
[this.controlIdLat]: this.lat,
[this.controlIdLon]: this.lon,
};
this.$emit("input", vals);
console.log(p.getCoordinates());
view.setCenter(p.getCoordinates());
},
},
});
const view = new View({
center: fromLonLat([13.486, 68.131]),
zoom: 10,
constrainResolution: true,
});
</script>
<style scoped>
div.map {
border: 5px solid white;
margin: 0 auto;
height: 100%;
width: 100%;
}
div.mapBox {
width: 100%;
height: 40vh;
}
</style>
Testing in the browser, this kind of works. The user can tap each of the two maps and two separate locations will be saved to the variables. However, when panning or zooming one of the maps, the other one follows so that they both show exactly the same map area.
As you can see, I've tried assigning a random id to each map to separate them from each other. That didn't work.
Testing on an Android phone, only the latter of the two maps are displayed. The first mainMap is left undefined.
My guess is that if I can figure out the first nuisance, then the second problem will also be solved. Any tips?
Edit: I tried making a new identical map component and use one for each map. Now the maps work as expected in the browser, being controlled separately. However the first map is still undefined on Android.

Axios response happening after component is rendered

I have a parent component making an Ajax request using Axios, The response is then assigned to a variabled called 'carousel' and is then passed down to the child component.
In the child component on 'created()' I am assigning the passed prop 'carousel' to a new variable called 'slides'
Problem is when I do this is returns undefined and my thinking is the Axios query hasn't returned before this happens.
Is there a way to delay the axios request before the prop is passed and the child component always gets the expected response.
My code is below.
Parent
<template>
<div class='product-container'>
<home-carousel :carousel="carousel"></home-carousel>
<profiler></profiler>
<cta-sections :panels="panels"></cta-sections>
</div>
</template>
<script>
import api from '../api/Home'
import CtaSections from '../components/CtaSections'
import HomeCarousel from '../components/HomeCarousel'
import Profiler from '../components/Profiler'
export default {
components: {
CtaSections,
HomeCarousel,
Profiler,
},
data() {
return {
panels: [],
slides: 'test',
carouselPass: [],
carousel: [],
}
},
created() {
axios.get(window.SETTINGS.API_BASE_PATH + 'pages/5')
.then(response => {
this.panels = response.data.acf.split_panels;
this.carousel = response.data.acf.carousel;
this.carousel.forEach(function (item, index) {
if (index === 0) {
item.active = true;
item.opacity = 1;
} else {
item.active = false;
item.opacity = 0;
}
item.id = index
})
})
},
}
</script>
Child
<template>
<div class='slider'>
<transition-group class='carouse carousel--fullHeight carousel--gradient' tag="div" name="fade">
<div v-for="slide in slides"
class="carousel__slide"
v-bind:class="{ active: slide.active }"
:key="slide.id"
:style="{ 'background-image': 'url(' + slide.image.url + ')' }"
v-show="slide.active"
>
<div class="carousel__caption carousel__caption--centered">
<h2 class="heading heading--white heading--uppercase heading--fixed">{{ slide.tagline }}</h2>
</div>
</div>
</transition-group>
<div class='carousel__controls carousel__controls--numbered carousel__controls--white carousel__controls--bottomRight carousel__controls--flex'>
<div #click="next" class="in">
<img src="/static/img/svg/next-arrow.svg" />
<span v-if="carousel.length < 10">0</span>
<span>{{ slideCount }}</span>
<span>/</span>
<span v-if="carousel.length < 10">0</span>
<span>{{ carousel.length }}</span>
</div>
</div>
</div>
</template>
<script>
import bus from '../bus'
import Booking from './Booking'
export default {
name: 'HomeCarousel',
props: ['carousel'],
data() {
return {
slideCount: 1,
slides: [],
/*
slides: [{
image: this.themepath + 'home-banner.jpg',
active: true,
captionText: 'A PLACE AS UNIQUE AS YOU ARE',
buttonText: 'book now',
buttonUrl: '#',
opacity: 1,
id: 1
},
{
image: this.themepath + 'home-banner2.jpg',
active: false,
captionText: 'A PLACE AS UNIQUE AS YOU ARE',
buttonText: 'book now',
buttonUrl: '#',
opacity: 0,
id: 2
}
]
*/
}
},
methods: {
showBooking: function() {
this.$store.state.showBooking = true;
},
next() {
const first = this.slides.shift();
this.slides = this.slides.concat(first)
first.active = false;
this.slides[0].active = true;
if (this.slideCount === this.slides.length) {
this.slideCount = 1;
} else {
this.slideCount++;
}
},
previous() {
const last = this.slides.pop()
this.slides = [last].concat(this.slides)
// Loop through Array and set all active values to false;
var slideLength = this.slides.length;
for (var slide = 0; slide < slideLength; slide++) {
this.slides[slide].active = false;
}
// Apply active class to first slide
this.slides[0].active = true;
this.slideCount--;
},
loopInterval() {
let self = this;
setInterval(function () {
self.next()
}, 8000);
}
},
created() {
this.slides = this.carousel;
}
}
</script>
You can just watch the prop and set this.slides when it changes, i.e. when the async call has finished:
watch:{
carousel(value) {
this.slides = value
}
}
Here's a JSFiddle: https://jsfiddle.net/nwLh0d4w/