Web Modules Core
- Objective
- Definition
- Collaboration Guidelines
- Monorepository Guidelines
- Installation
- Initial configuration
- Localization
- Skins
- Skin Development
- Lists
- Redux
- Q & A
- Dev Server & Demo
- Check Build Consistency
- Documentation
- Testing
- ESLint
- Migration
Objective
As a part of ongoing effort to make Web Modules available for use outside of Service Web and also to simplify cross-location collaboration process, we plan make Web Modules sub-packages available in NPM.
Definition
This repo consists of following main sub-packages:
build
— build utilities (skin/i18n loaders, NWB config builder)core
- UI librarydnd
- drag'n'drop helpersredux
- Redux helpersutils
- client-side skin/i18n support
Core (e.g. UI) package consists of following big entities:
- Skin — adaptation of Bootstrap SASS project for SW look and feel, including special brands
- UI — adaptation of React Bootstrap project which includes:
- Automation tools support
- Easy nested forms
- Grids (could be replaced with Redux Grids)
- Wizards
Benefits
- Independent release cycle, teams will have an ability to share code before “official” syncs with mercurial’s default
- Not only SW will have an ability to use component library and skins: express setup, for example
- Demo pages with all available components will help design teams to know their building blocks better
- Transparent industry-standard open source contribution process via pull requests
- In future we can set up a separate CI process. In order to use externalized NPM version in SW a traditional Web Module Package still will have to exist, but this package should just re-export everything from NPM package.
- Transition is a one-time effort when we will have to gather all changes of WMC, put them in GIT, refer to GIT repo
from
package.json
in SW and do cleanup. Ideally, this should be done on default branch, so that all teams will be able to sync right away.
Collaboration Guidelines
- FT will work with two DEV branches – Mercurial (SW) and Github (WM core). Before the Mercurial code merge, FT will
need to issue a WM pull request first if updates were made. This action should generate a new version number
such as
major.minor.patch
. Other FT with WM Dev branch would need to resolve conflicts if any. At the time of the Mercurial check in request, the WM core new version number will need to be registered. Since WM doesn’t report the CI result, we assume the GCI passing rate for SW can represent the WM code quality. - The key point is that team responsible for SW merge must bump the version of WM Core in package.json, and in order to do that, this team needs to finalize any pending pull requests, if any. This process ensures that SW will always have it’s own codebase in sync with certain WMC version.
- If a bug in WM needs to be fixed after Code Freeze, FT needs to file a bug to get the WM version upgraded for SW. So this action should trigger the WM update alert after CF.
- GIT repository of WMC must follow same branching model that SW repo has. With one difference — actual versions are
GIT tags. For example: Assume SW has a bug in SW
elease_81
branch. It means, that in GIT the branch with same name WMCrelease/8.1
should be cut from the same version that SW was pointing. After fixes GITrelease/8.x
branch should get a tag8.1.x
. Appropriate8.1.x
version should be referenced inpackage.json
in SWrelease_81
branch - Before SW release_81 branch will be merged to default, WMC release/8.1 branch should be merged to master and should
get a new version, say
8.2.x
, since there could be changes for 8.2 already (if there were no updates for 8.2 in WMC then version will still be8.1.x
) - After that SW release_81 branch is merged to default and correct version should be referenced in
package.json
Monorepository Guidelines
Track dependencies between src/*
directories (sub-packages), core
that depends on utils
is OK because it is the
lowest level, redux
or dnd
that depend on core
is NOT OK because redux
can easily live without core
and it
will also cause issues with SW packaging mechanism
Installation
npm install web-modules-core --save
Initial configuration
; ;
After that application may access configuration at any time:
;console;
Localization
Where should I store localized strings?
The root namespace for all localization files is src/app/lang
. All strings are grouped by relevance into packages.
How to include localization?
See full example repo for more information.
In order to enable 3.0 workflow you need to execute the rc-generate
script:
You need to include localization only once before you bootstrap the entire application:
Then create a langLoader.js
file:
// langLoader.js; // this file is generated by Webpack utility; const langLoader = ; ;
After that you can use it in main entry point:
// index.js;; // Step 1. Set the language and all other configuration (brand, etc.); // Step 2. Load strings;
Then you can use localization in components:
// components/*.js;;; // you may import files directly // Step 1. Set the language and all other configuration (brand, etc.); // Step 2. Load strings;
In case translation contains HTML you still need to use translate()
and wrap it into <DangerousHTML/>
component. DangerousHTML designed specially to mark that values of translated variables should be escaped to prevent XSS vector.
Say you have this key in your localization dictionary:
// content of src/lang/home/index-en_US// GREETING_MESSAGE: 'Hello, <strong>{userName}</strong>';
;// ... { const userName = thisprops; // DangerousHTML will inject unsafe HTML into the DOM, so You have to make sure that html parameters unescaped return <p> <DangerousHTML> </DangerousHTML> </p> ;}
<Translate/>
tag is deprecated and should not be used.
Always include en_US
version of file. Content of the file will be dynamically rewritten during runtime.
What is the structure of strings file?
The structure is a simple object exported as usual:
/** @namespace SOME_NAME_SPACE */ ALERT: "Alert" CONFIRMATION: "Confirmation" // ICU Message Format is used to tokenize the string MF_TEST: "You have received {NUM, plural, one{# call} other{# calls}} today \ Much wow, such new line" TOKENIZED: "A string with {TOKEN}" // If the same token can have different value in different brands COMPANY_NAME: 0: 'WhiteLabel' 1210: 'RingCentral' 2222: 'SomethingNew' ;
Skins
;; // this file is generated by Webpack utility // Step 1. Set the brand and all other configuration (langage, etc.); // Step 2. Set the loader; // Step 3. Load skin, promise is returned, but it's optional to handle it;
Skin Development
Variables
There are two variables files:
- Skinnable (colors, images): located in
src/skin/variables/_colors*.scss
!!! NOT ALLOWED TO USE ANYWHERE OUTSIDE SKIN PACKAGE OR PROJECT'S ROOT SKIN FILES!!! - Non-skinnable (dimensions, sizes, paddings, etc):
src/skin/variables/_dimensions*.scss
, allowed to be used in components since dimensions does not vary per brand
In Service Web skin is included in Bootstrap.js
with brand ID postfix.
If bootstrap element has to be extended
- Start with changing of variables
- Copy-paste bootstrap selector (including nesting) and alter only
- Create a JS file and extend Bootstrap's class
If something has to be shared between components/projects
It should go to core, no inter-component dependencies (rule of a thumb: if anything is used twice – it goes to core). Same applies to anything that has color.
skin/styles/*.scss
) a variable in skin/variables/colors
has to be created
For each custom color in skin files (- Give a semantic name to variables (like $checkboxGroupHeaderBorderColor)
- Camel Case
$table-bg-hover
In skin files it is allowed to use pre-defined Bootstrap variables like Such variables must either have a spec color, say $some-random-var: $light-gray
) or initialized explicitly
in colors.sass
.
Patterns
Colors and dimensions
- Each color must have a name (default brand color, default background, data table highlight, default text color)
- Colors that are derived from base ones (default brand color, etc.) should be defined as lighten(default-brand-color, 50%) and not as resulting HEX value
- Give names to default paddings/margins
- Give names to default font sizes
- Icons should have default dimensions (16x16, 24x24, 32x32 and so on)
- Names must be based on the purpose (default blue is a bad name since in another brand it could be purple/red/whatever) – all colors/widths/paddings/dimensions will eventually be converted to SCSS variables
- Generally, don't name variables based upon the values - name them semantically, so based upon logical function in the styling, Don't name a variable "light background" or "wide margin" or "large font", for example. The variable names should not presume or indicate anything about the specific values that are used.
Alignment
- Components should be aligned by using default Bootstrap containers
- If custom paddings are introduced they should have a value of an existing Bootstrap variable (gutter width for example) or a fraction of it
- Default alignments
- 12 column grid
- Left / center / right
- Provide instructions in PNGs
- For non-grid alignments or when it's not obvious
- For grid blocks when they have sub-grids
- Non-grid elements should be drawn off the grid to reduce confusion
- Highlight grid blocks to make it clear how many columns it occupies
Dynamic behavior
- Provide mocks with different states of dynamic elements (collapsed vs expanded left panel, expanded table row)
- If standard behaviors for some components in a mock have already been established and are part of a UX style guide, then provide the relevant documents or links as reference.
- Provide overflow guidelines (multiple numbers in table row, too long text labels, etc.)
Layouts
Blocks
Popups and panels have default padding in header, footer and body. This padding is synchronized with bottom margin of
H1
, H2
, H3
, P
, DL
tags, etc.
In order to cancel this padding use noPadding
and noHeaderPadding
props in <Popup>
and <Panel>
.
If you need to lift/shift component, you can wrap it with <Block>
component like the following:
<Block > <TabPanel .../></Block>
This will move the tab panel right close to left, right & bottom borders of container.
If you need to have more space around component use rc-block-accent
class.
If you need less than usual block space (device order or role permissions, for example) use rc-block-condensed
class.
All custom components (like TabPanel
) should not have any margins.
Grid System
TBD (see demo for now).
Inline Layouts
TBD (see demo for now).
Icons
Use npm run icommon
task to download new icons, don't forget to commit files after that.
Lists
In order to create a list a new class that extends List class must be created:
; { return <table className="rc-grid table-striped"> <thead> <th style=width: '5%'><ListHeaderCheckbox/></th> <GridSorter property="status" className="status-column" style=width: '10%'></GridSorter> <GridSorter property="name" className="name-column" style=width: '20%'></GridSorter> <GridSorter property="numbers" className="number-column" style=width: '25%'></GridSorter> <GridSorter property="pin" className="extension-column" style=width: '15%'></GridSorter> <GridSorter property="department" className="department-column" colSpan="2" style=width: '25%'></GridSorter> </thead> <tbody> this ? this : this </tbody> </table>; } { var actions = ; var gridActions = actionslength > 0 ? <div className="rc-grid-visibleOnRowHover rc-grid-actions"> <ActionsButton popoverClassName='rc-grid-actionsButton-popover' actions=actions/> </div> : null; return <tr className= key=recordid> <td><ListCheckbox record=record/></td> <GridCell></GridCell> <GridCell onClick=SWopenUserSettings>recordname</GridCell> <GridCell>ExtensionsHelperutils</GridCell> <GridCell>recordpin</GridCell> <GridCell>recorddepartment</GridCell> <td>gridActions</td> </tr>; } { return <tr className="empty-row"> <td colSpan="8">super</td> </tr>; }
The way we approach lists is basically a composition of reusable small React components with very specific functionality. Those components can talk to each other via shared objects in context.
List of available components:
ListSorter
– control responsible for sorting of the storeGridSorter
– grid (table) version of ListSorterListCheckbox
– allows to select items from storeListHeaderCheckbox
– controls all ListCheckboxesListHighlight
– highlights matched characters based on filter inputGridCell
– short hand for<td><ListHighlight>...</ListHighlight></td>
that can be used in tables
List class itself has some useful methods that can be accessed by it's descendants:
- isSelectedRow(record) – use this to highlight a selected row in a certain way
- highlight(text) – functional analogue of component
Redux
Step 0. Imports
;;;;
Step 1. Create a Setup
const setup = ;
Step 2. Create Redux Store with Setup
You can use createStore
helper or create a store with all middlewares by yourself.
const store persistor = ;
Step 3. Define and connect UI components
UI elements may also be injected into Panel
props via function connectUI
(Highlight
in this example). Actions and
selected state items are injected via connectSetup
:
let { const onFilterChange = { ; }; return <div> <div> <input type="text" value=filter onChange=onFilterChange /> </div> items </div>;} Panel = Panel;Panel = Panel;
3.1. Mapping props
If names of props provided by connectSetup
does not satisfy you, you can optionally provide a mapper function:
let HL = Highlight;
connectSetup
3.2. Individually connect UI elements via UI elements may be connected separately and then used as regular React elements w/o injection into props:
let HL = connectSetup(setup)(Highlight);
connect
3.3. Individually connect UI elements via If you don't like how connectSetup
wires things, you can also manually connect anything to anything using React Redux
connect
function, this requires deeper knowledge of Setup
API:
let HL = Highlight;
3.4. Contextual way
Another way to connect UI is through contextual components:
let { return <div> items </div>;}; const ContextPanel = <SetupContext setup=setup> <Panel/></SetupContext>;
Step 4. Render
Don't forget to add <PersistGate persistor={persistor}>
inside <Provider store={store}>
, otherwise persist will work
with errors.
const App = <PersistGate persistor=persistor> <Provider store=store> <ContextPanel/> </Provider> </PersistGate>; ReactDOM;
Understanding modes
There are 2 modes: client and server. In client mode all sorting & filtering is done on client, in server mode client relies on backend, that does all the work.
You need to define loadFn
which will return either an array of items or an object
{items: [], total: 0, filteredTotal}
. In server mode this function will be called each time anything changes.
In client mode this function can be called manually by your code (for example, when it's time to mount component).
It is safe to call this function as often as needed.
Redux Tips And Tricks
Please read useful blog article.
combineReducers()
and primitive reducers instead of complex state and one reducer
Use // BAD { } // GOODconst gridReducerGood = ;
One action may cause multiple reducers to change state
Actions never match reducers 1-to-1, one action may cause a ripple effect in sub-states. Use this technique to make linked changes in many states based on user interactions.
{ } const DEFAULT_PAGE = 1; { }
Store only pure data and unmodified user inputs
All derived data has to be selected from state by reselect's optimized createSelector()
. This allows to manipulate
the state freely without worrying that some derived data could become stale.
// state {allItems: [], filter: 'filter string'} // dumb straightforward selectors are also needed sometimesconst getAllItems = stateallItems;const getAllItemsLength = length; const getFilter = statefilter; // private filter functionconst filterFn = ~itemname; const getFilteredItems = ; const getFilteredItemsLength = ;
connect()
-wrapped ones in the same file
Export unwrapped dumb components and default export Good for tests, good for development.
const Cmp = { };Cmp;
connect()
Make any heavy iterable block as a separate component wrapped with Connect makes component to act as a pure function, which boosts performance since React will not re-render it if data has not changed.
Make sure you bind all actions only once and you don't provide anything mutable as props <Cmp fn={this.makeClick} />
in render of parent will ruin any pure render optimizations in Cmp because this.makeClick
will be a different
instance each time parent is re-rendered and it will cause children to re-render too.
Use imported actions in mapDispatchToProps and keep it as plain object
;const Cmp = { };null onSave: actionssavePost // this is perfectly reachable by any IDE and is very clean way to define the relationshipCmp;
Prefer using functional components over class-based ones
You write less code and make the behavior of components a lot cleaner if you work with them as if they are pure functions. Output can be cached in this case.
Prefer using uncontrolled inputs if state does not need to be maintained between page reloads
This makes overall global state a lot simpler and reduces the amount of reactions on user inputs.
Don't mix routing with app state and component state
There has to be always only one source of truth.
React Router should be responsible for URL params which affect page state
URLs that can be handed over from one user to another, good example is a JIRA search which can be copy-pasted from location bar, the rest of the UI state (that is not reflected in location bar) should be a part of redux or a part of component (if this state is accessible only by one component).
Read more: https://github.com/reactjs/redux/issues/1287.
Parent components must provide the bare minimum of props for child components
Do not provide things that are not required, e.g. do not spread the entire props of parent into a child, this will cause child to render more often than needed.
Children should get actions via mapDispatchToProps (not from parent via props)
This ensures parent knows as less as possible about it's children and we are free to change internal structure if needed.
Only show as much of the redux state to a component as it needs by slicing exact parts of state
Keep all components as stupid as possible: React pure render optimizations allows to re-render components ONLY if something visible data changes, so never supply a whole chunk of state to a component, first, break it to simpler things or even to primitives.
Each component should have it's own file unless it will only ever be found inside another component and it is simple
Saves space and makes navigation easy. If two components are placed in one file, then both should be exported, the main one should be exported as default.
Simple actions are better, type and payload is all that is necessary for most
If an action is more complex – sometimes it's better to break it down to a set of simpler actions and call them one by one – easier to test.
Component's action props naming may not match redux actions, use the most appropriate based on placement
Redux actions are usually named as "makeSomething" whereas components params should be names in a more event-like manor
onSomething
and even can have UI details in naming (onSaveClick
– click is a UI detail).
Make authentication data available via redux store
Even if authentication is maintained by separate component and stored, for example, in localStorage, it's useful to make it available in react app via redux state (at least as a boolean) so that app will have an easy way to show logged in or logged out statuses:
// Somewhere in Store setup ; { platform
// LoggedInWrapper.js used as route wrapper around authenticated routes ;; const loggedInWrapper = { if !user ; // setTimeout is needed to make async transition outside of sync render process return null; const onClickLogout = { ; }; return <div> <div><button onClick=onClickLogout>Log Out userusername</button></div> <div>children</div> </div>}; user: goToLogin: goToLogin logout: logoutloggedInWrapper;
Q & A
What does stateName in new ThingsStore({stateName: 'thingsList'}) do?
Sets the key for persistent storage of filter settings.
EventBus.emit("afterStoreReload");
(or before) does not provide event information? Why we need such event?
Why It is used for automation to and to show/hide RC.Loader
.
EventBus.on("resetStore", this.onRefresh);
?
Why components subscribe to To reload data if linked store has been reset and for other cross-store syncs
this.register('Things');
do?
What does Sets up a key that may be used to externally emit an event on which the store have to react
Dev Server & Demo
npm startopen http://localhost:5000
Check Build Consistency
npm run build
Documentation
This command generates API documentation, which then needs to be committed to repository just like any other file.
npm run docs
Please read JSDoc 2 Markdown Wiki to get more information and recipes.
Testing
In order to enforce test creation developers must supply unit tests along with merge requests.
For special cases unit tests are mandatory, merge requests without such tests will not be accepted. Special cases are:
- Critical or not obvious functionality
- Complicated logic
- Critical data components like stores
Merge requests will not be accepted if Gitlab CI pipelines are be broken.
# single run npm test # watch mode npm run test-watch
Tests should be located near the appropriate source files with names OrigName.spec.js
.
WMC offers a set of tools which allow to simplify React-based tests:
- Enzyme https://github.com/airbnb/enzyme
- Chai
- Karma
- Sinon
Chai, Karma and Sinon are globally accessible, no need to import.
Minimal visual test setup:
;; ;
Coverage can be found in the build directory.
Code Quality Tools
This repository using Prettier for consistent auto formatting. And provides a few scripts to simplify development.
Also, ESLint is used to follow our best practices and to avoid unnecessary questions on code-review. See ringcentral-javascript for details
Development tools integration in IntelliJ IDEA
-
Enable eslint
-
Add custom 'prettier' command as a handy tool for reformat current file in IDE.
Open Tools > External Tools > Add
set arguments field "./scripts/prettier/index.js -wf $FilePathRelativeToProjectRoot$"
set Working Directory to "$ProjectFileDir$" -
Setup hotkey for custom command
open Keymap > Externa Tools > External Tools > prettier click right button on "Add Keyboard Shurtcut" and set "Ctrl+Shift+P" or any other You like to use. -
Now You are ready to use this tool:
Open any .js file, edit it and type "Ctrl+Shift+P" to apply automatically code formatting at this file. Any eslint issue will be highlighted.
Commit flow
Say, you are ready to commit and push your changes.
We enable pre-commit hook to ensure that new code is free from linter (thanks ESlint) and formatting (thanks Prettier) issues.
So, during commit, pre-commit scripts run and may detect issues You need to fix.
-
attempt to commit, but formatting issues detected
-
autofix formatting issues and attempt to commit, but linter issues detected
-
fix lint errors, then commit
Migration
9.2 to 9.3
-
Update
web-modules-core
pacakge to 9.3.x -
Install NWB as dev dependency:
$ npm install nwb --save-dev -
Uninstall
web-modules-*
packages (exceptweb-modules-core
):$ npm uninstall web-modules-utils web-modules-dnd web-modules-redux -
Remove
webpack.config.js
and create newnwb.config.js
, use Boilerplate as reference. Move all custom sections to new file, you will have to change format from Webpack config to NWB. -
Create
src/index.html
andsrc/index.js
entry points. -
Change build/test scripts to call
nwb
instead ofwebpack
, use Boilerplate as reference too. -
All packages were merged into one mono repository, so you have to change references. Suggested auto-replaces:
web-modules-core
->web-modules-core/src/core
web-modules-core/src/...
->web-modules-core/src/core/...
web-modules-utils
->web-modules-core/src/utils
web-modules-utils/src/...
->web-modules-core/src/utils/...
web-modules-dnd
->web-modules-core/src/dnd
web-modules-redux
->web-modules-core/src/redux
web-modules-utils/utils
->web-modules-core/src/build
11.0
to 11.1
-
Replace NWB configs with new
build-config.js
andwebpack.config.*.js
-
Update
start
andbuild
scripts: addrc-webpack
usage -
Add
.babelrc.js
-
Update tests
19.2
to 19.3
(former 11.2
)
-
Prepend
start
andbuild
scripts with usage ofrc-generate --config build-config.js
script -
Use full package imports:
import {Whatever} from "web-modules-core";
, tree-shaking will do the rest
19.3.4
to 19.3.20
In 19.3.20 monorepository has been introduced. SW used following remap: web-modules-xxx
-> web-modules-all/xxx
.
19.3.24
to 19.3.25
Localization mechanism has been changed. Boilerplate/SW/etc. code no longer needs to import localize
function and
provide it to createStringsLoader
:
import {localize as localizeCore, createStringsLoader, moment} from 'web-modules-core';
^^^^^^^^^^^^^^^^^^^^^^^^^
export default createStringsLoader({loader, defaultStrings, localizeCore, moment});
^^^^^^^^^^^^^
Code underlined with ^^^
should be removed.