How can I render a component into an Angular modal? - javascript

I want to render a component into a Angular dialog. This component is used on on a full page of my application and it has children.
When I add the selector into my modal, I've got an error : ExpressionChangedAfterItHasBeenCheckedError
This error happens on the page which loads the modal.
Is it possible to reuse a component into an Angular modal with his inputs and outputs ?
<!-- modal -->
<ng-container>
<div class="close-button-wrapper">
<button (click)="onCloseClick()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
</div>
<div class="dialog-content">
<picture-container></picture-container> <!-- not working ExpressionChangedAfterItHasBeenCheckedError -->
</div>
<div class="dialog-footer">
<raised-button-round class="action-button">Cancel</raised-button-round>
<raised-button-round icon="save" color="primary" class="action-button">OK</raised-button-round>
</div>
</ng-container>
<!-- component I want to reuse -->
#Component({
selector: 'picture-container',
template: `
<ng-container *ngIf="data$ | async as data">
<picture-presenter
[images]="data.pictures"
[tagImages]="data.tagImages"
[totalCountItems]="data.totalCountItems"
[tags]="data.tags"
[pageSize]="20"
[isSelectablePicture]="data.isSelectablePicture"
(displayableContent)="onChangeDisplay($event)"
(searchTagSelect)="onSearchTagSelect($event)"
(importPicture)="onImportPicture()"
(pictureAction)="onPictureAction($event)"
(changePage)="pageChange$.next($event)"
></picture-presenter>
</ng-container>
<upload-widget #uploadWidget (upload)="onUpload($event)"></upload-widget>
`,
styles: [``],
})
export class PictureContainerComponent implements OnDestroy, OnInit {
#ViewChild('uploadWidget') uploadWidget?: UploadWidgetComponent
data$: Observable<
Maybe<{
pictures: Maybe<PictureFragment>[]
tagImages: TagImage[]
tags: Maybe<{ name: string; count: number }>[]
totalCountItems: number,
isSelectablePicture: boolean
}>
> = of(null)
destroyed$ = new Subject<boolean>()
pageChange$ = new BehaviorSubject<PaginatorEvent>({})
personalImages$ = new BehaviorSubject<boolean>(true)
searchTags$ = new BehaviorSubject<string[]>([])
constructor(public store: Store) { }
ngOnInit(): void {
combineLatest([
this.store.select(selectStore),
this.store.select(selectGalleries),
this.searchTags$,
this.pageChange$,
this.personalImages$,
this.store.select(selectPictureUpdated),
])
.pipe(takeUntil(this.destroyed$))
.subscribe(
([store, galleries, tags, direction, isPersonal, picUpdated]) => {
if (!galleries) return
// TODO - High - Get right gallery
const galleriesId = galleries.map((gal) => gal?.id || '')
const filters = {
galleries: !isPersonal ? galleriesId : [],
tags: !isPersonal ? tags : [],
owner: store?.id,
}
this.store.dispatch(
loadPictures({
filters,
paginate: {
count: 15,
},
paginatorEvent: direction,
noCache: picUpdated != null ? true : false,
})
)
this.store.dispatch(
loadTags({
search: {
galleries: galleriesId,
owner: store?.id,
},
})
)
}
)
this.data$ = combineLatest([
this.store.select(selectPicturesState),
of(TAG_IMAGES),
this.store.select(selectPagination),
this.store.select(selectTags),
this.store.select(selectIsSelectablePicture),
]).pipe(
takeUntil(this.destroyed$),
map(([picturesState, tagImages, pagination, tags, isSelectablePicture]) => {
return {
pictures: picturesState?.pictures || [],
tagImages,
totalCountItems: pagination?.totalCount || 0,
tags,
isSelectablePicture
}
})
)
}
ngOnDestroy(): void {
this.destroyed$.next(true)
this.destroyed$.complete()
}
onSearchTagSelect(tags: string[]) {
this.pageChange$.next({})
this.searchTags$.next(tags)
}
onImportPicture() {
if (this.uploadWidget) {
this.uploadWidget?.openDialog({ pictureType: 'food' })
}
}
onPictureAction($event: {
picture: Maybe<PictureFragment>
action: 'edit' | 'delete'
}) {
if (!$event.picture) return
const picture = parsePictureFragmentToPictureInput($event.picture)
if (!picture) return
if ($event.action === 'edit') {
this.store.dispatch(
setUpdatePicture({
picture,
})
)
} else {
this.store.dispatch(setDeletePicture({ picture }))
}
}
onUpload($event: any) {
this.store.select(selectStore).subscribe((store) => {
this.store.dispatch(
setUploadPicture({
picture: { ...$event, owner: store?.id },
})
)
})
}
onChangeDisplay($event: 'import' | 'suggestions' | 'search') {
this.personalImages$.next($event !== 'search')
}
Thank you for your help

Related

How to remember or keep the current tab when the page is refreshed?

How do we keep the current tab when a page is refreshed? I have a component which is TransactionsDetailsComponent which renders the tabs I want and when the page is refreshed it should keep the current selected tab.
I have put my HTML and TS code below if someone is interested.
Any idea how to made this possible in angular?
#html code
<div headerTabs>
<div style="margin-top: -49px;">
<mat-tab-group animationDuration="0ms" [(selectedIndex)]="transactionTabIndex"
(selectedTabChange)="setTransactionTabIndex($event)">
<mat-tab label="{{tab}}" *ngFor="let tab of tabs">
<ng-template matTabContent>
<div class="mat-tab-shadow-container">
<div class="mat-tab-shadow"></div>
</div>
<div class="tab-content">
<app-transaction-overview *ngIf="tab === 'Overview' && transaction != null" [transaction]="transaction"
(teamAndUsers)="teamAndUsersEvent($event)" #transactionOverView
(saveTransactionEvent)="saveTransactionEvent($event)"></app-transaction-overview>
<app-deals-transaction *ngIf="tab === 'Deals' && transaction != null" [transaction]="transaction">
</app-deals-transaction>
<app-transaction-activity *ngIf="tab === 'Activity' && transaction != null" [transactionId]="transactionId" [isLocked]="transaction.isLocked">
</app-transaction-activity>
<app-document-management-list #DocumentManagementList *ngIf="tab === 'Documents' && transaction != null"
[dto]="transaction" [documentType]="documentType" [isLocked]="transaction.isLocked"></app-document-management-list>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>
</div>
</app-page-header>
</div>
#tscode
export class TransactionsDetailsComponent implements OnInit {
documentType = EnumDocumentManagementType.TRANSACTION;
selectedIndex = 0;
transactionId: number;
propertyId: string;
#ViewChild('transactionOverView') transactionOverView: any;
isInProgress = false;
isEdit = false;
accountId: number;
accountRole: string;
tabs = ['Overview', 'Documents', 'Deals', 'Activity'];
transaction: any = null;
partialTransaction: any = null;
transactionTabIndex: number = 0;
/*page header component Inputs*/
// pageHeaderTitleData = {
// title: {
// primary: null
// }
// }
pageHeaderTitleData = {
title: {
main: "",
sub: ""
},
status: {
text: 'in progress',
},
otherInfo: []
}
breadCrumbsData = {
paths: [
{
text: "My Transactions",
link: "/transactions"
}
],
currentPage: ''
}
constructor(
private _notificationService: NotificationService,
private formBuilder: FormBuilder,
private _activatedRoute: ActivatedRoute,
private _transactionService: TransactionsService,
ngOnInit(): void {
this._activatedRoute.queryParams
.subscribe(
params => {
if (params.tab) {
this.transactionTabIndex = params.tab;
}
}
)
}
setTransactionTabIndex(e: any) {
this.setIsEdit(false);
this.transactionTabIndex = e.index;
}
}
You can use localStorage for this purpose, it will persist across multiple sessions until the localStorage is cleared.
Change setTransactionTabIndex to store the selection to localStorage
setTransactionTabIndex(e: any) {
this.setIsEdit(false);
this.transactionTabIndex = e.index;
localStorage.setItem('transactionTabIndex', e.index);
}
and then in the ngOnInit, check if you have transactionTabIndex set in localStorage and set it:
ngOnInit(): void {
this._activatedRoute.queryParams
.subscribe(
params => {
if (params.tab) {
this.transactionTabIndex = params.tab;
}
}
);
this.transactionTabIndex = +localStorage.getItem('transactionTabIndex') || 0
}

How to write a plugin that shows a modal popup using vue. Call should be made as a function()

I am trying to make a VueJS plugin that exports a global method, which when called, will popup a message with an input text field. Ideally, I want to be able to make the following call from any Vue component:
this.$disaplayMessageWithInput("Title","Body","Value");
And a popup should come on the screen.
I've tried building it but when the install() calls this.$ref., it isn't recognized:
DeleteConfirmation.vue
<template>
<b-modal size="lg" ref="deleteConfirmationModal" :title="this.title" header-bg-variant="danger" #ok="confirmDelete" #cancel="confirmCancel">
<p>
{{this.body}}
</p>
</b-modal>
</template>
<script>
export default {
data()
{
return {
title: null,
body: null,
valueCheck: null,
value: null
};
},
install(vue, options)
{
Vue.prototype.$deleteConfirmation = function(title, body, expectedValue)
{
this.title = title;
this.body = body;
this.valueCheck = expectedValue;
this.$refs.$deleteConfirmation.show()
}
},
}
</script>
app.js
import DeleteConfirmation from './components/global/DeleteConfirmation/DeleteConfirmation';
Vue.use(DeleteConfirmation);
The call I am trying to make is:
$vm0.$deleteConfirmation("title","body","val");
I get the below error at the run time:
app.js?id=c27b2799e01554aae7e1:33 Uncaught TypeError: Cannot read property 'show' of undefined
at Vue.$deleteConfirmation (app.js?id=c27b2799e01554aae7e1:33)
at <anonymous>:1:6
Vue.$deleteConfirmation # app.js?id=c27b2799e01554aae7e1:33
(anonymous) # VM1481:1
It looks like, this.$refs in DeleteConfirmation.vue is undefined.
Try to avoiding $ref with vue ( $ref is here for third party and some very special case )
$ref isn't reactive and is populate after the render ...
the best solution for me is using a event bus like this :
const EventBus = new Vue({
name: 'EventBus',
});
Vue.set(Vue.prototype, '$bus', EventBus);
And then use the event bus for calling function of your modal ...
(
this.$bus.on('event-name', callback) / this.$bus.off('event-name');
this.$bus.$emit('event-name', payload);
)
You can create a little wrapper around the bootstrap modal like mine
( exept a use the sweet-modal)
<template>
<div>
<sweet-modal
:ref="modalUid"
:title="title"
:width="width"
:class="klass"
class="modal-form"
#open="onModalOpen"
#close="onModalClose"
>
<slot />
</sweet-modal>
</div>
</template>
<script>
export default {
name: 'TModal',
props: {
eventId: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
width: {
type: String,
default: null,
},
klass: {
type: String,
default: '',
},
},
computed: {
modalUid() {
return `${this._uid}_modal`; // eslint-disable-line no-underscore-dangle
},
modalRef() {
return this.$refs[this.modalUid];
},
},
mounted() {
if (this.eventId !== null) {
this.$bus.$on([this.eventName('open'), this.eventName('close')], this.catchModalArguments);
this.$bus.$on(this.eventName('open'), this.modalRef ? this.modalRef.open : this._.noop);
this.$bus.$on(this.eventName('close'), this.modalRef ? this.modalRef.close : this._.noop);
}
},
beforeDestroy() {
if (this.eventId !== null) {
this.$off([this.eventName('open'), this.eventName('close')]);
}
},
methods: {
onModalOpen() {
this.$bus.$emit(this.eventName('opened'), ...this.modalRef.args);
},
onModalClose() {
if (this.modalRef.is_open) {
this.$bus.$emit(this.eventName('closed'), ...this.modalRef.args);
}
},
eventName(action) {
return `t-event.t-modal.${this.eventId}.${action}`;
},
catchModalArguments(...args) {
if (this.modalRef) {
this.modalRef.args = args || [];
}
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .sweet-modal {
.sweet-title > h2 {
line-height: 64px !important;
margin: 0 !important;
}
}
</style>
AppModal.vue
<template>
<div class="modal-wrapper" v-if="visible">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<div class="modal-buttons">
<button class="modal-button" #click="hide">Close</button>
<button class="modal-button" #click="confirm">Confirm</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
title: '',
text: ''
}
},
methods: {
hide() {
this.visible = false;
},
}
}
</script>
Modal.js (plugin)
import AppModal from 'AppModal.vue'
const Modal = {
install(Vue, options) {
this.EventBus = new Vue()
Vue.component('app-modal', AppModal)
Vue.prototype.$modal = {
show(params) {
Modal.EventBus.$emit('show', params)
}
}
}
}
export default Modal
main.js
import Modal from 'plugin.js'
// ...
Vue.use(Modal)
App.vue
<template>
<div id="app">
// ...
<app-modal/>
</div>
</template>
This looks pretty complicated. Why don't you use a ready-to-use popup component like this one? https://www.npmjs.com/package/#soldeplata/popper-vue

How to create infinite scroll in Vuetify Autocomplete component?

I have a page with Vuetify Autocomplete component, and REST API backend with '/vendors' method. This method takes limit, page and name parameters and returns JSON with id and name fields.
I made some code with lazy list load on user input event. But now I want to add the ability to load this list on user scroll event.
For example, by default there is a list of 100 vendors. User scrolled this list until the end, then "some event" is called and loads next 100 of vendors. Then user keeps scrolling and the action is repeated.
Is it possible to made this with Vuetify Autocomplete component, or should i use another library?
Example code of current component is shown below:
<template>
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
></v-autocomplete>
</template>
<script>
export default {
data () {
return {
page: 0,
limit: 100,
selectedVendorId: null,
vendors: [],
loading: true
}
},
created: function (){
this.getVendorsFromApi();
},
methods: {
getVendorsFromApi (event) {
return new Promise(() => {
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + this.limit
+ '&page=' + this.page
+ '&name=' + (event ? event.target.value : ''))
.then(response => {
this.vendors = response.data;
})
})
}
}
}
</script>
I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
>
<template v-slot:append-item>
<div v-intersect="endIntersect" />
</template>
</v-autocomplete>
...
export default {
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
let moreVendors = loadMoreFromApi()
this.vendors = [ ...this.vendors, ...moreVendors]
}
}
}
}
In my use case, I was using API Platform as a backend, using GraphQL pagination using a cursor.
<component
v-bind:is="canAdd ? 'v-combobox' : 'v-autocomplete'"
v-model="user"
:items="computedUsers"
:search-input.sync="search"
item-text="item.node.userProfile.username"
hide-details
rounded
solo
:filter="
(item, queryText, itemText) => {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
} "
:loading="loading"
item-value="username"
class="text-left pl-1"
color="blue-grey lighten-2"
:label="label"
>
<template v-slot:selection="{ item }">
<v-chip v-if="typeof item == 'object'">
<v-avatar left>
<v-img v-if="item.node.userProfile.image" :src="item.node.userProfile.image" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-avatar>
{{ item.node.userProfile.firstName }} {{ item.node.userProfile.lastName }}
</v-chip>
<v-chip v-else-if="typeof item == 'string'">
{{ item }}
</v-chip>
</template>
<template v-slot:item="{ item: { node } }">
<v-list-item-avatar >
<img v-if="node.userProfile.avatar" :src="node.userProfile.avatar" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-list-item-avatar>
<v-list-item-content class="text-left">
<v-list-item-title>
{{ $t('fullName', { firstName: node.userProfile.firstName, lastName: node.userProfile.lastName } )}}
</v-list-item-title>
<v-list-item-subtitle v-html="node.userProfile.username"></v-list-item-subtitle>
</v-list-item-content>
</template>
<template v-slot:append-item="">
<div v-intersect="endIntersect" >
</div>
</template>
</component>
import { VCombobox, VAutocomplete } from "vuetify/lib";
import debounce from "#/helpers/debounce"
import { SEARCH_USER_BY_USERNAME } from "#/graphql/UserQueries";
const RESULTS_TO_SHOW = 5
export default {
props: {
canAdd: {
type: Boolean,
default: false,
},
value: [Object, String],
label: String,
},
components: { VCombobox, VAutocomplete },
apollo: {
users: {
query: SEARCH_USER_BY_USERNAME,
variables() {
return {
username: this.search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
}
},
watchLoading(isLoading) {
this.loading = isLoading
},
skip() {
if (this.search) {
return !(this.search.length > 1)
}
return true
},
},
},
data() {
return {
user: this.value,
search: "",
cursor: null,
loading: false,
};
},
watch: {
user(newValue) {
let emit = newValue
if (newValue) {
emit = newValue.node
}
this.$emit("input", emit);
},
value(newValue) {
if (this.user && this.user.node != newValue) {
if (newValue == null) {
this.user = null
}
else {
this.user = { node: newValue };
}
}
},
search(newValue) {
this.debouncedSearch(newValue)
},
},
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting && this.users && this.users.pageInfo.hasNextPage) {
let cursor = this.users.pageInfo.endCursor
this.$apollo.queries.users.fetchMore({
variables: { cursor: cursor},
updateQuery: (previousResult, { fetchMoreResult }) => {
let edges = [
...previousResult.users.edges,
...fetchMoreResult.users.edges,
]
let pageInfo = fetchMoreResult.users.pageInfo;
return {
users: {
edges: edges,
pageInfo: pageInfo,
__typename: previousResult.users.__typename,
}
}
}
})
}
},
debouncedSearch: debounce(function (search) {
if (this.users) {
this.$apollo.queries.users.refetch({
username: search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
});
}
}, 500),
filter(item, queryText) {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
}
},
computed: {
computedUsers() {
if (this.users){
return this.users.edges
}
return []
},
skip() {
if (this.search) {
return this.search.length > 1
}
return false
}
}
};
</script>
Update June 12, 2021:
If you are using Vuetify 2.X, use Brettins' solution based on append-item slot and v-intersect directive.
Old answer:
Looks like it's not possible with default v-autocomplete component (at least in vuetify 1.5.16 or lower).
The component that provides the most similar functionality is VueInfiniteAutocomplete.
But keep in mind that in this case there may be problems with styles, validation, etc.
There is an example with this library.
<template>
<div>
<vue-infinite-autocomplete
:data-source="getAsyncOptions"
:fetch-size="limit"
v-on:select="handleOnSelect"
:value="autocompleteViewValue"
>
</vue-infinite-autocomplete>
</div>
</template>
<script>
export default {
data () {
return {
selectedVendorId : null,
limit: 100,
autocompleteViewValue: null
}
},
methods: {
getAsyncOptions(text, page, fetchSize) {
return new Promise((resolve, reject) => {
resolve(
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + fetchSize
+ '&page=' + page
+ '&name=' + text)
.then(response => {
//Response MUST contain 'id' and 'text' fields, and nothing else.
//If there are other fields, you should remove it here
//and create 'id' and 'text' fields in response JSON by yourself
return response.data;
})
)
});
},
handleOnSelect(selectedItem) {
this.autocompleteViewValue = selectedItem.text;
this.selectedVendorId = selectedItem.id;
}
}
}
</script>
P.S.: If you just want to use v-autocomplete component with server-side pagination, you could create a "Load more..." button using append-item slot, as suggested in this issue.

Passing custom template to stencil component

I'm working on to create a custom component using Stencil to replicate UI-select.
The component will be used like:
let items = [{name:"Abc", age: 10}, {name:"Xyz", age: 10}];
let itemString = JSON.stringify(items);
<dropdown-search placeholder="Select User" item-string='${itemString}'>
</dropdown-search>
Then the component is defined as
import {
Component,
State,
Prop,
Element,
} from '#stencil/core';
#Component({
tag: 'dropdown-search',
})
export class DropdownSearch {
#Element() dropdownEl: HTMLElement;
#State() isOpen: boolean = false;
#State() items: any = [];
#Prop() itemString: string = '';
#Prop() placeholder: string = '';
componentDidLoad() {
try {
this.items = JSON.parse(this.itemString);
} catch(e) {}
}
onClickDropdownHandler = (e: UIEvent) => {
e.preventDefault();
this.toggleDropdown();
}
toggleDropdown = () => {
this.isOpen = !this.isOpen;
if (this.isOpen) {
window.setTimeout(
() => {
this.dropdownEl.querySelector('input').focus();
},
0,
);
}
}
renderOptions = () => {
if (!Array.isArray(this.items) || !this.items.length) {
return null;
}
return this.items.map((item) => (
<li
>
<a href="javascript:" title="{item.name}">
{item.name}
<small class="d-block">age: {item.age}</small>
</a>
</li>
));
}
render() {
let dropdownClassName = (this.isOpen ? 'open' : '');
return (
<form name="myForm">
<div class="form-group">
<div
class={`btn-group dropdown ${dropdownClassName}`}
>
<button
class="btn btn-default dropdown-toggle"
onClick={this.onClickDropdownHandler}
>
{this.placeholder}
</button>
<ul class="dropdown-menu" role="menu">
<li>
<div class="input-group input-group-search">
<input
class="form-control"
type="search"
/>
</div>
</li>
{this.renderOptions()}
</ul>
</div>
</div>
</form>
);
}
}
The items are rendering fine. As as the user can pass a custom array of objects, so I need to customize the options template. So the user can pass it while using the component.
Right now I'm using a static template for the options within the component, like
<a href="javascript:" title="{item.name}">
{item.name}
<small class="d-block">age: {item.age}</small>
</a>
but I need a way to pass this template from where I'm using the template. I can't use slot there as I'm using the same template within all the options that running in a loop.
I am new with stencil but just tried to utilize an #Prop function for this, and it worked:
Component
#Component({
tag: 'dropdown-search',
styleUrl: 'dropdown-search.css',
shadow: true,
})
export class DropdownSearch {
// allow template function to be passed from outside
#Prop template!: (item: SelectItem) => any;
#State items!: SelectItem[] = [{name: 'dog'}, {name: 'cat'}, {name: 'elephant'}];
render() {
return (
<Host>
<p>Before Template</p>
{/* render the custom template here: */}
{this.items.map(item => this.template(item))}
<p>After Template</p>
</Host>
);
}
}
Providing custom template to this component:
class AppComponent {
render() {
// Here we define our custom template to be rendered in MyComponent
const CustomTemplate = (item: SelectItem) => <p key={item.name}>Hi, I am {item.name}</p>;
return (
<div class="app-home">
<dropdown-search template={CustomTemplate} />
</div>
);
}
}
result looks like this:

Errors upon Injecting services into my component in Angular 2

I am working on an app for my portfolio with Angular 2 where I have filtering functionality via checkboxes. These checkboxes apply name filtering to the lists of technologies I've used for each project I've worked on. In the app I also have a like functionality that allows you to indicate that you like a project. For this app I need to keep track of the number of likes that each project receives. In my code I would like to add a service to my project component that allows me to not loose the number of likes and filtered projects as I switch views with my router. In my code I have an array of Project objects, each take the following form- new Project( "Web Design Business Website", ["Wordpress", "Bootstrap","PHP","HTML5","CSS3"], 0 ) I am able to get the filtering and favoriting functionality to work if I don't use a service, but I need to use one for the aformentioned reasons and when I attempt to import in my service and use it, I am unable to see my template html on the screen and I get the following errors-
EXCEPTION: Error: Uncaught (in promise): EXCEPTION: Error in :0:0
ORIGINAL EXCEPTION: TypeError: Cannot read property 'filter' of undefined
ERROR CONTEXT:
[object Object]
My Project component code is as follows:
import { ROUTER_DIRECTIVES, Routes } from '#angular/router';
import { Component, OnInit } from '#angular/core';
import { Project } from './project';
import { ProjectService } from './project.service';
#Component({
selector: 'my-app',
host: {class: 'dashboard'},
templateUrl: 'app/app.component.html',
providers: [ProjectService]
})
export class ProjectsComponent implements OnInit {
allProjects: Project[];
constructor(private projectService: ProjectService) {
this.updateSelectedList();
}
getProjects(){
this.allProjects = this.projectService.getProjects();
}
ngOnInit(){
this.getProjects();
}
title: string = 'Filter Projects by Technology';
/* all possible tech */
technologyList : Array<any> = [
{
name:"Javascript",
checked: true
}, {
name: "PHP",
checked: true
}, {
name: "HTML5",
checked: true
}, {
name: "CSS3",
checked: true
}, {
name: "AngularJS",
checked: true
}, {
name: "BackboneJS",
checked: true
}, {
name: "KnockoutJS",
checked: true
}, {
name: "Bootstrap",
checked: true
}, {
name: "Wordpress",
checked: true
},
{
name: "Photoshop",
checked: true
}
];
/* projects that match the selected tech */
matchedProjects: Array<any> = []
/* The checked items in the list */
selectedTechnology: Array<string> = [];
favUp(project): boolean {
project.favUp();
return false;
}
onInteractionEvent(event: Event) {
var item = this.technologyList.find(
(val) => val.name === event.target.value
);
item.checked = !item.checked;
this.updateSelectedList();
}
updateSelectedList() {
let checkedNames =
this.technologyList.filter( (val) => val.checked === true).map(n => n.name);
this.matchedProjects = this.allProjects.filter(project => {
return this.containsAny(project.technologies, checkedNames)
});
}
containsAny(arr1, arr2) {
for(var i in arr1) {
if(arr2.indexOf( arr1[i] ) > -1){
return true;
}
}
return false;
};
}
My project class is located in another file that I import and is as follows:
export class Project {
name: string;
technologies: Array<any>;
favs: number;
constructor(name: string, technologies: Array<any>, favs: number) {
this.name = name;
this.technologies = technologies;
this.favs = favs || 0;
}
favUp(): void {
this.favs += 1;
}
}
My array of project objects is located in another file and is as follows:
import { Project } from './project';
/* all the projects worked on */
export var ALLPROJECTS: Project[] = [
new Project( "Web Design Business Website", ["Wordpress", "Bootstrap","PHP","HTML5","CSS3"], 0 ),
new Project( "Vocab Immersion Trainer App", ["AngularJS","Javascript","Bootstrap","HTML5","CSS3"], 0)
];
My project service is located in another file that I import and is as follows:
import { Injectable } from '#angular/core';
import { ALLPROJECTS } from './all-projects';
#Injectable()
export class ProjectService {
getProjects() {
return ALLPROJECTS;
}
}
My template html for the project component is as follows:
<p>
<a class="btn btn-large btn-primary" data-toggle="collapse" href="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
{{title}}
</a>
</p>
<div class="collapse" id="collapseExample">
<div class="card card-block">
<label *ngFor="let item of technologyList">
<input type="checkbox"
value="{{item.name}}"
[checked]="item.checked"
(change)="onInteractionEvent($event)">
{{ item.name }}
</label>
</div>
</div>
<h2>Featured Projects</h2>
<div *ngFor="let project of matchedProjects" class="container">
<div class="row">
<div class="col-sm-4 col-sm-offset-1 card">
<img src="http://placehold.it/350x150" class="card-img-top img-fluid img-rounded center-block" data-src="..." alt="Card image cap">
<div class="card-block text-xs-center">
<h4 class="card-title">Project Name: {{project.name}} </h4>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content. This is alot of text. It adds length to the paragraph. It adds bulk. I had to do it. It was very necessary for this example</p>
<a class="btn btn-primary">See live site</a>
</div>
<p> {{ project.favs }} Likes <a href (click)="favUp(project)">Like</a></p>
</div>
<div class="col-sm-6 text-xs-center">
<h2 >Technology used</h2>
<p>{{project.technologies}}</p>
</div>
</div>
</div>
My previous project component code that works but doesn't retain data between views is as follows
import { ROUTER_DIRECTIVES, Routes } from '#angular/router';
import { Component } from '#angular/core';
/* import { Suggestion, SuggestionsComponent, SuggestionsView } from './suggestions.component'; */
export class Project {
name: string;
technologies: Array<any>;
favs: number;
constructor(name: string, technologies: Array<any>, favs: number) {
this.name = name;
this.technologies = technologies;
this.favs = favs || 0;
}
favUp(): void {
this.favs += 1;
}
}
#Component({
selector: 'my-app',
host: {class: 'dashboard'},
templateUrl: 'app/projects.component.html'
})
export class ProjectsComponent {
title: string = 'Filter Projects by Technology';
/* all possible tech */
technologyList : Array<any> = [
{
name:"Javascript",
checked: true
}, {
name: "PHP",
checked: true
}, {
name: "HTML5",
checked: true
}, {
name: "CSS3",
checked: true
}, {
name: "AngularJS",
checked: true
}, {
name: "BackboneJS",
checked: true
}, {
name: "KnockoutJS",
checked: true
}, {
name: "Bootstrap",
checked: true
}, {
name: "Wordpress",
checked: true
},
{
name: "Photoshop",
checked: true
}
];
/* all the projects worked on */
allProjects = [
new Project( "Web Design Business Website", ["Wordpress", "Bootstrap","PHP","HTML5","CSS3"], 0 ),
new Project( "Vocab Immersion Trainer App", ["AngularJS","Javascript","Bootstrap","HTML5","CSS3"], 0)
];
/* projects that match the selected tech */
matchedProjects: Array<any> = []
/* The checked items in the list */
selectedTechnology: Array<string> = [];
favUp(project): boolean {
project.favUp();
return false;
}
constructor() {
this.updateSelectedList();
}
onInteractionEvent(event: Event) {
var item = this.technologyList.find(
(val) => val.name === event.target.value
);
item.checked = !item.checked;
this.updateSelectedList();
}
updateSelectedList() {
let checkedNames =
this.technologyList.filter( (val) => val.checked === true).map(n => n.name);
this.matchedProjects = this.allProjects.filter(project => {
return this.containsAny(project.technologies, checkedNames)
});
}
containsAny(arr1, arr2) {
for(var i in arr1) {
if(arr2.indexOf( arr1[i] ) > -1){
return true;
}
}
return false;
};
}
Here is the github link to the working version of the project before the incorporation of the service: Portfolio Application link

Categories

Resources