I am trying do develop my own simple drag and drop functionality for a form builder. I have got a list of items which is rendered in a v-for loop with a computed property. On onDragStart I set the state isDragging. This should alter the list in the computed property and insert some slots where I can drop the dragged item. But immediatly after dragging an item, it triggers the onDragEnd Event.
Initialy i was missing the :key property, but adding it didnt solve the problem. Hopefully someone knows a solution or has an idea whats wrong.
Here is a Link to the code: https://jsfiddle.net/elrihor/vwb3k6qc/49/
Vue.createApp({
data() {
return {
list: [{
id: 'i1',
name: 'Apfel',
},
{
id: 'i2',
name: 'Banane',
},
{
id: 'i3',
name: 'Kirsche',
},
{
id: 'i4',
name: 'Orange',
},
],
props: {
item: {
props: {
draggable: true,
ondragstart: ($event) => {
this.startDrag();
},
ondragend: ($event) => {
this.endDrag();
},
},
},
slot: {
props: {
ondrop: ($event) => {
$event.preventDefault();
this.onDrop($event);
},
ondragover: ($event) => {
$event.preventDefault();
}
},
},
},
isDragging: false
}
},
computed: {
dragList() {
let dragList = this.list;
if (this.isDragging) {
dragList = dragList.reduce((r, a) => r.concat(a, {
type: 'slot',
name: 'Slot'
}), [{
type: 'slot',
name: 'Slot'
}]);
}
dragList = dragList.map((item, index) => {
return {
...item,
id: !item.id ? 's' + index : item.id,
type: item.type == 'slot' ? 'slot' : 'item',
};
});
return dragList;
}
},
methods: {
getProps(type) {
return this.props[type].props;
},
startDrag() {
console.log('start');
this.isDragging = true;
},
endDrag() {
console.log('end');
this.isDragging = false;
},
onDrop(e) {
console.log('drop');
}
}
}).mount('#app')
I think the issue lies in choosing to use the ondragstart event. When this is fired, the DOM is re-rendered to display your slots, which moves things around so that your mouse cursor is no longer over the element itself. dragend is then fired before the drag actually commenced. Change this simply to ondrag and it will work:
https://jsfiddle.net/m7ps3f40/
To debug and figure this out, I simplified your fiddle a lot. I felt the dragList computed and the :bind="getProps" used more code and is less readable than the a simpler approach of just using HTML elements and Vue-style events for dragging. You might find it interesting to take a look:
https://jsfiddle.net/o5pyhtd3/2/
but of course your fiddle might be a cut down version of a bigger piece of code, so perhaps you need to stick to your approach.
Related
What we need to do: We need to feature flag a few things in our current state machine
My ideal solution: Always load, no matter what state is, all feature flags and assign them to the state machine context
Attempts: Tried using async actions and invoke services, however, I cannot find a way to always run either of them
This basically my state machine and how I envisioned loading feature flag. However, the invoke.src function just gets called for the first time when I'm first loading the state machine.
Every time that I hydrate the state machine and the state machine is in one of the state, for example create, the invoke.src function does not get called therefore no FF is loaded into the context
const stateMachine = createStateMachine({
id: 'state-machine',
invoke: {
src: async () => {
return await featureFlagService.load();
},
onDone: {
actions: assign(_context, event) => ({ featureFlagEvaluations: event.data }),
}
},
states: {
'create': { ... },
'retrieve': { ... },
}
});
Does anyone have any idea of how to implement such use case?
You should use the actor model approach. Each time when you need to refresh/fetch the FF you should spawn FF-machine and on done call parentSend() message which will update the context to your main SM(state-machine)
const stateMachine = createStateMachine({
id: 'state-machine',
invoke: {
src: async () => {
return await featureFlagService.load();
},
onDone: [{
actions: assign({
ffActorRef: () => spawn(featureFlagMachine, 'ffActor'),
}),
}],
states: {
'create': { ... },
'retrieve': { ... },
},
on:{
REFRESH_FEATURE_FLAG : [{
actions: assign(_context, event) => ({ featureFlagEvaluations: event.data }),
}]
}
});
const featureFlagMachine = createStateMachine({
id: 'ff-machine',
initial: 'retrieve',
invoke: {
src: async () => {
return await featureFlagService.load();
},
onDone: [{
actions: ['notifyRefresh']
}],
states: {
'create': { ... },
'retrieve': { ... },
},
},
{
actions: {
notifyRefresh: sendParent((ctx, event) => {
return {
type: 'REFRESH_FEATURE_FLAG',
data: { featureFlagEvaluations: event.data },
};
}),
},
}
}
);
I would like to have a dynamic breadcrumbs based on where I clicked on a category but I get an error that says my variable is undefined: TypeError: Cannot read properties of undefined (reading 'homeMenu'). Yet in my getHomeCategory function, the console.log of homeCategory displays Perma'Thèque. I don't understand how to do it, thanks
Here is the code :
<script>
export default {
props: {
},
data: () => ({
homeMenu: "",
breadcrumbs: [
{
text: 'Accueil',
disabled: false,
href: '/',
},
{
text: this.homeMenu,
disabled: false,
href: "/" + this.homeMenu,
},
],
}),
computed: {
...mapGetters({
console: () => console,
homeCategory: 'home/getCategory',
})
},
methods: {
getHomeCategory () {
if (this.homeCategory === "Perma'Thèque") {
console.log(this.homeCategory)
return this.homeMenu = "permatheque"
} else {
return this.homeMenu = "null"
}
},
},
mounted() {
if (this.plantActive) this.loading = false;
this.getHomeCategory()
}
}
</script>
data() is declared here as an arrow function, so this refers to the outer scope, not the Vue component instance, but even as a regular function here, this.homeMenu won't yet exist.
It seems that you actually want breadcrumbs to be reactive to homeMenu, so you should move breadcrumbs to a computed prop:
export default {
data: () => ({
homeMenu: '',
}),
computed: {
breadcrumbs() {
return [
{
text: 'Accueil',
disabled: false,
href: '/',
},
{
text: this.homeMenu,
disabled: false,
href: '/' + this.homeMenu,
},
]
}
}
}
demo
I used Element Plus UI and my mind was blown. Check out my blog and the codes. Element Plus is a free library that is great for Vue JS. They have very nice UI for breadcrumb and can be implemented with v-for loop.
https://medium.com/#samchowdhury/create-a-breadcrumb-with-vue-js-and-element-plus-ui-f3e2fde50a3e
I am trying to setup a button that changes a data value in Vue but also have it set using localStorage initally. This way I can have it keep the previous state it was in before a page refresh. Below is the code I'm using and I'm able to get it to work but know that it would be preferable to use the computed section but haven't been able to get that to work properly.
Would anyone know what is going wrong?
My button is triggered using the testing method and the variable in question is isGrid.
export default {
data() {
return {
option: 'default',
}
},
components: {
FileUploader,
},
mixins: [
visibilitiesMixin,
settingsMixin
],
props: {
vehicleId: {
type: Number,
required: true,
default: null,
}
},
computed: {
...mapState([
'isLoading',
'images',
'fallbackImageChecks',
'selectedImages'
]),
isGrid: {
get() {
return localStorage.getItem('isGrid');
},
},
imagesVModel: {
get() {
return this.images;
},
set(images) {
this.setImages(images);
}
},
selectedImagesVModel: {
get() {
return this.selectedImages;
},
set(images) {
this.setSelectedImages(images);
}
},
removeBgEnabled() {
return this.setting('nexus_integration_removebg_enabled') === 'enabled';
},
},
mounted() {
this.loadImages(this.vehicleId);
},
methods: {
testing() {
if (this.isGrid === 'false' || this.isGrid === false) {
localStorage.setItem('isGrid', true);
this.isGrid = true;
console.log(this.isGrid);
console.log(localStorage.getItem('isGrid'));
} else {
localStorage.setItem('isGrid', false);
this.isGrid = false;
console.log('b');
console.log(this.isGrid);
console.log(localStorage.getItem('isGrid'));
}
},
}
I suggest you use vuex with vuex-persistedstate.
https://www.npmjs.com/package/vuex-persistedstate
Let's say I have a component which repeats with a v-for loop like so:
<hotel v-for="(hotel, index) in hotels"></hotel>
And my hotels array would look like so:
[
{
name: false
},
{
name: false
},
{
name: true
},
{
name: true
}
]
How could I perform an action when my v-for loop encounters the property name set to true only on the very first time it encounters this truthy property?
I know I could probably cache a property somewhere and only run something once and not run again if it has been set but this does not feel efficient.
Use a computed to make a copy of hotels where the first one has an isFirst property.
computed: {
hotelsWithFirstMarked() {
var result = this.hotels.slice();
var first = result.find(x => x.name);
if (first) {
first.isFirst = true;
}
return result;
}
}
Just use computed source
HTML
<div v-for="(hotel, index) in renderHotels"></div>
JS
export default {
name: 'message',
data () {
return{
hotels:[
{
name: false
},
{
name: false
},
{
name: true
},
{
name: true
}
] ,
wasFirst : false
}
},
methods:{
},
computed:{
renderHotels(){
return this.hotels.map((hotel)=>{
if(hotel.name && !this.wasFirst){
this.wasFirst = true;
alert('First True');
console.log(hotel);
}
})
}
}
}
Best way is to use filter function.
data() {
return {
firstTime: false,
};
},
filters: {
myFunc: function (hotel) {
if (hotel.name && !this.firstTime) {
//perform some action
this.firstTime = true;
}
}
}
<hotel v-for="(hotel, index) in hotels | myFunc"></hotel>
Trying to get the hang of Mithril, can't really understand one thing. Can I render components on events?
Let's assume I have one parent component:
var MyApp = {
view: function() {
return m("div", [
m.component(MyApp.header, {}),
m("div", {id: "menu-container"})
])
}
};
m.mount(document.body, megogo.main);
It renders the header component (and a placeholder for the menu (do I even need it?)):
MyApp.header = {
view: function() {
return m("div", {
id: 'app-header'
}, [
m('a', {
href: '#',
id: 'menu-button',
onclick: function(){
// this part is just for reference
m.component(MyApp.menu, {})
}
}, 'Menu')
])
}
}
When the user clicks on the menu link I want to load the menu items from my API and only then render the menu.
MyApp.menu = {
controller: function() {
var categories = m.request({method: "GET", url: "https://api.site.com/?params"});
return {categories: categories};
},
view: function(ctrl) {
return m("div", ctrl.categories().data.items.map(function(item) {
return m("a", {
href: "#",
class: 'link-button',
onkeydown: MyApp.menu.keydown
}, item.title)
}));
},
keydown: function(e){
e.preventDefault();
var code = e.keyCode || e.which;
switch(code){
// ...
}
}
};
This part will obviously not work
onclick: function(){
// this part is just for reference
m.component(MyApp.menu, {})
}
So, the question is what is the correct way render components on event?
Try This:
http://jsbin.com/nilesi/3/edit?js,output
You can even toggle the menu.
And remember that you get a promise wrapped in an m.prop from the call to m.request. You'll need to check that it has returned before the menu button can be clicked.
// I'd stick this in a view-model
var showMenu = m.prop(false)
var MyApp = {
view: function(ctrl) {
return m("div", [
m.component(MyApp.header, {}),
showMenu() ? m.component(MyApp.menu) : ''
])
}
};
MyApp.header = {
view: function() {
return m("div", {
id: 'app-header'
}, [
m('a', {
href: '#',
id: 'menu-button',
onclick: function(){
showMenu(!showMenu())
}
}, 'Menu')
])
}
}
MyApp.menu = {
controller: function() {
//var categories = m.request({method: "GET", url: "https://api.site.com/?params"});
var categories = m.prop([{title: 'good'}, {title: 'bad'}, {title: 'ugly'}])
return {categories: categories};
},
view: function(ctrl) {
return m("div.menu", ctrl.categories().map(function(item) {
return m("a", {
href: "#",
class: 'link-button',
onkeydown: MyApp.menu.keydown
}, item.title)
}));
},
keydown: function(e){
e.preventDefault();
var code = e.keyCode || e.which;
switch(code){
// ...
}
}
};
m.mount(document.body, MyApp);
First of all, you'll want to use the return value of m.component, either by returning it from view, or (more likely what you want) put it as a child of another node; use a prop to track whether it's currently open, and set the prop when you wish to open it.
To answer the actual question: by default Mithril will trigger a redraw itself when events like onclick and onkeydown occur, but to trigger a redraw on your own, you'll want to use either m.redraw or m.startComputation / m.endComputation.
The difference between them is that m.redraw will trigger a redraw as soon as it's called, while m.startComputation and m.endComputation will only trigger a redraw once m.endComputation is called the same amount of times that m.startComputation has been called, so that the view isn't redrawn more than once if multiple functions need to trigger a redraw once they've finished.