AngularJS: Creating a directive for converting a string to a Date on the fly - angular-ui-bootstrap

Angular UI Bootstrap changed the way of what the datepicker expects as ng-model in some version after 1.13.0. Before it was fine to give it an ISO date string, now it wants a Date object.
I consume ISO date strings from my API though, so I have to
convert them into Date objects before giving it to the datepicker and
convert them back to an ISO date string when storing it.
In the past I used a directive like this:
function DateObjectDirective() {
const directive = {
restrict: "A",
require: ["ngModel"],
link(scope, element, attributes, controllers) {
const ngModel = controllers[0];
ngModel.$formatters.unshift(value => {
let output = null;
if(value) {
output = moment(value).toDate();
}
return output;
});
ngModel.$parsers.unshift(value => {
let output = null;
if(value) {
output = moment(value).format();
}
return output;
});
},
};
return directive;
}
This no longer works though, as the following error is reported:
this.activeDate.getFullYear is not a function
My guess is that the datepicker still uses the string as reference. Is there any other way I can convert before giving my data to the datepicker?

I found out that the directive I posted does indeed still work. The only problem was the order in which AngularJS evaluated the directives.
For example:
<input ng-model="someDateString" uib-datepicker-popup="yyyy-MM-dd" woo-date-object>
In my case, woo-date-object was always evaluated before uib-datepicker-popup. The result was that the datepicker has always pushed its own formatter on top of ngModel.$formatters, thus eliminating the possibility for me to intervene.
The solution is to give the own directive a higher priority. UI's datepicker doesn't have one set, so anything above 0 (which is the default) works:
{
restrict: "A",
require: "ngModel",
priority: 9999,
link(scope, element, attributes, ngModel) {
ngModel.$formatters.push(value => {
let output = new Date();
if(value) { output = moment(value).toDate(); }
return output;
});
ngModel.$parsers.push(value => {
let output = null;
if(value) { output = moment(value).format(); }
return output;
});
},
}

Related

Validate raw data from the field in react-final-form

I have a field in react-final-form where the user enters a date. For the internal value this gets normalized to YYYY-MM-DD but the user may enter it as DD.MM.YYYY.
For valid data this is all fine, I can use parse to normalize and format to convert back.
However, if a user enters garbage, there's not much I can do in parse... I ended up doing this awful hack which works, but I wonder if there's a cleaner way that allows me to separate the parsed data that will be fed into the form values, and the data that will be used to display the component and validate the user input.
const formatDate = (value) => {
// console.log(`format: ${value}`);
if (!value) {
return '';
}
// something invalid => keep as-is
if (value.startsWith('INVALID:')) {
return value.substr(8);
}
// we have a valid value => format using our preferred display format
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
return `${m[3]}.${m[2]}.${m[1]}`;
};
const parseDate = (value) => {
if (!value) {
return undefined;
}
// console.log(`parse: ${value}`);
const m = value.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
if (m) {
return `${m[3]}-${m[2]}-${m[1]}`;
}
return 'INVALID:' + value;
};
const validateDate = (value) => {
// console.log(`validate: ${value}`);
if (value && value.startsWith('INVALID:')) {
return 'Invalid date';
}
};
<Field
name="date"
component="input"
type="text"
format={formatDate}
parse={parseDate}
validate={validateDate}
placeholder="dd.mm.yyyy"
/>
Here's an executable codesandbox: https://codesandbox.io/s/react-final-form-format-on-blur-example-forked-5oowz?file=/src/index.js
Note: I'm NOT looking for date pickers or similar widgets that would rely on the field not being directly editable.
Another kind of field where the current behavior feels a bit lacking is for number inputs:
If i parse them to an actual number, I can no longer distinguish null/empty because the field is empty (valid) or because the field contains garbage (invalid)
I can kind of work around this if he field is required (empty is invalid as well), but otherwise I'd once again need a hack like above...
You may keep both raw and parsed strings as a value for the field:
const formatDate = (value) => {
// console.log(`format: ${value}`);
if (!value) {
return "";
}
// something invalid => keep as-is
if (!value.parsed) {
return value.raw;
}
// we have a valid value => format using our preferred display format
const m = value.parsed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
return `${m[3]}.${m[2]}.${m[1]}`;
};
const parseDate = (value) => {
if (!value) {
return undefined;
}
// console.log(`parse: ${value}`);
const m = value.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
if (m) {
return { parsed: `${m[3]}-${m[2]}-${m[1]}`, raw: value };
}
return { parsed: null, raw: value };
};
const validateDate = (value) => {
// console.log(`validate: ${value}`);
if (value && !value.parsed) {
return "Invalid date";
}
};
So the value of the field is actually an object of shape { raw:string, parsed:string}. When parsed is empty means the date is invalid.

Run custom JavaScript in Chromium with CodeceptJS?

I need to mock the time for my CodeceptJS tests.
My React component uses the new Date() function:
const Component = () => {
console.log(new Date())
return <h1>Im a component</h1>
}
I need the component to think it's 2018. For my Jest unit tests this was straightforward:
import MockDate from 'mockdate';
MockDate.set('2018-10');
test("test something", ()=>{
// Actual test here
})
MockDate.reset();
How can I do the same with CodeceptJS? Ive tried using the date mocking module in the test:
Scenario('#test', async (CheckoutPage) => {
const MockDate = require('mockdate');
MockDate.set('2018-10');
// Actual test here
});
I also tried dependancy injection. The code within FIX-DATE monkey patches the date:
Scenario(
'#test',
(CheckoutPage, FixDate) => {
FixDate();
CheckoutPage.load();
pause();
}
).injectDependencies({ FixDate: require('./FIX-DATE') });
Neither of these have any affect on the date.
The issue is that CodeceptJS is running inside the browser, so you need to override date object of the browser.
Basically you need to override the Date Object of the browser, or the function that is used, for Example:
// create a date object for this Friday:
var d = new Date(2018, 0, 20);
//override Date constructor so all newly constructed dates return this Friday
Date = function(){return d;};
var now = new Date()
console.log(now);
Date.now = function () { return d};
console.log(Date.now());
This is the way to do that in pure JS, the second step is to integrate into codeceptjs, and this can be done using I.executeScript
for Example:
I.executeScript(function () {
var d = new Date(2018, 0, 20);
Date = function(){return d;};
})
You can also create a custom step, for example, I.fakeDate(new Date(2018, 0, 20))
module.exports = function() {
return actor({
fakeDate: function(date) {
I.executeScript(function (fakeDate) {
var d = fakeDate;
window.__original_date = Date;
Date = function(){return d;};
}, date);
},
fakeDateRestore: function() {
I.executeScript(function () {
Date = window.__original_date;
});
}
});
}
Then you just Fake the date when you need, and restore it.
I.Click('beofre');
I.fakeDate(new Date(2018,10,01));
// The test code here
I.fakeDateRestore();
Hope this helps #:-)

Kendo DateTimePicker format HHmm

I have a kendo datetimepicker control and when a user manually types in an incorrect format missing the colon in the time, the validation does not catch this and in the MVC controller, the models property has a null date/time.
My client side validation is able to parse 21/01/2015 1230 but by the time it reaches the model in the controller server side its null, as it cannot map and parse the datetime.
Some console.log output of the value input and kendo.parseDate's effort.
21/01/2015 0000
Wed Jan 21 2015 00:00:00 GMT+0000 (GMT Standard Time)
Here's my client-side validation below.
So how can I force the validation to work client-side?
$("#accidentForm").kendoValidator({
rules: {
date: function (input) {
if (input.is("[name=Accident.IncidentDate]")) {
console.log(input.val());
var d = kendo.parseDate(input.val());
console.log(d);
return d instanceof Date;
}
return true;
}
},
messages: {
customRuleDateTimePick: "Incident date time format incorrect."
}
});
// attach a validator to the container and get a reference
var validatable = $("#accidentForm").kendoValidator().data("kendoValidator");
$("#btnSave").click(function () {
//validate the input elements and check if there are any errors
if (validatable.validate() === false) {
// get the errors and write them out to the "errors" html container
var errors = validatable.errors();
$(errors).each(function () {
$("#errors").html(this);
});
return false;
}
return true;
});
OK specifying a format and culture parameter on the kendo.parseDate seems to help and stops the post to the server until a valid date AND time is input.
$("#accidentForm").kendoValidator({
rules: {
date: function (input) {
if (input.is("[name=Accident.IncidentDate]")) {
var d = kendo.parseDate(input.val(), ["dd/MM/yyyy HH:mm"], 'en-GB');
return d instanceof Date;
}
return true;
}
},
messages: {
date: "Incident date time format incorrect."
}
});
Another alternative, simpler I think, was to extend the validator methods for date types. This then picked up the MVC error message attributes, so I get to use the right resource.resx file rather than hard code my error text.
<script type="text/javascript">
$(function () {
$.validator.methods.date = function (value, element) {
// Custom validation required for DateTimePicker, so use kendo.parseDate as it works better than jquery unobtrusive validation.
return this.optional(element) || kendo.parseDate(value, ["dd/MM/yyyy HH:mm"], 'en-GB') !== null;
}
});
</script>

Full-featured autocomplete widget for Dojo

As of now (Dojo 1.9.2) I haven't been able to find a Dojo autocomplete widget that would satisfy all of the following (typical) requirements:
Only executes a query to the server when a predefined number of characters have been entered (without this, big datasets should not be queried)
Does not require a full REST service on the server, only a URL which can be parametrized with a search term and simply returns JSON objects containing an ID and a label to display (so the data-query to the database can be limited just to the required data fields, not loading full data-entities and use only one field thereafter)
Has a configurable time-delay between the key-releases and the start of the server-query (without this excessive number of queries are fired against the server)
Capable of recognizing when there is no need for a new server-query (since the previously executed query is more generic than the current one would be).
Dropdown-stlye (has GUI elements indicating that this is a selector field)
I have created a draft solution (see below), please advise if you have a simpler, better solution to the above requirements with Dojo > 1.9.
The AutoComplete widget as a Dojo AMD module (placed into /gefc/dijit/AutoComplete.js according to AMD rules):
//
// AutoComplete style widget which works together with an ItemFileReadStore
//
// It will re-query the server whenever necessary.
//
define([
"dojo/_base/declare",
"dijit/form/FilteringSelect"
],
function(declare, _FilteringSelect) {
return declare(
[_FilteringSelect], {
// minimum number of input characters to trigger search
minKeyCount: 2,
// the term for which we have queried the server for the last time
lastServerQueryTerm: null,
// The query URL which will be set on the store when a server query
// is needed
queryURL: null,
//------------------------------------------------------------------------
postCreate: function() {
this.inherited(arguments);
// Setting defaults
if (this.searchDelay == null)
this.searchDelay = 500;
if (this.searchAttr == null)
this.searchAttr = "label";
if (this.autoComplete == null)
this.autoComplete = true;
if (this.minKeyCount == null)
this.minKeyCount = 2;
},
escapeRegExp: function (str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
replaceAll: function (find, replace, str) {
return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
},
startsWith: function (longStr, shortStr) {
return (longStr.match("^" + shortStr) == shortStr)
},
// override search method, count the input length
_startSearch: function (/*String*/ key) {
// If there is not enough text entered, we won't start querying
if (!key || key.length < this.minKeyCount) {
this.closeDropDown();
return;
}
// Deciding if the server needs to be queried
var serverQueryNeeded = false;
if (this.lastServerQueryTerm == null)
serverQueryNeeded = true;
else if (!this.startsWith(key, this.lastServerQueryTerm)) {
// the key does not start with the server queryterm
serverQueryNeeded = true;
}
if (serverQueryNeeded) {
// Creating a query url templated with the autocomplete term
var url = this.replaceAll('${autoCompleteTerm}', key, this.queryURL);
this.store.url = url
// We need to close the store in order to allow the FilteringSelect
// to re-open it with the new query term
this.store.close();
this.lastServerQueryTerm = key;
}
// Calling the super start search
this.inherited(arguments);
}
}
);
});
Notes:
I included some string functions to make it standalone, these should go to their proper places in your JS library.
The JavaScript embedded into the page which uses teh AutoComplete widget:
require([
"dojo/ready",
"dojo/data/ItemFileReadStore",
"gefc/dijit/AutoComplete",
"dojo/parser"
],
function(ready, ItemFileReadStore, AutoComplete) {
ready(function() {
// The initially displayed data (current value, possibly null)
// This makes it possible that the widget does not fire a query against
// the server immediately after initialization for getting a label for
// its current value
var dt = null;
<g:if test="${tenantInstance.technicalContact != null}">
dt = {identifier:"id", items:[
{id: "${tenantInstance.technicalContact.id}",
label:"${tenantInstance.technicalContact.name}"
}
]};
</g:if>
// If there is no current value, this will have no data
var partnerStore = new ItemFileReadStore(
{ data: dt,
urlPreventCache: true,
clearOnClose: true
}
);
var partnerSelect = new AutoComplete({
id: "technicalContactAC",
name: "technicalContact.id",
value: "${tenantInstance?.technicalContact?.id}",
displayValue: "${tenantInstance?.technicalContact?.name}",
queryURL: '<g:createLink controller="partner"
action="listForAutoComplete"
absolute="true"/>?term=\$\{autoCompleteTerm\}',
store: partnerStore,
searchAttr: "label",
autoComplete: true
},
"technicalContactAC"
);
})
})
Notes:
This is not standalone JavaScript, but generated with Grails on the server side, thus you see <g:if... and other server-side markup in the code). Replace those sections with your own markup.
<g:createLink will result in something like this after server-side page generation: /Limes/partner/listForAutoComplete?term=${autoCompleteTerm}
As of dojo 1.9, I would start by recommending that you replace your ItemFileReadStore by a store from the dojo/store package.
Then, I think dijit/form/FilteringSelect already has the features you need.
Given your requirement to avoid a server round-trip at the initial page startup, I would setup 2 different stores :
a dojo/store/Memory that would handle your initial data.
a dojo/store/JsonRest that queries your controller on subsequent requests.
Then, to avoid querying the server at each keystroke, set the FilteringSelect's intermediateChanges property to false, and implement your logic in the onChange extension point.
For the requirement of triggering the server call after a delay, implement that in the onChange as well. In the following example I did a simple setTimeout, but you should consider writing a better debounce method. See this blog post and the utility functions of dgrid.
I would do this in your GSP page :
require(["dojo/store/Memory", "dojo/store/JsonRest", "dijit/form/FilteringSelect", "dojo/_base/lang"],
function(Memory, JsonRest, FilteringSelect, lang) {
var initialPartnerStore = undefined;
<g:if test="${tenantInstance.technicalContact != null}">
dt = {identifier:"id", items:[
{id: "${tenantInstance.technicalContact.id}",
label:"${tenantInstance.technicalContact.name}"
}
]};
initialPartnerStore = new Memory({
data : dt
});
</g:if>
var partnerStore = new JsonRest({
target : '<g:createLink controller="partner" action="listForAutoComplete" absolute="true"/>',
});
var queryDelay = 500;
var select = new FilteringSelect({
id: "technicalContactAC",
name: "technicalContact.id",
value: "${tenantInstance?.technicalContact?.id}",
displayValue: "${tenantInstance?.technicalContact?.name}",
store: initialPartnerStore ? initialPartnerStore : partnerStore,
query : { term : ${autoCompleteTerm} },
searchAttr: "label",
autoComplete: true,
intermediateChanges : false,
onChange : function(newValue) {
// Change to the JsonRest store to query the server
if (this.store !== partnerStore) {
this.set("store", partnerStore);
}
// Only query after your desired delay
setTimeout(lang.hitch(this, function(){
this.set('query', { term : newValue }
}), queryDelay);
}
}).startup();
});
This code is untested, but you get the idea...

Set Umbraco Property Editor Input to jQueryUI Datepicker

I'm close but still can't quite get this to work.
I have a new custom property editor that is loading correctly and is doing almost everything expected until I try to set the text field to be a jQuery UI element.
As soon as I add a directive in Angular for setting it to call the jQuery UI datepicker function, I get the following error suggesting it hasn't loaded the jQueryUI script library correctly:
TypeError: Object [object Object] has no method 'datepicker'
Trouble is, I can't see where I should be adding it as the logical places (to my mind, at least) seem to make no difference. Here is the code in full:
function MultipleDatePickerController($scope, assetsService) {
//tell the assetsService to load the markdown.editor libs from the markdown editors
//plugin folder
//assetsService
// .load([
// "http://code.jquery.com/ui/1.10.4/jquery-ui.min.js"
// ])
// .then(function () {
// //this function will execute when all dependencies have loaded
// });
//load the seperat css for the editor to avoid it blocking our js loading
assetsService.loadCss("/css/jquery-ui.custom.min.css");
if (!$scope.model.value) {
$scope.model.value = [];
}
//add any fields that there isn't values for
//if ($scope.model.config.min > 0) {
if ($scope.model.value.length > 0) {
for (var i = 0; i < $scope.model.value.length; i++) {
if ((i + 1) > $scope.model.value.length) {
$scope.model.value.push({ value: "" });
}
}
}
$scope.add = function () {
//if ($scope.model.config.max <= 0 || $scope.model.value.length < $scope.model.config.max) {
if ($scope.model.value.length <= 52) {
$scope.model.value.push({ value: "" });
}
};
$scope.remove = function (index) {
var remainder = [];
for (var x = 0; x < $scope.model.value.length; x++) {
if (x !== index) {
remainder.push($scope.model.value[x]);
}
}
$scope.model.value = remainder;
};
}
var datePicker = angular.module("umbraco").controller("AcuIT.MultidateController", MultipleDatePickerController);
datePicker.directive('jqdatepicker', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
$(function () {
element.datepicker({
dateFormat: 'dd/mm/yy',
onSelect: function (date) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(date);
});
}
});
});
}
}
});
I faced the same problem when adapting a jQuery Date Range Picker for my Date Range Picker package for Umbraco 7. It's frustrating! The problem (I think) is that Angular's ng-model listens for "input" changes to trigger events and so doesn't pick up on a jQuery triggered event.
The way around it I found was to force the input event of the element you wish to update to fire manually, using jQuery's .trigger() event.
For example, the date picker I was using had this code for when a date was changed:
updateInputText: function () {
if (this.element.is('input')) {
this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format));
}
},
I just adapted it to force an input trigger by adding this.element.trigger('input') to the code block, so it now reads:
updateInputText: function () {
if (this.element.is('input')) {
this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format));
this.element.trigger('input');
}
},
This forces Angular to "see" the change and then ng-model is updated. There may well be a more elegant way (as I'm an Angular newbie), but I know this worked for me.
Got it. This is probably a bit of a hack, but it's simple and effective so it's a win nonetheless.
The assetsService call is the key, where I've put code into the deferred .then statement to call jQueryUI's datepicker on any item that has the "jqdp" CSS class:
//tell the assetsService to load the markdown.editor libs from the markdown editors
//plugin folder
assetsService
.load([
"/App_Plugins/Multidate/jquery-ui.min.js"
])
.then(function () {
//this function will execute when all dependencies have loaded
$('.jqdp').datepicker({ dateFormat: 'dd/mm/yy' });
});
I've then gone and added that class to my view:
<input type="text" jqdatepicker name="item_{{$index}}" ng-model="item.value" class="jqdp" id="dp-{{model.alias}}-{{$index}}" />
Finally, I've added a directive to ensure that dynamically-added items also display a datepicker:
datePicker.directive('jqdatepicker', function () {
return function (scope, element, attrs) {
scope.$watch("jqdatepicker", function () {
try{
$(element).datepicker({ dateFormat: 'dd/mm/yy' });
}
catch(e)
{}
});
};
});
As I said, this is possibly a bit hacky but it achieves the right result and seems like a simple solution.

Resources