-
Notifications
You must be signed in to change notification settings - Fork 1
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
feat: make stepper bubbles collapse and refine css working #702
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although I'm impressed with how you've got this feature working with so few lines, I'm not happy with the implementation. There are two main reasons for this.
First, I believe the representation of the stepper produced by build_stepper()
should be "correct" and complete. Right now, we are collapsing stepper items by completely ommitting children that do exist in stepper.items
. This means that there are items with parents that don't have children. Also, the resulting stepper's shape is very different depending on which item is current. This makes it unnecessarily hard to reason and potentially debug the output of build_stepper
Of course, the css classes and such are also different depending on which item is current, but I consider that to be part of the presentation rather than the internal representation. Collapsing of stepper items should in my view also be part of the presentation, not the internal representation.
Second, but somewhat related, this implementation is not very flexible. This implementation only works for top-level items: collapsing children of a child would get messy very quickly. We might also want to move to JS-expandable stepper items in the future, but ommitting children entirely does not allow for that.
My recommendation would be to keep the collapsing part of the stepper to the presentation side of things, in this case I would suggest moving it to the template:
{% if not item.is_collapsed %}
{% if item.children %}
... Render children ...
{% endif %}
{% endif %}
The is_collapsed
flag can easily be repurposed into a CSS class if we move to JS collapsing. Also, we can inspect the complete structure of the stepper from within a breakpoint without needing to worry about children being ommitted.
What I do think we should keep is the one-time search for a current item, that's a great optimization. I don't think it belongs in build_stepper()
which I would rather keep clean if possible. I would recommend a separate loop, or at least a separate method that checks for currentness.
proposals/utils/stepper.py
Outdated
for item in self.items: | ||
#Only check item.is_current until there is a current item found | ||
while not self.current_item: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love that this makes sense in English but programmatically this while loop makes no sense. Anything below it will only ever run once so it should be an if statement.
proposals/utils/stepper_helpers.py
Outdated
) | ||
return " ".join(classes) | ||
|
||
def get_ancestor(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useful!
proposals/utils/stepper_helpers.py
Outdated
return True | ||
return False | ||
|
||
def is_disabled(self): | ||
if not self.get_url(): | ||
return True | ||
return False | ||
|
||
def get_css_classes(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When writing a method that might be overwritten, it should have the same signature (i.e. return value types) as whatever might overwrite it.
A method that just passes returns None
, and that is not the same as a string. So you might get an error if you do super().get_css_classes() + my_css_class
.
It should in this case return an empty string. Or an empty list which you append to, and then finally you turn the list into a space-separated string somewhere else.
proposals/utils/stepper.py
Outdated
self.current_item_ancestor = item.get_ancestor() | ||
else: | ||
break | ||
item.get_css_classes() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason that we need to get the CSS classes at this stage? Can't we just call item.get_css_classes()
from the template?
proposals/utils/stepper_helpers.py
Outdated
child_errors = [] | ||
for child in self.children: | ||
child_errors += child.get_errors() | ||
if child_errors != []: | ||
css_classes += "incomplete" | ||
self.css_classes += "incomplete" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There isn't enough consistency in where the space goes. I think we might better avoid the issue entirely and provide a list
of css classes, and only turn them into a string when needed for HTML.
So here is the new version of this PR, some notes on what has been changed:
This feels right to me :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So here is the new version of this PR, some notes on what has been changed:
* StepperItem's now have an `is_available attribute. This is false by default. This relates to whether its children are displayed. A more correct name would be `is_not_collapsed` ...
How about is_expanded
? No biggie either way
* The new way of searching for the current item is retained, but this now happens in a separate method (`item_is_current_check()`) of the stepper, until a current item is found. * When a current item is found, its `get_ancestors()` gets called, which returns a list of the item + all of its ancestors (so parent's, parent.parent's etc) These items' `is_available` are then set to True
See my code comment, I think it would be better if the current check or build_css_classes
would check if it's the current item and set the css class there, rather than in is_current()
.
Otherwise this is great.
* The get_errors function and get_css_classed have had a bit of an overhaul. get_errors, can now get an `include_children` boolean argument, so that the errors of an item's descendants are also taken into account. * The current design is, when an item is collapsed, its completeness gets evaluated with `include_children`, when an item `is_availabe` just the model_form errors and the optional `get_checker_errors` get evaluated.
This feels right to me :)
Perfect!
proposals/utils/stepper_helpers.py
Outdated
""" | ||
if request.path == self.get_url(): | ||
self.css_classes.add("active") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method was previously stateless, being more of a "check" and not an action with consequences. If we add the CSS class here, we have to remember to do that for every time we write a custom is_current()
, like in the Session item (which currently doesn't do this correctly).
I see how this makes sense, but I would prefer if the css class gets added in is_current_check()
so we don't have so many different places to think about CSS classes.
proposals/utils/stepper.py
Outdated
@@ -36,6 +36,8 @@ def __init__( | |||
# which item is current | |||
self.request = request | |||
self.items = [] | |||
self.current_item_ancestors = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep consistent with types, this should be an empty list.
main/templates/base/stepper.html
Outdated
<span class="stepper-bubble {{ bubble_size.0 }}">{% counter counter value %}</span> | ||
<span>{{ item.title }}</span> | ||
</a> | ||
{% endif %} | ||
{% if item.children %} | ||
{% if item.children and item.is_available %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this collapsing implementation a lot more!
If in testing we decide, for example, that we want to leave the Trajectories items expanded by default, we can easily implement that. Very flexible.
So, I've adressed your comments. Good ideas! I also noticed that FundingForm did not really include validation, so I just added some cuz it was bugging me ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! I have some small suggestions but functionally this works very well. Approving.
while parent.parent: | ||
parent = parent.parent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my favourite part of this PR
def get_errors(self, include_children=False): | ||
""" | ||
By default, this returns the errors of the instantiated form, which get | ||
evaluated as a boolean. By passing a custom get_checker_errors function | ||
from the checker, custom errors can be specified also. | ||
By default, this returns the errors of the instantiated form and, | ||
optionally, its children. This then gets evaluated as a boolean. | ||
By passing a custom get_checker_errors function | ||
from the checker, custom errors also be added. | ||
""" | ||
if hasattr(self, "get_checker_errors"): | ||
return self.get_checker_errors() or self.model_form.errors | ||
return self.model_form.errors | ||
errors = [] | ||
errors += self.model_form.errors | ||
|
||
def css_classes(self): | ||
css_classes = super().css_classes() | ||
if hasattr(self, "get_checker_errors"): | ||
errors += self.get_checker_errors() | ||
if include_children and self.children: | ||
for child in self.children: | ||
errors += child.get_errors(include_children=True) | ||
return errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like there's some unnecessary repetition here. Can't we just start with super().get_errors(include_children=include_children)
and append the checker errors to that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I don't think so because the super() just goes to StepperItem's get_errors which just returns an empty list. This is where the behaviour of this function gets specified. Up the chain, include children becomes basically meaningless.
proposals/utils/stepper_helpers.py
Outdated
if not self.is_expanded and self.children: | ||
if self.get_errors(include_children=True): | ||
self.css_classes.add("incomplete") | ||
else: | ||
self.css_classes.add("complete") | ||
elif self.get_errors(): | ||
self.css_classes.add("incomplete") | ||
else: | ||
css_classes += " complete" | ||
return css_classes | ||
self.css_classes.add("complete") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also feel like this could be simplified now that we have the is_expanded
variable.
Wouldn't something like if self.get_errors(include_children=self.is_expanded)
remove the need for the double if
statements?
I might be missing something though, just a quick suggestion.
proposals/utils/stepper_helpers.py
Outdated
is currently on and adds the active class to the item's | ||
self.css_classes set. This gets called when building the stepper, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is no longer accurate
So, I was just puzzling a bit with the stepper, and to my surprise, came up with a solution for making the bubbles collapse ...
This is still a bit rough, but I thought I'd share it with you before the (my) weekend. Will add more notes on monday!