How to stop propagation with AlpineJS and Flatpickr? - javascript

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.

Related

How to apply onClick in the input

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>
);

Why does *ngIf seem to break my reactive form? How do I handle conditional Inputs and validation in Angular?

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.

Vuelidate — Date validation (Compare dates, lower and higher than)

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.

Next.js - Expected server HTML to contain a matching <div> in <div>

Live example available here
I'm trying to make a basic layout where, on mobiles, only the latest posts appear. On desktop, the left column should be the posts and the right column the top categories and most popular posts.
Here is the layout:
const IndexLayout: React.FC<IndexLayoutProps> = ({}) => {
const cols = useScreenType()
return cols === '2-cols' ? (
<div className="w-full flex justify-between items-start">
<ListPosts data-comp="ListPosts" className="w-4/6" />
<div className="sticky ml-12 w-2/6 flex flex-col">
<TopCategories data-comp="TopCategories" className="w-full" />
<PopularPosts data-comp="PopularPosts" className="mt-4" />
</div>
</div>
) : (
<ListPosts data-comp="ListPosts" className="w-full" />
)
}
Here's the useScreenType hook:
import { useMediaQuery } from 'react-responsive'
export const useScreenType = () => {
const is2Cols = useMediaQuery({ minWidth: 1300 })
const is1Cols = useMediaQuery({ minWidth: 800 })
if (is2Cols) {
return '2-cols'
}
if (is1Cols) {
return '1-cols'
}
return 'fullscreen'
}
And I keep getting this error:
Warning: Expected server HTML to contain a matching <div> in <div>.
div
ListPosts#webpack-internal:///./components/posts/ListPosts.tsx:31:19
div
IndexLayout#webpack-internal:///./components/layout/IndexLayout.tsx:28:149
div
Index#webpack-internal:///./pages/index.tsx:24:149
ApolloProvider#webpack-internal:///./node_modules/#apollo/client/react/context/ApolloProvider.js:13:18
s#webpack-internal:///./node_modules/next-apollo/dist/index.es.js:26:1911
div
div
MyApp#webpack-internal:///./pages/_app.tsx:37:19
ErrorBoundary#webpack-internal:///./node_modules/#next/react-dev-overlay/lib/internal/ErrorBoundary.js:23:47
ReactDevOverlay#webpack-internal:///./node_modules/#next/react-dev-overlay/lib/internal/ReactDevOverlay.js:73:20
Container#webpack-internal:///./node_modules/next/dist/client/index.js:155:20
AppContainer#webpack-internal:///./node_modules/next/dist/client/index.js:643:18
Root#webpack-internal:///./node_modules/next/dist/client/index.js:779:19
Now I think the issue is due to the useScreenType hook not being able to get a width because window isn't defined on the server. But how can I fix this issue? And not only do I get an error, but my HTML renders weirdly.
The final render ends up being something like this (when it renders as '2-cols'):
<div class="flex flex-col justify-start items-start w-full">
<div class="mt-6 w-full"></div>
<div class="mt-4 flex items-center cursor-pointer transform transition hover:scale-105 text-sm">
<div class="w-full p-6 rounded-lg flex flex-col dark:bg-gray-800 shadow-md"></div>
<div class="mt-4 p-6 rounded-lg flex flex-col dark:bg-gray-800 shadow-md"></div>
</div>
</div>
Note: I am using Next.js v10.2.0
Code can be found on GitHub
As you notice, you cant access window object on server, so if you want to server-render something based on window object - you must hardcode these values.
The only thing you can rely on is user-agent in request headers, which gives you some understanding of user device.
For example this way you can detect user device in _app.js:
const device = deviceDetector.detect(isServer() ? ctx.req.headers['user-agent'] : window.navigator.userAgent)
deviceDetector is any kind of device detection implementation based on user agent
For anyone wondering how I fixed this, I ditched the responsive design with logic and switched to CSS. Here is my layout post fix (changed some classes with the lg prefix [documentation]):
const IndexLayout: React.FC<IndexLayoutProps> = ({}) => {
return (
<div className="mt-12 lg:mt-24 w-5/6 mx-auto flex items-start">
<div className="w-full flex justify-between items-start">
<ListPosts className="lg:w-4/6 w-full" />
<div className="hidden sticky ml-12 w-2/6 lg:flex flex-col">
<TopCategories className="w-full" />
<PopularPosts className="mt-4" />
</div>
</div>
</div>
)
}

alpine.js $dispatch modal -> submit Form

I am using django and i have a base template where i defined a modal using alpine.js with $dispatch sender.
base.html:
<div x-data="modal()" class="mt-6" x-cloak>
<template x-on:show-modal.window="isOpenModal = $event.detail.show; modalHeader = $event.detail.modalHeader; modalData = showData($event.detail.modalData); "></template>
<div class="absolute z-50 top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50" x-show="isOpenModal">
<div class="z-50 text-left bg-gray-200 px-4 shadow-xl rounded-lg mx-2 md:max-w-lg md:p-6 lg:p-8 md:mx-0 h-auto " >
<div class="flex justify-between">
<h2 id="modalHeader" class="text-2xl" x-text="modalHeader"> </h2>
</div>
<div class="w-full border border-gray-600 mt-4" ></div>
<div id="modalContent" class="text-lg w-auto" > </div>
</div>
</div>
</div>
in script tags ....
function modal(){
return{
isOpenModal: false,
modalHeader:'',
modalData: '',
showData(data){
document.getElementById('modalContent').innerHTML = data
let fp = flatpickr(".pickerDate", {locale: "at", dateFormat: "d.m.Y"});
},
}
}
then in the other html which is extendet from the base.html i want to use the modal where i want to get with axios form data from the server and put it into the modal. This is working perfect. But i don't know how to realize the submit button ?
new.html
<div x-data="test()" #click="getCreateForm($dispatch)">
test click
</div>
this is the point where i go to function getCreateForm ....
function test(){
return{
getPatientCreateForm($dispatch){
axios.get("{% url 'user:createForm'%}")
.then(response => {
var modalHeader = response.data.header
var modalData = "<form id='createUser' class='' method='POST' action='' x-on:submit.prevent='?????????'> {% csrf_token %}" +response.data.seite.seite
$dispatch('show-modal', { show: true, modalHeader: modalHeader, modalData: modalData })
})
.catch(error => {
console.log(error);
})
}
}
}
The problem is when i put a sendForm function into the submit.prevent like: x-on:submit.prevent='sendForm()' alpine searches for the function on the base.html (where the modal is defined) and i don't want to implement a function there. I want by clicking the submit button that the data should be send with axios on the new.html (where i started with the getCreateForm ) and not at the base.html. Is that possible with alpine.js and $dispatch ? or is that impossible
Thanks for helping!
Martin

Categories