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

Allow to pass in boolean to noDirtyCheck directive #8638

Merged
merged 16 commits into from
Aug 30, 2021

Conversation

bjarnef
Copy link
Contributor

@bjarnef bjarnef commented Aug 17, 2020

Prerequisites

  • I have added steps to test this contribution in the description below

Description

At the moment we have some duplicated logic to be able to set no-dirty-check directive on e.g. <umb-checkbox> and <umb-radiobutton>.

I would however like just to pass in a boolean value whether to set this or not.

For example:

  • no-dirty-check without attribute value set $dirty to false.
  • no-dirty-check="true" with attribute value true works as before mentioned.
  • no-dirty-check="false" with attribute value true works as default behaviour.

The directive was originally created based on this https://stackoverflow.com/a/27430980 but note sure why we need the local variable with getter and setter. Furthermore why it change $pristine. It seems this was changed in #2476

I found some different variants setting $dirty to false or angular.noop:
https://stackoverflow.com/a/29225603

2020-08-17_14-07-38

Test notes

  • Check that changes to input fields in content trigger the "Unsaved changes" dialog
  • Add a custom dashboard with use on no-dirty-check, no-dirty-check="true" and no-dirty-check="true" on <input> field works as expected. Also check with<umb-checkbox> and <umb-radiobutton> using disable-dirty-check property with true/false value.
<div>
    <label>Text 1</label>:<br>
    <input type="text" name="text1" placeholder="Enter value..." ng-model="text1" no-dirty-check />
</div>

<div>
    <label>Text 2</label>:<br>
    <input type="text" name="text2" placeholder="Enter value..." ng-model="text2" no-dirty-check="false" />
</div>

<div>
    <label>Text 3</label>:<br>
    <input type="text" name="text3" placeholder="Enter value..." ng-model="text3" no-dirty-check="true" />
</div>

<div>
    <label>Checkbox</label>:<br>
    <umb-checkbox model="check1" name="check1" disable-dirty-check="false">Test Check</umb-checkbox>
</div>

<div>
    <label>Radiobutton</label>:<br>
    <umb-radiobutton model="radio1" name="radio1" disable-dirty-check="true">Test Radio</umb-radiobutton>
</div>

MyDashboard.zip

@bjarnef bjarnef marked this pull request as ready for review August 17, 2020 12:36
@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 17, 2020

@nul800sebastiaan @nielslyngsoe I believe this will clean up code a bit and we don't need some duplicated code.
Something similar could be done for prevent-enter-submit:
https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html#L5-L26

It looks like we have something like this for preventEnterSubmit, but I can't see the enabled variable is used for anything - it simply always prevent enter submit when this directive is added 🙈
https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js#L10-L17

@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 17, 2020

@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 17, 2020

I have submitted another PR to fix the issue with preventEnterSubmit: #8639

@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 17, 2020

@kjac maybe you want to test this? 😎

@nul800sebastiaan
Copy link
Member

This one is too intricate for me to evaluate, I will need some help from @kjac, @nathanwoulfe or if neither can help then @nielslyngsoe. Any takers? 🤞

@nul800sebastiaan nul800sebastiaan added the state/needs-investigation This requires input from HQ or community to proceed label Aug 31, 2020
@kjac
Copy link
Contributor

kjac commented Aug 31, 2020

Sure I'll give it a spin in the morning 👍

@@ -50,7 +50,7 @@

function onInit() {
vm.inputId = vm.inputId || "umb-check_" + String.CreateGuid();

vm.disableDirtyCheck = vm.disableDirtyCheck === true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this necessary? And won't it break backwards compat with people using a truthly value that's not strictly true?

@@ -49,7 +49,7 @@

function onInit() {
vm.inputId = vm.inputId || "umb-radio_" + String.CreateGuid();

vm.disableDirtyCheck = vm.disableDirtyCheck === true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above: Is this necessary? And won't it break backwards compat with people using a truthly value that's not strictly true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if we need to support both true/false and 'true'/'false' as string values. Here is just do the comparison in the controller so the view is a bit cleaner :)

Copy link
Contributor

Choose a reason for hiding this comment

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

While certainly cleaner it's also more stringent. My point is that the directive (presumably) supports truthly values today, so it should keep on doing that to preserve compatibility with current usages in 3rd party extensions etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can add more checks here to make it work with other truthly values. E.g.

vm.disableDirtyCheck = vm.disableDirtyCheck === true || vm.disableDirtyCheck === 'true' || vm.disableDirtyCheck === 1;

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 noticed we have this isBool function in color picker:
https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js#L142-L145

Maybe we should have a global function somewhere to check for truthly values?

Copy link
Contributor

Choose a reason for hiding this comment

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

Come to think of it we already do - there's an Object.toBoolean(value) method (defined in Extensions.js).

Object.defineProperty(ctrl, '$pristine', alwaysFalse);
Object.defineProperty(ctrl, '$dirty', alwaysFalse);
// If no attribute value is "false", then skip and use default behaviour.
var dirtyCheck = scope.$eval(attrs.noDirtyCheck) === false;
Copy link
Contributor

Choose a reason for hiding this comment

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

There must be a native JS function to evaluate a boolean instead of depending on AngularJS to do it for us?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, probably. However I have noticed the $eval function is used several other places to parse attribute values.

Copy link
Contributor

Choose a reason for hiding this comment

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

With the above changes in mind, shouldn't this be var dirtyCheck = Object.toBoolean(attrs.noDirtyCheck) === false; ?


ctrl.$setDirty = Utilities.noop;
Copy link
Contributor

Choose a reason for hiding this comment

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

While I'm sure this works, I'm a tad worried it's too good to be true. The quite intricate solution with the alwaysFalse binding to $pristine and $dirty must be there for a reason... this is just a hunch, but I'm afraid your solution over simplifies things. Wouldn't it be prudent to keep the old solution here, just after the conditional dirty check handling?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if there is a specific reason it change $pristine. In other alternatives of no-dirty-check directives I have seen in only change $dirty. Regarding the "Unsaved changes" dialog it only rely on $dirty property as far as I know.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep I agree with you. As I tried to explain, it should work fine, but the current solution seems quite explicitly done as-is. There can be two reasons for this: Either the current solution is over complicated and implemented as such due to lack of knowledge/understanding, or else the current solution covers edge cases we can't right now. My point is it can't hurt to retain the current solution from a better-safe-than-sorry standpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, not sure if $pristine has any effect on the "Unsaved changes" dialog though. The reason we have the no-dirty-check directive is to prevent the "Unsaved changes" dialog to show in certain circumstances.

It seems it only rely on $dirty before opening the overlay here:

if (!formCtrl.$dirty && infiniteEditors.length === 0 || isSavingNewItem && infiniteEditors.length === 0) {
return;
}

kjac
kjac previously requested changes Sep 1, 2020
Copy link
Contributor

@kjac kjac left a comment

Choose a reason for hiding this comment

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

@bjarnef I see where you're going with this. Code cleanup is always nice 👍 I have left a bunch of comments for you to have a look at.

@bjarnef
Copy link
Contributor Author

bjarnef commented Sep 1, 2020

@kjac it should now check for truthly value 👍

@kjac
Copy link
Contributor

kjac commented Sep 2, 2020

Thanks @bjarnef . I left another remark about the usage of scope.$eval() for you to consider, now that we discovered Object.toBoolean() 😄

Also would you please revert the dirty handling to the previous solution? If you feel very strongly for changing it, you can do so in a new PR, but please be absolutely certain that you know exactly why the previous solution is written in such a cumbersome way before attempting to change it.

@bjarnef
Copy link
Contributor Author

bjarnef commented Sep 2, 2020

@kjac I have reverted the dirty handling for now and changes to Object.toBoolean() 😊

Regarding the "Unsaved changes" I don't think the $pristine has any effect on this, since it only check if the form is $dirty.

if (!formCtrl.$dirty && infiniteEditors.length === 0 || isSavingNewItem && infiniteEditors.length === 0) {
return;
}

To me it seems it was just taken from one of the first results on Google, but no comment left, why it need to change $pristine.
https://stackoverflow.com/a/27430980

@kjac
Copy link
Contributor

kjac commented Oct 1, 2020

@bjarnef finally got around to giving this a test run. It seems your solution didn't take into account the default (undefined) values when the directives were applied without true/false. I have pushed a fix in 0a5b442 - have a look.

Here's a test dashboard you can use with all combinations of dirty checking:

<div ng-controller="MyDashboardController as vm">
    <div>
        <input type="text" name="text1" placeholder="no-dirty-check" ng-model="text1" no-dirty-check />
    </div>

    <div>
        <input type="text" name="text2" placeholder="no-dirty-check=&quot;false&quot;" ng-model="text2" no-dirty-check="false" />
    </div>

    <div>
        <input type="text" name="text3" placeholder="no-dirty-check=&quot;true&quot;" ng-model="text3" no-dirty-check="true" />
    </div>

    <div>
        <umb-checkbox model="check0" name="check0" disable-dirty-check>disable-dirty-check</umb-checkbox>
    </div>

    <div>
        <umb-checkbox model="check1" name="check1" disable-dirty-check="false">disable-dirty-check="false"</umb-checkbox>
    </div>

    <div>
        <umb-checkbox model="check2" name="check2" disable-dirty-check="true">disable-dirty-check="true"</umb-checkbox>
    </div>

    <div>
        <umb-radiobutton model="radio0" name="radio0" disable-dirty-check>disable-dirty-check</umb-radiobutton>
    </div>

    <div>
        <umb-radiobutton model="radio1" name="radio1" disable-dirty-check="false">disable-dirty-check="false"</umb-radiobutton>
    </div>

    <div>
        <umb-radiobutton model="radio2" name="radio2" disable-dirty-check="true">disable-dirty-check="true"</umb-radiobutton>
    </div>
</div>

@bjarnef
Copy link
Contributor Author

bjarnef commented Oct 1, 2020

@kjac strange Object.toBoolean() should already handle undefined and null values passed in right?
https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js#L344

Thus returning false and the evaluating the condition would be false.

@kjac
Copy link
Contributor

kjac commented Oct 1, 2020

@bjarnef exactly; but "no value" is supposed to mean true in this case, as adding the directive without a value should enable it 😄

@bjarnef
Copy link
Contributor Author

bjarnef commented Oct 1, 2020

@kjac okay, then we might need a null check as well?

@kjac
Copy link
Contributor

kjac commented Oct 1, 2020

@bjarnef that was my thought too, and surely it can't hurt to add them, but try as I might I could only manage to pass undefined to the directives.

@bjarnef
Copy link
Contributor Author

bjarnef commented Mar 29, 2021

@nathanwoulfe I wonder if you may have an idea why I am starting to see this console error? I wonder if something has changed in Angular v1.8.0?

It would be an issue if disableDirtyCheck property wasn't optional, but it is :)

@nathanwoulfe
Copy link
Contributor

Not sure, but it could be due to using attrs rather than scope to access the value - if the value is bound in the view, and has changes, perhaps Angular is aware that it has changed, but can't propagate the change to the directive since it's not a scoped input.

That's a guess, and a big one...

@bjarnef
Copy link
Contributor Author

bjarnef commented Mar 29, 2021

@nathanwoulfe it doesn't seem to be related to the use of $attrs since I get the console errors without the use of this change.

If I comment out the line where vm.disableDirtyCheck is assigned in controller I don't get the console errors, but of course the logic doesn't work as expected then.

@mikecp
Copy link
Contributor

mikecp commented Mar 30, 2021

Hello @bjarnef ,

Thanks for all the investigations!

I went and tested again and I think there were some remaining issues if we wanted values undefined, null, "", "something", to behave as no-dirty-check without value as you mentioned, because they should then produce a true result while the Object.ToBoolean produces a false result.

After scratching my head for a while, I kind of "started from scratch" for the construction of the conditions. From what I understood, the behaviour we want is something like "skip the no-dirty-check logic only if the attribute is not provided (in which case you don't get into the logic in fact :-)) or if its value is explicitly set to false, go for it in all other cases".

So I rewrote the conditions in the noDirtyCheck directive to match exactly that. The Object.ToBoolean does not fit for this since it actually does the opposite: anything is false except if it's explicitly set to true.

I also rewrote the conditions in the radio/checkbox controllers so that they provide the correct true/false values to the no-dirty-check attribute.

I made tests with all the possibilities you mentioned, and it behaves as expected: only when we do not set the attribute or when we explicitly set no-dirty-check="false" (or 0), we get the "Unsaved Changed" warning.

Let me know what you think 😁

I'll try to have a look tonight for the console errors

Cheers!

@bjarnef
Copy link
Contributor Author

bjarnef commented Mar 30, 2021

@mikecp yeah the Object.toBoolean() extension may not fit the needs here, so it is great explicit to check the value.

Regarding noDirtyCheck I think we want e.g. this no-dirty-check="something" to behave like no-dirty-check or no-dirty-check=""

So the value need to be "0" (int or string) or "false" (bool or string) to "disable" this (like if this attribute isn't added which make the form field dirty and trigger the "Unsaved changes" dialog.

Regarding disable-dirty-check in checkbox and radiobutton components, I think we want the opposite - so it explicit need to be "1" (Int or string) or "true" (bool or string) 👍

@mikecp
Copy link
Contributor

mikecp commented Apr 1, 2021

Hi @bjarnef,

Thanks for your feedback!
I'm probably overseeing something, but I'm not sure why we would want a different behavior in the checkbox/radiobutton components. As I understand them, no-dirty-check and disable-dirty-check are two synonyms for saying "don't check for changes", so I guess I would expect them to behave in a similar way. But maybe there is a difference I did not catch?

@bjarnef
Copy link
Contributor Author

bjarnef commented Apr 1, 2021

I think it comes down to that no-dirty-check directive original didn't had a attribute value, so when added it would disable the form field dirty check.
And with any invalid values it should probably just fallback to default behaviour as without attribute value.

With disable-dirty-check component property the value should be true or false, but a random string value should probably just return false which is passed into no-dirty-check="false" in the component meaning the form field would be dirty, while no-dirty-check="true" and no-dirty-check is the same.

@bjarnef bjarnef closed this Apr 1, 2021
@bjarnef bjarnef reopened this Apr 1, 2021
@mikecp
Copy link
Contributor

mikecp commented Apr 2, 2021

Hey @bjarnef ,

I start to mixup what was said in the different messages😃 . So if I try to summarize, what we want is:

  • no-dirty-check
    • not there => display unsaved changes warning
    • there without value => no display
    • there with value true, 1 or '1' => no display
    • there with value false, 0 or '0' => display unsaved changes warning
    • there with value null, undefined or any other invalid value => no display

Regarding disable-dirty-check, the more I look at it, the more it looks like just a pass-through to the "no-dirty-check" attribute of the form control beneath it. Son I am starting to think that we might just pass its value as-is to the no-dirty-check directive, and let that one interpret it following the rules above...

Since no-dirty-check now handles all possible values sent, is that not the easiest way to keep the logic homogeneous?

Cheers!

@bjarnef
Copy link
Contributor Author

bjarnef commented Apr 2, 2021

@mikecp correct about the no-dirty-check attribute 👍

Yeah, er han probably just pass the value through. Not that important as it from start was supposed to use true/false values and is a property, while the other is a directive. It might could to check what happens with no disable-dirty-check, empty value, null, undefined though 😎🐥

Could you give this a last spin, then I think we are pretty close and definitely cleanup the views.

@bjarnef
Copy link
Contributor Author

bjarnef commented Jun 30, 2021

@mikecp did you had a change to look at this again? would be great to close this and make the code a bit cleaner 😎

@mikecp
Copy link
Contributor

mikecp commented Jul 1, 2021

Hey @bjarnef ,
Thanks for the reminder 😉I'll have a look back at this so we can finalize it!
Cheers!

@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 4, 2021

@mikecp did you found some time to look at this again? Let me know if you need further info or I can help with anything 😎👍

@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 21, 2021

@mikecp anything I can help with to get this closed/merged. I would really like to cleanup some of the redundant code 🙈🤓

@mikecp
Copy link
Contributor

mikecp commented Aug 22, 2021

Hi @bjarnef ,
My apologies for my very slow reaction, it seems I needed a longer summer break than expected, but I'll definitely look at this and finalize this in the coming days 😅
Cheers!

@mikecp mikecp dismissed kjac’s stale review August 30, 2021 13:57

Changes have been integrated + adapted further

@mikecp
Copy link
Contributor

mikecp commented Aug 30, 2021

Hi @bjarnef ,

I finally found the time to make the last checks on this PR 🎉😂

Testing on undefined, null and empty values for disable-dirty-check gave indeed some last glitches but I made some last adjustments and now everything is working as it should, meaning that disable-dirty-check gives the same behavior as no-dirty-check for the same values/bound values.

So I can finally merge this, with all my apologies for this long delay 😅

Thanks again for this PR and for your patience 👍

@mikecp mikecp merged commit 7757c4d into umbraco:v8/contrib Aug 30, 2021
@mikecp mikecp added community/pr release/8.17.0 type/feature and removed state/needs-investigation This requires input from HQ or community to proceed labels Aug 30, 2021
@bjarnef bjarnef deleted the v8/feature/adjust-no-dirty-check branch August 30, 2021 14:04
@bjarnef
Copy link
Contributor Author

bjarnef commented Aug 30, 2021

Hi @mikecp

Thanks for testing it again and reviewing the code. Great to have this redundant code cleaned up and will be useful in future development easy to enable/disable the dirty check. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants