-
-
Save Akryum/05964e81d09fb5088b7769cff15f5e7c to your computer and use it in GitHub Desktop.
<script> | |
import { isValidMultiName } from '@/util/folders' | |
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql' | |
import FOLDERS_FAVORITE from '@/graphql/folder/foldersFavorite.gql' | |
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql' | |
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql' | |
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql' | |
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql' | |
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql' | |
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders' | |
export default { | |
data () { | |
return { | |
loading: 0, | |
error: false, | |
editingPath: false, | |
editedPath: '', | |
folderCurrent: {}, | |
foldersFavorite: [], | |
showHidden: localStorage.getItem(SHOW_HIDDEN) === 'true', | |
showNewFolder: false, | |
newFolderName: '' | |
} | |
}, | |
apollo: { | |
folderCurrent: { | |
query: FOLDER_CURRENT, | |
fetchPolicy: 'network-only', | |
loadingKey: 'loading', | |
async result () { | |
await this.$nextTick() | |
this.$refs.folders.scrollTop = 0 | |
} | |
}, | |
foldersFavorite: FOLDERS_FAVORITE | |
}, | |
computed: { | |
newFolderValid () { | |
return isValidMultiName(this.newFolderName) | |
} | |
}, | |
watch: { | |
showHidden (value) { | |
if (value) { | |
localStorage.setItem(SHOW_HIDDEN, 'true') | |
} else { | |
localStorage.removeItem(SHOW_HIDDEN) | |
} | |
} | |
}, | |
beforeRouteLeave (to, from, next) { | |
if (to.matched.some(m => m.meta.needProject)) { | |
this.resetProjectCwd() | |
} | |
next() | |
}, | |
methods: { | |
async openFolder (path) { | |
this.editingPath = false | |
this.error = null | |
this.loading++ | |
try { | |
await this.$apollo.mutate({ | |
mutation: FOLDER_OPEN, | |
variables: { | |
path | |
}, | |
update: (store, { data: { folderOpen } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderOpen } }) | |
} | |
}) | |
} catch (e) { | |
this.error = e | |
} | |
this.loading-- | |
}, | |
async openParentFolder (folder) { | |
this.editingPath = false | |
this.error = null | |
this.loading++ | |
try { | |
await this.$apollo.mutate({ | |
mutation: FOLDER_OPEN_PARENT, | |
update: (store, { data: { folderOpenParent } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderOpenParent } }) | |
} | |
}) | |
} catch (e) { | |
this.error = e | |
} | |
this.loading-- | |
}, | |
async toggleFavorite () { | |
await this.$apollo.mutate({ | |
mutation: FOLDER_SET_FAVORITE, | |
variables: { | |
path: this.folderCurrent.path, | |
favorite: !this.folderCurrent.favorite | |
}, | |
update: (store, { data: { folderSetFavorite } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderSetFavorite } }) | |
let data = store.readQuery({ query: FOLDERS_FAVORITE }) | |
// TODO this is a workaround | |
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473 | |
data = { | |
foldersFavorite: data.foldersFavorite.slice() | |
} | |
if (folderSetFavorite.favorite) { | |
data.foldersFavorite.push(folderSetFavorite) | |
} else { | |
const index = data.foldersFavorite.findIndex( | |
f => f.path === folderSetFavorite.path | |
) | |
index !== -1 && data.foldersFavorite.splice(index, 1) | |
} | |
store.writeQuery({ query: FOLDERS_FAVORITE, data }) | |
} | |
}) | |
}, | |
cwdChangedUpdate (previousResult, { subscriptionData }) { | |
return { | |
cwd: subscriptionData.data.cwd | |
} | |
}, | |
async openPathEdit () { | |
this.editedPath = this.folderCurrent.path | |
this.editingPath = true | |
await this.$nextTick() | |
this.$refs.pathInput.focus() | |
}, | |
submitPathEdit () { | |
this.openFolder(this.editedPath) | |
}, | |
refreshFolder () { | |
this.openFolder(this.folderCurrent.path) | |
}, | |
resetProjectCwd () { | |
this.$apollo.mutate({ | |
mutation: PROJECT_CWD_RESET | |
}) | |
}, | |
async createFolder () { | |
if (!this.newFolderValid) return | |
const result = await this.$apollo.mutate({ | |
mutation: FOLDER_CREATE, | |
variables: { | |
name: this.newFolderName | |
} | |
}) | |
this.openFolder(result.data.folderCreate.path) | |
this.newFolderName = '' | |
this.showNewFolder = false | |
}, | |
slicePath (path) { | |
const parts = [] | |
let startIndex = 0 | |
let index | |
const findSeparator = () => { | |
index = path.indexOf('/', startIndex) | |
if (index === -1) index = path.indexOf('\\', startIndex) | |
return index !== -1 | |
} | |
const addPart = index => { | |
const folder = path.substring(startIndex, index) | |
const slice = path.substring(0, index + 1) | |
parts.push({ | |
name: folder, | |
path: slice | |
}) | |
} | |
while (findSeparator()) { | |
addPart(index) | |
startIndex = index + 1 | |
} | |
if (startIndex < path.length) addPart(path.length) | |
return parts | |
} | |
} | |
} | |
</script> |
<script> | |
import { useQuery, mutate } from 'vue-apollo' | |
import { nextTick, state, value, watch } from 'vue' | |
import { onBeforeRouteLeave } from 'vue-router' | |
import { isValidMultiName } from '@/util/folders' | |
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql' | |
import FOLDERS_FAVORITE from '@/graphql/folder/favoriteFolders.gql' | |
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql' | |
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql' | |
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql' | |
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql' | |
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql' | |
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders' | |
export default { | |
setup (props, { refs }) { | |
const { networkState } = useNetworkState() | |
// Folder | |
const { currentFolderData } = usecurrentFolderData(networkState) | |
const folderNavigationFeature = useFolderNavigation({ | |
networkState, | |
currentFolderData | |
refs, | |
}) | |
const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData) | |
const { showHiddenFolders } = useHiddenFolders() | |
const createFolderFeature = useCreateFolder(folderNavigationFeature.openFolder) | |
// Current working directory | |
const { updateOnCwdChanged } = useCwd() | |
// Utils | |
const { slicePath } = usePathUtils() | |
return { | |
networkState, | |
currentFolderData, | |
...folderNavigationFeature, | |
refreshFolder, | |
favoriteFolders, | |
toggleFavorite, | |
showHiddenFolders, | |
...createFolderFeature, | |
updateOnCwdChanged, | |
slicePath, | |
} | |
} | |
} | |
function useNetworkState () { | |
const networkState = state({ | |
loading: 0, | |
error: false, | |
}) | |
return { | |
networkState | |
} | |
} | |
function usecurrentFolderData (networkState) { | |
const currentFolderData = useQuery({ | |
query: FOLDER_CURRENT, | |
fetchPolicy: 'networkState-only', | |
networkState, | |
async result () { | |
await nextTick() | |
refs.folders.scrollTop = 0 | |
} | |
}, {}) | |
return { | |
currentFolderData | |
} | |
} | |
function useFolderNavigation ({ networkState, currentFolderData, refs }) { | |
// Path editing | |
const pathEditing = state({ | |
editingPath: false, | |
editedPath: '', | |
}) | |
async function openPathEdit () { | |
pathEditing.editedPath = currentFolderData.path | |
pathEditing.editingPath = true | |
await nextTick() | |
refs.pathInput.focus() | |
} | |
function submitPathEdit () { | |
openFolder(pathEditing.editedPath) | |
} | |
// Folder opening | |
const openFolder = async (path) => { | |
pathEditing.editingPath = false | |
networkState.error = null | |
networkState.loading++ | |
try { | |
await mutate({ | |
mutation: FOLDER_OPEN, | |
variables: { | |
path | |
}, | |
update: (store, { data: { folderOpen } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpen } }) | |
} | |
}) | |
} catch (e) { | |
networkState.error = e | |
} | |
networkState.loading-- | |
} | |
async function openParentFolder () { | |
pathEditing.editingPath = false | |
networkState.error = null | |
networkState.loading++ | |
try { | |
await mutate({ | |
mutation: FOLDER_OPEN_PARENT, | |
update: (store, { data: { folderOpenParent } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpenParent } }) | |
} | |
}) | |
} catch (e) { | |
networkState.error = e | |
} | |
networkState.loading-- | |
} | |
// Refresh | |
function refreshFolder () { | |
openFolder(currentFolderData.path) | |
} | |
return { | |
pathEditing, | |
openPathEdit, | |
submitPathEdit, | |
openFolder, | |
openParentFolder, | |
refreshFolder | |
} | |
} | |
function useFavoriteFolders (currentFolderData) { | |
const favoriteFolders = useQuery(FOLDERS_FAVORITE, []) | |
async function toggleFavorite () { | |
await mutate({ | |
mutation: FOLDER_SET_FAVORITE, | |
variables: { | |
path: currentFolderData.path, | |
favorite: !currentFolderData.favorite | |
}, | |
update: (store, { data: { folderSetFavorite } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderSetFavorite } }) | |
let data = store.readQuery({ query: FOLDERS_FAVORITE }) | |
// TODO this is a workaround | |
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473 | |
data = { | |
favoriteFolders: data.favoriteFolders.slice() | |
} | |
if (folderSetFavorite.favorite) { | |
data.favoriteFolders.push(folderSetFavorite) | |
} else { | |
const index = data.favoriteFolders.findIndex( | |
f => f.path === folderSetFavorite.path | |
) | |
index !== -1 && data.favoriteFolders.splice(index, 1) | |
} | |
store.writeQuery({ query: FOLDERS_FAVORITE, data }) | |
} | |
}) | |
} | |
return { | |
favoriteFolders, | |
toggleFavorite | |
} | |
} | |
function useHiddenFolders () { | |
const showHiddenFolders = value(localStorage.getItem(SHOW_HIDDEN) === 'true') | |
watch(showHiddenFolders, value => { | |
if (value) { | |
localStorage.setItem(SHOW_HIDDEN, 'true') | |
} else { | |
localStorage.removeItem(SHOW_HIDDEN) | |
} | |
}, { lazy: true }) | |
return { | |
showHiddenFolders | |
} | |
} | |
function useCwd () { | |
async function resetProjectCwd () { | |
await mutate({ | |
mutation: PROJECT_CWD_RESET | |
}) | |
} | |
onBeforeRouteLeave((to, from, next) => { | |
if (to.matched.some(m => m.meta.needProject)) { | |
resetProjectCwd() | |
} | |
next() | |
}) | |
// Update apollo cache | |
const updateOnCwdChanged = (previousResult, { subscriptionData }) => { | |
return { | |
cwd: subscriptionData.data.cwd | |
} | |
} | |
return { | |
updateOnCwdChanged | |
} | |
} | |
function useCreateFolder (openFolder) { | |
const showNewFolder = value(false) | |
const newFolderName = value('') | |
const newFolderValid = computed(() => isValidMultiName(newFolderName.value)) | |
async function createFolder () { | |
if (!newFolderValid.value) return | |
const result = await mutate({ | |
mutation: FOLDER_CREATE, | |
variables: { | |
name: newFolderName.value | |
} | |
}) | |
openFolder(result.data.folderCreate.path) | |
newFolderName.value = '' | |
showNewFolder.value = false | |
} | |
return { | |
showNewFolder, | |
newFolderName, | |
newFolderValid, | |
createFolder | |
} | |
} | |
function usePathUtils () { | |
const slicePath = (path) => { | |
const parts = [] | |
let startIndex = 0 | |
let index | |
function findSeparator () { | |
index = path.indexOf('/', startIndex) | |
if (index === -1) index = path.indexOf('\\', startIndex) | |
return index !== -1 | |
} | |
const addPart = index => { | |
const folder = path.substring(startIndex, index) | |
const slice = path.substring(0, index + 1) | |
parts.push({ | |
name: folder, | |
path: slice | |
}) | |
} | |
while (findSeparator()) { | |
addPart(index) | |
startIndex = index + 1 | |
} | |
if (startIndex < path.length) addPart(path.length) | |
return parts | |
} | |
return { | |
slicePath | |
} | |
} | |
</script> |
<script> | |
import { useQuery, mutate } from 'vue-apollo' | |
import { nextTick, state, value, watch } from 'vue' | |
import { onBeforeRouteLeave } from 'vue-router' | |
// Reusable functions not specific to this component | |
import { useNetworkState } from '@/functions/network' | |
import { usePathUtils } from '@/functions/path' | |
import { resetCwdOnLeave, useCwdUtils } from '@/functions/cwd' | |
// GraphQL | |
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql' | |
import FOLDERS_FAVORITE from '@/graphql/folder/favoriteFolders.gql' | |
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql' | |
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql' | |
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql' | |
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql' | |
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql' | |
// Misc | |
import { isValidMultiName } from '@/util/folders' | |
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders' | |
export default { | |
setup (props, { refs }) { | |
const { networkState } = useNetworkState() | |
// Folder | |
const { currentFolderData } = usecurrentFolderData(networkState) | |
const folderNavigationFeature = useFolderNavigation({ | |
networkState, | |
currentFolderData | |
refs, | |
}) | |
const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData) | |
const { showHiddenFolders } = useHiddenFolders() | |
const createFolderFeature = useCreateFolder(folderNavigation.openFolder) | |
// Current working directory | |
resetCwdOnLeave() | |
const { updateOnCwdChanged } = useCwdUtils() | |
// Utils | |
const { slicePath } = usePathUtils() | |
return { | |
networkState, | |
currentFolderData, | |
...folderNavigationFeature, | |
refreshFolder, | |
favoriteFolders, | |
toggleFavorite, | |
showHiddenFolders, | |
...createFolderFeature, | |
updateOnCwdChanged, | |
slicePath, | |
} | |
} | |
} | |
// Reusable functions specific to this component | |
export function usecurrentFolderData (networkState) { | |
const currentFolderData = useQuery({ | |
query: FOLDER_CURRENT, | |
fetchPolicy: 'networkState-only', | |
networkState, | |
async result () { | |
await nextTick() | |
refs.folders.scrollTop = 0 | |
} | |
}, {}) | |
return { | |
currentFolderData | |
} | |
export } | |
export function useFolderNavigation ({ networkState, currentFolderData, refs }) { | |
// Path editing | |
const pathEditing = state({ | |
editingPath: false, | |
editedPath: '', | |
}) | |
async function openPathEdit () { | |
pathEditing.editedPath = currentFolderData.path | |
pathEditing.editingPath = true | |
await nextTick() | |
refs.pathInput.focus() | |
} | |
function submitPathEdit () { | |
openFolder(pathEditing.editedPath) | |
} | |
// Folder opening | |
const openFolder = async (path) => { | |
pathEditing.editingPath = false | |
networkState.error = null | |
networkState.loading++ | |
try { | |
await mutate({ | |
mutation: FOLDER_OPEN, | |
variables: { | |
path | |
}, | |
update: (store, { data: { folderOpen } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpen } }) | |
} | |
}) | |
} catch (e) { | |
networkState.error = e | |
} | |
networkState.loading-- | |
} | |
async function openParentFolder () { | |
pathEditing.editingPath = false | |
networkState.error = null | |
networkState.loading++ | |
try { | |
await mutate({ | |
mutation: FOLDER_OPEN_PARENT, | |
update: (store, { data: { folderOpenParent } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderOpenParent } }) | |
} | |
}) | |
} catch (e) { | |
networkState.error = e | |
} | |
networkState.loading-- | |
} | |
// Refresh | |
function refreshFolder () { | |
openFolder(currentFolderData.path) | |
} | |
return { | |
pathEditing, | |
openPathEdit, | |
submitPathEdit, | |
openFolder, | |
openParentFolder, | |
refreshFolder | |
} | |
} | |
export function useFavoriteFolders (currentFolderData) { | |
const favoriteFolders = useQuery(FOLDERS_FAVORITE, []) | |
async function toggleFavorite () { | |
await mutate({ | |
mutation: FOLDER_SET_FAVORITE, | |
variables: { | |
path: currentFolderData.path, | |
favorite: !currentFolderData.favorite | |
}, | |
update: (store, { data: { folderSetFavorite } }) => { | |
store.writeQuery({ query: FOLDER_CURRENT, data: { currentFolderData: folderSetFavorite } }) | |
let data = store.readQuery({ query: FOLDERS_FAVORITE }) | |
// TODO this is a workaround | |
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473 | |
data = { | |
favoriteFolders: data.favoriteFolders.slice() | |
} | |
if (folderSetFavorite.favorite) { | |
data.favoriteFolders.push(folderSetFavorite) | |
} else { | |
const index = data.favoriteFolders.findIndex( | |
f => f.path === folderSetFavorite.path | |
) | |
index !== -1 && data.favoriteFolders.splice(index, 1) | |
} | |
store.writeQuery({ query: FOLDERS_FAVORITE, data }) | |
} | |
}) | |
} | |
return { | |
favoriteFolders, | |
toggleFavorite | |
} | |
} | |
export function useHiddenFolders () { | |
const showHiddenFolders = value(localStorage.getItem(SHOW_HIDDEN) === 'true') | |
watch(showHiddenFolders, value => { | |
if (value) { | |
localStorage.setItem(SHOW_HIDDEN, 'true') | |
} else { | |
localStorage.removeItem(SHOW_HIDDEN) | |
} | |
}, { lazy: true }) | |
return { | |
showHiddenFolders | |
} | |
} | |
export function useCreateFolder (openFolder) { | |
const showNewFolder = value(false) | |
const newFolderName = value('') | |
const newFolderValid = computed(() => isValidMultiName(newFolderName.value)) | |
async function createFolder () { | |
if (!newFolderValid.value) return | |
const result = await mutate({ | |
mutation: FOLDER_CREATE, | |
variables: { | |
name: newFolderName.value | |
} | |
}) | |
openFolder(result.data.folderCreate.path) | |
newFolderName.value = '' | |
showNewFolder.value = false | |
} | |
return { | |
showNewFolder, | |
newFolderName, | |
newFolderValid, | |
createFolder | |
} | |
} | |
</script> |
You can easily find where the folder creation data and logic lives in the code and make adjustment, without even bothering looking at the rest of the code.
I see what you mean but the problem this is solving is that your FileExplorer.vue
is just not well implemented. It's too big and does too many things with business logic mixed in with the UI. You have to break it down and abstract away things like the file creation logic via Vuex or even a vanilla js service class.
True it could be split in multiple components, but then the setup
function would be very small and your initial concern wouldn't apply?
business logic mixed in with the UI
I don't agree here, I don't like MVC and I do think the logic should be encapsulated in components as much as possible.
True it could be split in multiple components, but then the setup function would be very small and your initial concern wouldn't apply?
Yes, but also there would be no need for setup
and all the hooks in the first place because each component would be small enough to be manageable.
Do you put all your CSS in a style folder?
Yes, absolutely. Styles have no place in JavaScript.
even a vanilla js service class
Yes, absolutely. Styles have no place in JavaScript.
🙅♂️
Yes, but also there would be no need for setup and all the hooks in the first place because each component would be small enough to be manageable.
Having too many components also has its issues: performance impact, unmanageable tree in the devtools, more cognitive load to understand the flows... While the setup
API doesn't have those drawbacks.
How is the cognitive load of a massive component with dozens of hook calls less than that of multiple simple components that each do one thing well?
Proper clean code architecture is not at all about MVC. It's about separation of concerns so that changes are easier to implement. What happens when you need to refactor some API call from GraphQL to a normal REST request? Suddenly you have to update 10 Vue components littered with Apollo calls instead one single service that abstracts your API.
How is the cognitive load of a massive component with dozens of hook calls less than that of multiple simple components that each do one thing well?
There would be even simpler plain functions each doing one thing well with very simple ways to interact with each other (plain variables). So yes cognitive load would be even lower.
separation of concerns
It seems I have a very different point of view on this subject than you and I guess it's fine. My opinion is that you are mostly doing something closer to separation of technologies instead.
What happens when you need to refactor some API call from GraphQL to a normal REST request? Suddenly you have to update 10 Vue components littered with Apollo calls instead one single service that abstracts your API.
One of the major benefits of GraphQL and Apollo is co-locating data requirements in the relevant components themselves.
one single service that abstracts your API.
Please don't do GraphQL like this 🙀
Isn't the conversation going a little bit off-topic here? 😸
My opinion is that you are mostly doing something closer to separation of technologies instead.
No, I am talking about separation of concerns and single responsibility principle – this means that, for example, changing styles, shouldn't force you to update the files where business logic resides or changing something about the database shouldn't force you to change styles or views and so on. You might want to read up on Design Patterns, the SOLID principle, Domain Driven Development etc. These are basic programming concepts, I'm not talking about anything new here...
One of the major benefits of GraphQL and Apollo is co-locating data requirements in the relevant components themselves.
This absolutely doesn't solve the problem I described. What happens if you decide to refactor to a different library instead of Apollo? You still have to change a bunch of unrelated files. The view shouldn't care how or where the data comes from.
Isn't the conversation going a little bit off-topic here?
Yes, I see that we are talking past each other so I am leaving.
shouldn't force you to update the files where business logic resides or changing something about the database shouldn't force you to change styles or views and so on.
Editing a file shouldn't be a metric of how good you separate concerns. Maybe a component-based architecture isn't the right abstraction for you. Because in a component architecture, components should be as self-sufficient and encapsulated as possible, so that concerned are cleanly separated and maintainability is as high as possible.
What happens if you decide to refactor to a different library instead of Apollo?
So you are saying you would have to edit dozens of JavaScript file instead of dozens of Vue files. There is not much difference. The more I read you, the more I'm convinced you are mixing up separation of concerns and separation of technologies. Maybe I'm wrong, I don't know the projects you are working on.
You still have to change a bunch of unrelated files.
Those files are related, they are components of your application. But at the same time they each (should) have their own responsibilities, at user-experience level.
Editing a file shouldn't be a metric of how good you separate concerns.
That's literally the definition of Single Responsibility Principle:
As an example, consider a module that compiles and prints a report. Imagine such a module can be changed for two reasons. First, the content of the report could change. Second, the format of the report could change. These two things change for very different causes; one substantive, and one cosmetic. The single responsibility principle says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes or modules.
I see where the problem stems from.
The more I read you, the more I'm convinced you are mixing up separation of concerns and separation of technologies.
Those files are related, they are components of your application. But at the same time they each (should) have their own responsibilities, at user-experience level.
You are mixing up separation of concerns with domain driven development, where code related to the same functionality is grouped together in the same module, not file).
Here's the definition of Separation of Concerns from Wikipedia:
Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface. Encapsulation is a means of information hiding. Layered designs in information systems are another embodiment of separation of concerns (e.g., presentation layer, business logic layer, data access layer, persistence layer).
E.g. a button in your UI shouldn't directly know about the database.
That in no way negates the use case for component architecture. Only you are abusing them by stuffing them with logic that should not be there.
This is from the Vue docs:
Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces.
@Akryum @martinsotirov sorry to bother. My 2 cents.
As a full-stack developer, I feel development for web UI has something fundamentally different from traditional backend development. The backend world can be perfectly divided in some purely logical and self-containing layers: web API, business logic, shared services, data access layer, database ... And together with division of logical layers comes very common division of labor. Different people in charge of different tasks. So we have OOP, single responsibility principle, DDD ...
But in web frontend it seems too heavy to apply all the traditional software engineering doctrines. Most UI components are something quite special: relatively simple business logic (since heavy business logic always goes to backend), huge view layer (DOM API, vue template, react jsx...), and more importantly, high frequency of behavior changing. I guess more than 90% of frontend code is about:
- fetching data from backend
- restructuring fetched data to feed to UI
- rendering UI
- handling UI input events
most of which are heavily entangled with the UI framework (vue & quasar? react & material design? ...) and the API framework (rpc? rest? graphql & apollo?) and are changing very quickly and frequently due to the ever swinging/evolving requirements from product team.
So after trying MVC/MVVM pattern for some time, now I kinda agree with @Akryum and choose to use vue SFC and vue-apollo(graphql), making vue component as my basic self-containing logic unit: a component is like a class instance from the OOP view, having its own states, behavior (including data fetching, css styles, UI rendering, etc.). All in one component. (EDIT: unlike what @Akryum does above, I even tend not to import gql
files into components, but rather to write const gql
strings directly in the method bodies of components, in order to make it clear what kind of queries/mutations I am doing here.)
Yes, this leads to heavy framework binding. it is hard to transfer from vue to react, or from apollo to rest. But, anyway there are no that much business logics in the frontend, therefore even if we had carefully designed a framework-independent architecture and had extracted all pure business logics into service files ... we would found that when we really tried, for example, to switch vue to react, there weren't that much codes that can be reused. We basically always had to rewrite the frontend part from scratch ;P
From that aspect I think the new composition function pattern is quite enough, and in fact quite suitable, for what I know as frontend development.
EDIT: unlike what @Akryum does above, I even tend not to import gql files into components, but rather to write const gql strings directly in the method bodies of components, in order to make it clear what kind of queries/mutations I am doing here.
I do that now too, the example above is pending some refactoring in vue-cli-ui. 😸
It's basically the return object of
setup
.How I see it is that it would actually be easier than before. For example, a new requirement comes to this component that is it should automatically check if the new folder name being typed already exists to display a warning and disable the create button in the corresponding modal (not shown in the example code).
You can easily find where the folder creation data and logic lives in the code and make adjustment, without even bothering looking at the rest of the code.
1- Look at the
setup
function2-
Ctrl
+ click onuseCreateFolder
to go to definition (on VS Code)3- Make some changes
4- Save!