I created a new Angular 7 app using the Angular CLI. I left the configuration/setup the default and started adding my code on-top of it. The AppComponent makes a service call to fetch some blog posts and pass them to a child component that renders them.
app.component.html
<div class="container">
<header class="header-site">
<p class="site-title">Sully<p>
<p class="site-tagline">Code-monkey</p>
<p class="site-description">A sotware development blog with a touch of tech and a dash of life.</p>
</header>
<div *ngFor='let post of posts'>
<app-blog-post [post]="post"></app-blog-post>
</div>
</div>
<router-outlet></router-outlet>
app.component.ts
#Component({selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css']})
export class AppComponent {
posts: Post[];
constructor(private blogService: BlogService) {}
async ngOnInit() {
await this.loadPosts();
}
async loadPosts() {
this.posts = await this.blogService.getPosts();
}
}
This loads the first 10 posts and renders them with my app-blog-post child component.
blog-post-component.html
<article>
<header>
<h1 class="post-title"><a [routerLink]="['/blog', post.path]">{{post.title}}</a></h1>
<h4 class="post-details">{{post.details}}</h4>
</header>
<div>
<markdown ngPreserveWhitespaces [data]="post.content">
</markdown>
</div>
</article>
blog-post-component.ts
#Component({
selector: 'app-blog-post',
templateUrl: './blog-post.component.html',
styleUrls: ['./blog-post.component.css']
})
export class BlogPostComponent implements OnInit {
#Input() post: Post;
}
What happens is that it renders the first 10 blog posts as expected. Now, in the app-routing.module.ts class, I've added /blog/:id as a route, which my child component routes to when you click the post title.
const routes: Routes = [
{
path: 'blog/:id',
component: BlogPostComponent
}
];
I don't understand the routing mechanics here. When I navigate to the post, via the router, nothing happens. I see the URL path change in the browsers URL bar, but the content doesn't change.
What I'm trying to do is replace the 10 posts in the current view with the single post that's already been fetched as the only post on the page, when I hit that specific route. I've read through the docs but can't tell how to replace the content already in the view, with a subset of that content using the component already created. I'm not sure if I have to move the rendering of 10 posts off to a unique route, and just keep the router-outlet as the only element in the app.component.html, and for the '/' route, route to the component containing the top 10 posts. I'm worried that makes sharing the post data I've already fetched, between sibling components, more difficult as the parent now has to push/pull between the two children. Is that a backwards way of handling it?
Further still, each blog post has a unique route. I'm porting my blog from an existing hosted service where the path to a post is /blog/year/month/day/title. Can my Routes object be as simple as /blog/:year/:month/:day/:title? Since I can't get the routing working, I'm unable to test the routing itself and see if that's doable. I want to keep the same routing so existing bookmarks and search engine result links aren't broken.
When you utilise the routing mechanism, the specified component gets rendered in the appropriate router-outlet. I suspect that what you see when you navigate to a route, is the selected post rendered at the very bottom of the page.
As you allude to, if you would like to render the list of default blog posts, you will need to create a separate route/component for it, letting the router-outlet render it for you.
The way the routing works, is by going through each entry in your routes list and checking the current path for a match. This means the order is important. If you want to track both /blog/:year/:month/:day/:title and blog/:id, you will want to order them most specific first. And if you want an empty URL to point to a landing page, as you have above you could put something like { path: '', pathMatch: 'full', [component/redirectTo] } at the bottom of your route list.
Related
In React, you can pass entire components into other components, and then easily display those components in the respective div. This referring not to imports, but rather function parameters so what is displayed will vary.
In Angular, you use #Input() and #Output() to do the same thing with values and functions, but what about components? How do you do this with components? I'm not talking about imports handled by the module file or at the top of the file; I'm talking about parameters that will vary based on the runtime of your program.
I.e, I want to convert the following React code into Angular, where children is another React component passed in via ReactNode:
const ReactComponent = (props) => {
return (
<div>
{props.children}
</div>
);
};
Also I apologize if any of my terminology is incorrect; I'm new to Angular and I'm coming from a (limited) React background.
I tried using #Input() with a parameter of type "any" but this doesn't seem right.
Dynamically, like React, you can use ng-content. This is the opposite of Reacts { props.children }
Use it so (Component - Code Behind)
#Component({
selector: 'btn',
template: `
<button class='btn' (click)="do()">
<ng-content></ng-content>
</button>
`
})
export class ButtonComponent{
do(){
console.log("Ok")
}
}
And the HTML
<div>
<btn>
This is a dynamic Button
</btn>
</div>
This is a dynamic Button can be a component, too. So like this:
<div>
<btn>
<div><span>Hello! </span><button>I'm a button inside a button!?</button></div>
<app-my-custom-component [data]="bindAnything"></app-my-custom-component>
</btn>
</div>
The Result can be look like this:
And here you can play with this on Stackblitz.
Read all about it in the official documentation here.
Content projection
This topic describes how to use content projection to create flexible, reusable components.
To view or download the example code used in this topic, see the live example / download example.
Content projection is a pattern in which you insert, or project, the content you want to use inside another component. For example, you could have a Card component that accepts content provided by another component.
parent.component.html
<div>
<child-component></child-component>
</div>
child.component.ts
#Component({ selector: 'child-component' })
export class ChildComponent {}
This is a very quick way to show how Angular components can be nested. In this example you have a parent component template file (parent.component.html) and inside this file you can include any other component by their selector. In the example I include 'child-component' and show child component ts file.
If you want to make child component dynamic (if I understand you correctly - to pass data from parent to child) and for example pass string value into it, you can:
parent.component.html
<div>
<child-component name="Andrei"></child-component>
</div>
child.component.ts
#Component({ selector: 'child-component' })
export class ChildComponent {
#Input() name: String;
}
More on Angular components communication: https://angular.io/guide/component-interaction
I have the following problem, I haven't been able to find a solution to this:
I have a list of videos(they are played on the listing page) when I load a listing page
<ion-col class="category-item travel-category" size="12" *ngFor="let item of videos"
[routerLink]="['/app/video-admin/details', item.IdVideo]">
<app-video-player [videoId]="item.IdVideo">
</app-video-player >
</ion-col>
So what I need is to continue playing the video that is selected when it goes to the video detail page, Since the video is already playing I don't want to connect to the webrtc source again, it would take about 10 seconds to re-connect, how could I reuse the (app-video-player) component in the detail page "/app/video-admin/details" and keeping its state?
The easiest would be to reconnect and search where it was when clicked, but that means reconnection and searching where it was on the previous page(route) but that is exactly what I want to avoid, I think that's not a good solution.
The detail layout would have more controls, and basically only the selected video running
Any idea guys? thanks
You would need to design the layout such that the video player component is not destroyed when navigating to the new route.
That means your route should not be navigating to a new component, but rather it should be navigating within a component that contains both a video player and a details component. Any component can have a router outlet in it, and you can use child routes to navigate.
It's not clear what you want the layout to be, but here's the simplest example, with the details component underneath the list of videos:
Component HTML
<ion-col class="category-item travel-category" size="12" *ngFor="let item of videos"
[routerLink]="['details', item.IdVideo]">
<app-video-player [videoId]="item.IdVideo">
</app-video-player >
</ion-col>
<router-outlet></router-outlet>
Routing Module
const routes: Routes = [
{
path: 'app/video-admin',
component: VideoAdminComponent,
children: [
{
path: 'details/:id',
component: VideoDetailsComponent,
},
],
},
];
Obviously this isn't the layout you want, but this should give you the general knowledge to follow through with what you actually want.
Docs: https://angular.io/guide/router#nesting-routes
I will preface this by saying that there are several answers online for this exact same purpose with an Angular app. However, none of these appear to be working for me.
I have so far tried nearly everything from these issues
1 , 2 , 3 , 4 and a couple of other but I am still not getting the correct behavior.
Currently, if I am scrolled down and viewing a route, then I click on the item to view a new route via routerLink, the page stays in the same position. This causes some issues especially when on a smaller screen or when looking down a long list of items.
In the current app, my AppComponent is as follows (I am using Angular Material):
<app-header></app-header>
<mat-sidenav-container class="main-sidenav">
<mat-sidenav class="sidenav"#sidenav [mode]="mode" [opened]="openSidenav">
<app-sidenav></app-sidenav>
</mat-sidenav>
<mat-sidenav-content>
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
The router-outlet is displaying one of the following:
A workout page with a list and filter
A workout "detail page" that shows the workout description
In the workout list, when an item is clicked, it has a routerLink: <a [routerLink]="[i]"> where i is the index of the current workout coming from an array (from a firebase database in my case).
Here is the stackblitz to this code. The workouts-page can be found at src/app/features/workouts-page/ and then the workout-list, workout-filter, and workout-detail showing up accordingly. However, when clicked on the routerLink, the scroll position stays the same on the workout detail page.
Since Angular 6+, the recommended method has been to use, within your routing module:
...
imports: [RouterModule.forRoot(routes,{scrollPositionRestoration: 'top'})
...
At the same time, you could also use "enabled" and get the same result. This does not change any behavior in the application I am running. Furthermore, I have tried a few "work-arounds" such as adding the following to my main component:
export class MyAppComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit() {
this.router.events.subscribe((evt) => {
if (!(evt instanceof NavigationEnd)) {
return;
}
window.scrollTo(0, 0)
});
}
}
This workaround and many others have once again not changed the behavior and it leaves the page in the same scroll position as before the router was clicked. One different method from the GitHub request is to add an (activate)="onActivate($event) to the <router-outlet>, where the function runs window.scrollTo(0, 0);. Still, this doesn't seem to be working. I can even put the window.scrollTo(0,0) or scrollTop() in the ngOnInit of any component related to the router outlet, and you guessed it, still no change.
I figured this had to do with the css of the page altogether so I changed my body and html positioning as mentioned in stack overflow issues with the same end result.
Is there a way that I can "reset" the scroll position when a route is clicked with the routerLink property so the page doesn't stay in the same scrolled position when the route is loaded?
ScrollPositionRestoration works properly with full page scroll (body getting scrolled).
In your case internal divs are scrollable. To restore scroll for those you need to customize the scroll restore process.
Here is a blog link which talks about the same https://www.bennadel.com/blog/3534-restoring-and-resetting-the-scroll-position-using-the-navigationstart-event-in-angular-7-0-4.htm
Base idea is to storage scroll position before navigation and restore them if you visit the page due to pop state.
Nikhil gave a method for the ScrollPositionRestoration and helped me find the reason this wasn't working, but I ended up finding a way to do this with a simple activate function.
Like he mentioned, the reason this wasn't working is due to the "body" being reset on the scroll. Since the fixed header was at the top of the body, it was already in the "top" scroll position all the time. To fix this, I added the following simple code:
In the AppComponent HTML
...
<mat-sidenav-content id="detail">
<router-outlet (activate)="resetPosition();"></router-outlet>
</mat-sidenav-content>
...
Note, the (activate)="resetPosition(); causes the function to run any time a router is activated (or any time you go to a new router). Here is the function added to the app.component.ts:
AppComponent TS
resetPosition() {
let myDiv = document.getElementById("detail");
myDiv.scrollTop = 0;
}
This takes the id="detail" from the html container and then sets the scroll position to the top of this div rather than the entire body which was already at the top.
So I have many components which have a sidebar and navbar code written in all of them except one component. So I did the reasonable thing and made both of them(sidebar and navbar) separate components.
Now if I import them in App.vue, it shows in all the components including the one I don't need them in.
How do I go about it? Btw, the App.vue is my central point and where I'm loading router-view from
As answered by #Badgy
Make a v-if="$route.path !== 'yourpathwhereyoudontwantthenavbar
Example:
// this will not display the sidebar on the register page
<Sidebar v-if="$route.path !== '/register'" />
Another Alternative by myself:
Define where you want these components to appear in the meta options of your route objects https://router.vuejs.org/guide/advanced/meta.html. It's more or less just a way to attach arbitrary information which you can fetch from the current route to determine e.g. your layouting
const router = new Router({
routes: [
{
path: '/users',
name: 'users',
component: Users,
meta: { sidebar: true, navbar: true },
},
},
})
// This will make the sidebar appear in the user page
<sidebar v-if="$route.meta.sidebar">
You can do <sidebar v-if="!$route.meta.sidebar"> if you don't want it just in 1 component
I'm working on an app in Angular 4. In one view I'm using some svgs placed between data that is retrieved from API request. It looks like this:
<div>
<div>
<svg><use xlink:href="./assets/svg/machine.svg#message-icon"></use></svg>
</div>
<div>{{machine.state}}</div>
<div>
<svg><use xlink:href="./assets/svg/machine.svg#settings-icon"></use></svg>
</div>
</div>
All svgs are put in one file (machine.svg), and are defined in < symbol > tags with ids.
Now my problem is when I set interval every 2 seconds to update data from API request, my svg icons appear to flicker with every update, but it only happens in Chrome.
I've checked the network logs and it seems that whole svg file is downloaded with every API request:
While in Mozilla everything works fine, svg is downloaded only once:
I've tried to put svg in < object > tag, but the requests are even more numerous. Putting every single svg in < img > tag seemed to resolve the problem with requests, but I would prefer to be in control of "fill" property. Putting the whole svg directly on the page solved problem too, but it doesn't seem to be a clean solution.
My question is if there is a way to retrieve svg from a file without Chrome downloading it constantly?
Also faced with this situation, the easiest way for me is to create a separate shared component that contains all the icons I need and which takes the parameters I need. For me, this is the fastest and most flexible solution.
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'ngx-svg-icon',
templateUrl: './svg-icon.component.html',
styleUrls: ['./svg-icon.component.scss']
})
export class SvgIconComponent implements OnInit {
#Input() icon: string;
#Input() fill: string;
constructor() {}
ngOnInit() {}
}
Usage:
<ngx-svg-icon [icon]="'settings'" [fill]="'#ededed'"></ngx-svg-icon>
Try to configure your server to add cache-control headers to those requests. I have the same issue on localhost, but on my production server icons come with cache-control: max-age=172800 header and are not re-requested again.