Open Closed

Client side date operation issue converting from date to moment and moment to date #8866


0
adamphones created

When RAD is used to generate fields for DateTime the client side code generated something like below assuming that date fields are called EffectiveFrom and EffectiveTo:

In angular create-edit component class:

effectiveFrom: Date; effectiveTo: Date;

in the save method:

if (this.effectiveTo) { if (!this.product.effectiveTo) { this.product.effectiveTo = moment(this.effectiveTo).startOf('day'); } else { this.product.effectiveTo = moment(this.effectiveTo); } } else { this.product.effectiveTo = null; } ... //similar for effective from

The problem is that when we are in a different time zone (GMT) then and we want to auto populate 'Effective To' once user selects a date for Effecitve From as below:

effectiveFromChanged(newValue): void { this.effectiveFrom = newValue; this.effectiveTo = moment(this.effectiveFrom).add(12, 'M').toDate();
} } This sets the effective to 1 day before the actual expected date. For example if the user selects effective from 01/04/2020 then effective to should be set to 01/04/2021 instead of 31/03/2021.

I guess this is happening due the fact that conversation takes the timezone into account. But we do not want that! How can we over come this sort of date issues? Any option that would disable this behaviour? Maybe the date pickers should return moment object instead of a date?

Note: Clock.Provider = ClockProviders.Utc; is set in the core module.


24 Answer(s)
  • 0
    maliming created
    Support Team

    We are solving this problem. If there is a result, I will reply to you.

  • 0
    musa.demir created
    Support Team

    Hello @adamphones It is because of you use moment.toDate();.It just creates a JavaScript date object without handling almost anything. Here is how it works

    That methods give you javascript date object. By default, almost every date method in JavaScript gives you a date/time in local time. You only get UTC if you specify UTC. But as you see it does not specify it, if you use toDate();

    So you can use moment instead of javascript date. Or you can use toISOString to get UTC value from javascript date

    Edit: Here is more explained document. https://css-tricks.com/everything-you-need-to-know-about-date-in-javascript/

  • 0
    adamphones created

    Hi Musa,

    Not sure if this answers my question... I am aware of the issue actually but I was looking for a solution. The RAD tool defines those date fields as Date object. And to be able to assign value to them I use toDate() to convert moment to date. If I use toISOString() then I will get a string representation of the date but actually I would need the date unless if I replace the Date types.

    Are you planning to replace those Date type completely in the RAD tool in the future? As you can see you cannot rely on them for date operations.

    This is my solution for now to overcome the problem:

               let userTimezoneOffset = this.effectiveFrom.getTimezoneOffset() * 60000;
                let selectedRealDate = this.effectiveFrom.getTime() - userTimezoneOffset;
                this.effectiveTo = moment(selectedRealDate).add(this.contractProductLine.duration, 'months').toDate();
    

    Considering above how would you fix the issue with toISOString()?

    I also tried to replace data type from Date to Moment but in this case the date shows in the field as 'Invalid Date' at first load.

  • 0
    musa.demir created
    Support Team

    Sory about that. I missed to read RadTool part.

    Can you please share your entities json file if it is not problem for you. It is located in aspnet-core\AspNetZeroRadTool folder. The file pattern is [YourNameSpace].[YourEntityName].json.

  • 0
    adamphones created

    { "IsRegenerate": false, "MenuPosition": "main", "RelativeNamespace": "Contracts", "EntityName": "Product", "EntityNamePlural": "Products", "TableName": "Products", "PrimaryKeyType": "int", "BaseClass": "AuditedEntity", "EntityHistory": true, "AutoMigration": false, "UpdateDatabase": false, "CreateUserInterface": true, "CreateViewOnly": true, "CreateExcelExport": true, "PagePermission": { "Host": false, "Tenant": false }, "Properties": [ { "Name": "EffectiveFrom", "Type": "DateTime", "MaxLength": 0, "MinLength": 0, "Range": { "IsRangeSet": false, "MinimumValue": 0, "MaximumValue": 0 }, "Required": false, "Nullable": true, "Regex": "", "UserInterface": { "AdvancedFilter": true, "List": true, "CreateOrUpdate": true } }, { "Name": "EffectiveTo", "Type": "DateTime", "MaxLength": 0, "MinLength": 0, "Range": { "IsRangeSet": false, "MinimumValue": 0, "MaximumValue": 0 }, "Required": false, "Nullable": true, "Regex": "", "UserInterface": { "AdvancedFilter": true, "List": true, "CreateOrUpdate": true } } ], "NavigationProperties": [ { "Namespace": "AdamP.Contracts", "ForeignEntityName": "ContractProduct", "IdType": "int", "IsNullable": false, "PropertyName": "ContractProductId", "DisplayPropertyName": "SupplierRef", "DuplicationNumber": 0, "RelationType": "single" }, { "Namespace": "AdamP.Contracts", "ForeignEntityName": "ContractStatusHistory", "IdType": "int", "IsNullable": true, "PropertyName": "ContractStatusHistoryId", "DisplayPropertyName": "Description", "DuplicationNumber": 0, "RelationType": "single" }, { "Namespace": "AdamP.ProductLines", "ForeignEntityName": "ProductLine", "IdType": "int", "IsNullable": false, "PropertyName": "ProductLineId", "DisplayPropertyName": "Description", "DuplicationNumber": 0, "RelationType": "single" } ], "EnumDefinitions": [] }

    Here is one of the entities config.

    On the client side (angular) we get angular components auto generated.

    If the DateTime is set as nullable in the Rad tool the following gets generated:

    //.ts effectiveTo: Date;

    save(): void { this.saving = true; if (this.effectiveTo) { if (!this.test.effectiveTo) { this.test.effectiveTo = moment(this.effectiveTo).startOf('day'); } else { this.test.effectiveTo = moment(this.effectiveTo); } } else { this.test.effectiveTo = null; }

    //.html

    <div class="form-group"> <label for="Test_EffectiveTo">{{l("EffectiveTo")}}</label> <input class="form-control m-input" type="datetime" bsDatepicker [(ngModel)]="effectiveTo" id="Test_EffectiveTo" name="Test_EffectiveTo"> </div>

    As our dates need to be nulllable how can I achive the above hack without a hack? It seems very complicated to handle nullable dates as Rad Tool generates a Date local variable first and do operation on that variable instead of using Moment. I guess there is a reason behind that decision that I wonder...

  • 0
    ismcagdas created
    Support Team

    Hi @adamphones,

    Sorry for our late reply, we are working on your problem and will inform you tomorrow.

  • 0
    musa.demir created
    Support Team

    It is fixed and merged to Power Tools. That fix will be included in next release. Until release, you can change it manually. Remove date properties, use the data you use on save method. Use datepicker just like that: https://github.com/aspnetzero/aspnet-zero-core/blob/844f192e2fdeca9127da0a60b2a3d0ae2847c219/angular/src/app/admin/demo-ui-components/demo-ui-date-time.component.html#L15-L23

    Here is the current page for your entity:

    create-or-edit-product-modal.component.ts

    import { Component, ViewChild, Injector, Output, EventEmitter} from '@angular/core';
    import { ModalDirective } from 'ngx-bootstrap';
    import { finalize } from 'rxjs/operators';
    import { ProductsServiceProxy, CreateOrEditProductDto } from '@shared/service-proxies/service-proxies';
    import { AppComponentBase } from '@shared/common/app-component-base';
    import * as moment from 'moment';
    
    @Component({
        selector: 'createOrEditProductModal',
        templateUrl: './create-or-edit-product-modal.component.html'
    })
    export class CreateOrEditProductModalComponent extends AppComponentBase {
        @ViewChild('createOrEditModal', { static: true }) modal: ModalDirective;
        @Output() modalSave: EventEmitter<any> = new EventEmitter<any>();
    
        active = false;
        saving = false;
    
        product: CreateOrEditProductDto = new CreateOrEditProductDto();
    
        constructor(
            injector: Injector,
            private _productsServiceProxy: ProductsServiceProxy
        ) {
            super(injector);
        }
        
        show(productId?: number): void {
            if (!productId) {
                this.product = new CreateOrEditProductDto();
                this.product.id = productId;
                this.product.effectiveFrom = moment().startOf('day');
                this.product.effectiveTo = moment().startOf('day');
                this.active = true;
                this.modal.show();
            } else {
                this._productsServiceProxy.getProductForEdit(productId).subscribe(result => {
                    this.product = result.product;
                    this.active = true;
                    this.modal.show();
                });
            }        
        }
    
        save(): void {
                this.saving = true;			
                this._productsServiceProxy.createOrEdit(this.product)
                 .pipe(finalize(() => { this.saving = false;}))
                 .subscribe(() => {
                    this.notify.info(this.l('SavedSuccessfully'));
                    this.close();
                    this.modalSave.emit(null);
                 });
        }
    
        close(): void {
            this.active = false;
            this.modal.hide();
        }
    }
    
    

    create-or-edit-product-modal.component.html

    <div bsModal #createOrEditModal="bs-modal" class="modal fade" tabindex="-1" role="dialog"
        aria-labelledby="createOrEditModal" aria-hidden="true" [config]="{backdrop: 'static'}">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <form *ngIf="active" #productForm="ngForm" novalidate (ngSubmit)="save()" autocomplete="off">
                    <div class="modal-header">
                        <h4 class="modal-title">
                            <span *ngIf="product.id">{{l("EditProduct")}}</span>
                            <span *ngIf="!product.id">{{l("CreateNewProduct")}}</span>
                        </h4>
                        <button type="button" class="close" (click)="close()" aria-label="Close" [disabled]="saving">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <label for="Product_EffectiveFrom">{{l("EffectiveFrom")}}</label>
                            <input class="form-control m-input" type="datetime" bsDatepicker datePickerMomentModifier
                                [(date)]="product.effectiveFrom" id="Product_EffectiveFrom" name="Product_EffectiveFrom">
                        </div>
                        <div class="form-group">
                            <label for="Product_EffectiveTo">{{l("EffectiveTo")}}</label>
                            <input class="form-control m-input" type="datetime" bsDatepicker datePickerMomentModifier
                                [(date)]="product.effectiveTo" id="Product_EffectiveTo" name="Product_EffectiveTo">
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button [disabled]="saving" type="button" class="btn btn-default"
                            (click)="close()">{{l("Cancel")}}</button>
                        <button type="submit" class="btn btn-primary blue" [disabled]="!productForm.form.valid"
                            [buttonBusy]="saving" [busyText]="l('SavingWithThreeDot')"><i class="fa fa-save"></i>
                            <span>{{l("Save")}}</span></button>
                    </div>
                </form>
            </div>
        </div>
    </div>
    
  • 0
    musa.demir created
    Support Team

    It seems very complicated to handle nullable dates as Rad Tool generates a Date local variable first and do operation on that variable instead of using Moment. I guess there is a reason behind that decision that I wonder

    Datepicker does not work with moment. We fixed in 7.2.0 with datePickerMomentModifier but missed to change that part of Power Tools. Sorry about that.

  • 0
    adamphones created

    Nope! <div class="form-group"> <label for="Product_DateTo">{{l("DateTo")}} *</label> <input required class="form-control m-input" type="datetime" [bsConfig]="{dateInputFormat: 'MM/YYYY', adaptivePosition: true}" (onShown)="onOpenCalendar($event)" bsDatepicker datePickerMomentModifier [(date)]="product.dateTo" id="Product_DateTo" name="Product_DateTo"> </div>

    This a month selection field. When 05/2020 is selected that date (2020-05-01) should be sent back as payload. Not taking into account of user location time zone and changing it to utc with toIsoString(). As a result the date ends up in the server as "2020-04-30".

    I understand that this could be an issue when the user selects times. But what is the intention here is to allow user to select a month or even a specific date. Ignore the time completely. So what ever the user selects that is the date or month should be sent back to the server by ignoring the timezone of the user.

  • 0
    ismcagdas created
    Support Team

    Hi @adamphones

    Sorry I didn't saw you have mentioned using Clock.Provider = ClockProviders.Utc;. In that case, all dates sent from client to server will be converted to UTC. If you send the date to server in 2020-05-01T00:00:000Z format, it will not be modified. So, you must specify that the date you send is already in UTC format.

    You can do it like this using moment;

    moment("2020-05").format(moment.defaultFormatUtc)

  • 0
    adamphones created

    Hi Ismail,

    Thank you coming back.

    AFAIK that conversion is happening in the service-proxies.ts and I don't think I should touch that at all... Can you please be more specific as this causing us big issue.

    html:

    <div class="form-group"> <label for="DateTo">{{l("DateTo")}} *</label> <input required class="form-control m-input" type="datetime" [bsConfig]="{format: 'YYYY-MM-DD', dateInputFormat: 'YYYY-MM', adaptivePosition: true}" (onShown)="onOpenCalendar($event)" bsDatepicker datePickerMomentModifier [(date)]="product.dateTo" id="DateTo" name="DateTo"> </div> ts:

    `save(): void { this.saving = true;

    this._productServiceProxy.createOrEdit(this.product) .pipe(finalize(() => { this.saving = false;})) .subscribe(() => { this.notify.info(this.l('SavedSuccessfully')); this.close(); this.modalSave.emit(null); });} In service-proxies.ts file (which generated by nswag) I can see the following:

    toJSON(data?: any) { data = typeof data === 'object' ? data : {}; data["description"] = this.description; data["dateTo"] = this.dateTo ? this.dateTo.toISOString() : &lt;any&gt;undefined; data["postedDate"] = this.postedDate ? this.postedDate.toISOString() : &lt;any&gt;undefined; data["id"] = this.id; return data; }

    When DateTo field selected as "2020-05" and save button is hit. the payload becomes as below due the fact that client time zone is GMT+1 and  toISOString() in the service-proxies.ts converts that to utc time.

    <br> Request Payload:

    So what do we change to make sure the date goes back as selected date instead of taking into account time zone?

  • 0
    ismcagdas created
    Support Team

    Hi,

    I can thin of two options;

    1. set the related date field right before calling this._productServiceProxy.createOrEdit(this.product) in your page.
    this.product.dateTo = moment(moment("2020-05").format(moment.defaultFormatUtc));
    this._productServiceProxy.createOrEdit(this.product)
    
    1. You can stop using ClockProviders.Utc if you don't want AspNet Zero to modify your dates between server-client communication.
  • 0
    adamphones created

    I tried option 2 already and i didn't see any change that dates still gets converted to utc. Is there any process that I should be running to prevent nswag to generate toISOString() after commenting out ClockProviders.Utc ?

  • 0
    adamphones created

    Neither of the solution works.

    this.product.dateTo type is moment.Moment. I cannot have operation without wrapping it in a moment. when I call this.product.dateTo.utc() I get excecption as "utc is not a function".

    Then when I wrap it in a moment as below:

    moment(this.product.dateTo)

    As soon as I wrap it in moment then the date becomes previous day.

    I tried to remove ClockProviders.Utc as well with no success.

    console.log(this.product.dateTo); console.log(moment(this.product.dateTo).utc(true).format()); console.log(moment(this.product.dateTo).utcOffset(0, false).format()); console.log(moment(this.product.dateTo).utc(true).format(moment.defaultFormatUtc)); console.log(moment(this.product.dateTo).format(moment.defaultFormatUtc));

    here is the output

    Wed Apr 01 2020 00:00:00 GMT+0100 (British Summer Time) 2020-03-31T23:00:00Z 2020-03-31T23:00:00Z 2020-03-31T23:00:00Z 2020-03-31T23:00:00Z

    We really need a fix for this...

  • 0
    ismcagdas created
    Support Team

    Hi,

    We override toIsoString here https://github.com/aspnetzero/aspnet-zero-core/blob/dev/angular/src/AppPreBootstrap.ts#L201. Maybe you can modify this part to achieve what you want. But, this code is not used if you don't use UtcClockProvider.

    Can you share your project with info@aspnetzero.com if you can't solve this problem ? If you can, please explain steps to reproduce the problem for us to test.

    Thanks,

  • 0
    adamphones created

    Hi Ismail,

    The poject is quite big to send by email. However, it is very simple to reproduce the issue on your side as well. If you use RAD tool to add an entity (product) with a property DateTo (DateTime not null) then change your timezone on your machine to : "British Summer Time". Then try to add a product with datetime selected "01/05/2020". Once you hit the save then the date gets saved in the database as "2020-04-30 23:00:00.0000000". I don't want that. The date should be saved in the database as it is selected by not taking into account user timezone. it should just ignore client time zone and it should treat the selected date as UTC time and save it as it is.

    I tried to add "Clock.Provider = ClockProviders.Utc;" in the core project but with no success.

    Note that the screenshot are taken from freshly downloaded demo project from aspzero and only added Product Entity by using the Rad Tool.

  • 0
    ismcagdas created
    Support Team

    Hi @adamphones

    Thank you for the explanation. We are working on this and inform you soon.

  • 0
    musa.demir created
    Support Team

    I could not reproduce it. Are you sure you set clock provider to utc ? You can set it in *.CoreModule's PreInitialize method. Clock.Provider = ClockProviders.Utc;

  • 0
    adamphones created

    Hi @demirmusa,

    I wish you tried the date "01 May 2020"... And I am hoping that you also changed your time zone to British summer time as in the issue described? If not please also see the gif below..

  • 0
    musa.demir created
    Support Team

    I could not reproduce it. Can you please share recurring project to info@aspnetzero.com

  • 0
    adamphones created

    Hi @demirmusa,

    I sent the project to the email. Can you please have a look at this issue? It is causing us a lot of trouble...

    Regards,

  • 0
    ismcagdas created
    Support Team

    Hi,

    Could you change configureMoment in AppPreBootstrap.ts like below;

    private static configureMoment() {
    	moment.locale(new LocaleMappingService().map('moment', abp.localization.currentLanguage.name));
    	(window as any).moment.locale(new LocaleMappingService().map('moment', abp.localization.currentLanguage.name));
    
    	if (abp.clock.provider.supportsMultipleTimezone) {
    		moment.tz.setDefault(abp.timing.timeZoneInfo.iana.timeZoneId);
    		(window as any).moment.tz.setDefault(abp.timing.timeZoneInfo.iana.timeZoneId);
    	} else {
    		moment.fn.toJSON = function () {
    			return this.locale('en').format();
    		};
    		moment.fn.toISOString = function () {
    			return this.locale('en').format();
    		};
    
    		Date.prototype.toISOString = function () {
    			return moment(this).locale('en').format();
    		};
    	}
    }
    
  • 0
    adamphones created

    Hi Ismail,

    I am not sure what I am missing here and also wondering whether no one else expereineced this issue...

    Sure you can agree with me that time is crucial for systems.

    I will put it in this way: Here are what we are trying to achive:

    Rule 1 : Capture any datetime (for audit purposes creation time modification time etc) in UTC time regardless of users timezone. (I believe this is achieved by setting the Clock.Provider = ClockProviders.Utc; in core module. Rule 2 : If user selects a Date ( no time just the date or even a month selection) that date should be sent back to the server as it is without converting the date to utc. If user selects a date as 2020-05-01 and the user in British Summer Time zone that date must NOT be sent back to the server as 2020-04-30T11:00. This is wrong!

    What we tried:

    As we want the capture the times in UTC we anable Clock.Provider = ClockProviders.Utc;. Then you have a selection appears under Settings. What that option should be set to? default is UTC. But do you think we should set that to Servers time zone? I tried both GMT and UTC. Both gives me the same result. What is the purpose of this option and how it should be used?

    I tried your code in AppPreBootstrap.ts and nothing has changed.

    The only way I could make the code work as I wanted is as below (I have to say that I think there must be a better way for step 3)

    1- Set: Clock.Provider = ClockProviders.Utc; in core mode 2- Change default time zone under "Settings" to GMT time zone 3- Change client side code before saving as this.product.dateTo = moment(moment(this.product.dateTo).format(moment.defaultFormatUtc)); (Not sure why I have to wrap this.product.dateTo in a 'moment' even though the type of it is moment.Moment. Without wrapping you can't call format..).

    I just don't want to move on because I found a solution that works for me. I'd better understand why it needs the step 3? and how can we overcome that part.

  • 0
    ismcagdas created
    Support Team

    Hi @adamphones

    I think this is a braeking change with the latest version of moment or nswag, I couldn't find which one yet. Below code block in your project was responsible for formatting date values;

    moment.fn.toISOString = function () {
    			return this.locale('en').format();
    		};
    

    But, this is not working anymore. Instead, below line worked for me;

    Date.prototype.toISOString = function () {
    			return moment(this).locale('en').format();
    		};