Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single Select #1

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft

Single Select #1

wants to merge 13 commits into from

Conversation

M-Kusumgar
Copy link
Contributor

@M-Kusumgar M-Kusumgar commented Nov 2, 2023

Description

Don't be afraid! Most of the diff lines are package-lock.json and boilerplate. This PR adds the nested single select into this package. I have not updated the README, incorporated CodeCov or added a test workflow yet, these will happen once the multiselect is added but to see demo run:

npm i
npm run dev

Feel free to change the data in the App.vue file to test out the different cases.

To test run:

npm run test

This project uses vite.

Guide

My general approach was to use an existing dropdown component and modify it so we can have nested options. The way this works is we first flattenOptions which will convert

[{
  id: "id1",
  label: "label1",
  children: {
    id: "id1_1",
    label: "label1_1"
  }
}]

into

[
  {
    id: "id1",
    label: "label1",
    hasChildren: true,
    open: false,
    show: true,
    path: "/id1"
  },
  {
    id: "id1_1",
    label: "label1_1",
    hasChildren: false,
    show: false,
    path: "/id1/id1_1"
  }
]

The path keeps track of the directory like structure so we can find if something is a child of another by comparing strings. The show will control what options are shown in the menu so when we expand a collapsible item, all the shows of the children will flip to true. The collapse will scan for any path with the same prefix as the item being collapsed and collapse those.

Copy link
Member

@r-ash r-ash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for talking me through my dumb questions on this. This looks really great to me and I like doing the recursion step in your utils so it is really easy to test. Might be worth changing your path to an array to avoid having to do the string splitting which would fail is someone has a / in their ID. But otherwise this looks really solid to me

@M-Kusumgar M-Kusumgar requested a review from r-ash November 8, 2023 14:36
Copy link

@EmmaLRussell EmmaLRussell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat! Works really well - couple of suggestions but up to you.

Are you going to add a gha to run the tests and coverage?

package.json Outdated
@@ -0,0 +1,44 @@
{
"name": "vue-nested-multiselect",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've been prefacing our npm packages with @reside-ic, and including repository, author, license, homepage so they show up nicely on npm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

src/App.vue Outdated
@@ -0,0 +1,66 @@
<template>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this doesn't really belong in src if it's just for manual testing, and same for main.ts. Could they be pulled out into folder called something like demo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +7 to +12
<vue-feather type="chevron-right"
v-show="!option.open"
class="icon"/>
<vue-feather type="chevron-down"
v-show="option.open"
class="icon"/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a bit more compact, but not too fussed

Suggested change
<vue-feather type="chevron-right"
v-show="!option.open"
class="icon"/>
<vue-feather type="chevron-down"
v-show="option.open"
class="icon"/>
<vue-feather :type="option.open ? 'chevron-down' : 'chevron-right'"
class="icon"/>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the reason i did this was because v-show will actually load both of them so if someone were to spam click, this performs slightly better, obviously not noticeably but thats why i tried to do it this way, happy to change it though! up to you really XD

Copy link

@EmmaLRussell EmmaLRussell Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok - leave it as it is if it behaves more nicely this way!

@@ -0,0 +1,65 @@
import { FlatOption, Option } from "./types";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should types.ts and utils.ts live outside components since they aren't... ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep can move them! done

Comment on lines 29 to 36
expand: {
type: Function as PropType<(payload: MouseEvent) => void>,
default: () => null
},
collapse: {
type: Function as PropType<(payload: MouseEvent) => void>,
default: () => null
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why these are props? These are emitted events aren't they?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha i noticed this mistake when wrapping up the multiselect but was just gonna change it in the multiselect branch if no one noticed 😆 , done. This was from a previous commit where i was passing closures in as props and calling them, no idea why but decided to use emits now

return JSON.stringify(array1) === JSON.stringify(array2);
};

export const expandOptions = (array: FlatOption[], optionPath: string[]): FlatOption[] => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder if it would be a bit more Vue-y to modify the array in place rather than map to a whole new array. I "think" that should work if the FlatOption array was created as reactive when initialised.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe ref would be fine - can never remember how it treats arrays, but I'm sure it can handle changing arrays!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref was indeed fine XD

}

// if you are a child of a previously opened option then you should show
const show = openOptionsPaths.some(openOpPath => {
Copy link

@EmmaLRussell EmmaLRussell Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand why previously opened paths are relevant here. Shouldn't it just be modifying show to true for anything immediately under the selected path, and leaving everything else alone?
And would it make sense to take advantage of the fact that we know the options are in order, so that if we find the next sibling (or higher?) of the selected path we can stop changing things (and we don't need to change anything before the selected path)?
So, could you have something like this (for modifying in place)?

const expandOptions = (array: FlatOption[], optionPath: stirng[]) => {
    let index = array.findIndex((fo: FlatOption) => arraysAreEqual(fo.path, optionPath)) + 1;
    // loop until end of all options, or next sibling (or higher level option)
    while (index < array.length && array[index].path.length > optionPath.length ) {
        // only change immediate children
        if (array[index].path.length === optionPath.length + 1) {
            array[index].show = true;
        }
        index = index + 1;
    }
}

Alternatively while loop could check that the path starts with the optionPath path (rather than just looking at path length).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i really like this idea of just mutating that slice of the array, vue seems to work well with just the ref so all good on that front too! the open paths are relevant because they keep track of what someone has opened before and we should open that. For example lets say something expands the options "parent" > "child" > "grandchild" and then they collapse parent. When they expand parent they should see grandchild too, this is how the current treeselect does it and I think it is actually quite a helpful functionality especially in deeper structures

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, and that's going to use the 'open' prop which is retained rather than 'show' which just determines whether it's being displayed right now? Don't think I'd quite spotted the distinction there, but yeah that makes total sense.

});
};

export const collapseOptions = (array: FlatOption[], optionPath: string[]) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do something similar here in terms of only examining/modifying the descendants of the optionsPath option..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep done!

@@ -0,0 +1,23 @@
# Logs

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't need all of this I guess, but no harm!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yh this just came with vite XD thought i would just leave it in there

@@ -0,0 +1,9 @@
.dropdown-menu {
--cui-dropdown-link-active-bg: #ebedef !important;
--cui-dropdown-link-active-color: rbga(44, 56, 74, 0.95) !important;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably document how package client can override style.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall i leave this for another PR? i would like to add styling and configuration last once the single select, multiselect and search things are all done, ticket: https://mrc-ide.myjetbrains.com/youtrack/agiles/103-61/current?issue=mrc-4697

@M-Kusumgar
Copy link
Contributor Author

Are you going to add a gha to run the tests and coverage?

Yes I will be adding these once single select and multiselect components are done, i think these things are better once theres a decent base!

Copy link

@EmmaLRussell EmmaLRussell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Couple of tiny comments, but good to go.

overflow-wrap: break-word;
text-align: left;
}
</style>../utils

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this ../utils doing here?


while (index < array.length && array[index].path.length > optionPath.length) {
const currentOption = array[index];
const show = openOptionsPaths.some(openOpPath => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this bit merits a comment maybe something like.

Suggested change
const show = openOptionsPaths.some(openOpPath => {
// Show options whose immediate parents are marked as open
const show = openOptionsPaths.some(openOpPath => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed this comment in #6, it is added in

@M-Kusumgar M-Kusumgar marked this pull request as draft December 12, 2023 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants