How to access a click handler in custom attribute in Aurelia? - aurelia

Is it possible to access click handler of the element in the custom attribute? I would like to achieve something like this:
<button click.delegate="callSomeMethod()" log-click>Click</button>
where log-click is a custom attribute that wraps the click call and decorates it with some behavior.
A non-working example, but showing what I want to achieve:
class LogClickCustomAttribute {
#bindable click;
attached() {
let originalClick = this.click;
this.click = () => {
console.log('decoreated!');
return originalClick();
};
}
}
The real use case I am trying to achieve is a button that disables itself until promise returned by click handler resolves. Like promise-btn for Angular.
<button click.delegate="request()" disable-until-request-resolves>Click</button>

I have no idea if it is possible to access attributes of standard HTML elements like button within a custom attribute. However this is easy if you create a custom element for buttons:
GistRun: https://gist.run/?id=d18de213112c5f21631da457f218ca3f
custom-button.html
<template>
<button click.delegate="onButtonClicked()">Test</button>
</template>
custom-button.js
import {bindable} from 'aurelia-framework';
export class CustomButton {
#bindable() onClicked;
onButtonClicked() {
if (typeof this.onClicked === 'function') {
this.onClicked();
}
}
}
log-click.js
import {inject} from 'aurelia-framework';
import {CustomButton} from 'custom-button';
#inject(CustomButton)
export class LogClickCustomAttribute {
constructor(customButton) {
this.customButton = customButton;
}
bind() {
let originalOnClicked = this.customButton.onClicked;
this.customButton.onClicked = () => {
console.log('decorated!');
return originalOnClicked();
};
}
}
app.html
<template>
<require from="./custom-button"></require>
<require from="./log-click"></require>
<custom-button on-clicked.call="test()" log-click>Test</custom-button>
</template>
app.js
export class App {
test() {
console.log("The button was clicked.");
}
}

You can add event handlers to the element in the constructor of the custom attribute.
#inject(Element)
export class ClickThisCustomAttribute {
constructor(element) {
element.addEventListener('click', () => {
this.doSomething();
});
}
}

Given how Aurelia attaches event handlers, you're not going to be able to do exactly what you want.
That being said, you could use a simple custom attribute like the one below to log out an event to the console:
log-event.js
import { inject } from 'aurelia-framework';
#inject(Element)
export class LogEventCustomAttribute {
constructor(el) {
this.el = el;
}
attached() {
const eventName = this.value || 'click';
let handler = (e) => console.log('event logged', e);
if (this.el.addEventListener) { // DOM standard
this.el.addEventListener(eventName, handler, false)
} else if (this.el.attachEvent) { // IE
this.el.attachEvent(eventName, handler)
}
}
}

The closest thing to a promise click I made was this:
import { autoinject, bindable } from "aurelia-framework";
#autoinject
export class PromiseClickCustomAttribute {
#bindable({ primaryProperty: true }) delegate: Function;
constructor(private element: Element) {
this.element.addEventListener("click", async () => {
try {
this.element.classList.add("disabled");
this.element.classList.add("loading");
await this.delegate();
}
catch (error) {
console.error(error);
}
finally {
this.element.classList.remove("disabled");
this.element.classList.remove("loading");
}
})
}
}
<div class="ui container">
<h2>Promise Click</h2>
<div class="ui input">
<button class="ui button" promise-click.call="alertLater()">Toast Later</button>
</div>
</div>
alertLater = () => {
return new Promise((resolve) => {
setTimeout(() => {
alert("Promise Resolved");
resolve();
}, 3000);
});
}

Related

How to navigate to another page and make the badge count empty, on click of mat-icon button?

Requirement:
I am getting badge count based on API value.
I want onclick of icon button it should navigate to another page and make the badge count empty.
Problem:
I am able to navigate to another page but badge count remains same its not becoming empty.
Please anyone help me to resolve this.
I have tried with below code
import { MatBadgeModule } from '#angular/material/badge';
`app-bar-alert.html
<div class="alert-notification">
<button class="mat-icon-button" (click)="navigateTo()">
<mat-icon>notifications_active</mat-icon>
<span class="badge" *ngIf="notificationNumberCount > 0 || null">{{notificationNumberCount}}</span>
</button>
</div>
`app-bar-alert.ts
import { Component, OnInit } from "#angular/core";
import { Router } from '#angular/router';
import { BehaviorSubject, interval, Subscription } from 'rxjs';
import { AlertActions, StoreState } from "../../store";
import { Store } from "#ngrx/store";
import { Actions, ofType } from "#ngrx/effects";
#Component({
selector: "jci-app-bar-alert",
templateUrl: "./app-bar-alert.html",
styleUrls: ["./app-bar-alert.scss"],
})
export class AppBarAlert implements OnInit {
notificationNumberCount: number;
hidden = false;
private subscriptions: Subscription[] = [];
public loading: BehaviorSubject<boolean> = new BehaviorSubject(true);
constructor(private router: Router, protected store: Store<StoreState.IState>, private actions: Actions,) {
this.subscriptions[1] = this.actions.pipe(
ofType<AlertActions.AlertSuccess>(AlertActions.ActionTypes.AlertSuccess))
.subscribe((response: AlertActions.AlertSuccess) => {
this.notificationNumberCount = response.payload.alert.total;
console.log(this.notificationNumberCount);
this.loading.next(false);
});
}
navigateTo() {
this.router.navigateByUrl('/alert/information');
// this.notificationNumberCount = 0;
}
ngOnInit() {
this.subscriptions[0] = interval(30000).subscribe(
(val) => {
this.getAlert(false);
});
this.getAlert(true);
}
private getAlert(isInitialLoad: boolean) {
if (isInitialLoad) {
this.loading.next(true);
}
this.store.dispatch(new AlertActions.AlertRequest());
}
}`

Vue3 Emit Event not triggering method in parent

After making an API call I want to stop a loading spinner from displaying. Before calling the child component I set the this.showLoader property to true. This displays the spinner graphic. However once the API call has been made the graphic does not disapear. The updateLoader method never gets called.
child.vue
export default {
methods: {
fetchData() {
fetch(url, options)
.then((response) => response.json())
.then(this.$emit('hideLoaderEvent', false));
}
}
}
parent.vue
<template>
<MyComponent #hideLoaderEvent="updateLoader" />
</template>
export default {
data() {
return {
showLoader: false,
},
methods: {
updateLoader() {
this.showLoader = false;
}
}
}

Vue 3 access child component from slots

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');

Cannot find data-testid attribute in Vue Component with Jest

I am trying to build a test that will target an element with the data-testid attribute. I have a BaseTile component that looks like this:
<template>
<div
data-testid="base-tile-icon"
v-if="!!this.$slots.icon"
>
<slot name="icon"></slot>
</div>
</template>
<script>
export default {};
</script>
<style></style>
And my test looks like this:
import { mount } from '#vue/test-utils';
import BaseTile from '#/components/BaseTile';
const factory = (slot = 'default') => {
return mount(BaseTile, {
slots: {
[slot]: '<div class="test-msg"></div>'
}
});
};
it('has an icon slot if an icon is provided', () => {
let wrapper = factory({ slot: 'icon' });
const input = wrapper.find('[data-testid="base-tile-icon"]');
expect(input.findAll('.test-msg').length).toBe(1);
});
How do I appropriately target the data-testid attribute with this test?
The named parameter of the factory is implemented incorrectly. The correct method is described in this post: Is there a way to provide named parameters in a function call in JavaScript?
The correct way to implement this is as follows:
import { mount } from '#vue/test-utils';
import BaseTile from '#/components/BaseTile';
const factory = ({ slot = 'default' } = {}) => {
return mount(BaseTile, {
slots: {
[slot]: '<div class="test-msg"></div>'
}
});
};
it('has an icon slot if an icon is provided', () => {
let wrapper = factory({ slot: 'icon' });
const input = wrapper.find('[data-testid="base-tile-icon"]');
expect(input.findAll('.test-msg').length).toBe(1);
});

How to use ionic 4 search bar with *ngFor

I have build a page that use a search bar to filter through an *ngFor array. When I type in the search bar it behaves normally, but when I delete or back space text it does not update. It works normally if I pull an array from a static list from a data service but not with the data I am pulling from an ApolloQueryResult. Any help would be greatly appreciated.
html
<ion-content padding>
<div *ngIf="loading">Loading...</div>
<div *ngIf="error">Error loading data</div>
<ion-toolbar>
<ion-searchbar [(ngModel)]="searchTerm" (ionChange)="setFilteredItems()" showCancelButton="focus"></ion-searchbar>
</ion-toolbar>
<ion-card *ngFor="let data of info">
<ion-card-content>
{{data.TypeOfNotification}}
</ion-card-content>
</ion-card>
</ion-content>
ts
import { Component, OnInit } from '#angular/core';
import { Apollo } from 'apollo-angular';
import { ApolloQueryResult } from 'apollo-client';
import { QueryTodoService } from '../../services/query-todo.service';
import { Storage } from '#ionic/storage';
#Component({
selector: 'app-tab-to-do',
templateUrl: './tab-to-do.page.html',
styleUrls: ['./tab-to-do.page.scss'],
})
export class TabToDoPage implements OnInit {
info: any;
error: any;
loading: boolean;
searchTerm: string;
constructor(
private apollo: Apollo,
private queryTodoService: QueryTodoService,
private storage: Storage
) { }
setFilteredItems() {
this.info = this.filterItems(this.searchTerm);
}
filterItems(searchTerm){
return this.info.filter((item) => {
return item.TypeOfNotification.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1;
});
}
// or
setFilteredItemsAlt(event) {
const searchTerm = event.srcElement.value;
if (!searchTerm) {
return;
}
this.info = this.info.filter(item => {
if (item.TypeOfNotification && searchTerm) {
if (item.TypeOfNotification.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1) {
return true;
}
return false;
}
});
}
ngOnInit() {
this.storage.get('AccessToken').then((_token) => {
this.apollo.watchQuery({
query:this.queryTodoService.ToDoQuery,
fetchPolicy: 'cache-first',
})
.valueChanges.subscribe((result: ApolloQueryResult<any> ) => {
this.loading = result.loading;
this.info = result.data.notifications.Notifications;
console.log('first info', this.info );
this.error = result.errors;
});
});
}
}
It's because you are overwriting this.info every time you fire setFilteredItems():
setFilteredItems() {
//Overwrite this.info with new filtered data set.
this.info = this.filterItems(this.searchTerm);
}
The old values were filtered out and no longer exist - which is why *ngFor="let data of info" is not displaying them.
What you can do is set a new variable equal to this.info in your ts file - e.g. "dataDisplay":
dataDisplay: Array<object> = this.info;
Set this variable during an Ionic lifecycle change like ionViewWillEnter or whenever this.info gets set.
Then swap out the variable in setFilteredItems():
setFilteredItems() {
this.dataDisplay = this.filterItems(this.searchTerm);
}
Now change your *ngFor to the new variable:
*ngFor="let data of dataDisplay"
This should do the trick for you, because now filterItems(searchTerm) is always filtering the full, original this.info data set.