Implement D3 multiple brushes using Vue3 composition API - vue.js

I want to implement multi-brushing functionality using D3 in a Vue3 application. Minimal examples using vanilla JS and D3 are available here and here.
Brushing functionality is correctly implemented by creating a computed variables (newBrush) and calling it in the onMounted() lifecycle hook. A minimal example is available here:
https://stackblitz.com/github/ajmoralesa/multiBrushD3Vue?embed=1
The template contains:
<template>
<div>
<svg :height="height" :width="width">
<g>
<rect :width="width" :height="height" class="rect"></rect>
<g ref="gBrushes"></g>
</g>
</svg>
</div>
</template>
And the script:
<script setup>
import { computed } from "#vue/reactivity";
import { onMounted, reactive, ref } from "vue";
import * as d3 from "d3";
const margin = reactive({
top: 10,
right: 10,
bottom: 10,
left: 10,
});
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const gBrushes = ref();
// Keep the actual d3-brush functions and their IDs in a list:
const brushes = ref([]);
onMounted(() => {
d3.select(gBrushes.value).call(newBrush.value);
});
const newBrush = computed(() => {
var brush = d3.brush().on("end", brushend);
function brushend() {
brushes.value.push({ id: brushes.value.length, brush: brush });
// // Figure out if our latest brush has a selection
// var lastBrushID = brushes.value[brushes.value.length - 1].id;
// var lastBrush = document.getElementById("brush-" + lastBrushID);
// console.log("lastBrush:", lastBrush);
// var selection = d3.brushSelection(lastBrush);
// // If it does, that means we need another one
// if (selection && selection[0] !== selection[1]) {
// newBrush();
// }
// Always draw brushes
drawBrushes();
}
return brush;
});
function drawBrushes() {
console.log("hey there!");
var brushSelection = d3
.select(gBrushes.value)
.selectAll(".brush")
.data(brushes.value, function (d) {
return d.id;
});
// Set up new brushes
brushSelection
.enter()
.insert("g", ".brush")
.attr("class", "brush")
.attr("id", function (brush) {
return "brush-" + brush.id;
})
.each(function (brushObject) {
//call the brush
d3.select(this).call(d3.brush(brushObject));
});
brushSelection.each(function (brushObject) {
d3.select(this)
.attr("class", "brush")
.selectAll(".overlay")
.style("pointer-events", function () {
var brush = brushObject.brush;
if (
brushObject.id === brushes.value.length - 1 &&
brush !== undefined
) {
return "all";
} else {
return "none";
}
});
});
brushSelection.exit().remove();
}
</script>
What is wrong in the drawBrushes() function ?
Code: https://stackblitz.com/github/ajmoralesa/multiBrushD3Vue

Related

Move object in array from composition API with vue 3

I would like to know how can I move an object in array from composition API with draggable (vuedraggable).
Currently I have this :
// State
const state = reactive({
post: null,
});
export default function postStore() {
// Mutations
const MOVE_COMMENT = (payload) => {
let comment = state.post.comments[payload.oldIndex]
state.post.comments.splice(payload.oldIndex, 1)
state.post.comments.splice(payload.newIndex, 0, comment)
};
// Actions
const moveComment = (payload) => {
MOVE_COMMENT(payload)
};
return {
...toRefs(state),
moveComment
}
}
And I call my function from my component :
import draggable from 'vuedraggable';
...
<draggable :list="post.comments" #end="onEndDraggable">
<template #item="{element}">
<div>{{element.title}}</div>
</template>
</draggable>
...
setup() {
let { post, moveComment } = postStore();
let onEndDraggable = (data) => {
moveComment({
newIndex: data.newIndex,
oldIndex: data.oldIndex
})
}
return { onEndDraggable }
}
When I drag the first item on second position, the first item stay first item. But if I drag the first item on third position, the first item become second item..
Use :modelValue instead of :list demo
<draggable :modelValue="post.comments" item-key="title" #end="onEndDraggable">
<template #item="{element}">
<div>{{element.title}}</div>
</template>
</draggable>

How to stop BootstrapVue carousel cycling?

I've tried this:
<template lang="pug">
b-carousel(
id='categoryRoulette'
controls
no-animation
ref="myRoulette"
)
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
mounted(): void {
this.$refs.myRoulette.pause()
},
But, I've got the following error:
From bootstrap-vue docs:
To pause the carousel from auto sliding, set the interval prop to 0.
To restart a paused carousel, set the interval back to the desired
number of ms.
CHECK THIS DEMO: https://jsfiddle.net/me3610uy/3/
<b-carousel v-model="slide" :interval="interval" >
// your content here
</b-carousel>
new Vue({
//...
data() {
return {
slide: 0,
interval: 3000
}
},
mounted() {
this.interval = 0; // Set the interval variable to zero to pause the carousel
}
})
Get the input value of timer you want to set or set conditional value as below.
setSliderTimer: function (e) {
var setSliderTime = document.getElementById("time-input")
var newTimer = setSliderTime.value
if(newTimer > 0){
newTimer = newTimer;
}else{
newTimer = 3000;
}
this.interval = newTimer;
},

How to implement debounce in vue3

I have a filter input field and want to filter a list of items. The list is large so I want to use debounce to delay the filter being applied until the user has stopped typing for improved user experience. This is my input field and it's bound to filterText that is used to filter the list.
<input type="text" v-model="state.filterText" />
I didn't find any nice solution as I wanted to see my binding in my template so I decided to share my solution. I wrote a simple debounce function and use the following syntax to bind the behavior:
setup() {
...
function createDebounce() {
let timeout = null;
return function (fnc, delayMs) {
clearTimeout(timeout);
timeout = setTimeout(() => {
fnc();
}, delayMs || 500);
};
}
return {
state,
debounce: createDebounce(),
};
},
And the template syntax:
<input
type="text"
:value="state.filterText"
#input="debounce(() => { state.filterText = $event.target.value })"
/>
Hi first time answering something here, so correct my answer as much as you want, I'd appreciate it.
I think that the prettiest and lightest solution is to create a directive globally that you can use as much as you want in all of your forms.
you first create the file with your directive, eg.
debouncer.js
and you create the function for the debouncing
//debouncer.js
/*
This is the typical debouncer function that receives
the "callback" and the time it will wait to emit the event
*/
function debouncer (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
/*
this function receives the element where the directive
will be set in and also the value set in it
if the value has changed then it will rebind the event
it has a default timeout of 500 milliseconds
*/
module.exports = function debounce(el, binding) {
if(binding.value !== binding.oldValue) {
el.oninput = debouncer(function(){
el.dispatchEvent(new Event('change'))
}, parseInt(binding.value) || 500)
}
}
After you define this file you can go to your main.js import it and use the exported function.
//main.js
import { createApp } from 'vue'
import debounce from './directives/debounce' // file being imported
const app = createApp(App)
//defining the directive
app.directive('debounce', (el,binding) => debounce(el,binding))
app.mount('#app')
And its done, when you want to use the directive on an input you simply do it like this, no imports or anything.
//Component.vue
<input
:placeholder="filter by name"
v-model.lazy="filter.value" v-debounce="400"
/>
The v-model.lazy directive is important if you choose to do it this way, because by default it will update your binded property on the input event, but setting this will make it wait for a change event instead, which is the event we are emitting in our debounce function. Doing this will stop the v-model updating itself until you stop writing or the timeout runs out (which you can set in the value of the directive).
I hope this was understandable.
<template>
<input type="text" :value="name" #input="test" />
<span>{{ name }}</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function debounce<T> (fn: T, wait: number) {
let timer: ReturnType<typeof setTimeout>
return (event: Event) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
if (typeof fn === 'function') {
fn(event)
}
}, wait)
}
}
export default defineComponent({
setup () {
const name = ref('test')
function setInputValue (event: Event) {
const target = event.target as HTMLInputElement
name.value = target.value
}
const test = debounce(setInputValue, 1000)
return { name, test }
}
})
</script>
With Lodash, you have an easier solution:
<template>
<input type="text" :value="name" #input="onInput" />
<span>{{ name }}</span>
</template>
<script>
import debounce from "lodash/debounce"
export default {
setup () {
const onInput = debounce(() => {
console.log('debug')
}, 500)
return { onInput }
}
}
</script>
<input #input="updateValue"/>
const updateValue = (event) => {
const timeoutId = window.setTimeout(() => {}, 0);
for (let id = timeoutId; id >= 0; id -= 1) {
window.clearTimeout(id);
}
setTimeout(() => {
console.log(event.target.value)
}, 500);
};
You can try this one
Here's an example with Lodash and script setup syntax using a watcher to fire the debounced api call:
<script setup>
import { ref, watch } from 'vue'
import debounce from 'lodash.debounce'
const searchTerms = ref('')
const getFilteredResults = async () => {
try {
console.log('filter changed')
// make axios call here using searchTerms.value
} catch (err) {
throw new Error(`Problem filtering results: ${err}.`)
}
}
const debouncedFilter = debounce(getFilteredResults, 250) // 250ms delay
watch(() => searchTerms.value, debouncedFilter)
</script>
<template>
<input v-model="searchTerms" />
</template>
https://www.npmjs.com/package/vue-debounce now works for vue 3
It can be registered also with composition API like this
setup() {
...
},
directives: {
debounce: vue3Debounce({ lock: true })
}

Launch a Vue modal component outside of a SPA context

We're in the process of retrofitting a mature website with some updated forms. We're replacing some systems entirely with SPAs and some just don't warrant it.
However, we have some system-global modal screens that we need to be available. Some of these have been ported to Vue and work well enough inside SPAs and we'd like to reap the benefits of heightened interactivity in our non-SPA pages.
So whereas before we would have had our event bound like so:
$('a.newBooking').on('click', function(ev){
// code to open bootstrap modal screen, download HTML, etc.
});
How do we spin up a component?
I know I haven't included any serious code here but our modals are basically just embellished versions of the documented example modal component. The difference is we don't have the button. We'll want to be able to launch these modals from all around the page.
My opinion:
For your Modal components:
use singleton pattern for your modal (because basically we only allow one modal popup at the same time), it will make the logic more simple.
customize one install function to add the Vue instances of your Modals to Vue.prototype, like _Vue.prototype.$my = yourModals
then register your plugins in demand like Vue.use(installFunc, {plugins: [SModal, AModal, BModal]})
At your JQuery (or other non-Vue) Apps:
Register Modals to Vue, then create Vue instance
show or hide your modals like vueInstance.$my.SModal.show
Below is one simple demo:
Vue.config.productionTip = false
/*---Modal Plugin---*/
let vm = null // the instance for your Vue modal
let timeout = null //async/delay popup
const SModal = {
isActive: false,
show ({
delay = 500,
message = '',
customClass = 'my-modal-class'
} = {}) {
if (this.isActive) {
vm && vm.$forceUpdate()
return
}
timeout = setTimeout(() => {
timeout = null
const node = document.createElement('div')
document.body.appendChild(node)
let staticClass = ''
vm = new this.__Vue({
name: 's-modal',
el: node,
render (h) { // uses render() which is a closer-to-the-compiler alternative to templates
return h('div', {
staticClass,
'class': customClass,
domProps: {
innerHTML: message
}
})
}
})
}, delay)
this.isActive = true
},
hide () {
if (!this.isActive) {
return
}
if (timeout) {
clearTimeout(timeout)
timeout = null
} else {
vm.$destroy()
document.body.removeChild(vm.$el)
vm = null
}
this.isActive = false
},
__Vue: null,
__installed: false,
install ({ $my, Vue }) {
if (this.__installed) { return }
this.__installed = true
$my.SModal = SModal // added your SModal object to $my
this.__Vue = Vue //get the Vue constructor
}
}
/*---Modal Plugin End---*/
/*---Custom Install Function in order to manage all modals---*/
let installFunc = function (_Vue, opts = {}) {
if (this.__installed) {
return
}
this.__installed = true
const $my = {
'memo': 'I am a plugin management.'
}
if (opts.plugins) {
Object.keys(opts.plugins).forEach(key => {
const p = opts.plugins[key]
if (typeof p.install === 'function') {
p.install({ $my, Vue: _Vue })
}
})
}
_Vue.prototype.$my = $my
}
/*---Install Plugins---*/
Vue.use(installFunc, {
plugins: [SModal]
})
let globalVue = new Vue({
el: '#vue-app'
})
$('#test').on('click', 'span', function () {
globalVue.$my.SModal.isActive ? globalVue.$my.SModal.hide() : globalVue.$my.SModal.show({'message':'test', 'delay':100})
})
span {
cursor:pointer;
color:red;
}
.my-modal-class {
position:absolute;
top:50px;
left:150px;
width:200px;
height:200px;
background-color:red;
z-index:9999;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="vue-app">
</div>
<div id="test">
<h3>One Example</h3>
<p><span>Hello</span>, how are you?</p>
<p>Me? I'm <span>good</span>.</p>
</div>

How can I test a custom input Vue component

In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this
<currency-input v-model="price"></currency-input>
The full implementation can be found at https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
The documentation says
So for a component to work with v-model, it should (these can be configured in 2.2.0+):
accept a value prop
emit an input event with the new value
How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.
You can do it:
Using Vue Test Utils, and
Mounting a parent element that uses <currency-input>
Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
Verify if, in the parent, the price property (bound to v-model) has changed.
Example code (using Mocha):
import { mount } from '#vue/test-utils'
import CurrencyInput from '#/components/CurrencyInput.vue'
describe('CurrencyInput.vue', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data: { price: null },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
})
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
In-browser runnable demo using Jasmine:
var CurrencyInput = Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)">\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function(value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
});
// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data() { return { price: null } },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
});
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
// load jasmine htmlReporter
(function() {
var env = jasmine.getEnv()
env.addReporter(new jasmine.HtmlReporter())
env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/vue#2.5.15/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-template-compiler#2.5.15/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>
Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.
I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.
import { mount } from "#vue/test-utils";
import Input from "Input.vue";
describe('Input.vue', () => {
test('changing the input element value updates the v-model', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Brendan Eich';
await wrapper.find('input').setValue(name);
expect(wrapper.vm.$data.name).toBe(name);
});
test('changing the v-model updates the input element value', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Bjarne Stroustrup';
await wrapper.setData({ name });
const inputElement = wrapper.find('input').element;
expect(inputElement.value).toBe(name);
});
});
Input.vue component:
<template>
<input :value="$attrs.value" #input="$emit('input', $event.target.value)" />
</template>