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 ddb-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)
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 Adgangsplatfomen and OpenPlatform.
This project include a client id that matches the storybook setup which can be
used for development purporses. You can use the /auth
story to sign into
Adgangsplatformen for the storybook context.
Installation
make up
When storybook is started, you can access it at: ddb-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.
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
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
.ddb-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
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
.ddb-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:
- As standalone JavaScript applications mounted within HTML pages generated by a separate system.
- 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-ddb-app='add-to-checklist' data-text="Chromatic dragon"
data-error-text="Minor mistake"></div>
<div data-ddb-app='a-none-existing-app'></div>
<!-- Load order og scripts is of importance here -->
<script src="/dist/runtime.js"></script>
<script src="/dist/polyfills.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.ddbReact.setToken("user","XXXXXXXXXXXXXXXXXXXXXX");
window.ddbReact.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.ddbReact.mount(document);
</script>
</body>
</html>
As a minimum you will need the runtime.js
and bundle.js
. polyfills.js
is
needed to support older browsers - primarily Internet Explorer 11. 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-ddb-app='add-to-checklist'></div>
The name of the data attribute should be data-ddb-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-ddb-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-ddb-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 />;
}
React components
Applications in the project may also be used within larger React applications.
For this use case the project provides an NPM package containing the React components contained within the application as well as a few core classes for integrating with related services.
To use the package you must first register the GitHub NPM package registry by
adding @danskernesdigitalebibliotek:registry=https://npm.pkg.github.com
to the
.npmrc
file of your project.
Then you can add the package to your project: yarn add @danskernesdigitalebibliotek/ddb-react
or npm install @danskernesdigitalebibliotek/ddb-react
Finally you can use the components within your project.
React example
A simple example of how the package can be used looks like this:
import React from "react";
import ReactDOM from "react-dom";
import { AddToCheckListEntry } from "@danskernesdigitalebibliotek/ddb-react";
import "@danskernesdigitalebibliotek/ddb-react/components.css";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<b>Here be dragons!</b>
<AddToCheckListEntry text="Chromatic dragon"
errorText="Minor mistake" />
</React.StrictMode>,
rootElement
);
In this situation you can either choose to reuse an entire application including
its behavior by importing the *Entry
version. In the example above this is
achieved by importing the AddToCheckListEntry
application. You can also just
import the visual representation and provide your own behavior. In the case
above that would be handled by importing AddToCheckList
.
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:
- Integration with various webservices
- User authentication and token management
- Visual atoms or components
- Visual representations of existing applications
- Styling using SCSS
- Test infrastructure
- Application mounting
Once the customization is complete the result can be packaged for distribution by pushing the changes to the forked repository:
- Changes pushed to the
master
branch of the forked repository will automatically update the latest release of the fork. - 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.