Angular 2 Beta0 - Cant get Event Emitters to work - events

This is driving me nuts. I have an Angular2 application that I am trying to finish the login feature on. All I'm trying to do at this point is update the Navbar component template with the logged in users username when they login successfully. I am attempting to use EventEmitter to do this but the subscribe part never fires.
Everything builds/compiles OK - no errors. And the navbar does update, but only after a hard refresh. I'm trying to update it without the user needing to refresh
LoginService
#Injectable()
export class LoginService {
constructor(..., private userService: UserService) {
//
}
postLogin(f: ControlGroup) {
// Submits Login Form Etc.
// Calls UserService to 'Set User'
this.userService.setUser(user);
}
}
UserService
#Injectable()
export class UserService {
// My EventEmitter Here (I've tried without #Ouput() too)
#Output() userChange: EventEmitter<any> = new EventEmitter();
constructor() {
//
}
setUser(user: any) {
// Do some stuff with user
this.localStorage.setObject('user', user);
// Emit User Change Event (This logs to console OK)
console.log('Emitting User');
this.userChange.emit({ user: user });
}
}
NavBar Component
This is where I want to update User on Event Fire.
#Component({
selector: "navbar",
directives: [<any>RouterLink, <any>NgIf],
providers: [UserService],
template: require('./index.html'),
//... I've tried 'events:[], inputs:[], properties[] - No success
})
export class Navbar {
/**
* #type {}
*/
user: any = {};
constructor(private userService: UserService) {
let self = this;
// I expect this to log to console, but never does
userService.userChange.subscribe((data) => {
console.log('Caught Event');
self.user = data.user;
});
}
}
Navbar Template
<!-- User Authed Menu -->
<li *ngIf="loggedIn" class="nav-item dropdown">
<a class="nav-link dropdown-toggle" (click)="userDrop = !userDrop" href="#" role="button">
{{ user.username }}
</a>
<div class="dropdown-menu" [hidden]="!userDrop">
<a class="dropdown-item" [routerLink]="['./Login']" (click)="userDrop = !userDrop">Logout</a>
</div>
</li>
NavbarComponent is under an AppComponent as well, not sure if that matters.
I have also tried just updating a user object on the shared UserService and referencing it from nav component, but that wasn't working either. I have a fairly strong angular 1 background and I am still lost on why this isn't working. Any help would be really appreciated.

As discussed in the OP comments, because the NavBar component has providers: [UserService], it was injecting its own instance of the service.
Removing providers: [UserService] from the NavBar component and adding it further up the component tree allows multiple components to share one instance.
For something like a UserService, where you likely only want one instance, you can put providers: [UserService] in the root component, or specify the service in the call to bootstrap(AppComponent, [UserService]).

Related

How to Change Navbar Component Link Buttons Based on User being Logged in or Logged out using JavaScript and React JSX

I am trying to have the navigation component in React change from Login to Logout after the user logs in to their profile page.
I am loading the pages in my localhost while in development. The user can already log in successfully. I have tried importing the loggedIn method from my auth.js file in my utils folder on the client side. I then placed a conditional of if logged in then render the Logout link else render the Login and Sign Up link/button. VScode is giving me errors once I place the else conditional in the JSX. What am I doing wrong?
Here is the code for my Navigation.js Component:
import "bootstrap/dist/css/bootstrap.css";
import { Container, Nav, Navbar, Button } from "react-bootstrap";
import siteLogo from "../../assets/images/logo.png";
import loggedIn from '../../utils/auth';
function Navigation() {
return (
<div className="App">
<Navbar
bg="dark"
variant="dark"
sticky="top"
expand="sm"
collapseOnSelect
>
<Container>
<Navbar.Brand>
<Nav.Link href="/">
<img alt="" src={siteLogo} className="site-logo"></img>
</Nav.Link>
</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
<Nav className="me-auto">
<Nav.Link href="/">Home</Nav.Link>
<Nav.Link href="/about">About</Nav.Link>
<Nav.Link href="/contact">Contact</Nav.Link>
</Nav>
<Nav>
{{if:loggedIn}}
<Nav.Link href="/">Logout</Nav.Link>
{{else}}
<Nav.Link href="/signin">Login</Nav.Link>
<Button href="/signup" className="mx-2" variant="primary">
Sign up
</Button>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
</div>
);
}
export default Navigation;
Here is the code for my Auth.js utils:
import decode from 'jwt-decode';
class AuthService {
getProfile() {
return decode(this.getToken());
}
loggedIn() {
// Checks if there is a saved token and it's still valid
const token = this.getToken();
return !!token && !this.isTokenExpired(token); // handwaiving here
}
isTokenExpired(token) {
try {
const decoded = decode(token);
if (decoded.exp < Date.now() / 1000) {
return true;
} else return false;
} catch (err) {
return false;
}
}
getToken() {
// Retrieves the user token from localStorage
return localStorage.getItem('id_token');
}
login(idToken) {
// Saves user token to localStorage
localStorage.setItem('id_token', idToken);
window.location.assign('/Profile');
}
logout() {
// Clear user token and profile data from localStorage
// axios.defaults.headers.common["Authorization"] = null;
localStorage.removeItem('id_token');
// this will reload the page and reset the state of the application
window.location.assign('/');
}
}
export default new AuthService();
Here is a screenshot of the navigation after the user has already logged in:
I'm still working on the profile page itself, so please ignore the white space.
you can use conditional rendering, Here you can find the documentation

How to use the pageContext in SPFx?

I am trying to get a value from the current page by using the pageContext but I am getting either undefined or 404.
This is the situation:
In the Site pages library there are several news pages. Each news page has some tags attached to them. This tags lives in a custom column in the Site Pages library.
There are news that have 1 tag and other several tags. It can be the situation where two or more news share the same tag(s).
The goal is when I open a news page the tags that are attached to that news are also visible.
Until now I am using #pnp/pnpjs and the code looks like this:
var result: any = await sp.web.lists.getByTitle("Site Pages")
.items.getById(15)
.select("Tags")
.get();
return await result.Tags;
And it is giving me 404 error
I also tried this one:
this.context.pageContext.list('Site Pages').listItem['Tags'].get().then((items: any[]) => {
console.log(items);
});
But it giving me Cannot read property 'list' of undefined
Du you have an idea how can get the value of the Tags column asociated with the current news?
Here is an update
Now I am getting the right tag. The question now is how to show it in the screen?
import * as React from 'react';
import styles from './ReadTags.module.scss';
import { IReadTagsProps } from './IReadTagsProps';
import { sp } from '#pnp/pnpjs';
export default class ReadTags extends React.Component<IReadTagsProps, {}> {
constructor(props: IReadTagsProps) {
super(props);
}
private async getTags() {
var id = this.props.context.pageContext.listItem.id;
var result: any = await sp.web.lists.getByTitle("Site Pages")
.items.getById(id)
.select("Tags")
.get();
return await result.Tags;
}
public render(): React.ReactElement<IReadTagsProps> {
console.log(this.getTags());
return (
<div className={ styles.readTags }>
<div className={ styles.container }>
<div className={ styles.row }>
<div className={ styles.column }>
</div>
</div>
</div>
</div>
);
}
}
Regards
Amerco
What you'll probably want to do is store your tags in the state of your component. Then you can show these (if the value from state is not empty) during your render. I can highly recommend working through the React tutorial to understand React lifecycle and state/props.
https://reactjs.org/tutorial/tutorial.html
https://reactjs.org/docs/state-and-lifecycle.html
Something with getting your data in componentDidMount, storing it in the state by using this.setState and then running through them in render with this.state.tags. It's more of a React question then a SPFx question :)
There's a ton of samples here with SPFx and React:
https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples

Aurelia - Passthrough Routes

Is it possible to render static HTML fragments without having an associated view-model in Aurelia? For instance, I have a typical header, body, footer layout. In body, I have the router view. There are a set of links in the footer such as FAQs upon clicking which I want to render a view in the body area.
When I try to define a route config for the faq route, the config is expecting one of You must specify a "moduleId:", "redirect:", "navigationStrategy:", or "viewPorts:".
The temporary work around that I have is to create a passthrough view model that doesn't do anything. This is resulting in a bunch of passthrough view model classes. I am sure I am doing something wrong.
I couldn't find any help online with this use-case. Any references will be highly appreciated.
It seems like you're looking for an inlineView("<your html here/>") type of functionality for routes so that navigating to the target route will directly render the HTML in the router-view element.
This is not directly possible with aurelia-router because without a ViewModel, no ActivationStrategy can be invoked. Aurelia-router wants to call canActivate, activate, canDeactivate, deactivate on something.
However, if you simply want to define markup programmatically, and you don't want to declare a ViewModel for each individual piece of markup, then that can be solved quite neatly with the compose element in combination with inlineViewStrategy.
With this approach, you only need one View/ViewModel pair which is responsible for retrieving the correct HTML based on the current route, and then render that HTML.
There are also other ways to do this, but AFAIK this approach involves the least amount of plumbing.
Of course you also need an object to store the HTML/route pairs in, and a service to store/retrieve those objects.
You can see a live working version here (including a few comments to clarify things):
https://gist.run/?id=8c7e02ce1ee0e25d966fea33b826fe10
app.js
import { inject } from "aurelia-framework";
import { Router } from "aurelia-router";
import { FaqService } from "./faq-service";
#inject(Router, FaqService)
export class App {
constructor(router, faqService) {
router.configure(config => {
config.map({ route: "", moduleId: "./empty" });
config.map({ route: "faq/:route", moduleId: "./faq-detail" });
});
this.router = router;
this.faqService = faqService;
}
openFaq(item) {
this.router.navigate(`faq/${item.route}`);
}
}
app.html
<template>
<router-view></router-view>
<ul>
<li repeat.for="item of faqService.faqItems" click.delegate="openFaq(item)">
${item.title}
</li>
</ul>
</template>
empty.js (just a convenience for default empty route):
import { inlineView } from "aurelia-framework";
#inlineView("<template>no content</template>")
export class Empty {}
faq-service.js
import { singleton } from "aurelia-framework";
class FaqItem {
constructor(route, title, markup) {
this.route = route;
this.title = title;
this.markup = markup;
}
}
#singleton(false)
export class FaqService {
constructor() {
this.faqItems = [
new FaqItem("question-1", "Question 1", "<h4>Question 1</h4><p>Answer 1</p>"),
new FaqItem("question-2", "Question 2", "<h4>Question 2</h4><p>Answer 2</p>"),
new FaqItem("question-3", "Question 3", "<h4>Question 3</h4><p>Answer 3</p>")
];
}
getByRoute(route) {
return this.faqItems.find(i => i.route === route);
}
}
faq-detail.js
import { inject, InlineViewStrategy } from "aurelia-framework";
import { FaqService } from "./faq-service";
#inject(FaqService)
export class FaqDetail {
constructor(service) {
this.service = service;
}
activate(param) {
let item = this.service.getByRoute(param.route);
this.viewStrategy = new InlineViewStrategy(`<template>${item.markup}</template>`)
}
}
faq-detail.html
<template>
<compose view.bind="viewStrategy"></compose>
</template>

Angular2 how to call a method only after subscribed data is completely bounded to a table using ng-for? [duplicate]

In Angular 1 I have written a custom directive ("repeater-ready") to use with ng-repeat to invoke a callback method when the iteration has been completed:
if ($scope.$last === true)
{
$timeout(() =>
{
$scope.$parent.$parent.$eval(someCallbackMethod);
});
}
Usage in markup:
<li ng-repeat="item in vm.Items track by item.Identifier"
repeater-ready="vm.CallThisWhenNgRepeatHasFinished()">
How can I achieve a similar functionality with ngFor in Angular 2?
You can use #ViewChildren for that purpose
#Component({
selector: 'my-app',
template: `
<ul *ngIf="!isHidden">
<li #allTheseThings *ngFor="let i of items; let last = last">{{i}}</li>
</ul>
<br>
<button (click)="items.push('another')">Add Another</button>
<button (click)="isHidden = !isHidden">{{isHidden ? 'Show' : 'Hide'}}</button>
`,
})
export class App {
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
#ViewChildren('allTheseThings') things: QueryList<any>;
ngAfterViewInit() {
this.things.changes.subscribe(t => {
this.ngForRendred();
})
}
ngForRendred() {
console.log('NgFor is Rendered');
}
}
origional Answer is here
https://stackoverflow.com/a/37088348/5700401
You can use something like this (ngFor local variables):
<li *ngFor="#item in Items; #last = last" [ready]="last ? false : true">
Then you can Intercept input property changes with a setter
#Input()
set ready(isReady: boolean) {
if (isReady) someCallbackMethod();
}
For me works in Angular2 using Typescript.
<li *ngFor="let item in Items; let last = last">
...
<span *ngIf="last">{{ngForCallback()}}</span>
</li>
Then you can handle using this function
public ngForCallback() {
...
}
The solution is quite trivial. If you need to know when ngFor completes printing all the DOM elements to the browser window, do the following:
1. Add a placeholder
Add a placeholder for the content being printed:
<div *ngIf="!contentPrinted">Rendering content...</div>
2. Add a container
Create a container with display: none for the content. When all items are printed, do display: block. contentPrinted is a component flag property, which defaults to false:
<ul [class.visible]="contentPrinted">
...items
</ul>
3. Create a callback method
Add onContentPrinted() to the component, which disables itself after ngFor completes:
onContentPrinted() {
this.contentPrinted = true;
this.changeDetector.detectChanges();
}
And don't forget to use ChangeDetectorRef to avoid ExpressionChangedAfterItHasBeenCheckedError.
4. Use ngFor last value
Declare last variable on ngFor. Use it inside li to run a method when this item is the last one:
<li *ngFor="let item of items; let last = last">
...
<ng-container *ngIf="last && !contentPrinted">
{{ onContentPrinted() }}
</ng-container>
<li>
Use contentPrinted component flag property to run onContentPrinted() only once.
Use ng-container to make no impact on the layout.
Instead of [ready], use [attr.ready] like below
<li *ngFor="#item in Items; #last = last" [attr.ready]="last ? false : true">
I found in RC3 the accepted answer doesn't work. However, I have found a way to deal with this. For me, I need to know when ngFor has finished to run the MDL componentHandler to upgrade the components.
First you will need a directive.
upgradeComponents.directive.ts
import { Directive, ElementRef, Input } from '#angular/core';
declare var componentHandler : any;
#Directive({ selector: '[upgrade-components]' })
export class UpgradeComponentsDirective{
#Input('upgrade-components')
set upgradeComponents(upgrade : boolean){
if(upgrade) componentHandler.upgradeAllRegistered();
}
}
Next import this into your component and add it to the directives
import {UpgradeComponentsDirective} from './upgradeComponents.directive';
#Component({
templateUrl: 'templates/mytemplate.html',
directives: [UpgradeComponentsDirective]
})
Now in the HTML set the "upgrade-components" attribute to true.
<div *ngFor='let item of items;let last=last' [upgrade-components]="last ? true : false">
When this attribute is set to true, it will run the method under the #Input() declaration. In my case it runs componentHandler.upgradeAllRegistered(). However, it could be used for anything of your choosing. By binding to the 'last' property of the ngFor statement, this will run when it is finished.
You will not need to use [attr.upgrade-components] even though this is not a native attribute due to it now being a bonafide directive.
I write a demo for this issue. The theory is based on the accepted answer but this answer is not complete because the li should be a custom component which can accept a ready input.
I write a complete demo for this issue.
Define a new component:
import {Component, Input, OnInit} from '#angular/core';
#Component({
selector: 'app-li-ready',
templateUrl: './li-ready.component.html',
styleUrls: ['./li-ready.component.css']
})
export class LiReadyComponent implements OnInit {
items: string[] = [];
#Input() item;
constructor() { }
ngOnInit(): void {
console.log('LiReadyComponent');
}
#Input()
set ready(isReady: boolean) {
if (isReady) {
console.log('===isReady!');
}
}
}
template
{{item}}
usage in the app component
<app-li-ready *ngFor="let item of items; let last1 = last;" [ready]="last1" [item]="item"></app-li-ready>
You will see the log in the console will print all the item string and then print the isReady.
I haven't yet looked in depth of how ngFor renders elements under the hood. But from observation, I've noticed it often tends to evaluate expressions more than once per each item it's iterating.
This causes any typescript method call made when checking ngFor 'last' variable to get, sometimes, triggered more than once.
To guarantee a one call to your typescript method by ngFor when it properly finishes iterating through items, you need to add a small protection against the multiple expression re-evaluation that ngFor does under the hood.
Here is one way to do it (via a directive), hope it helps:
The directive code
import { Directive, OnDestroy, Input, AfterViewInit } from '#angular/core';
#Directive({
selector: '[callback]'
})
export class CallbackDirective implements AfterViewInit, OnDestroy {
is_init:boolean = false;
called:boolean = false;
#Input('callback') callback:()=>any;
constructor() { }
ngAfterViewInit():void{
this.is_init = true;
}
ngOnDestroy():void {
this.is_init = false;
this.called = false;
}
#Input('callback-condition')
set condition(value: any) {
if (value==false || this.called) return;
// in case callback-condition is set prior ngAfterViewInit is called
if (!this.is_init) {
setTimeout(()=>this.condition = value, 50);
return;
}
if (this.callback) {
this.callback();
this.called = true;
}
else console.error("callback is null");
}
}
After declaring the above directive in your module (assuming you know how to do so, if not, ask and I'll hopefully update this with a code snippet), here is how to use the directive with ngFor:
<li *ngFor="let item of some_list;let last = last;" [callback]="doSomething" [callback-condition]="last">{{item}}</li>
'doSomething' is the method name in your TypeScript file that you want to call when ngFor finishes iterating through items.
Note: 'doSomething' doesn't have brackets '()' here as we're just passing a reference to the typescript method and not actually calling it here.
And finally here is how 'doSomething' method looks like in your typescript file:
public doSomething=()=> {
console.log("triggered from the directive's parent component when ngFor finishes iterating");
}

Login with ionic and material design

I have an ionic project with side menu and all.
Now I want to add in simple way and login cool form, like
http://ionicmaterial.com/
But the issue I didn't see any examples how to add it in exciting project that it will load the login form first and after that will redirect to regular page.
My project looks like:
app.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: "/app",
abstract: true,
templateUrl: "templates/menu.html",
controller: 'AppCtrl'
})
.state('app.placeslists', {
url: "/placeslists",
views: {
'menuContent': {
templateUrl: "templates/placeslists.html",
controller: 'PlaceslistsCtrl'
}
}
})
How can I add the login page with authentication (token) that it will load first and how can I add the material for login page in easy way.
Thanks!!!
For implementing login, you would require these things
A Login State
A Login Template
Logic to handle your token
$stateProvider
.state('Login', {
url: "/Login",
templateUrl: "app/templates/Login.html"
})
<ion-view view-title="Login" align-title="left">
<ion-content style="background: url(img/login.jpg) center; background-size: cover;">
<div class="hero no-header flat">
<div class="content">
<div class="app-icon"></div>
<h1>Thronester</h1>
</div>
</div>
<div class="list">
<ion-md-input placeholder="Username" highlight-color="balanced" type="text" ng-model='user.username'></ion-md-input>
<ion-md-input placeholder="Password" highlight-color="energized" type="password" ng-model='user.password'></ion-md-input>
</div>
<div class="padding">
<button ui-sref="app.profile" class="button button-full button-assertive ink">Login</button>
</div>
<div class="button-bar social-login">
<button class="button button-small button-border icon-left ion-social-google button-assertive-900" ng-click='DoLogin(user)'>Login</button>
</div>
</ion-content>
</ion-view>
In your DoLogin function, you would need to handle hit your API for login, and receive your token. You would need to store this token. I use SQLlite plugin to store my token into a token table. There are different ways of storing token.
SQLite plugin
Local Storage
WebSQL
File ngCordova
and many more, I can provide you with code snippet using SQLlite.
var DB = window.sqlitePlugin.openDatabase({name: "Token.db", location: 1})
var CreateQuery = 'CREATE TABLE IF NOT EXISTS Token (id integer primary key, access_token TEXT)'
var InsertQuery = 'INSERT INTO Token (access_token) VALUES (?)'
var selectQuery = 'SELECT access_token FROM Token WHERE id = 1'
var Token = // the token you get from your server, make a $http resquest and get it
$cordovaSQLite.execute( DB,CreateQuery ).then(function () {
//table created
})
$cordovaSQLite.execute(DB, InsertQuery, [Token]).then(function(){
// token inserted into table
})
$cordovaSQLite.execute(DB, selectQuery).then(function (res) {
//getting token from table
$rootScope.TokenFromTable = res.rows.item(0).access_token;
})
Don't just copy paste from the code (it wont work), you would need build the logic on where to place all this code and in which order.
After you have received the authToken, you can set it as a common header for all you $http requests, and when user clicks on logout, just drop the table or drop the DB. ( go through the blogs in the link)
you can add new state login in app.js which will load login.html and controller and load it by defalut like:
.state('login', {
url: '/login',
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/login');
and in login controller when you successfully login then you can redirect it to any page using $state.go('app.placeslists'); it will load regular pages.
I found at the end all info with demos
you can find also in:
https://github.com/zachsoft/Ionic-Material

Resources