Adding Masonry Grid Layout to loklak Media Wall

Working on loklak media walls, I wanted to add a responsive self-adjusting grid layout for media walls. Going through the most trending media wall, I concluded that the most commonly used view for media walls is Masonry view. This view is a much similar view to the Pinterest grid layout. In fact, Masonry Desandro states that Masonry view can be one of the most responsive and most pretty ways to present cards. It is also beneficial to use masonry view as it avoids unnecessary gaps and makes full use of the display screen.

In this blog, we are going to see how I added a masonry view to the media walls without any other dependency than CSS using column-count and column-gap. We would also be looking upon how to adjust font-size using rem units to make text readable for all screen-sizes depending on number of columns.

HTML File

<span class=“masonry”>
<span class=“masonry-panel” *ngFor=“let item of (apiResponseResults$ | async)”>
<span class=“masonry-content”>
<mediawallcard [feedItem]=“item”></mediawallcard>
</span>
</span>
</span>
  • The first span with class masonry is like a container in which all the cards will be embedded. This div will provide a container to adjust the number of columns in which cards will be adjusted.
  • The second span with class masonry-panel will be the column division. These panels are like the elements of a matrix. These panels are responsive and will adjust according to the screen size.
  • The third span with class masonry-content are like the content box in which all the content will be embedded. This div will create a space in the panel for the card to be adjusted.
  • The fourth element media-wall-card are the cards in which all the feed items are placed.

CSS File

  • Adjusting columns – The column-count and column-gap property was introduced in CSS3 to divide the element in a specified number of column and to keep the specified number (whole number) of column gap between the elements respectively. For different screen sizes, these are the column count that are kept. We need adjust the number of columns according to various screen sizes so that cards neither look too stretched or too bleak. Media wall is now responsive enough to adjust on smaller screens like mobiles with one column and on larger screens too with five columns.

@media only screen and (max-width: 600px) {
.masonry {
columncount: 1;
columngap: 0;
}
} @media only screen and (min-width: 600px) and (max-width: 900px) {
.masonry {
columncount: 2;
columngap: 0;
}
} @media only screen and (min-width: 1280px) and (max-width: 1500px) {
.masonry {
columncount: 3;
columngap: 0;
}
} @media only screen and (min-width: 1500px) and (max-width: 1920px) {
.masonry {
columncount: 4;
columngap: 0;
}
} @media only screen and (min-width: 1920px) {
.masonry {
columncount: 5;
columngap: 0;
}
}
  • Adjusting Font-Size – For a fixed aspect ratio of various divisions of the media wall card, we need a central unit that can be adjusted to keep this ratio fixed. Using px will rather make the sizes fixed and adjusting these sizes for various screen sizes will make it hectic and would spoil the ratio. We will be instead using rem as a font-unit to adjust the font size for different screen sizes. Firstly, we need to assign a certain font size to all the elements in the media wall card. Now, we can configure the central font-size of the root unit for all the screen sizes using @media tag.

One thing that should be kept in mind is that The root font size should be kept more than 14px always.

@media only screen and (max-width: 600px) {
:root {
font-size: 14px;
}
}@media only screen and (min-width: 600px) and (max-width: 800px) {
:root {
font-size: 16px;
}
}@media only screen and (min-width: 800px) and (max-width: 1200px) {
:root {
font-size: 17px;
}
}@media only screen and (min-width: 1200px) and (max-width: 1500px) {
:root {
font-size: 18px;
}
}@media only screen and (min-width: 1500px) {
:root {
font-size: 20px;
}
}

 

This will create a masonry layout and now, all the cards can be adjusted in this self-adjusting grid and will look readable on all types of screen.

Reference

Continue ReadingAdding Masonry Grid Layout to loklak Media Wall

Adding Unit Test for Reducer in loklak search

Ngrx/store components are an integral part of the loklak search. All the components are dependent on how the data is received from the reducers. Reducer is like a client-side database which stores up all the data received from the API response. It is responsible for changing the state of the application. Reducers also supplies data to the Angular components from the central Store. If correct data is not received by the components, the application would crash. Therefore, we need to test if the reducer is storing the data in a correct way and changes the state of the application as expected.

Reducer also stores the current state properties which are fetched from the APIs. We need to check if reducers store the data in a correct way and if the data is received from the reducer when called from the angular components.

In this blog, I would explain how to build up different components for unit testing reducers.

Reducer to test

This reducer is used for store the data from suggest.json API from the loklak server.The data received from the server is further classified into three properties which can be used by the components to show up auto- suggestions related to current search query.

  • metadata: – This property stores the metadata from API suggestion response.
  • entities: – This property stores the array of suggestions corresponding to the particular query received from the server.
  • valid: – This is a boolean which keeps a check if the suggestions are valid or not.

We also have two actions corresponding to this reducer. These actions, when called, changes the state properties which , further, supplies data to the components in a more classified manner. Moreover, state properties also causes change in the UI of the component according to the action dispatched.

  • SUGGEST_COMPLETE_SUCCESS: – This action is called when the data is received successfully from the server.
  • SUGGEST_COMPLETE_FAIL: – This action is called when the retrieving data from the server fails.

export interface State {
metadata: SuggestMetadata;
entities: SuggestResults[];
valid: boolean;
}export const initialState: State = {
metadata: null,
entities: [],
valid: true
};export function reducer(state: State = initialState, action: suggestAction.Actions): State {
switch (action.type) {
case suggestAction.ActionTypes.SUGGEST_COMPLETE_SUCCESS: {
const suggestResponse = action.payload;return {
metadata: suggestResponse.suggest_metadata,
entities: suggestResponse.queries,
valid: true
};
}case suggestAction.ActionTypes.SUGGEST_COMPLETE_FAIL: {
return Object.assign({}, state, {
valid: false
});
}default: {
return state;
}
}
}

Unit tests for reducers

  • Import all the actions, reducers and mocks

import * as fromSuggestionResponse from ‘./suggest-response’;
import * as suggestAction from ‘../actions/suggest’;
import { SuggestResponse } from ‘../models/api-suggest’;
import { MockSuggestResponse } from ‘../shared/mocks/suggestResponse.mock’;

 

  • Next, we are going to test if the undefined action doesn’t a cause change in the state and returns the initial state properties. We will be creating an action by const action = {} as any;  and call the reducer by const result = fromSuggestionResponse.reducer(undefined, action);. Now we will be making assertions with expect() block to check if the result is equal to initialState and all the initial state properties are returned

describe(‘SuggestReducer’, () => {
describe(‘undefined action’, () => {
it(‘should return the default state’, () => {
const action = {} as any;const result = fromSuggestionResponse.reducer(undefined, action);
expect(result).toEqual(fromSuggestionResponse.initialState);
});
});

 

  • Now, we are going to test SUGGEST_COMPLETE_SUCCESS and SUGGEST_COMPLETE_FAIL action and check if reducers change only the assigned state properties corresponding to the action in a correct way.  Here, we will be creating action as assigned to the const action variable in the code below. Our next step would be to create a new state object with expected new state properties as assigned to variable const expectedResult below. Now, we would be calling reducer and make an assertion if the individual state properties of the result returned from the reducer (by calling reducer) is equal to the state properties of the expectedResult (Mock state result created to test).

describe(‘SUGGEST_COMPLETE_SUCCESS’, () => {
it(‘should add suggest response to the state’, () => {
const ResponseAction = new suggestAction.SuggestCompleteSuccessAction(MockSuggestResponse);
const expectedResult: fromSuggestionResponse.State = {
metadata: MockSuggestResponse.suggest_metadata,
entities: MockSuggestResponse.queries,
valid: true
};const result = fromSuggestionResponse.reducer(fromSuggestionResponse.initialState, ResponseAction);
expect(result).toEqual(expectedResult);
});
});describe(‘SUGGEST_COMPLETE_FAIL’, () => {
it(‘should set valid to true’, () => {
const action = new suggestAction.SuggestCompleteFailAction();
const result = fromSuggestionResponse.reducer(fromSuggestionResponse.initialState, action);expect(result.valid).toBe(false);
});
});

Reference

Continue ReadingAdding Unit Test for Reducer in loklak search

Improving Performance of the Loklak Search with Lazy Loading Images

Loklak Search initially faced a problem of huge load time because of a high number of parallel HTTP requests. Feeds page of the search engine made near to 50 parallel HTTP requests on every new search. One of the possible ways to reduce load time in long pages is to lazily load images. Since loklak is a multi-platform web application, a majority of your site’s visitors might use the application from high-latency devices (i.e. mobiles, tablets), then lazy-loading is required for a smooth user experience (as website speed equals User Experience).

I am explaining in this blog about how I took advantage of a directive to implement lazy loading of images and how the performance of the application improved.

What is lazy loading?

As this project website states, “Lazy loading is just the opposite of ‘pre-loading images’. The basic idea behind lazy loading is to keep a number of parallel request low. The amount of request is kept low until the user scrolls down then the images will load.” This idea is used by Google images to reduce the number of parallel requests.

As we can look in the image below, the amount of parallel request for images sent to the server without lazy loading:

Using viewport directive in loklak to lazily load images

Lazy loading can be implemented in Angular using viewport directive. We need to setup viewport and start using it in feed component to lazy load the profile pictures of the users and the images.

Using viewport directive in Components

  • In this directive, we have two observables :-
  1. Scroll which keeps a check on the scroll position of the page
    this.scroll =
    Observable.fromEvent(window, ‘scroll’).subscribe((event) =>
    {
    this.check();
    });

     

  2. Resize which keeps a check on the resize event when the browser window changes size
    this.resize =
    Observable.fromEvent(window, ‘resize’).subscribe((event) =>
    {
    this.check();
    });

     

Now, whenever the resize and scroll event occurs, it calls a method check which calculates the dimensional parameters of the element and whenever element has entered the viewport it emits an event (which is of type boolean) with value true. check() function takes up reference of the element. Next, it calculates area occupied by this element i.e. elementSize. Next, it looks for the position of the element within the viewport with getBoundingClientRect(). Parameter partial can be set to true if the we need to check if image appears in viewport partially. Moreover, we can specify parameter direction to specify the direction from which the reference element will be entering. Now, we would use conditional statements to check if element exists within the dimensions of viewport and would emit an event.

check(partial:boolean = true, direction:string = ‘both’) {
const el = this._el.nativeElement;const elSize = (el.offsetWidth * el.offsetHeight);const rec = el.getBoundingClientRect();const vp = {
width: window.innerWidth,
height: window.innerHeight
};const tViz = rec.top >= 0 && rec.top < vp.height;
const bViz = rec.bottom > 0 && rec.bottom <= vp.height;const lViz = rec.left >= 0 && rec.left < vp.width;
const rViz = rec.right > 0 && rec.right <= vp.width;const vVisible = partial ? tViz || bViz : tViz && bViz;
const hVisible = partial ? lViz || rViz : lViz && rViz;let event = {
target: el,
value: false
};if (direction === ‘both’) {
event[‘value’] = (elSize && vVisible && hVisible) ? true : false;
}
else if (direction === ‘vertical’) {
event[‘value’] = (elSize && vVisible) ? true : false;
}
else if (direction === ‘horizontal’) {
event[‘value’] = (elSize && hVisible) ? true : false;
}this.inViewport.emit(event);
}

 

  • Next, we need to import viewport directive in our component using this structure:

import { InViewportDirective } from ‘../shared//in-viewport.directive’;

declarations: [

InViewportDirective

]
})

 

  • Create a method in the respective component’s class that would keep a check on the boolean value returned by the directive

public inview(event) {
if (event.value === true) {
this.inviewport = event.value;
}
}

 

In this step we use viewport directive as an attribute directive and the $event value returned would be passed as a parameter to inview method of the component’s class. Here basically, if the feed card comes into viewport (even partially) then the event is emitted and image element is displayed on the screen.Now as the image is displayed, a call is made to receive the images. Control over img is made using *ngIf statement and checks if inviewport is true or false.

<span class=“card” in-viewport (inViewport)=”inview($event)”>
<img src={{feedItem.user.profile_image_url_https}}” *ngIf=“inviewport”/>
</span>

 

The images will load lazily only when the element is in the viewport of the device. Consequently, the amount of parallel image requests sent would decrease and application performance will increase. In fact, not just images but this directive can be used for lazy loading of all media elements in your application.

The image below shows that the reduced amount of image requests sent during the initialization of the feed result when viewport directive is used:-

Resources

Continue ReadingImproving Performance of the Loklak Search with Lazy Loading Images

Implementing Auto-Suggestions in loklak search

Auto-suggestions can add a friendly touch to the search engine. Loklak provides suggest.json API to give suggestions based on the previous search terms entered by the users. Moreover, suggest results needs to be reactive and quick enough to be displayed as soon as the user types a new character.

The main demand of the auto-suggestion feature was to make it really quick so as to make it look reactive.

I will explain how I implemented auto-suggestion feature and try to explain issues I faced and solution for that.

Ngrx Effects

The cycle for implementing auto-suggest goes like this:

The most important component in this cycle is effects as it is the event listener and it recognises the action immediately after it is dispatched and makes a call to loklak suggest.json API. We will look at how effects should look like for a reactive implementation of auto-suggestion. Making effects run effectively can make Rate Determining Step to be the API response time instead of any other component.

@Injectable()
export class SuggestEffects {@Effect()
suggest$: Observable<Action>
= this.actions$
.ofType(suggestAction.ActionTypes.SUGGEST_QUERY)
.debounceTime(300)
.map((action: suggestAction.SuggestAction) => action.payload)
.switchMap(query => {
const nextSuggest$ = this.actions$.ofType(suggestAction.ActionTypes.SUGGEST_QUERY).skip(1);return this.suggestService.fetchQuery(query)
.takeUntil(nextSuggest$)
.map(response => {
return new suggestAction.SuggestCompleteSuccessAction(response);
})
.catch(() => of(new suggestAction.SuggestCompleteFailAction()));
});constructor(
private actions$: Actions,
private suggestService: SuggestService,
) { }}

 

This effect basically listens to the action  SUGGEST_QUERY and recognises the action, next it makes a call to the Suggestion service which receives data from the server. Now, as the data is received, it maps the response and passes the response to the SuggestCompleteSuccessAction so that it could change the considered state properties. The debounce time is kept low (equal to 300) so as to detect next SUGGEST_QUERY within next 300ms of the API suggest.json call. This will help to whole cycle of suggest response reactive and help in early detection of the action.

Angular Components

In this component, I will explain what changes I made to the feed header component for autocomplete to run effectively.

@Component({
selector: ‘feed-header-component’,
templateUrl: ‘./feed-header.component.html’,
styleUrls: [‘./feed-header.component.scss’],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FeedHeaderComponent {
public suggestQuery$: Observable<Query>;
public isSuggestLoading$: Observable<boolean>;
public suggestResponse$: Observable<SuggestResults[]>;
public searchInputControl = new FormControl();
constructor(
private store: Store<fromRoot.State>,
) { }ngOnInit() {
this.getDataFromStore();
this.setupSearchField();
}private getDataFromStore(): void {
this.suggestQuery$ = this.store.select(fromRoot.getSuggestQuery);
this.isSuggestLoading$ = this.store.select(fromRoot.getSuggestLoading);
this.suggestResponse$ = this.store.select(fromRoot.getSuggestResponseEntities);
}private setupSearchField(): void {
this.__subscriptions__.push(
this.searchInputControl
.valueChanges
.subscribe(query => {
this.store.dispatch(new suggestAction.SuggestAction(value));
})

);
}
}

 

We have created a FormControl searchInputControl which would keep a check on the input value of the search box and would dispatch an action SuggestAction() as soon as its value changes. Next, we have to subscribe to the observables of suggestion entities from the store as in the function “this.getDataFromStore()”.The data is received from the store and now we can proceed with displaying suggestion data in the template.

Template

HTML language has an tag datalist which can be used to display auto-suggestion in the template but datalist shows less compatibility with the Angular application and makes rendering HTML slow with next suggestion based on the some different history of the query.

Therefore, we can use either Angular Material’s md-autocomplete or create an auto-suggestion box to display suggestions from scratch (this would add an advantage of customising CSS/TS code). md-autocomplete would add an advantage with of this element being compatible with the Angular application so in loklak search, we would prefer that.

<input mdInput required search autocomplete=“off” type=“text” id=“search” [formControl]=“searchInputControl” [mdAutocomplete]=“searchSuggestBox”
(keyup.enter)=“relocateEvent.emit(query)” [(ngModel)]=“query”/><mdautocomplete #searchSuggestBox=”mdAutocomplete”><mdoption *ngFor=“let suggestion of (suggestResponse$ | async).slice(0, 4)” [value]=“(suggestQuery$ | async)”>{{suggestion.query}}</mdoption>

</mdautocomplete>

 

In the input element, we added an attribute [mdAutocomplete]=”searchSuggestBox” which links md-autocomplete to the input box #searchSuggestBox=”mdAutocomplete”. This makes auto suggestion box attached to the input.

Now, the chain is created with user input being the source of action and display of auto-suggestion data being the reaction.

References:

Continue ReadingImplementing Auto-Suggestions in loklak search

Displaying Youtube Videos using iframe in loklak search and Preventing Cross-site Scripting (XSS) Risks

While adding video sub-tabs to the loklak search, we faced an issue regarding how could we possibly embed YouTube videos returned by the Loklak search.json API and avoid security risks. Either way was to add iframely APIs or use automatic sanitization. I made reference to Angular Documentation to get this approach for DOM Sanitization. The main issue faced when using an iframe element to display videos in Loklak Search is that the web application became vulnerable to cross-site scripting (XSS) attack .

I would be explaining in this blog about I implemented lazy loading in loklak search and used automatic sanitization to make video resource trusted and safe to be displayed on loklak search. This is the error we got when <iframe> was used in loklak search without sanitizing it.

ERROR Error: unsafe value used in a resource URL context (see http://g.co/ng/security#xss)
Stack trace:
DomSanitizerImpl.prototype.sanitize@http://localhost:4200/vendor.bundle.js:30605:23 [angular]
setElementProperty@http://localhost:4200/vendor.bundle.js:9990:58 [angular]
checkAndUpdateElementValue@http://localhost:4200/vendor.bundle.js:9910:13 [angular]
checkAndUpdateElementInline@http://localhost:4200/vendor.bundle.js:9844:24 [angular]
checkAndUpdateNodeInline@http://localhost:4200/vendor.bundle.js:12535:23 [angular]
checkAndUpdateNode@http://localhost:4200/vendor.bundle.js:12510:16 [angular]
debugCheckAndUpdateNode@http://localhost:4200/vendor.bundle.js:13139:38 [angular]
debugCheckRenderNodeFn@http://localhost:4200/vendor.bundle.js:13118:13 [angular]
View_FeedCardComponent_6/<@ng:///FeedModule/FeedCardComponent.ngfactory.js:134:5 [angular]

 

How to display Youtube videos and avoid security risks?

These steps were used in loklak search and show how to get embeddable youtube links and bypass Security risks by marking resource URL as trusted.

  • Importing DomSanitizer into the component from the @angular/platform-browser module and then injecting into the constructor of the component.

import { DomSanitizer } from ‘@angular/platform-browser’;

export class FeedCardComponent implements OnInit {

constructor(private sanitizer: DomSanitizer) {}

}

 

This step will inject Domsantizer and provides some bypassSecurityTrust APIs.

  • Calling a method during the initialization of the component and passing all the video URLs as a parameter which needs to bypass security

ngOnInit() {
this.sanitizeAndEmbedURLs(this.videosURL);
}

 

As, the Angular Documentation states ,it is better to call it during initialization so that  bypasses security as early as possible and it is declared that no security risks are created by its use at an early stage.

  • Make youtube links embeddable and bypass security to mark video URLs as safe and trusted.

private sanitizeAndEmbedURLs(links) {
links.forEach((link, i) => {
let videoid = links[i].match(/(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([^\s&]+)/);
if (videoid !== null) {
links[i] = ‘http://www.youtube.com/embed/’ + videoid[1];
}
links[i] = this.sanitizer.bypassSecurityTrustResourceUrl(links[i]);
})
}

 

This method performs two functions:-

  • Firstly, for each link, it tries matches the URLs of the youtube videos to standard formats of the youtube URLs using regular expressions and then extracts the unique video ID and brings it to an embeddable format for the iframe.

http://www.youtube.com/embed/videoID

 

  • Secondly, it also makes the sanitizer to bypass security and trusts the Resource URL.

 

  • Now links are ready to be used as the value for the source attribute for the iframe element.

<iframe [src]=“videosURL[i]” width=“100%”allowfullscreen></iframe>

 

Done! This makes our application risk-free for the users, Youtube Videos can now be displayed safely using iframe tag.

 

Resources

Continue ReadingDisplaying Youtube Videos using iframe in loklak search and Preventing Cross-site Scripting (XSS) Risks

Solving slow template rendering in loklak search

I was working on one of the issues which was to render top hashtags of the last 24 hours on the home-page of loklak search. The loklak search.json API took just milliseconds to return the response. However, it took about 3 minutes to render the HTML. Generally, API response acts as a Rate Determining Step but here, template rendering process was slow so, that optimization became crucial.

This blog will explain how I improved slow template rendering problem using some APIs of Angular. I would be explaining common mistakes we often make and solution about how to quickly render HTML after data is received from the APIs.

Previous Code

In this code, we have made an API request with query ‘since:day’ on the initialization of the Home page component. Next, we have subscribed to the store which will return an observable of hashtags. Then, we are subscribing to the observable of hashtags and extract the data from it.

In the HTML file, now we are presenting hashtags using *ngFor and *ngIf property.

This code working is fine. The only fault is that the template is not aware if some input is received or some state property has changed. Therefore, we need a trigger to do detect the change and render the HTML.

TypeScript File

export class HomeComponent implements OnInit, OnDestroy {
    public apiResponseHashtags$: Observable<Array<{ tag: string, count: number }>>;
    public apiResponseHashtags: <Array<{ tag: string, count: number }>>;

constructor(private store: Store<fromRoot.State>)
ngOnInit() {
this.getTopHashtags();
this.getDataFromStore();}
}

    private getTopHashtags() {
    this.store.dispatch(new queryAction.RelocationAfterQuerySetAction());
this.store.dispatch(new queryAction.InputValueChangeAction('since:day'));
    }

    private getDataFromStore() {
this.apiResponseHashtags$ =  this.store.select(fromRoot.getApiResponseTags);
    this.apiResponseHashtags$.subscribe((data) => {
this.apiResponseHashtags = data;
});
    }
}

HTML File

<div class="top-hashtags">
    <div *ngIf="apiResponseHashtags.length !== 0">
        <span *ngFor ="let item of apiResponseHashtags">
            <a [routerLink]="['/search']" [queryParams]="{ query : '#' + item.tag }">#{{item.tag}}</a>&nbsp;
        </span>
    </div>
</div>

Solution

These are the following changes that need to be introduced for quick HTML rendering.

  • Here, we will be using ‘Change Detection Strategy: OnPushwhile defining component for pushing changes in the template. Using ‘OnPush’ will make changes to the template as soon as some input is received.

 

  • Instead of subscribing to the observables of hashtags, we need to use the async pipe to extract data from observable in the HTML file. This is because, in the previous code, we were using subscribed value for hashtags. Now, the observable of hashtags will work like an input for the template and  ‘Change Detection Strategy: OnPush’ will act as a trigger.

TypeScript file

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit, OnDestroy {
public apiResponseHashtags$: Observable<Array<{ tag: string, count: number }>>;

constructor(private store: Store<fromRoot.State>)
ngOnInit() {
this.getTopHashtags();
this.getDataFromStore();}
}


private getTopHashtags() {
    this.store.dispatch(new queryAction.RelocationAfterQuerySetAction());
this.store.dispatch(new queryAction.InputValueChangeAction('since:day'));
    }

    private getDataFromStore() {
this.apiResponseHashtags$ = this.store.select(fromRoot.getApiResponseTags);
    }
}

HTML File

<div class="top-hashtags">
    <div *ngIf="(apiResponseHashtags$ | async).length !== 0">
        <span *ngFor ="let item of (apiResponseHashtags$ | async)">
            <a [routerLink]="['/search']" [queryParams]="{ query : '#' + item.tag }">#{{item.tag}}</a>&nbsp;
        </span>
    </div>
</div>

Conclusion

Whenever the template data depends on the API response or user interaction, it is beneficial to use ChangeDetectionStrategy  and reduce template rendering time. Moreover, instead of subscribing to observables of data received from the API, we will be using an async pipe to make observable as an input for the template.

Resources

  • https://angular-2-training-book.rangle.io/handout/change-detection/change_detection_strategy_onpush.html
  • https://stackoverflow.com/questions/39795634/angular-2-change-detection-and-changedetectionstrategy-onpush/39802466#39802466
  • https://www.youtube.com/watch?v=X0DLP_rktsc

 

 

 

Continue ReadingSolving slow template rendering in loklak search