Learn about our RFC process, Open RFC meetings & more.Join in the discussion! »

web-modules-core

19.4.3 • Public • Published

Web Modules Core


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 library
  • dnd - drag'n'drop helpers
  • redux - Redux helpers
  • utils - client-side skin/i18n support

Core (e.g. UI) package consists of following big entities:

  1. Skin — adaptation of Bootstrap SASS project for SW look and feel, including special brands
  2. 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 WMC release/8.1 should be cut from the same version that SW was pointing. After fixes GIT release/8.x branch should get a tag 8.1.x. Appropriate 8.1.x version should be referenced in package.json in SW release_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 be 8.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

import {setConfig} from "web-modules-core/src/utils";
 
setConfig({
    brandId: 1210,
    userLanguage: getLocale(),
    brandDisplayName: 'RingCentral',
    shortBrandName: 'RC'
});

After that application may access configuration at any time:

import {getConfig} from "web-modules-core/src/utils";
console.log(getConfig().brandId);

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:

{
  "scripts": {
    "generate": "rc-generate --config build-config.js",
    "start": "npm run generate && rc-webpack --config webpack.config.dev.js"
  }
}

You need to include localization only once before you bootstrap the entire application:

Then create a langLoader.js file:

// langLoader.js
import {loader, defaultLocale} from 'src/lang/loader'; // this file is generated by Webpack utility
import {localize, createStringsLoader} from 'web-modules-core';
 
const langLoader = createStringsLoader(loader, defaultLocale, localize);
 
export default langLoader;

After that you can use it in main entry point:

// index.js
import {getStrings, translate, setConfig} from "web-modules-core/src/utils";
import langLoader from "./langLoader";
 
// Step 1. Set the language and all other configuration (brand, etc.)
setConfig({
    //...
    userLanguage: 'en_US'
});
 
// Step 2. Load strings
langLoader().then(() => {
    // Now it is safe to use localization
    alert(translate(getStrings().SOME_KEY));
});

Then you can use localization in components:

// components/*.js
import {translate, setConfig, getConfig} from "web-modules-core/src/utils";
import stringsLoader from "../langLoader";
import page1 from "../lang/page1/index-en_US"; // you may import files directly
 
// Step 1. Set the language and all other configuration (brand, etc.)
setConfig({
    //...
    userLanguage: 'en_US'
});
 
// Step 2. Load strings
stringsLoader(getConfig().userLanguage).then(() => {
    // Now it is safe to use localization, but keep in mind not to re-map page1's properties, use page1 as is
    alert(translate(page1.SOME_KEY));
});

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
//
export default {
    GREETING_MESSAGE: 'Hello, <strong>{userName}</strong>'
};
import langHome from "src/lang/home/index-en_US";
// ...
render() {
    const {userName} = this.props;
    // DangerousHTML will inject unsafe HTML into the DOM, so You have to make sure that html parameters unescaped
    return (
        <p>
            <DangerousHTML>
                {translate(langHome.GREETING_MESSAGE, {userName: sanitize(userName)})}
            </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 */
export default {
    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

import {setSkinLoader, loadSkin, setConfig} from "web-modules-core/src/utils";
import skinLoader from "src/skin/loader"; // this file is generated by Webpack utility
 
// Step 1. Set the brand and all other configuration (langage, etc.)
setConfig({
    //...
    brandId: 1210
});
 
// Step 2. Set the loader
setSkinLoader(skinLoader);
 
// Step 3. Load skin, promise is returned, but it's optional to handle it
loadSkin();

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

  1. Start with changing of variables
  2. Copy-paste bootstrap selector (including nesting) and alter only
  3. 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.

For each custom color in skin files (skin/styles/*.scss) a variable in skin/variables/colors has to be created

  • Give a semantic name to variables (like $checkboxGroupHeaderBorderColor)
  • Camel Case

In skin files it is allowed to use pre-defined Bootstrap variables like $table-bg-hover

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 cancelBottomOffset cancelHorizontalOffset>
    <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:

import {ActionsButton, List, ListCheckbox, ListHeaderCheckbox, ListHighlight, GridSorter, GridCell} from 'web-modules-core/src/core';
 
class UsersGrid extends List {
 
    render() {
 
        return <table className="rc-grid table-striped">
            <thead>
            <th style={{width: '5%'}}><ListHeaderCheckbox/></th>
            <GridSorter property="status" className="status-column" style={{width: '10%'}}>{translate(langUsers.STATUS)}</GridSorter>
            <GridSorter property="name" className="name-column" style={{width: '20%'}}>{translate(langUsers.USER_NAME)}</GridSorter>
            <GridSorter property="numbers" className="number-column" style={{width: '25%'}}>{translate(langUsers.NUMBER)}</GridSorter>
            <GridSorter property="pin" className="extension-column" style={{width: '15%'}}>{translate(langUsers.EXTENSION)}</GridSorter>
            <GridSorter property="department" className="department-column" colSpan="2" style={{width: '25%'}}>{translate(langUsers.DEPARTMENT)}</GridSorter>
            </thead>
            <tbody>
            {this.hasItems() ? this.renderItems() : this.renderEmpty()}
            </tbody>
        </table>;
 
    }
 
    renderItem(record) {
 
        var actions = getItemActions(record);
 
        var gridActions = actions.length > 0 ? (<div className="rc-grid-visibleOnRowHover rc-grid-actions">
            <ActionsButton popoverClassName='rc-grid-actionsButton-popover' actions={actions}/>
        </div>) : null;
 
        return (<tr className={classnames({'is-selected': this.isSelectedRow(record)})} key={record.id}>
            <td><ListCheckbox record={record}/></td>
            <GridCell>{statusFormatter(record)}</GridCell>
            <GridCell onClick={SW.openUserSettings.bind(null, record.id)}>{record.name}</GridCell>
            <GridCell>{ExtensionsHelper.utils.numbersFormatter(record, this.getDetails())}</GridCell>
            <GridCell>{record.pin}</GridCell>
            <GridCell>{record.department}</GridCell>
            <td>{gridActions}</td>
        </tr>);
 
    }
 
    renderEmpty() {
        return <tr className="empty-row">
            <td colSpan="8">{super.renderEmpty()}</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 store
  • GridSorter – grid (table) version of ListSorter
  • ListCheckbox – allows to select items from store
  • ListHeaderCheckbox – controls all ListCheckboxes
  • ListHighlight – highlights matched characters based on filter input
  • GridCell – 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

import React from "react";
import ReactDOM from "react-dom";
import {Provider} from "react-redux";
import {createSetup, connectSetup, connectUI, createStore, Highlight, PersistGate} from "web-modules-core/src/redux";

Step 1. Create a Setup

const setup = createSetup({
    prefix: 'clientGrid',
    filterFn: filterFn,
    sortFn: sortFn,
    // loadFn: (options, dispatch) => USERS, // you can load items from server, this requires onLoad call when your component is mounted to DOM
    items: USERS, // or just set them synchronously
    sort: {by: 'firstName'}
});

Step 2. Create Redux Store with Setup

You can use createStore helper or create a store with all middlewares by yourself.

const {store, persistor} = createStore({
    reducers: {
        [setup.prefix()]: setup.createReducer()
    }
});

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 Panel = ({items, filter, onSetFilter, Highlight, ...props}) => {
 
    const onFilterChange = (e) => {
        onSetFilter(e.target.value);
    };
 
    return <div>
        <div>
            <input type="text" value={filter} onChange={onFilterChange} />
        </div>
        {items.map(item => (
            <div>
                <Highlight>{item}</Highlight>
            </div>
        ))}
    </div>;
}
 
Panel = connectSetup(setup)(Panel);
Panel = connectUI(setup)(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 = connectSetup(setup, ({filter, ...props}) => ({customFilter: filter, ...props}))(Highlight);

3.2. Individually connect UI elements via connectSetup

UI elements may be connected separately and then used as regular React elements w/o injection into props:

let HL = connectSetup(setup)(Highlight);

3.3. Individually connect UI elements via connect

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 = connect((state) => ({
    highlight: function someCustomHighlightFunction(){}, // optional
    filter: setup.getFilter(state)
}), {
    onSetFilter: setup.setFilter
})(Highlight);

3.4. Contextual way

Another way to connect UI is through contextual components:

let Panel = ({items, filter, onSetFilter, ...props}) => {
    return <div>
        {items.map(item => (
            <div>
                <ContextHighlight>{item}</ContextHighlight>
            </div>
        ))}
    </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 = ({store, persistor}) => (
    <PersistGate persistor={persistor}>
        <Provider store={store}>
            <ContextPanel/>
        </Provider>
    </PersistGate>
);
 
ReactDOM.render(<App  persistor={persistor} store={store}/>, document.getElementById('root'));

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.

Use combineReducers() and primitive reducers instead of complex state and one reducer

// BAD
function gridReducerBad(state = {items: [], pagination: {perPage: 10, page: 1}}, action) {  }
 
// GOOD
const gridReducerGood = combineReducers({
    items: (state = [], action) => {  },
    pagination: combineReducers({
        perPage: (state = 10, actions) => {  },
        page: (state = 1, actions) => {  }
    })
});

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.

function perPageReducer(state = 10, action) {
    switch (action.type) {
        case 'SET_PER_PAGE':
                return action.payload;
        default:
            return state;
    }
}
 
const DEFAULT_PAGE = 1;
 
function pageReducer(state = DEFAULT_PAGE, action) {
    switch (action.type) {
        case 'SET_PAGE':
            return action.payload;
        case 'SET_PER_PAGE': // handled also by another reducer
                return DEFAULT_PAGE; // you can have calculation logic to move user to correct page instead of page 1
        default:
            return state;
    }
}

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 sometimes
export const getAllItems = (state) => state.allItems;
export const getAllItemsLength = (state) => getAllItems(state).length;
 
export const getFilter = (state) => state.filter;
 
// private filter function
const filterFn = (filter) => (item) => ~item.name.indexOf(filter);
 
export const getFilteredItems = createSelector(
    [getAllItems, getFilter], 
    (allItems, filter) => allItems.filter(filterFn(filter)) // this filter will be executed only if allItems or filter will change
);
 
export const getFilteredItemsLength = createSelector(
    getFilteredItems, // you can use cached selector as input for another selector
    (filteredItems) => filteredItems.length
);

Export unwrapped dumb components and default export connect()-wrapped ones in the same file

Good for tests, good for development.

export const Cmp = ({onAction}) => {  };
export default connect()(Cmp);

Make any heavy iterable block as a separate component wrapped with connect()

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

import * as actions from "./someActions";
export const Cmp = ({onAction}) => {  };
export default connect(null, {
    onSave: actions.savePost // this is perfectly reachable by any IDE and is very clean way to define the relationship
})(Cmp);

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
 
import {login, logout} from "./actions";
 
function dispatchPlatformEventsToStore(platform, store) {
    platform.on('loginSuccess', () => { store.dispatch(login()); };
    platform.on('logoutSuccess', () => { store.dispatch(logout()); };
}
 
// Actions
 
export const goToLogin = () => { history.push('/login'); }; 
// LoggedInWrapper.js used as route wrapper around authenticated routes
 
import {getAuthUser} from './selectors';
import {goToLogin, logout} from './actions';
 
export const loggedInWrapper = ({user, goToLogin, logout, children}) => {
    if (!user) {
        setTimeout(goToLogin, 1); // setTimeout is needed to make async transition outside of sync render process
        return null;
    }
    const onClickLogout = (e) => { logout(); };
    return <div>
        <div><button onClick={onClickLogout}>Log Out {user.username}</button></div>
        <div>{children}</div>
    </div>
};
 
export default connect((state) => ({
    user: getAuthUser(state)
}), {
    goToLogin: goToLogin,
    logout: logout
})(loggedInWrapper);

Q & A

What does stateName in new ThingsStore({stateName: 'thingsList'}) do?

Sets the key for persistent storage of filter settings.

Why EventBus.emit("afterStoreReload"); (or before) does not provide event information? Why we need such event?

It is used for automation to and to show/hide RC.Loader.

Why components subscribe to EventBus.on("resetStore", this.onRefresh);?

To reload data if linked store has been reset and for other cross-store syncs

What does this.register('Things'); do?

Sets up a key that may be used to externally emit an event on which the store have to react

Dev Server & Demo

npm start
open 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:

Chai, Karma and Sinon are globally accessible, no need to import.

Minimal visual test setup:

import React from 'react';
import {shallow, mount, render} from 'web-modules-core/src/build/enzyme';
 
describe('Component name', () => {
    it('does stuff', () => {});
});

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

  1. Enable eslint
    idea-eslint-settings.png

  2. Add custom 'prettier' command as a handy tool for reformat current file in IDE.
    Open Tools > External Tools > Add idea-prettier-command.png
    set arguments field "./scripts/prettier/index.js -wf $FilePathRelativeToProjectRoot$"
    set Working Directory to "$ProjectFileDir$" idea-prettier-command-edit.png

  3. 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. idea-prettier-keymap.png idea-prettier-keymap-edit.png

  4. 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 idea-prettier-keymap-edit.png commit-step2-formatting-dailed.png

  • autofix formatting issues and attempt to commit, but linter issues detected commit-step3-formatting-autofix.png commit-step4-eslint-failed.png

  • fix lint errors, then commit commit-step5-commit-successful.png

Migration

9.2 to 9.3

  1. Update web-modules-core pacakge to 9.3.x

  2. Install NWB as dev dependency:

    $ npm install nwb --save-dev
  3. Uninstall web-modules-* packages (except web-modules-core):

    $ npm uninstall web-modules-utils web-modules-dnd web-modules-redux
  4. Remove webpack.config.js and create new nwb.config.js, use Boilerplate as reference. Move all custom sections to new file, you will have to change format from Webpack config to NWB.

  5. Create src/index.html and src/index.js entry points.

  6. Change build/test scripts to call nwb instead of webpack, use Boilerplate as reference too.

  7. 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

  1. Replace NWB configs with new build-config.js and webpack.config.*.js

  2. Update start and build scripts: add rc-webpack usage

  3. Add .babelrc.js

  4. Update tests

19.2 to 19.3 (former 11.2)

  1. Prepend start and build scripts with usage of rc-generate --config build-config.js script

  2. 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.

Keywords

none

Install

npm i web-modules-core

DownloadsWeekly Downloads

5

Version

19.4.3

License

ISC

Unpacked Size

8.74 MB

Total Files

1779

Last publish

Collaborators

  • avatar
  • avatar