How to use ionic 4 search bar with *ngFor - ionic4

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.

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());
}
}`

How to run a specific function if the component has been called by a specific component and not by other components in Vue?

I have a component called select-diagnosis which is used by many different components.
When select-diagnosis is called by a specific component called PtdTreatment, it needs to run a specific function inside the fetchDiagnosis function, while when called by other components it will not run that specific function.
The fetchDiagnosis needs to understand that select-diagnosis component has been called by the PtdTreatment component.
How to do something like that?
This is the code from PtdTreatment component:
<el-form-item
label="diagnosis"
prop="dimission_diagnosis"
v-if="form.data_dimission">
<select-diagnosis
v-model="form.diagnosis_dimission"
:defaultValue="_.get(record, 'clean_diagnosis_dimission')"
/>
</el-form-item>
And this is the select-diagnosis component:
<template>
<el-select
v-bind="$attrs"
:value="value"
#change="onChange"
#clear="onClear"
clearable
filterable
remote
:remote-method="fetchDiagnosis"
:loading="loadingSelect"
>
<el-option
v-for="item in items"
:key="`diagnosis-${item.id}`"
:label="item.code + ' - ' + item.description"
:value="item.code"
>
</el-option>
</el-select>
</template>
<script>
export default {
name: "SelectDiagnosis",
inheritAttrs: false,
props: ["value", "defaultValue"],
data() {
return {
loadingSelect: false,
items: []
};
},
methods: {
fetchDiagnosis(query) {
const valid = query !== "" && query.length > 2;
if (!valid) return;
this.loadingSelect = true;
let params = { string: query };
axios
.get("/config/diagnosi", { params })
.then(({ data }) => {
//pseudo code
// if this component is called by **select-diagnosis** then do this
this.items = data.filter(diagnosi => {
const code = diagnosi.codice.replace(/\b0+/g, "");
if (code.length >= 4) {
return diagnosi;
}
});
// else do this
this.items = data;
})
.finally(() => (this.loadingSelect = false));
},
onChange(x) {
this.$emit("input", x);
},
onClear() {
this.$emit("input", null);
this.items = [];
}
},
watch: {
defaultValue: {
immediate: true,
handler(newVal, oldVal) {
if (newVal && oldVal === undefined) {
this.items = [newVal];
this.$emit("input", newVal.codice);
}
}
}
}
};
</script>
There are a number of ways to accomplish this, the two that come to mind immediately use props.
You could pass a filterDiagnoses boolean prop to select-diagnosis. If it's true, run the filter logic.
<select-diagnosis v-model="form.diagnosis_dimission" :defaultValue="_.get(record, 'clean_diagnosis_dimission')" :filterDiagnoses="true" />
You could invert control to the parent function and expose a filterFn callback prop - the parent function passes a function prop to the child that the child will call upon fetching the diagnoses (this feels cleaner and more extensible):
/* in the PtdTreatment component */
/* ... */
methods: {
filterDiagnosis(data) {
// filter data
},
}
/* in the PtdTreatment template */
<select-diagnosis v-model="form.diagnosis_dimission" :defaultValue="_.get(record, 'clean_diagnosis_dimission')" :filterFn="filterDiagnosis" />
/* in the select-diagnosis component */
fetchDiagnosis(query) {
const valid = query !== "" && query.length > 2;
if (!valid) return;
this.loadingSelect = true;
let params = { string: query };
axios
.get("/config/diagnosis", { params })
.then(({ data }) => {
if (this.filterFn) {
this.items = this.filterFn(data);
} else {
this.items = data;
}
})
.finally(() => (this.loadingSelect = false));
},
}
You can set a prop on the child component which specifies the 'identity' of the parent component, then test for that in the child:
<select-diagnosis
v-model="form.diagnosis_dimission"
:defaultValue="_.get(record, 'clean_diagnosis_dimission')"
parent="PtdTreatment"
/>
Then in the child (simplified example):
export default {
props: ["value", "defaultValue", "parent"],
methods: {
fetchDiagnosis(query) {
if (this.parent === "PtdTreatment") {
// Parent-specific code here
}
}
}
}

Application gvies me a "Cannot read property" error, but only the layout is affected

I am really scratching my head at this.
I am making a CRUD application, and this problem started when I was working on the Edit component.
I am getting the error Cannot read property 'id' of null
BUT! The interesting thing is that the data actually DOES get updated, both in the application and on the server side.
The error however affects the layout. First of all, the delete button appears two places in the template instead of one, and instead of redirecting me to the main page when I update, the main page appears like a new div on the edit page. I have no idea what is going on.
Here are the different components/composables:
The Details component: Here the information about a specific document is stored based on it's ID.
<template>
<div v-if="playlist" class="playlist-details">
<div class="playlist-info">
<div class="cover">
<img :src="playlist.coverUrl">
</div>
<h2> {{ playlist.title }}</h2>
<p> {{ playlist.description }} </p>
</div>
</div>
<button #click="handleDelete">Delete</button>
<EditSong :playlist="playlist" />
</template>
<script>
import EditSong from '../components/EditSong'
import useDocument from '../composables/useDocument'
import getDocument from '../composables/getDocument'
import useStorage from '../composables/useStorage'
import { useRouter } from "vue-router";
export default {
props: ['id'],
components: { EditSong },
setup(props) {
const { document: playlist } = getDocument('playlists', props.id)
const { deleteDoc } = useDocument('playlists', props.id)
const router = useRouter();
const { deleteImage } = useStorage()
const handleDelete = async () => {
await deleteImage(playlist.value.filePath)
await deleteDoc()
confirm('Do you wish to delete this content?')
router.push({ name: "Home" });
}
return {
playlist,
handleDelete
}
}
}
</script>
Here is the Edit component: This is where I edit and update the data inside the Details component. This is where I am getting the TypeError.
It has something to do with the props.playlist.id field
<template>
<div class="edit-song">
<form #submit.prevent="handleSubmit">
<input type="text" required placeholder="title" v-model="title">
<input type="text" required placeholder="description" v-model="description">
<button v-if="!isPending">Update</button>
<button v-else disabled>Updating...</button>
</form>
</div>
</template>
<script>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import useDocument from '../composables/useDocument'
import useCollection from '../composables/useCollection'
export default {
props: ['playlist'],
setup(props) {
const title = ref('')
const description = ref('')
const { updateDoc } = useDocument('playlists', props.playlist.id)
const { error } = useCollection()
const isPending = ref(false)
const router = useRouter();
const handleSubmit = async () => {
await updateDoc({
title: title.value,
description: description.value,
})
isPending.value = false
if(!error.value) {
router.push({ name: "Home" })
}
}
return {
title,
description,
handleSubmit,
isPending,
error
}
}
}
</script>
And last, this is the Update composable: that stores the update function
import { ref } from 'vue'
import { projectFirestore } from '../firebase/config'
const useDocument = (collection, id) => {
const error = ref(null)
const isPending = ref(false)
let docRef = projectFirestore.collection(collection).doc(id)
const updateDoc = async (updates) => {
isPending.value = true
error.value = null
try {
const res = await docRef.update(updates)
isPending.value = false
return res
}catch(err) {
console.log(err.message)
isPending.value = false
error.value = 'Could not update document'
}
}
return {
error,
isPending,
updateDoc
}
}
export default useDocument
The likely scenario is getDocument() returns a ref to null for document, which gets updated asynchronously:
const getDocument = (collection, id) => {
const document = ref(null)
someAsyncFunc(() => {
document.value = {...}
})
return {
document
}
}
Since the document (renamed to playlist) is bound to the EditSong component, it receives both the initial value (null) and then the asynchronously populated value, which leads to the behavior you're seeing.
One solution is to conditionally render EditSong on playlist:
<EditSong v-if="playlist" :playlist="playlist" />
Another is to move the updateDoc initialization into handleSubmit, and add a null-check there:
const handleSubmit = async () => {
if (!props.playlist) return
const { updateDoc } = useDocument('playlists', props.playlist.id)
await updateDoc(...)
}

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

How to access a click handler in custom attribute in 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);
});
}