reload / dpl-react

React components for the Danish Public Libraries.

Home Page:https://danskernesdigitalebibliotek.github.io/dpl-react

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

https://raw.githubusercontent.com/danskernesdigitalebibliotek/dpl-react/master/logo.png

codecov

A set of React components and applications providing self-service features for Danish public libraries.

Development

Requirements

Development - alternative (no docker)

Howto

  • Run: sudo vim /etc/hosts

  • Add as the last line in the doc: 127.0.0.1 dpl-react.docker

  • Now storybook can be started by sudo yarn start:storybook:dev

  • Now you need to make sure that your node version is the right one for the project whenever a terminal is opened

  • (it is specified in the package.json file)

  • if you then wish to log in through adgangsplatformen, you need to change your url to: http://ddb-react.docker/ - this avoids getting log in errors

  • (note: if you enter adgangsplatform again after signing it, you will get signed out, and need to log in again. This is not a bug, as you stay logged in otherwise.)

Step Debugging in Visual Studio Code (no docker)

If you want to enable step debugging you need to:

  • Copy .vscode.example/launch.json into .vscode/
  • Mark 1 or more breakpoints on a line in the left gutter on an open file
  • In the top menu in VS Code choose: Run -> Start Debugging
  • Type in your user password if ask to
  • Start debugging 🤖∿💻

Access tokens

Access token must be retrieved from Adgangsplatformen, a single sign-on solution for public libraries in Denmark, and OpenPlatform, an API for danish libraries.

Usage of these systems require a valid client id and secret which must be obtained from your library partner or directly from DBC, the company responsible for running Adgangsplatformen and OpenPlatform.

This project include a client id that matches the storybook setup which can be used for development purposes. You can use the /auth story to sign in to Adgangsplatformen for the storybook context.

Library token

To test the apps that is indifferent to wether the user is authenticated or not it is possible to set a library token via the library component in Storybook. Workflow:

  • Retrieve a library token via OpenPlatform
  • Insert the library token in the Library Token story in storybook

Installation

Using go-task to handle the project. Before you can install the project you need to create the file ~/.npmrc to access the github package registry as described here using a personal access token.

task dev:start

When storybook is started, you can access it at: dpl-react.docker

Standard and style

JavaScript + JSX

For static code analysis we make use of the Airbnb JavaScript Style Guide and for formatting we make use of Prettier with the default configuration. The above choices have been influenced by a multitude of factors:

  • Historically Drupal core have been making use of the Airbnb JavaScript Style Guide.
  • Airbnb's standard is comparatively the best known and one of the most used in the JavaScript coding standard landscape.

This makes future adoption easier for onboarding contributors and support is to be expected for a long time.

Named functions Vs. Anonymous arrow functions

AirBnB's only guideline towards this is that anonymous arrow function are preferred over the normal anonymous function notation.

When you must use an anonymous function (as when passing an inline callback), use arrow function notation.

Why? It creates a version of the function that executes in the context of this, which is usually what you want, and is a more concise syntax.

Why not? If you have a fairly complicated function, you might move that logic out into its own named function expression.

Reference

This project stick to the above guideline as well. If we need to pass a function as part of a callback or in a promise chain and we on top of that need to pass some contextual variables that is not passed implicit from either the callback or the previous link in the promise chain we want to make use of an anonymous arrow function as our default.

This comes with the build in disclaimer that if an anonymous function isn't required the implementer should heavily consider moving the logic out into it's own named function expression.

The named function is primarily desired due to it's easier to debug nature in stacktraces.

Create a new application

1. Create a new application component
// ./src/apps/my-new-application/my-new-application.jsx
import React from "react";
import PropTypes from "prop-types";

export function MyNewApplication({ text }) {
  return (
      <h2>{text}</h2>
  );
}

MyNewApplication.defaultProps = {
  text: "The fastest man alive!"
};

MyNewApplication.propTypes = {
  text: PropTypes.string
};

export default MyNewApplication;
2. Create the entry component
// ./src/apps/my-new-application/my-new-application.entry.jsx
import React from "react";
import PropTypes from "prop-types";
import MyNewApplication from "./my-new-application";

// The props of an entry is all of the data attributes that were
// set on the DOM element. See the section on "Naive app mount." for
// an example.
export function MyNewApplicationEntry(props) {
  return <MyNewApplication text='Might be from a server?' />;
}

export default MyNewApplicationEntry;
3. Create the mount
// ./src/apps/my-new-application/my-new-application.mount.js
import addMount from "../../core/addMount";
import MyNewApplication from "./my-new-application.entry";

addMount({ appName: "my-new-application", app: MyNewApplication });
4. Add a story for local development
// ./src/apps/my-new-application/my-new-application.dev.jsx
import React from "react";
import MyNewApplicationEntry from "./my-new-application.entry";
import MyNewApplication from "./my-new-application";

export default { title: "Apps|My new application" };

export function Entry() {
  // Testing the version that will be shipped.
  return <MyNewApplicationEntry />;
}

export function WithoutData() {
  // Play around with the application itself without server side data.
  return <MyNewApplication />;
}
5. Run the development environment
  yarn dev

OR depending on your dev environment (docker or not)

  sudo yarn dev

Voila! You browser should have opened and a storybook environment is ready for you to tinker around.

Application state-machine

Most applications will have multiple internal states, so to aid consistency, it's recommended to:

  const [status, setStatus] = useState("<initial state>");

and use the following states where appropriate:

initial: Initial state for applications that require some sort of initialization, such as making a request to see if a material can be ordered, before rendering the order button. Errors in initialization can go directly to the failed state, or add custom states for communication different error conditions to the user. Should render either nothing or as a skeleton/spinner/message.

ready: The general "ready state". Applications that doesn't need initialization (a generic button for instance) can use ready as the initial state set in the useState call. This is basically the main waiting state.

processing: The application is taking some action. For buttons this will be the state used when the user has clicked the button and the application is waiting for reply from the back end. More advanced applications may use it while doing backend requests, if reflecting the processing in the UI is desired. Applications using optimistic feedback will render this state the same as the finished state.

failed: Processing failed. The application renders an error message.

finished: End state for one-shot actions. Communicates success to the user.

Applications can use additional states if desired, but prefer the above if appropriate.

Style your application

1. Create an application specific stylesheet
// ./src/apps/my-new-application/my-new-application.scss
.dpl-warm {
  color: maroon;
}
2. Add the class to your application
// ./src/apps/my-new-application/my-new-application.jsx
import React from "react";
import PropTypes from "prop-types";

export function MyNewApplication({ text }) {
  return (
      <h2 className='warm'>{text}</h2>
  );
}

MyNewApplication.defaultProps = {
  text: "The fastest man alive!"
};

MyNewApplication.propTypes = {
  text: PropTypes.string
};

export default MyNewApplication;
3. Import the scss into your story
// ./src/apps/my-new-application/my-new-application.dev.jsx
import React from "react";
import MyNewApplicationEntry from "./my-new-application.entry";
import MyNewApplication from "./my-new-application";

import './my-new-application.scss';

export default { title: "Apps|My new application" };

export function Entry() {
  // Testing the version that will be shipped.
  return <MyNewApplicationEntry />;
}

export function WithoutData() {
  // Play around with the application itself without server side data.
  return <MyNewApplication />;
}

Cowabunga! You now got styling in your application

Style using the DPL design system

This project includes styling created by its sister repository - the design system as a npm package.

By default the project should include a release of the design system matching the current state of the project.

To update the design system to the latest stable release of the design system run:

yarn add @danskernesdigitalebibliotek/dpl-design-system@latest

This command installs the latest released version of the package. Whenever a new version of the design system package is released, it is necessary to reinstall the package in this project using the same command to get the newest styling, because yarn adds a specific version number to the package name in package.json.

Using unreleased design

If you need to work with published but unreleased code from a specific branch of the design system, you can also use the branch name as the tag for the npm package, replacing all special characters with dashes (-).

Example: To use the latest styling from a branch in the design system called feature/availability-label, run:

yarn add @danskernesdigitalebibliotek/dpl-design-system@feature-availability-label

If the branch resides in a fork (usually before a pull request is merged) you can use aliasing and run:

yarn config set "@my-fork:registry" "https://npm.pkg.github.com"
yarn add @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-availability-label

If the branch is updated and you want the latest changes to take effect locally update the release used:

yarn upgrade @danskernesdigitalebibliotek/dpl-design-system

Note that references to unreleased code should never make it into official versions of the project.

Cross application components

If the component is simple enough to be a primitive you would use in multiple occasions it's called an 'atom'. Such as a button or a link. If it's more specific that that and to be used across apps we just call it a component. An example would be some type of media presented alongside a header and some text.

The process when creating an atom or a component is more or less similar, but some structural differences might be needed.

Creating an atom

1. Create the atom
// ./src/components/atoms/my-new-atom/my-new-atom.jsx
import React from "react";
import PropTypes from 'prop-types';

/**
 * A simple button.
 *
 * @export
 * @param {object} props
 * @returns {ReactNode}
 */
export function MyNewAtom({ className, children }) {
  return <button className={`btn ${className}`}>{children}</button>;
}

MyNewAtom.propTypes = {
  className: PropTypes.string,
  children: PropTypes.node.isRequired
}

MyNewAtom.defaultProps = {
  className: ""
}

export default MyNewAtom;
2. Create styles for the atom
// ./src/components/atoms/my-new-atom/my-new-atom.scss
.dpl-btn {
    color: blue;
}
3. Import the atom's styles into the component stylesheet
// ./src/components/components.scss
@import 'atoms/button/button.scss';
@import 'atoms/my-new-atom/my-new-atom.scss';
4. Create a story for your atom
// ./src/components/atoms/my-new-atom/my-new-atom.dev.jsx
import React from "react";
import MyNewAtom from "./my-new-atom";

export default { title: "Atoms|My new atom" };

export function WithText() {
  return <MyNewAtom>Cick me!</MyNewAtom>;
}
5. Import the atom into the applications or other components where you would want to use it
// ./src/apps/my-new-application/my-new-application.jsx
import React, {Fragment} from "react";
import PropTypes from "prop-types";

import MyNewAtom from "../../components/atom/my-new-atom/my-new-atom"

export function MyNewApplication({ text }) {
  return (
      <Fragment>
        <h2 className='warm'>{text}</h2>
        <MyNewAtom className='additional-class' />
      </Fragment>
  );
}

MyNewApplication.defaultProps = {
  text: "The fastest man alive!"
};

MyNewApplication.propTypes = {
  text: PropTypes.string
};

export default MyNewApplication;

Finito! You now know how to share code across applications

Creating a component

Repeat all of the same steps as with an atom but place it in it's own directory inside components.

Such as ./src/components/my-new-component/my-new-component.jsx

Editor example configuration

If you use Code we provide some easy to use and nice defaults for this project. They are located in .vscode.example. Simply rename the directory from .vscode.example to .vscode and you are good to go. This overwrites your global user settings for this workspace and suggests som extensions you might want.

Usage

There are two ways to use the components provided by this project:

  1. As standalone JavaScript applications mounted within HTML pages generated by a separate system.
  2. As components within a larger JavaScript application (Under development)

Naive app mount

So let's say you wanted to make use of an application within an existing HTML page such as what might be generated serverside by platforms like Drupal, WordPress etc.

For this use case you should download the dist.zip package from the latest release of the project and unzip somewhere within the web root of your project. The package contains a set of artifacts needed to use one or more applications within an HTML page.

HTML Example

A simple example of the required artifacts and how they are used looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Naive mount</title>
    <!-- Include CSS files to provide default styling -->
    <link rel="stylesheet" href="/dist/components.css">
</head>
<body>
    <b>Here be dragons!</b>
    <!-- Data attributes will be camelCased on the react side aka.
         props.errorText and props.text -->
    <div data-dpl-app='add-to-checklist' data-text="Chromatic dragon"
         data-error-text="Minor mistake"></div>
    <div data-dpl-app='a-none-existing-app'></div>

    <!-- Load order og scripts is of importance here -->
    <script src="/dist/runtime.js"></script>
    <script src="/dist/bundle.js"></script>
    <script src="/dist/mount.js"></script>
    <!-- After the necessary scripts you can start loading applications -->
    <script src="/dist/add-to-checklist.js"></script>
    <script>
      // For making successful requests to the different services we need one or
      // more valid tokens.
     window.dplReact.setToken("user","XXXXXXXXXXXXXXXXXXXXXX");
     window.dplReact.setToken("library","YYYYYYYYYYYYYYYYYYYYYY");

      // If this function isn't called no apps will display.
      // An app will only be displayed if there is a container for it
      // and a corresponding application loaded.
      window.dplReact.mount(document);
    </script>
</body>
</html>

As a minimum you will need the runtime.js and bundle.js. For styling of atoms and components you will need to import components.css.

Each application also has its own JavaScript artifact and it might have a CSS artifact as well. Such as add-to-checklist.js and add-to-checklist.css.

To mount the application you need an HTML element with the correct data attribute.

<div data-dpl-app='add-to-checklist'></div>

The name of the data attribute should be data-dpl-app and the value should be the name of the application - the value of the appName parameter assigned in the application .mount.js file.

Data attributes and props

As stated above, every application needs the corresponding data-dpl-app attribute to even be mounted and shown on the page. Additional data attributes can be passed if necessary. Examples would be contextual ids etc. Normally these would be passed in by the serverside platform e.g. Drupal, Wordpress etc.

<div data-dpl-app='add-to-checklist' data-id="870970-basis:54172613"
     data-error-text="A mistake was made"></div>

The above data-id would be accessed as props.id and data-error-text as props.errorText in the entrypoint of an application.

Example
// ./src/apps/my-new-application/my-new-application.entry.jsx
import React from "react";
import PropTypes from "prop-types";
import MyNewApplication from './my-new-application.jsx';

export function MyNewApplicationEntry({ id }) {
  return (
    <MyNewApplication
      // 870970-basis:54172613
      id={id}
    />
}

export default MyNewApplicationEntry;

To fake this in our development environment we need to pass these same data attributes into our entrypoint.

Example
// ./src/apps/my-new-application/my-new-application.dev.jsx
import React from "react";
import MyNewApplicationEntry from "./my-new-application.entry";
import MyNewApplication from "./my-new-application";

export default { title: "Apps|My new application" };

export function Entry() {
  // Testing the version that will be shipped.
  return <MyNewApplicationEntry id="870970-basis:54172613" />;
}

export function WithoutData() {
  // Play around with the application itself without server side data.
  return <MyNewApplication />;
}

Extending the project

If you want to extend this project - either by introducing new components or expand the functionality of the existing ones - and your changes can be implemented in a way that is valuable to users in general, please submit pull requests.

Even if that is not the case and you have special needs the infrastructure of the project should also be helpful to you.

In such a situation you should fork this project and extend it to your own needs by implementing new applications. New applications can reuse various levels of infrastructure provided by the project such as:

  1. Integration with various webservices
  2. User authentication and token management
  3. Visual atoms or components
  4. Visual representations of existing applications
  5. Styling using SCSS
  6. Test infrastructure
  7. Application mounting

Once the customization is complete the result can be packaged for distribution by pushing the changes to the forked repository:

  1. Changes pushed to the master branch of the forked repository will automatically update the latest release of the fork.
  2. Tags pushed to the forked repository also will be published as new releases in the fork.

The result can be used in the same ways as the original project.

About

React components for the Danish Public Libraries.

https://danskernesdigitalebibliotek.github.io/dpl-react

License:GNU Affero General Public License v3.0


Languages

Language:TypeScript 96.6%Language:JavaScript 2.9%Language:SCSS 0.4%Language:EJS 0.0%Language:Shell 0.0%