I have two inputs where the user can select two dates, These two dates allow the user to select if a product is new during those two dates. I want to validate these two fields using Vuelidate and would like to compare the two dates and more to do so. But I can't seem to make it work.
The validation I am trying to achieve:
New_from field
cannot be lower than today and cannot be higher than the New_to field (Because that will reverse the order of the fields)
New_to
field cannot be lower than the value of new_from
What I tried:
validations: {
fields: {
newFrom: {
required: requiredIf(function() {
return this.fields.newTo
}),
minValue: minValue(new Date()), // Make the minimal value today
maxValue: this.newTo ? this.newTo : null
},
newTo: {
required: requiredIf(function() {
return this.fields.newFrom
}),
minValue: this.fields.newFrom, // But this does not work
},
},
}
HTML
<div class="w-full flex-column">
<input v-model.trim="$v.fields.newFrom.$model" type="date" name="new_from" id="new_from" v-on:change="alert('test')" :class="{'border-red-600': submitted && !$v.fields.newFrom.required}" class="appearance-none block border border-gray-200 p-2 rounded-md w-full shadow-sm focus:border-indigo-500 focus:outline-none" placeholder="">
<p class="error text-red-600 my-3" v-if="submitted && !$v.fields.newFrom.required">New from is required!</p>
<p class="error text-red-600 my-3" v-if="submitted && !$v.fields.newFrom.minValue">New from cannot be lower than today</p>
<p class="error text-red-600 my-3" v-if="submitted && !$v.fields.newFrom.maxValue">New from cannot be higher than new_to</p>
</div>
<div class="mx-3 flex items-center justify-center">
<p class="text-gray-900 font-medium">To</p>
</div>
<div class="w-full flex-column">
<input v-model.trim="$v.fields.newTo.$model" type="date" name="new_to" id="new_to" :class="{'border-red-600': submitted && !$v.fields.newTo.required}" class="appearance-none block border border-gray-200 p-2 rounded-md w-full shadow-sm focus:border-indigo-500 focus:outline-none" placeholder="">
<p class="error text-red-600 my-3" v-if="submitted && !$v.fields.newTo.required">New to is required!</p>
<p class="error text-red-600 my-3" v-if="submitted && !$v.fields.newTo.minValue">New to cannot be lower than new_from!</p>
</div>
How could I make this work? Could a package such as Moment.js be useful in this case?
You can check custom validators as it helped me about vuelidate.
You can use pre-defined required validation from vuelidate using
import { required } from "vuelidate/lib/validators";
...
validations: {
fields: {
newFrom: {
required,
minValue(val) {
return new Date(val) > new Date();
},
maxValue(val, {newTo}){
return new Date(newTo) > new Date(val);
}
},
newTo: {
required,
minValue(val, { newFrom }) {
return new Date(val) > new Date(newFrom);
},
},
},
There might be better ways to define your logic on date comparisons , but I tried to stick to your point of view.
Moment.js might be an overkill for this, since you can do basic date comparison. Otherwise there is a moment plugin moment-range you can also use. However , I strongly believe that you have to keep it simple as much as you can for starters.
Related
As you can this is a string js and I want to use onClick on the input whose view is being toggled by selecting the boolean value edit. Please suggest how to use onchnage here.
Already tried normal HTML onchange (not working)
onchange="${onchnage}"
Pls, suggest if you happen to know the answer.
export const DefaultNode = (d, selectedNodeIds, edit, fomatOptions, inputOnclick) => {
const mainData = d.data.data
return `<div style='background:${selectedNodeIds.length!==0 ? (selectedNodeIds.includes(d.data.id) ? `rgba(${ fomatOptions.nodeBg.r }, ${ fomatOptions.nodeBg.g }, ${ fomatOptions.nodeBg.b }, ${ fomatOptions.nodeBg.a })`: "#fff"): `rgba(${ fomatOptions.nodeBg.r }, ${ fomatOptions.nodeBg.g }, ${ fomatOptions.nodeBg.b }, ${ fomatOptions.nodeBg.a })`};
color:${selectedNodeIds.length!==0 ?(selectedNodeIds.includes(d.data.id) ?`rgba(${ fomatOptions.textColor.r }, ${ fomatOptions.textColor.g }, ${ fomatOptions.textColor.b }, ${ fomatOptions.textColor.a })`:'#000'): `rgba(${ fomatOptions.textColor.r }, ${ fomatOptions.textColor.g }, ${ fomatOptions.textColor.b }, ${ fomatOptions.textColor.a })`}'
class=${`"w-[250px] p-3 rounded-[15px] relative border-[3px] h-[140px] ${selectedNodeIds.includes(d.data.id)? 'drop-shadow-md' :"shadow"} ${ selectedNodeIds.includes(d.data.id) && fomatOptions.fontFamily.value}"`}>
<div class='flex justify-between w-full '>
<div class="">
${edit? `<input onclick='${inputOnclick}' class="fullName text-[13px] font-semibold" value="${mainData.name}"/>` : `<div class=" text-[13px] font-semibold">${mainData.name} </div>`}
<div class=" text-[11px] opacity-70 mt-0.5 font-medium">${mainData.position } </div>
<div class='mt-2'>
<div class=" text-[11px] opacity-70 mt-0.5 font-medium">${mainData.email } </div>
<div class=" text-[11px] opacity-70 mt-0.5 font-medium">${mainData.phone } </div>
</div>
</div>
<img class='w-10 h-10 mr-2 rounded-[10px]' src=${mainData.imgUrl} />
</div>
<div class='flex pt-4 justify-between items-center'>
<p class='text-[10px] font-medium uppercase bg-theme-gray px-2 text-black rounded-full py-0.5'>${mainData.department}</p>
<p class='text-[10px] font-medium uppercase mr-2'>${mainData.location}</p>
</div>
${((selectedNodeIds.includes(d.data.id))) ? `<div class="absolute left-4 -top-5 font-semibold text-[10px] p-1 bg-gray-400 text-white rounded-t-md">
Selected
</div>`: `<p></p>`}
</div>`
}
I believe what you're missing here is the different naming conventions for default HTML event listeners in React, not all of your code is here so I'm assuming you do not have a custom function called onchange, but in React its called onChange (or onClick, etc) so you're looking something like this for your code snippet.
onChange="${onchnage}"
Also double-check to make sure you have all your syntax and spelling correct. Also for writing better JSX for returning HTML elements you can write code like the following
return (
<div>
<p>Text here</p>
</div>
);
I have a modal window for filters on my application. The filters modal has #click.outside="filters = false" so if the user clicks outside of the modal it will hide. Inside of that filters modal I have an option for choosing the minimum date for which I'm using Flatpickr.
The problem is when you click on the arrows to change the month - or the month or year at the top - the filters modal will hide.
I believe that I need to use e.stopPropagation or #click.prevent on the element but it hasn't worked in each spot that I've tried it.
How do I make it so that any clicks inside of the Flatpicker window doesn't propagate up and close the filters modal?
Here is my full code -
<div x-show="filters" #click.outside="filters = false" x-on:keydown.escape.window="filters = false" class="absolute shadow-lg z-40 mt-4">
<div x-trap="filters">
<div>
<label for="filter-date-min" class="block text-sm font-semibold leading-5 text-gray-700">Minimum Date</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div x-data="{
value: '',
init() {
let picker = flatpickr(this.$refs.picker, {
dateFormat: 'Y-m-d',
defaultDate: this.value,
onChange: (date, dateString) => {
this.value = dateString
},
})
},
}">
<input id="filter-date-min" placeholder="MM/DD/YYYY" x-ref="picker" x-bind:value="value" class="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 border active" autocomplete="off">
</div>
</div>
</div>
</div>
</div>
A simple solution for this issue is to introduce a pickerOpen variable that monitors the Flatpickr popup's state via the onOpen and onClose hooks. Then only close the modal window when Flatkpickr popup is inactive.
<div x-data="{filters: false, pickerOpen: false}">
<div x-show="filters"
#click.outside="if (!pickerOpen) {filters = false}"
x-on:keydown.escape.window="if (!pickerOpen) {filters = false}"
class="absolute shadow-lg z-40 mt-4">
<div x-trap="filters">
<div>
<label for="filter-date-min" class="block text-sm font-semibold leading-5 text-gray-700">Minimum Date</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div x-data="{
value: '',
init() {
let picker = flatpickr(this.$refs.picker, {
dateFormat: 'Y-m-d',
defaultDate: this.value,
onOpen: () => {this.pickerOpen = true},
onClose: () => {this.pickerOpen = false},
onChange: (date, dateString) => {
this.value = dateString
},
})
},
}">
<input id="filter-date-min" placeholder="MM/DD/YYYY" x-ref="picker" x-bind:value="value" autocomplete="off"
class="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 border active">
</div>
</div>
</div>
</div>
</div>
</div>
I don't believe AlpineJS is capable of doing this, so you can throw in some custom JS. You can add this code in a script tag right before the closing body tag:
[...document.getElementsByClassName("flatpickr-calendar")].forEach($el => {
$el.addEventListener("click", e => e.stopPropagation());
});
Once the user opens a Flatpickr popup, Flatpickr appends the calendar to the end of the body, not inside your div, so it's an "outside" click.
AlpineJS determines "outside" clicks by adding an event listener to window (or something similar), and when the event bubbles (propagates) up it tests whether the click target is the element that's requesting an outside click. If not, fire an event that there's an "outside" click.
What this code essentially does is, once there is a click on a Flatpickr popup calendar, we prevent the event from bubbling up to AlpineJS, so Alpine doesn't know there was any click on the window at all, thus #click.outside won't trigger when the calendar is clicked.
What I want to accomplish is for the form to change as the user changes the form type from the radio.
Standard basically has 2 selects (one classic and a fancier one, made with ng-select) and custom has a simple classic text input.
I am trying to change the form's functionality dynamically as the form type changes using the radio.
Besides trying to use formBuilder.group, I also tried using .setValidators on the individual inputs, but the result is the same: when I change the radio and the custom_channel_name input is shown i get this console error "Error: Cannot find control with name: 'custom_channel_name'"
What am I doing wrong and how do I properly handle reactive forms in this fashion?
What I have so far looks like this: https://i.imgur.com/n24mKs7.png , https://i.imgur.com/FfCgXFX.png
[ component.html ]
<div>
<form [formGroup]="organizationChannelForm" (ngSubmit)="submitOrganizationChannelsForm()">
<div *ngIf="isChannelTypeStandard" class="grid gap-4 grid-cols-2">
<!-- <form-picker label="Countries" [values]="selectCountriesSources" labelField="name" valueField="warehouse_id"
formControlName="warehouse_id"></form-picker> -->
<div>
<label for="channel_id" class="text-gray-700 dark:text-gray-400 block w-full pb-1 text-sm">Channel</label>
<select [(ngModel)]="selectedChannel" id="channel_id" formControlName="channel_id"
class="block w-full dark:bg-gray-700 dark:text-gray-300 form-input">
<option *ngFor="let channel of selectChannelsSources" [value]="channel">{{ channel }}</option>
</select>
</div>
<div>
<label for="countries_ids" class="text-gray-700 dark:text-gray-400 block w-full pb-1 text-sm">Countries</label>
<ng-select [items]="selectCountriesSources" [(ngModel)]="selectedCountries"
[ngModelOptions]="{ standalone: true }" id="countries_ids" [multiple]="true" bindLabel="name"
name="countries_ids"></ng-select>
</div>
</div>
<div *ngIf="!isChannelTypeStandard">
<label for="custom_channel_name" class="text-gray-700 dark:text-gray-400 block w-full pb-1 text-sm">Custom Channel</label>
<input class="form-input block w-full dark:bg-gray-700 dark:text-gray-300"
type="text" id="custom_channel_name" formControlName="custom_channel_name">
</div>
<div class="flex mt-5">
<div class="flex ml-auto items-center">
<span class="dark:text-gray-400">Channel Type</span>
<div class="ml-6 flex">
<label class="flex items-center cursor-pointer">
<input [(ngModel)]="isChannelTypeStandard" [ngModelOptions]="{ standalone: true }" (change)="updateOrganizationChannelForm($event)"
type="radio" class="form-radio text-purple-600 h-4 w-4" name="channelType" [value]="true">
<span class="dark:text-gray-300 font-medium ml-2">Standard</span>
</label>
<label class="flex items-center cursor-pointer ml-4">
<input [(ngModel)]="isChannelTypeStandard" [ngModelOptions]="{ standalone: true }" (change)="updateOrganizationChannelForm($event)"
type="radio" class="form-radio text-purple-600 h-4 w-4" name="channelType" [value]="false">
<span class="dark:text-gray-300 font-medium ml-2">Custom</span>
</label>
</div>
</div>
<div class="ml-8 min-w-0 text-white flex flex-col items-end rounded-lg shadow-xs">
<button type="submit" aria-label="add" [disabled]="organizationChannelForm.errors || organizationChannelForm.pristine"
class="flex items-end justify-between px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:bg-grey-600">
Assign Channel
<fa-icon class="ml-2" [icon]="icons.plus"></fa-icon>
</button>
</div>
</div>
</form>
</div>
[ component.ts ]
export class OrganizationChannelsComponent implements OnInit {
selectChannelsSources: Array<string> = ["eMag Marketplace", "Vtex", "Shopify", "Magento1", "Magento2", "WooCommerce", "Prestashop", "TeamShare", "Gomag", "Opencart", "MerchantPro", "Cscart", "Allegro", "Idosell", "ChannelAdvisor", "Shoprenter", "Transfer", "Defecte/Defects", "Manual Order"];
selectCountriesSources: Array<Country> = [];
icons = {
close: faTimes,
plus: faPlus
}
organizationChannelForm!: FormGroup;
selectedCountries: Array<Country> = [];
selectedChannel: Channel | undefined;
isChannelTypeStandard: boolean = true;
#Input() organizationId!: ID;
organizationChannels$: Observable<OrganizationChannel[]> = new BehaviorSubject<OrganizationChannel[]>([]);
channels$: Observable<Channel[]> = new BehaviorSubject<Channel[]>([]);
constructor(
private organizationChannelsService: OrganizationsChannelsService,
private organizationChannelsQuery: OrganizationChannelsQuery,
private countriesService: CountriesService,
private toasterService: ToasterService,
private formBuilder: FormBuilder,
) { }
ngOnInit(): void {
this.organizationChannelForm = this.formBuilder.group({
channel_id: ['', Validators.required],
});
this.organizationChannelsService.getOrganizationChannels(this.organizationId).subscribe();
this.organizationChannels$ = this.organizationChannelsQuery.selectOrganizationChannels(this.organizationId as number);
this.countriesService.get().subscribe(countries => this.selectCountriesSources = countries);
}
updateOrganizationChannelForm() {
if (this.isChannelTypeStandard) {
this.organizationChannelForm = this.formBuilder.group({
channel_id: ['', Validators.required],
});
}
else {
this.organizationChannelForm = this.formBuilder.group({
custom_channel_name: [Validators.required, Validators.minLength(8)]
});
}
}
}
Documentation to the rescue! here is the official link to creating dynamic forms:
https://angular.io/guide/reactive-forms#creating-dynamic-forms
basically you need formArray instead of formGroup for all the controls that are going to be conditionally visible on UI, read the docs and if it becomes difficult to understand then let me know I'll create a demo.
I have expanded this code for my own learning abilities. I understand I can definitely shorten this down a LOT but I am trying to learn and expand my frontend experience.
So I have the code below. When localstorage it set to true/false it picks up the right v-if/else section. Now, what I need to do is set the local storage based on button click.
What is the best way to accomplish this?
<div v-if="privateChat == 'false'">
<button type="button">
<a key="privateChat" href="#" class="bg-red-900 text-gray-100 hover:bg-gray-800 hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium">
<ChatIcon class="h-6 w-6 text-white"/>
<span class="pt-2">Private Chat OFF</span>
</a>
</button>
</div>
<div v-else>
<button type="button">
<a key="privateChat" href="#" class="bg-green-900 text-gray-100 hover:bg-gray-800 hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium">
<ChatIcon class="h-6 w-6 text-white"/>
<span class="pt-2">Private Chat ON</span>
</a>
</button>
</div>
<script>
export default {
data() {
return {
privateChat: (localStorage.getItem("privateChat") === 'true') ? 'true' : 'false',
}
},
methods: {
clickPrivateChat (value) {
this.privateChat = value === true ? "true" : "false";
localStorage.setItem("privateChat", value);
},
setup() {
const enabled = ref(privateChat)
let value = localStorage.getItem("privateChat");
let privateChat = (value === 'true');
}
</script>
There are several improvements you can make...
use actual true/false values instead of "true", "false" strings
DRY: you just need one button; use a Vue computed value to show "ON" or "OFF"
use conditional :class logic to apply bg-green-900 class
script:
data() {
return {
privateChat: (localStorage.getItem("privateChat") === true) ? true : false,
}
},
computed: {
onOrOff() {
return this.privateChat ? 'ON' : 'OFF'
}
},
methods: {
clickPrivateChat (value) {
this.privateChat = !this.privateChat
localStorage.setItem("privateChat", value)
},
setup() {
const enabled = ref(privateChat)
let value = localStorage.getItem("privateChat")
let privateChat = (value === true)
}
},
markup:
<div>
<button type="button" #click="clickPrivateChat">
<a key="privateChat" href="#" :class="privateChat?'bg-green-900':''" class="bg-red-900 text-gray-100 hover:bg-gray-800 hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium">
<span class="pt-2">Private Chat {{ onOrOff }}</span>
</a>
</button>
</div>
improved Vue approach
I created a sample in codepen with local data. Hoping it still works for you for troubleshooting but I am actually using vuex and an API endpoint that contains the data. I just can't share the API of course.
Anyway, so I am retrieving a collection of application numbers from an API and displaying them in removable chips. The form (not shown in the example) has a field that I can add more applications to this list. That part works fine. My problem is removing an application.
When someone enters an application by mistake, the user can click on the X in the chip to remove it and a manager can come and approve the removal. That part I haven't got to yet but I believe I can do that once I get there as long as I figure this small part first.
In order to remove the right application, I need to capture the one that the user clicked on so I can pass it to the API and then I can pop() it from the state in a mutation. Notice that I am succesfully capturing the right application in a console.log, but I can't capture it in the modal dialog. How can I do this?
<div id="q-app">
<div class="q-pa-md">
<span v-for="(batch, index) in applications" :key="index">
<q-chip removable dense outline color="grey-9" #remove="remove(batch.value)">
{{batch.value}}
</q-chip>
<!-- Manager Approval Dialog -->
<q-dialog v-model="removeApplication" persistent>
<q-card class="q-pa-lg" style="width: 600px">
<q-card-section class="row justify-center items-center">
<span class="q-ml-sm">
Enter your manager credentials to remove application number: {{batch.value}} from the current batch.
</span>
<q-form #submit="onSubmit" class="q-gutter-md q-mt-md" style="width: 100%">
<div class="row items-start justify-center">
<label class="col-4 text-weight-medium form-label">Admin Username:</label>
<div class="col-8">
<q-input
outlined
v-model="username"
color="cts-purple-faint"
bg-color="cts-purple-faint"
square
dense
type="text"
id="username">
</q-input>
</div>
</div>
<div class="row items-start justify-center">
<label class="col-4 text-weight-medium form-label">Admin Password:</label>
<div class="col-8">
<q-input
outlined
color="cts-purple-faint"
bg-color="cts-purple-faint"
square
dense
type="password"
v-model="password">
</q-input>
</div>
</div>
</q-form>
</q-card-section>
<q-card-actions align="right" class="q-pa-lg">
<q-btn label="Decline" color="secondary" v-close-popup></q-btn>
<q-btn label="Approve" color="primary" type="submit" v-close-popup></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</span>
</div>
</div>
In my script
new Vue({
el: '#q-app',
data() {
return {
appsinbatch:{
applications:[
{"value": 741000, "selected": true },
{"value": 742000, "selected": true },
{"value": 743000, "selected": true }
]
},
username: "",
password: "",
removeApplication: false,
}
},
computed: {
applications() {
return this.appsinbatch.applications;
},
},
methods: {
onSubmit() {
//remove the application selected
},
remove (applications) {
console.log(`${applications} has been removed`)
this.removeApplication = true
},
}
})
Here is the codepen playground Thanks in advance!
You have a one-to-one relationship of chip to form. when you click on any of the chips, it toggles the last added form/card. Instead, you should have one form and reuse a single form.
So for this solution, I used a computed and the model. I'm not familiar with quasar, but haven't found a way to toggle visibility based on whether an object is set, and I think it requires use of a model with a boolean value. So I wrapped the card content with a v-if as-well. This was needed, because otherwise selectedApplication.value will be rendered even if the selectedApplication is null.
<!--
Forked from:
https://quasar.dev/vue-components/chip#Example--Outline
-->
<div id="q-app">
<div class="q-pa-md">
<q-chip removable dense outline color="grey-9"
#remove="remove(index)"
v-for="(batch, index) in applications"
:key="index"
>{{batch.value}}</q-chip>
<!-- Manager Approval Dialog -->
<q-dialog v-model="removeApplication" persistent>
<q-card class="q-pa-lg" style="width: 600px" v-if="selectedApplication">
<q-card-section class="row justify-center items-center">
<span class="q-ml-sm">
Enter your manager credentials to remove application number: {{selectedApplication.value}} from the current batch.
</span>
<q-form #submit="onSubmit" class="q-gutter-md q-mt-md" style="width: 100%">
<div class="row items-start justify-center">
<label class="col-4 text-weight-medium form-label">Admin Username:</label>
<div class="col-8">
<q-input
outlined
v-model="username"
color="cts-purple-faint"
bg-color="cts-purple-faint"
square
dense
type="text"
id="username">
</q-input>
</div>
</div>
<div class="row items-start justify-center">
<label class="col-4 text-weight-medium form-label">Admin Password:</label>
<div class="col-8">
<q-input
outlined
color="cts-purple-faint"
bg-color="cts-purple-faint"
square
dense
type="password"
v-model="password">
</q-input>
</div>
</div>
</q-form>
</q-card-section>
<q-card-actions align="right" class="q-pa-lg">
<q-btn label="Decline" color="secondary" v-close-popup></q-btn>
<q-btn label="Approve" color="primary" type="submit" v-close-popup></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</div>
</div>
new Vue({
el: '#q-app',
data() {
return {
appsinbatch:{
applications:[
{"value": 741000, "selected": true },
{"value": 742000, "selected": true },
{"value": 743000, "selected": true }
]
},
selection: null,
username: "",
password: "",
removeApplication: false
}
},
computed: {
applications() {
return this.appsinbatch.applications;
},
selectedApplication() {
if (Number.isInteger(this.selection) && this.selection < this.applications.length){
this.removeApplication = true;
return this.applications[this.selection];
}
this.removeApplication = false;
return false
},
},
methods: {
onSubmit() {
//remove the application selected
},
remove (index) {
this.selection = index;
var application = this.applications[index]
this.selection = index;
console.log(`${application.value} has been removed`)
this.removeApplication = true
},
}
})