feat: simplify heading blur logic #727
|
@ -7,16 +7,17 @@
|
|||
<h1
|
||||
class="title input"
|
||||
:class="{'disabled': !canWrite}"
|
||||
@focusout="save()"
|
||||
@keydown.enter.prevent.stop="save()"
|
||||
@blur="save($event.target.textContent)"
|
||||
@keydown.enter.prevent.stop="$event.target.blur()"
|
||||
konrad marked this conversation as resolved
Outdated
|
||||
:contenteditable="canWrite ? 'true' : 'false'"
|
||||
spellcheck="false"
|
||||
ref="taskTitle">{{ task.title.trim() }}</h1>
|
||||
dpschen
commented
By trimming the title on saving (see in By trimming the title on saving (see in `save` function) there should never be a title with whitespace around.
Maybe there are old ones in the database but they should disappear :)
konrad
commented
I agree in principle, but remember the api can theoretically be used with other clients as well. Therefore we cannot be sure there won't be any spaces around the task title without the I agree in principle, but remember the api _can_ theoretically be used with other clients as well. Therefore we cannot be sure there won't be any spaces around the task title without the `trim()`. Unless we add a trim on task create/update in the api. That might actually be a good idea.
dpschen
commented
How about keeping both? How about keeping both?
konrad
commented
You mean on update/create and when displaying it? Sounds great. But then the trim on create/update should go into the TaskService. You mean on update/create and when displaying it? Sounds great. But then the trim on create/update should go into the TaskService.
dpschen
commented
Good point.
Good point.
Not sure where exactly to put this then in the TaskService.
I had already problems with the `processModel` method, because it modifies the original task in the store instead of creating a new one. I guess it would be something like:
```js
// NOTE: not sure if a task is able to have an empty string.
model.title = model.title.trim() || ''
```
dpschen
commented
Okay, I put this now in the TaskService > You mean on update/create and when displaying it? Sounds great. But then the trim on create/update should go into the TaskService.
Okay, I put this now in the TaskService `processModel` method.
I'm just a bit confused because it seems to me that almost the same stuff is done in the constructor of the TaskModel, so I added it there aswell.
Shouldn't those two operation be identical / happen just once?
Or what is the difference?
konrad
commented
It's almost the same, but not quite. I'm sure there are better ways to do this. Oh and by the way, check your email 🙂 It's almost the same, but not quite. `processModel` is called before a model is sent to the api, the task constructor is called when a model is returned from the api. They are mostly used to convert js date objects to iso dates and create releated models and such.
I'm sure there are better ways to do this.
Oh and by the way, check your email 🙂
dpschen
commented
So checking if I understood this right:
If that's correct I don't understand why Can you point me to an example where the constructor is called when e.g. the task model is returned from the api?
Checking! > the task constructor is called when a model is returned from the api.
So checking if I understood this right:
- the `processModel` functions in the services are some kind of validation / formatting so that the data has the right format for the api.
- the constructors of the models are intended to make some changes to make it easier to work with the data on client side.
If that's correct I don't understand why `new XYModel` (aka calling the model constructor) is called in so many places where there is no data coming from the api.
Can you point me to an example where the constructor is called when e.g. the task model is returned from the api?
> Oh and by the way, check your email 🙂
Checking!
konrad
commented
The services have a > If that's correct I don't understand why new XYModel (aka calling the model constructor) is called in so many places where there is no data coming from the api.
The services have a `modelFactory` function which receives data from the api and returns a `new XYZModel(data)`. The other cases where a new model is created is when creating these things, for example a new task. That might be a bit redundant though. Again, I'm sure there are better way to do this.
I created the whole Services / Model implementation based on some things I did in other languages or that I've seen with not much experience.
dpschen
commented
Sure, just want to make sure that I understand the imagined role of the parts :) Sure, just want to make sure that I understand the imagined role of the parts :)
|
||||
<transition name="fade">
|
||||
<span class="is-inline-flex is-align-items-center" v-if="loading && saving">
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
{{ $t('misc.saving') }}
|
||||
</span>
|
||||
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && saved">
|
||||
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && showSavedMessage">
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
I renamed this variable to make the meaning more clear. I renamed this variable to make the meaning more clear.
|
||||
<icon icon="check" class="mr-2"/>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
|
@ -25,22 +26,22 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {LOADING} from '@/store/mutation-types'
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Since Since `LOADING` is used here to get a state via `mapState` and (and not mutation-type via `mapMutation`) this seemed like a mistake.
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'heading',
|
||||
data() {
|
||||
return {
|
||||
task: {title: '', identifier: '', index: ''},
|
||||
taskTitle: '',
|
||||
saved: false,
|
||||
showSavedMessage: false,
|
||||
saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
|
||||
}
|
||||
},
|
||||
computed: mapState({
|
||||
loading: LOADING,
|
||||
}),
|
||||
computed: {
|
||||
...mapState(['loading']),
|
||||
task() {
|
||||
return this.value
|
||||
},
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
|
@ -50,50 +51,29 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.task = newVal
|
||||
this.taskTitle = this.task.title
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.task = this.value
|
||||
this.taskTitle = this.task.title
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.$refs.taskTitle.spellcheck = false
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
I was not sure why spellcheck is set to I removed the direct element access and added a default value. Maybe wrong assumptions? I was not sure why spellcheck is set to `false` here.
Also it's never set to `undefined` / `false` later.
I removed the direct element access and added a default value. Maybe wrong assumptions?
konrad
commented
Probably because I wanted to disable it before saving. What does the commit message on that line say? Probably because I wanted to disable it before saving. What does the commit message on that line say?
dpschen
commented
It was from the original commit of that component. It was from the [original commit]( https://kolaente.dev/vikunja/frontend/commit/148cc1dcca864032304a42d59088c48ce70dbc8c) of that component.
konrad
commented
I see. I think it makes sense to just always disable spellcheck, but I'm pretty sure I tried to do that back then and got bitten by it somehow. I see. I think it makes sense to just always disable spellcheck, but I'm pretty sure I tried to do that back then and got bitten by it somehow.
dpschen
commented
Okay, so shall I just set Okay, so shall I just set `spellcheck="false"` in the template?
konrad
commented
Yeah I think that would work. Yeah I think that would work.
dpschen
commented
Done ✅ Done ✅
|
||||
|
||||
// Pull the task title from the contenteditable
|
||||
let taskTitle = this.$refs.taskTitle.textContent
|
||||
this.task.title = taskTitle
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
By assigning By assigning `taskTitle` to `this.task.title` the `task` is getting modified.
I guess this is the only reason why a local copy of the task existed. By getting the value as a parameter from the `save` function there is no need to modify the task directly.
konrad
commented
I'm pretty sure there's a few cases like this in other places. I'm pretty sure there's a few cases like this in other places.
dpschen
commented
Yes! Those are the ones that give me headache with vue3 :) Yes! Those are the ones that give me headache with vue3 :)
|
||||
|
||||
// We only want to save if the title was actually change.
|
||||
// Because the contenteditable does not have a change event,
|
||||
// we're building it ourselves and only calling saveTask()
|
||||
save(title) {
|
||||
// We only want to save if the title was actually changed.
|
||||
// Because the contenteditable does not have a change event
|
||||
// we're building it ourselves and only continue
|
||||
// if the task title changed.
|
||||
if (this.task.title !== this.taskTitle) {
|
||||
this.$refs.taskTitle.blur()
|
||||
this.saveTask()
|
||||
this.taskTitle = taskTitle
|
||||
}
|
||||
},
|
||||
saveTask() {
|
||||
// When only saving with enter, the focusout event is called as well. This then leads to the saveTask
|
||||
// method being called twice, overriding some task attributes in the second run.
|
||||
// If we simply check if we're already in the process of saving, we can prevent that.
|
||||
if (this.saving) {
|
||||
if (title === this.task.title) {
|
||||
return
|
||||
}
|
||||
|
||||
this.saving = true
|
||||
|
||||
this.$store.dispatch('tasks/update', this.task)
|
||||
.then(() => {
|
||||
this.$emit('input', this.task)
|
||||
this.saved = true
|
||||
const newTask = {
|
||||
...this.task,
|
||||
title,
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks/update', newTask)
|
||||
.then((task) => {
|
||||
this.$emit('input', task)
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
We created a new task to prevent unwanted mutations We created a new task to prevent unwanted mutations
|
||||
this.showSavedMessage = true
|
||||
setTimeout(() => {
|
||||
this.saved = false
|
||||
this.showSavedMessage = false
|
||||
}, 2000)
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -15,6 +15,7 @@ export default class TaskModel extends AbstractModel {
|
|||
super(data)
|
||||
|
||||
this.id = Number(this.id)
|
||||
this.title = this.title?.trim()
|
||||
this.listId = Number(this.listId)
|
||||
|
||||
// Make date objects from timestamps
|
||||
|
|
|
@ -39,6 +39,8 @@ export default class TaskService extends AbstractService {
|
|||
|
||||
processModel(model) {
|
||||
|
||||
model.title = model.title?.trim()
|
||||
|
||||
// Ensure that listId is an int
|
||||
model.listId = Number(model.listId)
|
||||
|
||||
|
|
Wow, that's as elegant as it is obvious.
Thanks :) Was also new to me. Google helped :D
Before that i always used something like _.throttle ; but that seemed too overpowered here.
I've used good ol'
setTimeout
in the past but that never felt good. It didn't even work all the time...Yes :D
When you use setTimeout it's almost (!) always a hint that you are creating a workaround for something and not using the "right" ™️ way.