Hello everyone =) I have started to practice Angualar 9 and I really like the component approach. In order to practice property and eventbinding, i started a project of deckbuilding component with two lists. English is not my main language but I will try to explain clearly.
IMPORTANT : I really want to practice this tool, so could you, in the answer, explain your reasoning ? If you have solution tracks, i can search by myself too in order to learn.
GOAL :
I have two lists (CardlistComponent and DecklistComponent) and each time i click on one LI in CardListComponent (or DecklistComponent), this same element is removed but added to the other list.
COMPONENTS :
DeckbuilderComponent which contains my two other components (the two lists)
CardlistComponent (the first list)
DecklistComponent (the second list)
ISSUE :
I can remove the element in CardlistComponent, but I can't add the same element to the DecklistComponent. I guess the problem is the way I use arrays in Javascript.
DeckbuilderComponent (HTML) (Parent component)
<app-cardlist></app-cardlist>
<app-decklist
(cardAdded)="onCardAdded($event)">
</app-decklist>
*DeckbuilderComponent (TS) * (Parent component) I guess one of the problems is here because I duplicated the logic of add an item in the parent AND in DecklistComponent
#Output() cardAdded = new EventEmitter<{name: string}>();
decklist: Card[] = [
new Card('CardC'),
new Card('CardD')
]
onCardAdded(element : HTMLLIElement){
this.decklist.push({
name: element.textContent
})
}
CardlistComponent
#Output() cardRemoved = new EventEmitter<{cardName: string}>();
cardlist: Card[] = [
new Card('CardA'),
new Card('CardB')
]
removeCard(indexCard: number, element: HTMLLIElement){
this.cardlist.splice(indexCard, 1)
this.cardRemoved.emit({
cardName: element.textContent
});
DecklistComponent this list has to recieve the element removed in the first list
#Input() cardName: {name: string};
decklist: Card[] = [
new Card('CardC'),
new Card('CardD')
]
onCardAdded(element : HTMLLIElement){
this.decklist.push({
name: element.textContent
})
}
Here is the HTML of my two components just in case.
DecklistComponent (HTML)
<div class="container">
<div class="col-xs-12">
<ul class="list-group">
<li
*ngFor="let card of decklist; let indexCard=index"
class="list-group-item"
#cardName
>
<a href="#" class="list-group-item list-group-item-action" >{{ card.name }}</a>
</li>
</ul>
</div>
CardlistComponent (HTML)
<div class="container">
<div class="col-xs-12">
<ul class="list-group">
<li *ngFor="let card of cardlist; let indexCard=index" class="list-group-item">
<a href="#"
(click)="removeCard(indexCard, card)"
class="list-group-item list-group-item-action">{{ card.name }}</a>
</li>
</ul>
</div>
If you want something else I didn't mention, don't hesitate to tell me and have a nice day full of code =D
You can design DeckbuilderComponent as Container Component and CardlistComponent, DecklistComponent as Presentational Components
source
Container Components: These components know how to retrieve data from
the service layer. Note that the top-level component of a route is
usually a Container Component, and that is why this type of components
where originally named like that
Presentational Components - these components simply take data as input
and know how to display it on the screen. They also can emit custom
events
<deck-list (remove)="removeAndMoveTo($event, deckList, cardList)" [items]="deckList"></deck-list>
<card-list (remove)="removeAndMoveTo($event, cardList, deckList)" [items]="cardList"></card-list>
<div class="container">
<div class="col-xs-12">
<ul class="list-group">
<li *ngFor="let card of cardlist; let indexCard=index" class="list-group-item">
<a href="#"
(click)="removeCard(card)"
class="list-group-item list-group-item-action">{{ card.name }}</a>
</li>
</ul>
</div>
class CardlistComponent {
#Output("remove") removeCardEmitter: EventEmitter<Card>;
constructor() {
this.removeCardEmitter = new EventEmitter();
}
removeCard(card: Card){
this.removeCardEmitter.emit(card);
};
}
class DeckbuilderComponent {
cardList: Card[];
decklist: Card[];
constructor() {
this.cardList = [
new Card('CardA'),
new Card('CardB')
];
this.decklist = [
new Card('CardC'),
new Card('CardD')
];
}
removeAndMoveTo(card: Card, sourceList: Card[], targetList: Card[]) {
this.sourceList = this.sourceList.filter(pr => pr.id === card.id);
this.targetList.push(card);
}
}
Of course they are many solution to move element from one list to another. You can as well have one list and each card has some information to which group it belongs. Then value input binder would look like [items]="list.filter(pr => pr.isInCardList).
Another single-list approach might use rxjs. One observable which gets split with partition operator
Related
The premise of this question is that in the following TS block, I am creating an array that is made from the given map's keys and console logging to ensure that the arrays are created as needed.
public certKeys: String[];
public certMap: Map<String, DataObject[]> = new Map();
public allData: DataObject[];
#Input()
set data(data: DataObject[]) {
if (!data) { return; }
new Set(data.map(i => i.certTypeDescription)).forEach(i => {
this.certMap.set(i, []);
});
data.forEach(i => {
this.certMap.get(i.certTypeDescription).push(i);
});
this.certKeys = Array.from(this.certMap.keys());
this.allData = data;
console.log(this.certMap);
}
Now when this translates to the HTML portion of this, I am wanting to display the most recent record (or the [0] element) of each key array. This is already being accomplished. However, the other portion is that in the accordion drop down, I need to retrieve the rest of the elements save for the [0] element. below you will see what I have so far:
<app-basic-card myTitle="Data">
<i cardIcon class="uxd uxd-mode-edit uxd-lg uxd-pointer text-primary" (click)="openEditDialog()"></i>
<div cardBody class="accordion" *ngIf="allData; else loading">
<p *ngIf="allData?.length === 0">
No allData found...
</p>
<mat-accordion *ngIf="allData?.length>0">
<mat-expansion-panel *ngFor="let cert of certKeys">
<mat-expansion-panel-header>
<mat-panel-title class="list-group list-group-flush">
<ng-container>
<div>
<div class="w-50 float-left">{{cert}}</div>
<div class="w-50 float-right">
<i class="uxd uxd-lg" [ngClass]="getCertIcon(certMap.get(cert)[0]?.certificationResult)"></i>
{{getDateTaken(certMap.get(cert)[0].certificationDate)}}
</div>
</div>
</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container>
<div *ngFor = "let certKeys of allData">
<div class="w-50 float-left">{{cert}}</div>
<div class="w-50 float-right">
<i class="uxd uxd-lg" [ngClass]="getCertIcon(certMap.get(cert).certificationResult)"></i>
{{getDateTaken(certMap.get(cert).certDate)}}
</div>
</div>
</ng-container>
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template cardLoading #loading class="text-center">
<mat-spinner class="loading-spinner" style="margin:0 auto;" diameter="50"></mat-spinner>
</ng-template>
</app-basic-card>
My question is how do I accomplish retrieving every element but the [0] element of each key array? There is something that I very obviously am missing. I would appreciate any answers that are given and resources that may point me in the right direction. I thank you all for your time.
I don't know Angular tbh, but if you can modify the array you could use slice method, which returns a shallow copy of the original array, so the original will stay untouched.
Can you change this line:
<div *ngFor = "let certKeys of allData">
Into this:
<div *ngFor = "let certKeys of allData.slice(1)">
?
Working snippet of the slice() function.
const items = [1, 2, 3, 4];
const itemsWithoutTheFirstItem = items.slice(1);
// Target array contains all but first item
console.log(itemsWithoutTheFirstItem);
// Original array stays the same
console.log(items);
I think of two options right now. The first one is using *ngIf or a simple Pipe.
First option:
<mat-expansion-panel *ngFor="let cert of certKeys; let index = index">
<mat-expansion-panel-header *ngIf="index > 0">
...
</mat-expansion-panel>
Second option:
Create a Angular Pipe, which returns a new array except the first entry:
#Pipe({name: 'ignoreFirst'})
export class IgnoreFirstEntryPipe implements PipeTransform {
transform(arr: any[]) {
// Returns a new array without the original first entry
return arr.slice(1);
}
}
And in your html:
<mat-expansion-panel *ngFor="let cert of certKeys | ignoreFirst">
<mat-expansion-panel-header>
...
</mat-expansion-panel>
I'm using React version 16.6.3. I need some help to implement active menu item in sidebar. I need to do this thing: when the user clicks on any <li> in sidebar, the <li> must become active (for example, it gets class "active").
If the <li> is active and have treeview class, then, inside it, set to <ul> class "show" (if any)
Here's the code:
import React , { Component } from "react"
export default class Sidebar extends Component {
constructor(props) {
super(props);
this.state = { 'activeItem': 0 }
}
render() {
return (
<div className="main-sidebar">
<section className="sidebar">
<ul className="sidebar-menu tree">
<li className="nav-divider"></li>
<li className="header">PERSONAL</li>
<li>
<a href="#">
<span>Dashboard</span>
</a>
</li>
<li className="treeview">
<a href="#">
<span>Application</span>
</a>
<ul class="treeview-menu">
<li>Chat app</li>
<li>Project</li>
<li>Contact / Employee</li>
</ul>
</li>
<li className="treeview">
<a href="#">
<span>Application</span>
</a>
</li>
</ul>
</section>
</div>
)
}
}
The first question you should ask yourself is (if not always) how you store the data of the user selection. You might have multiple activeItem because this is a tree, ex. multiple branches can be open or close at the same time. You need to look for a data structure to hold that thinking first.
Here's a simple one, give each item an id/key, <li key="1-0-0"> and then you can track onChange event of this li and then inside onChange you can use a flat array structure to store the current state of this simple tree. For example, {'1-0-0': true }
There're lots of different ways to do data part, the above one is a simple idea to get you started. And after that, based on the captured data, you can then update the children attribute of each node, ex. <li className={ getNodeAttr('1-0-0')> assuming getNodeAttr is a utility function that give you back the class name string given the node name.
You should not bind active state on a navigation sidebar because if a user type a route directly in a browser the menu item won't be highlighted.
Instead, try to bind the active state to the route with NavLink (if you use react-router):
ref: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/NavLink.js
I want a component to return sibling tags instead of a single parent tag. For example render method of DefaultItems should return something that renders as <li/><li/> instead of say, <div><li/><li/></div>. But React needs enclosing tag when returning something.
Is there some null tag which does not insert into the final markup?
JSX:
<ul>
<DefaultItems/>
<OtherItems/>
<li>
Desired:
<ul>
<li>default 1</li>
<li>default 2</li>
<li>other 1</li>
<li>
The best I can do, notice the unwanted div around the li:
<ul>
<div>
<li>default 1</li>
<li>default 2</li>
</div>
<li>other 1</li>
<li>
I wish a "null tag" exists:
return <nulltag>
<li>default 1</li>
<li>default 2</li>
</nulltag>
A react components, render function must return a single element.
This 1 to 1 relationship makes components much easier to reason about.
What it means for us developers is that we have to divide our apps into components that follow this rule.
Rather than having a component that returns two li elements, you must return one. This is a good thing. It makes your components much more reusable.
A common pattern is to have one component that represents a list (of something), and a different component that represents the items in the list.
instead of:
...
<ul>
<ItemList items={list}/>
</ul>
...
you have:
...
<ItemList items={list}/>
...
you can still create a list of different items
...
makeList(items) {
return items.map(function (item) {
// return what ever component you want based on the item
});
},
render () {
return (
<ul>
{makeList(this.props.items)}
</ul>
);
},
...
You can work with arrays in React, but maybe you should reconsider the logic behind creating the list items.
Instead of having separate components for different data types, you can have a factory for creating arrays of list items inside the render method that passes the item data, f.ex something like this:
var i = 0
var listFactory = function(item) {
return <ListItem item={item} key={i++} />
}
var DefaultItems = defaultData.map(listFactory)
var OtherItems = otherData.map(listFactory)
<ul>
<DefaultItems />
<OtherItems />
</ul>
IMO, having a single component for a certain type (ListItem) is preferred over having separate components for different content or data.
Basically, what I am attempting to do is create a Slideshow. The reason why I am using Angular for this is to get the benifits from templating and hopefully load only one template to the page at a time.
I have my data in a JSON file which the controller gets:
JSON:
{
"prop_title" : "Hyde Lane, Hyde",
"prop_postcode" : "SP2 7AP",
"prop_price" : "",
"prop_image" : "",
"prop_desc" : "",
"template" : "views/prop-thumbs.html",
"info_image" : "",
"duration" : "4000"
},
Controller:
function AppCtrl($scope, $http) {
//GET THE JSON FILE
$http.get('json/pages.json').success(function (data) {
//ASSIGN THE PAGES TO THE SCOPE
$scope.pages = data;
//LOOP THROUGH THE DATA AND ASSIGN THE VIEW
for (var i = $scope.pages.length - 1; i >= 0; i--) {
};
});
}
As you can see from my JSON file, each element contains the view file to use, and the duration that it needs to stay on screen for. Also, as you can see in my controller, I have begun creating a for loop that will hopefully contain my code that will assign view on a timer. Is this possible? Whats the easiest and best way to do this?
Any help seriously appreciated, I have pretty tried everything now!
as Mark Rajcok pointed in the comments, you will probably need to use a $timeout
Here is what I would have done : http://jsfiddle.net/DotDotDot/zbg57/
The html part is quite simple, I just tell angular to include what is in the whatToInclude variable :
<div ng-include='whatToInclude'></div>
On the controller side, I initialize the data with a template and a counter, then I define a nextSlide function, which will call the next template with the timeout passed in parameter. I call this function to start the loop with the initials parameter (0s timeout, first element of the data)
$scope.whatToInclude='tpl1.html';
$scope.count=0;
$scope.nextSlide=function(timeOut){
$timeout(function(){
$scope.whatToInclude=$scope.data[$scope.count].template
$scope.count+=1
if($scope.count>=$scope.data.length)
$scope.count=0
$scope.nextSlide($scope.data[$scope.count].duration)
},timeOut);
};
$scope.nextSlide(0)
I think this can be a good start for a slideshow ;)
Have fun
I made a tutorial with a similar goal in mind:
https://www.youtube.com/watch?v=9COtsDovNpM
Basically store the views in an array of some sort and then based on a variable display that template. Angular will not load the template until ng-show='true' so they won't load all at once but as their clicked or cycled through.
Code example from my tutorial.
Replace ng-switch with whatever works for you.
The common premise is "show this template when that is equal to true".
<div class="tabbable tabs-left">
<ul class="nav nav-tabs">
<li ng-class="{active: main.active.tab == 'info'}">
<a ng-click='main.active.tab = "info"'>Settings</a>
</li>
<li ng-class="{active: main.active.tab == 'categories'}">
<a ng-click='main.active.tab = "categories"'>Categories</a>
</li>
<li ng-class="{active: main.active.tab == 'pages'}">
<a ng-click='main.active.tab = "pages"'>Pages</a>
</li>
<li ng-class="{active: main.active.tab == 'locations'}">
<a ng-click='main.active.tab = "locations"; main.locationgroup = {}'>Locations</a>
</li>
<li ng-class="{active: main.active.tab == 'experts'}">
<a ng-click='main.active.tab = "experts";'>Experts</a>
</li>
<li ng-class="{active: main.active.tab == 'resources'}">
<a ng-click='main.active.tab = "resources"'>Resources</a>
</li>
</ul>
<div class="tab-content">
<div ng-switch='main.active.tab'>
<div
ng-switch-when='info'
ng-include='"/apps/series/views/tabs/info.html"'></div>
<div
ng-switch-when='categories'
ng-include='"/apps/series/views/tabs/categories.html"'></div>
<div
ng-switch-when='pages'
ng-include='"/apps/series/views/tabs/pages.html"'></div>
<div
ng-switch-when='locations'
ng-include='"/apps/series/views/tabs/locations.html"'></div>
<div
ng-switch-when='experts'
ng-include='"/apps/series/views/tabs/experts.html"'></div>
<div
ng-switch-when='resources'
ng-include='"/apps/series/views/tabs/resources.html"'></div>
</div>
</div>
</div>
I am just getting started with Angular and am still trying to get a feel for how it works. I have 2 questions about a list with ng-repeat. I will post my code first.
HTML
<div class="row">
<div class="span2">
<ul class="nav nav-pills nav-stacked">
<li ng-repeat="class in classes | orderBy:orderProp" ng-class="activeClass">
<a ng-click="loadRoster(class)">{{class.class_name}}</a>
</li>
</ul>
</div>
<div class="span2">
<ul class="nav nav-pills nav-stacked">
<li ng-repeat="student in students | orderBy:orderProp">
<a ng-click="enterPassword(student)">{{student.student_name}}</a>
</li>
</ul>
</div>
</div>
Controller
function ClassListCtrl($scope, $http) {
$scope.loadedList=[];
$scope.students=[];
$scope.activeClass='';
$scope.orderProp = 'alphabetical';
$http.get('data/class.json').success(function(data) {
$scope.classes = data;
});
$scope.loadRoster=function(classlist){
$scope.selectedClass=classlist;
$scope.activeClass='active';
if($scope.loadedList[classlist.class_id]== undefined){
$http.get('data/class_'+classlist.class_id+'.json').success(function(data){
$scope.loadedList[classlist.class_id]=data;
$scope.students=data;
});
}
$scope.students=$scope.loadedList[classlist.class_id];
}
$scope.studentClick=function(student){
}
}
Ok, this pulls the data just fine. I can click on a class and it loads the students in that class. To cut down on AJAX calls, I store those that are clicked and if that class is clicked again, it will fetch it from memory instead of making the call. This is fine as class rosters are static for the most part.
I am trying to have it load a default class roster (the first alphabetically) and mark just that list item as active.
Secondly, when they click a list item, I want to change that list item to active, and change the class of the other ones back to nothing. I am not sure how to do that either. As it stands, none are active now, but when I click one, all become active.
in Html ng-class, if class_id matches $scope.activeClass set class of li to "active"
<li ng-repeat="class in classes | orderBy:orderProp" ng-class="{'active':class.class_id == activeClass}">
in Controller when $scope.loadRoster is called set $scope.activeClass to class_id of the class passed to function
$scope.loadRoster=function(classlist){
$scope.selectedClass=classlist;
$scope.activeClass=classlist.class_id;
for your default active class just set $scope.activeClass to class_id of the class