Showing posts with label React. Show all posts
Showing posts with label React. Show all posts

Tuesday, March 16, 2021

Attach onClick handler on div dynamically in runtime using css class selector in React and Fluent UI

Sometimes in React we may need to attach javascript handler on html element using "old" way which closer to DOM manipulation which we did in pure javascript code. I.e. if you can't get component reference because of some reason and the only thing you have is css class of html element in the DOM you may still need to work with it via DOM manipulations.

As example we will use Search element from Fluent UI. It renders magnifier icon internally and it is quite hard to get component ref on it. Highlighted magnifier is rendered as div with "ms-SearchBox-iconContainer" css class:

Imagine that we want to attach onClick handler on magnifier icon so users will be able to click it and get search results. Here is how it can be achivied:

private _onClick(self: Search) {
  // search logic
}

public componentDidMount() {
  let node = document.querySelector(".ms-SearchBox-iconContainer");
  if (node) {
    node.addEventListener("click", e => this._onClick(this));
  }
}

public componentWillUnmount() {
  let node = document.querySelector(".ms-SearchBox-iconContainer");
  if (node) {
    node.removeEventListener("onClick", e => this._onClick(this));
  }
}

So we add handler in componentDidMount and remove it componentWillUnmount. Inside these methods we use document.querySelector() for finding actual div to which we need to attach onClick handler. Note that we pass this as parameter of _onClick since this inside this method will point to div html element but not to search component itself.

Sunday, April 5, 2020

Host React web app on Azure

In this article I will show how to host React web app on Azure cloud platform. We will create new test React web app from scratch, will build it, then will create in Azure new Resource group and App service and after that will transfer our React web app there. Before to start ensure that you have working Azure account with valid subscription (you may create free subscription for 1 month to try things out).

First of all we need to create React app. Before to do that you need to install nodejs on your PC. By default it will also install popular npm package manager and will add its folders to PATH env variable. When it is done run the following command in your working folder:

npx create-react-app test-react-app

It will create test-react-app folder and new test React app inside it. In order to test it on local dev web server run the following command:

npm start

It will launch web server and will open default browser with React app running on http://localhost:3000:

In order to host our React app on Azure we need to build it first. In order to do that run the following command:

npm run build

It will create ready for deployment build subfolder and will copy web app files there.

Once React app is ready we need to configure Azure for hosting it. Go to Azure portal https://portal.azure.com > Resource groups > Create resource group. On the opened window give name for the new resource group and select closer location:

When new resource group will be created choose New > Web App:

On the opened Window choose created resource group, give name for new web app, set the following parameters:
Publish = Code
Runtime stack = Node 10.14
Operating system = Windows
also select region (usually the same region which was used for creating resource group) and App service plan name. As we create new Web app for testing purposes we will use Free app service plan: under Sku and size click Change size and change App service plan to Dev/Test > F1 free:

and click Create. After that go to created App service > Deployment center > FTP > Dashboard > App credentials and copy FTP host name, username and password:

Use your favorite FTP client in order to connect to Azure app service with credentials copied on previous step. When connection will be established copy files from local /test-react-app/build folder to FTP /site/wwwroot folder. When it will be done try to open https://{name-of-your-web-app}.azurewebsites.net in web browser (instead of {name-of-your-web-app} use name of Azure web app which was used during it’s creation). If everything was done correctly you will see your React web app hosted in Azure:

Monday, August 27, 2018

Combine async redux actions in SPFx react components

If you work with SPFx and e.g. implement web part which uses react/redux for its components then you may face with need to combine multiple async redux actions into single one. Let’s assume that we have components which shows O365 groups and has appropriate properties and actions for it:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import * as GroupActions from 'groupActions';

export interface GroupListProps {
  groups: IGroup[];
  actions: {
    getGroups: GroupActions.IGetGroups,
  };
}

class GroupList extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groups) {
      this.props.actions.getGroups();
    }
  }

  public render() {
    return (
      this.props.groups ? <GroupsList items={this.props.groups} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
    getGroups: () => dispatch(GroupActions.getGroupsImpl()),
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

After that we decide to show both regular Sharepoint sites together with groups. We need to action for getting sites themselves (asynchronously using promises) and another action for combining groups and sites into single list. Both actions ae asynchronous. Let’s see how we may combine them to single one.

At first start with adding new action for sites:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import { ISite } from 'ISite';
import * as GroupActions from 'groupActions';
import * as SiteActions from 'siteActions';

export interface GroupListProps {
  groups: IGroup[];
  sites: ISite[];
  actions: {
    getGroups: GroupActions.IGetGroups,
	getSites: SiteActions.IGetSites,
  };
}

class GroupListContainer extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groups) {
      this.props.actions.getGroups();
    }

    if (!this.props.sites) {
      this.props.actions.getSites();
    }
  }

  public render() {
    return (
      this.props.groups ? <GroupsList items={this.props.groups} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
  sites: state.sites
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
    getGroups: () => dispatch(GroupActions.getGroupsImpl()),
	getSites: () => dispatch(SiteActions.getSitesImpl()),
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

Then create new combined action which calls 2 other actions (getGroups and getSites) via dispatch and additional action which will combine 2 groups and sites to single list:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import { ISite } from 'ISite';
import { IGroupOrSite } from 'IGroupOrSite';
import * as GroupActions from 'groupActions';
import * as SiteActions from 'siteActions';
import * as GroupOrSiteActions from 'groupOrSiteActions';

export interface GroupListProps {
  groups: IGroup[];
  sites: ISite[];
  groupsOrSites: IGroupOrSite[];
  actions: {
	getGroupsAndSites: GroupOrSiteActions.IGetGroupsAndSites
	combineGroupsAndSites: GroupOrSiteActions.ICombineGroupsAndSites
  };
}

class GroupListContainer extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groupsOrSites) {
      this.props.actions.getGroupsAndSites();
    }
  }

  public componentDidUpdate(prevProps: GroupListProps) {
    // Combine groups and sites
    if ((!isEqual(this.props.groups, prevProps.groups) || !isEqual(this.props.sites, prevProps.sites)) &&
	  this.props.groups && this.props.sites) {
	  this.props.actions.combineGroupsAndSites(this.props.groups, this.props.sites);
    }
  }
  
  public render() {
    return (
      this.props.groupsOrSites ? <GroupsList items={this.props.groupsOrSites} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
  sites: state.sites,
  groupsOrSites: state.groupsOrSites
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
	getGroupsAndSites: () => {
		dispatch(GroupActions.getGroupsImpl());
		return dispatch(SiteActions.getSitesImpl());
	},
	getGroupsAndSites: (groups: IGroup[], sites: ISite[]) => dispatch(GroupOrSiteActions.combineSitesImpl(groups, sites))
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

As result when you will call this combined action it will perform 2 async sub actions.