Aurelia - Passthrough Routes - view

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>

Related

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

MatPaginator gets undefined

I have replicated thsi case: Angular 5 Material Table not getting data from service
But when I try access any property from paginator I get undefined error from this objet.
Any ideas?
Thanks
I got the same issue. Placing mat-paginator tag outside *ngIf resolved my issue. Make sure it is available to component class without any conditions.
Some issues that may cause mat-paginator is undefined:
You forgot to import in app.module.ts import { MatPaginatorModule } from '#angular/material'; and then declare the import in the imports array inside ngModule.
#NgModule({ declarations: [ somestuff], imports: [ MatPaginatorModule]});
Import MatPaginator inside the component you are using:
import {MatPaginator, MatSort, MatTableDataSource} from '#angular/material';
set the MatDataSource to be none. (only need to do this if you are going to be getting async data e.g. from a server)
this.dataSource = new MatTableDataSource([]);
Make sure you set the length property of the mat-paginator to the length of the data rows returned.
Set the paginator inside NgAfterViewInit method or if that doesn't work try:
private paginator: MatPaginator;
private sort: MatSort;
#ViewChild(MatSort) set matSort(ms: MatSort) {
this.sort = ms;
this.setDataSourceAttributes();
}
#ViewChild(MatPaginator) set matPaginator(mp: MatPaginator) {
this.paginator = mp;
this.setDataSourceAttributes();
}
setDataSourceAttributes() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
if (this.paginator && this.sort) {
this.applyFilter('');
}
}
In some cases, the issue is related to conditional outer div. Ex:
<div *ngIf="condition">
...
<mat-paginator ....>
</div>
For a such scenario just replace *ngIf="condition" with [hidden]="!condition" and it'll work.
Please refer to https://github.com/angular/components/issues/10205 for more details
I had a similar problem and this is how I got it working:
My Initial Code Setup
component.html
<div class="chart-wrapper" *ngIf="viewType === 'chart'; else table;">
// Load Chart Here
</div>
<ng-template #table>
// Load Table Here
<mat-paginator
#paginator
[length]="tableDataSource.data ? tableDataSource.data.length : 0"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)"
></mat-paginator>
</ng-template>
component.ts
columns: string[] = [];
tableDataSource: MatTableDataSource<any[]> = new MatTableDataSource([]);
#ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
pageIndex = 0;
pageSize = 10;
pageSizeOptions = [10, 15, 20];
ngOnInit() {
this.getTableData();
}
ngAfterViewInit() {
this.tableDataSource.paginator = this.paginator;
}
getTableData() {
// After getting data from API
this.tableDataSource.data = apiResponse;
}
The Solution
Put static: false while declaring Mat Paginator
#ViewChild(MatPaginator, { static: false }) paginator: MatPaginator;
and then set paginator onto tableDataSource after data gets loaded into it
this.tableDataSource.data = apiResponse;
this.tableDataSource.paginator = this.paginator;
Thanks to the solution by cisco336 on this thread
To my surprise nobody appraised that solution.
For me it was broken after angular upgrade from 7 to any version(although i checked in only angular 12).
In Angular 8 they introduced a parameter where we are supposed to give {static: true} if we need the value in ngOninit. From Angular 9 it was set to false by default if we are not mentioning it explicitly.
So changing code from this
#ViewChild(MatPaginator) paginator: MatPaginator;
To this
#ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
Fixed my issue.
"Inserting an *ngIf means that its contents cannot be queried until the view is initialized with change detection. This means the query for sort is empty at ngOnInit.
This can be resolved by changing the setup from ngOnInit to ngAfterViewInit. https://stackblitz.com/edit/angular-mewfek?file=src/app/app.component.ts "
reference
https://github.com/angular/components/issues/15966
In my case, paginator object was getting initiated in ngOnInit() and giving exception paginator is undefined.
Initial code i.e. not working code:
ngOnInit() {
this.dataSource = new StoreServiceDataSource<User>(this.userService, this.snackBar);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
After correction i.e. working code:
ngAfterViewInit() {
this.dataSource = new StoreServiceDataSource<User>(this.userService, this.snackBar);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
Just changed initializing life cycle hook. Earlier paginator was getting initialized in hgOnInit(), now it's initialized in ngAfterViewInit().
[length]="dataSource.filteredData.length" over "mat-paginator" worked for me.
<mat-paginator [pageSize]="10" [pageSizeOptions]="[10,25,50]" #paginator [length]="dataSource.filteredData.length"[showFirstLastButtons]="true"></mat-paginator>
MatPaginator being undefined most likely means you do not have the module imported. Sometimes angular mat does not explicitly tell you these things that you are missing. But always check the API tab under their documentation before using a component. At your module level, you should have the following in your app.module.ts file.
the import
import { MatButtonModule, MatTableModule, MatPaginatorModule, MatProgressSpinnerModule, MatTableDataSource } from '#angular/material';
Your component imported that used paginator of course
import { TableComponent } from './table/table.component';
Those modules imported in your imports array
imports: [
BrowserAnimationsModule,
NgbModule.forRoot(),
FormsModule,
RouterModule,
AppRoutingModule,
CommonModule,
MatButtonModule,
MatTableModule,
BrowserAnimationsModule,
MatPaginatorModule,
MatProgressSpinnerModule,
HttpClientModule
],
And those modules exported if necessary (different topic so I wont discuss here).
exports: [
MatButtonModule,
MatTableModule,
MatPaginatorModule
],
This is all happening in my App.Module
export class AppModule { }
This assuming you do bot have your project structured as feature modules. In that case you would really only need everything I talked about in the module in which your component lives. But in this case, where everything is under the app module, this works just fine.
<div class=" fixed-form-bottom" [hidden]="changeState">
<div class="team-footer">
<mat-paginator #paginator [pageSize]="pageSize" [showFirstLastButtons]="true" [length]="totalSize"
[pageIndex]="currentPage" (page)="pageEvent = handlePage($event)" [hidePageSize]="true">
</mat-paginator>
</div>
</div>
And in .ts file , you compare your condition and pass true/ false from there
if(this.totalSize>3){
this.changeState=true;
}
its worked for me
Check if you have *ngIf that wrap your mat-paginator or any top component, if you need hide you should use [hidden] attribute in your component
Another reason for the delay in pagination injection is one or more missing end tag such as within your html markup, which seems to impact the timing of the pagination markup DOM injection.
In this type of problem scenario a 2nd+ load of the table will show the pagination components.
There are two solutions to this problem
Solution 1:
If mat-paginator is inside *ngIf then either take it outside or use [hidden] attribute.
<div [hidden]="condition">
</div>
Solution 2:
Make sure you are not using static: true while initializing the paginator
#ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
change it to
#ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;

Custom Element Web Component Shadow DOM Vendor Scripts/Elements

When working with Custom Elements that leverage Shadow DOM, what is the official approach to injecting 3rd party scripts and elements such as Invisible reCAPTCHA which require scripts such:
<script src="https://www.google.com/recaptcha/api.js" async defer></script>`
for HTML elements such as a <button> to be leaded and reCAPTCHA to be rendered?
shadowRoot doesn't seem to have anything like head, is the script supposed to be added to the created template's innerHTML? Or is a <script> to be appended to the shadowRoot via appendChild in connectedCallback()? What is the official approach to working with 3rd party libraries within Shadow DOM? Loading the script on the page containing the rendered Custom Element doesn't seem to trigger a render due to Shadow DOM.
const template = document.createElement('template');
template.innerHTML = `
<form>
<button class="g-recaptcha"
data-sitekey="your_site_key"
data-callback='onSubmit'>Submit</button>
</form>
`;
class CustomElement extends HTMLElement {
constructor() {
super(); // always call super() first in the ctor.
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
...
}
disconnectedCallback() {
...
}
attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}
Thank you for any guidance you can provide.
There's no offical approach because the solution depends on the 3rd party library implementation.
However, in the case of reCaptcha, the workaround is to expose the <button> in the normal DOM, and inject it in the Shadow DOM via the <slot> element.
class extends HTMLElement {
constructor() {
super()
this.attachShadow( {mode: 'open'} ).innerHTML= `
<form>
<slot></slot>
</form>`
}
connectedCallback() {
this.innerHTML = `
<button
class="g-recaptcha"
data-sitekey="your_site_key"
data-callback="onSubmit">Submit</button>`
}
})

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");
}

Rendering a partial view in Zend for adding html to routing action layout

I am rendering a page with a lot of frames (XHR contentpanes via dojo). This is done through a request to IndexController which sets up regions 'header,left,right,center,footer' with the exception, that center is not filled in with contents. This in turn is set by calling PaneController in menu.onclick. Caveat; search engines indexing service does not get center region contents.. I wish to bypass AJAX loading of center, if user enters via /index/index.
Relevant snippets from IndexController:
class IndexController extends Zend_Controller_Action {
public function indexAction() {
$this->indexModel = $this->view->indexModel = new Application_Model_Index();
// Goal is to render "/pane/main/" action and capture the HTML
$this->view->mainPane = (string) $this->renderPaneMain();
return $this->render();
}
public function renderPaneMain() {
// ActionStack ?
// action() ?
return $HTML;
}
}
Relevant stuff in Pane
class PaneController extends Zend_Controller_Action {
public function preDispatch() {
// will only return a contentpane, dont render layout
if ($this->getRequest()->isXmlHttpRequest()) {
$this->_helper->layout()->disableLayout();
$this->view->doLayout = true;
}
}
public function mainAction() {
this.render("main.phtml");
}
public function init() {
$this->panesModel = new Application_Model_Panes();
$variant = $this->getRequest()->getParam('variant', '');
// routing variables need to be set, how?
if (empty($variant))
$this->_redirect('/');
}
}
Basically, i need the PaneController to _not render the global layout but call its .phtml view file, once it has been setup with relevant model entries and such.
Any ideas as to how I can achieve this in its most efficient form?
Very well, ill attach the workaround im using here
The forms and the fork-logic i have moved to the model that is coexisting with PanesController. For the IndexController, which will present the default Pane as inline HTML without AJAX - there is a couple of duplicated initializations going on.
So, IndexModel extends the PanesModel - without initializing it. In my index.phtml view (for Index action) i have following code to render the inline html from a pane.
in index controller
$this->view->model = new IndexModel(); // extends PanesModel
$this->view->model->setDefaultProperties($variant, $pagination, ...);
in index view:
$this->partial("panes/main/main.phtml", array("model", $this->model);
and from pane view:
<?php if($this->model->goThisDirection()): ?>
Switch 1 HTML contents
<?php endif; ?>
Caveat: I also had to not render any form of layout within the pane (dojox contentpanes allows for <script> and <style> tags) - and this ofc ripples to any other pane action of mine.

Resources