Создание независимого компонента для React и его публикация в npm.

Предисловие

Началось все с того, что нужно было разработать новый интерфейс для MS-SQL отчетов. Разработать его нужно было в уже готовом проекте, который достался «по наследству». Сам проект написан на ajax. Коллега очень просил сделать интерфейс «как в Delphi», собственно так и назовем наш проект. Для реализации формы я выбрал react, т.к. на ajax писать скучно и геморройно, а react на данный момент я освоил вполне хорошо. Чтобы не было проблемы с совместимостью, интерфейс буду подгружать как фрейм. В предыдущих проектах я использовал umd пакеты react и тут возник вопрос, нужно было подключить календарь. Самому писать его было лень — нашел пару готовых, ни один из них в таком режиме не заводился. В связи с этим пришлось озадачиться с упаковкой проекта webpack-ом.  И тут было куча мануалов, которые ни к чему не приводили, т.к. на актуальную 4-ю версию вебпака их просто не было. Официальный мануал тоже довольно запутан. Чуть ниже я опишу минимальный конфиг. Сам интерфейс построен на той же логике, но немного в иной реализации. (Исходники фронтэнда можно посмотреть тут: https://github.com/namedudko/farmin-mssqlreportfrontend , а реализацию, с отключенным бэкэндом, тут: https://test.sergdudko.tk/mssql/form1/ ). Логика обработки данных в интерфейсе такова:

  1. Непосредственная логика полностью управляется в диспатчере редакса.
  2. Изначально в state загружается клон данных редакса.
  3. В визуализации компонента в качестве свойств элементов DOM можно ссылаться прямо на редакс только в том случае, если эти свойства const. Иначе это должен быть либо клон редакса, либо state компонента.
  4. При изменении мы выкидываем новое свойство элемента DOM в обработчик, который обновляет данные в редаксе.
  5. При монтировании компонента мы подписываем state компонента на соответствующие данные в редаксе с обязательной проверкой, что они не эквивалентны. Если проверку опустить, даже PureComponent рендерится чаще чем нужно.
  6. Возвращаемся к пункту 3.

В примере я запихнул всю логику в state компонента. Но сам принцип связи DOM элементов не изменился.

 

Webpack

Для начала установим зависимости:

npm i webpack --save
npm i babel-cli babel-core babel-loader babel-preset-env babel-preset-react prop-types uglifyjs-webpack-plugin webpack-cli webpack-dev-server webpack-merge --save-dev

Предполагаю, что глобально у вас уже стоит webpack (у меня был версии 4.8.3) и webpack-cli.

Итак, минимальный конфиг для упаковки react приложения. Создадим в корне файл .babelrc:

{
  "presets": ["env", "react"]
}

Если, конечно, вы не пишете на ES5/ES6 синтаксисе — можете обойтись и без него. Далее нам нужен корректный package.json, рассмотрим на примере моего, разбирать по полям все не стану — только основное:

{
	"name": "delphiform",
	"version": "1.0.3",
	"description": "Forms for React 16+",
	"main": "./umd/delphiform.js",
	"dependencies": {
		"lodash": "^4.17.10",
		"react": "^16.3.2",
		"webpack": "^4.8.3"
	},
	"devDependencies": {
		"babel-cli": "^6.26.0",
		"babel-core": "^6.26.3",
		"babel-loader": "^7.1.4",
		"babel-preset-env": "^1.7.0",
		"babel-preset-react": "^6.24.1",
		"prop-types": "^15.6.1",
		"uglifyjs-webpack-plugin": "^1.2.5",
		"webpack-cli": "^2.1.3",
		"webpack-dev-server": "^3.1.4",
		"webpack-merge": "^4.1.2"
	},
	"scripts": {
		"build": "webpack --optimize-minimize --mode production"
	},
	"repository": {
		"type": "git",
		"url": "https://github.com/namedudko/delphiform/"
	},
	"keywords": [
		"react",
		"reactjs",
		"form",
		"forms",
		"react-forms",
		"reactdom",
		"delphiform"
	],
	"links": {
			"npm": "https://www.npmjs.com/package/delphiform",
			"homepage": "https://github.com/namedudko/delphiform/#README",
			"repository": "https://github.com/namedudko/delphiform/",
			"bugs": "https://github.com/namedudko/delphiform/issues"
		},
		"publisher": {
			"username": "Name Surname",
			"email": "admin@sergdudko.tk"
		},
	"author": "Name Surname",
	"license": "MIT",
	"style": "./umd/delphiform.css"
}
  • name — наименование пакета в npm
  • version — версия пакета в npm
  • main — путь к первому скрипту пакета
  • dependencies — зависимости, которые потянет пакет. Вебпак у меня не обязательный, просто без него не упакуется.
  • scripts — скрипты запускаемые командой npm run имя_скрипта

Ну и сам конфиг webpack.config.js:

const path = require('path');
const webpack = require("webpack");

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  plugins: [],
  entry: {
	filename: path.resolve(__dirname, './src/') + '/delphiform.js'
  },
  output: {
    path: path.resolve(__dirname, './umd/'),
    filename: 'delphiform.js',
	libraryTarget: "umd"
  }
};

Если не указать entry — он будет искать скрипт, указанный в main package.json, что в намем случае не вариант — в пакете будет уже скомпилированный umd (см. libraryTarget) пакет.

 

Компонент

В перспективе хочется разнообразить библиотеку компонентов, а значит её нужно предварительно создать. Для удобство сделаю компоненты в разных скриптах. Для этого создам первый скрипт-связку:

/** Delphi Form v1.0.0
 * 
 *
 * Copyright (c) 2018-present, 
 * by Name Surname (admin@sergdudko.tk).
 *
 * LICENSE MIT.
 */
 
"use strict"

import _ from 'lodash';
import React from 'react';

import DelphiForm1 from './DelphiForm1.js';
import DelphiForm2 from './DelphiForm2.js';

module.exports.form1 = DelphiForm1;
module.exports.form2 = DelphiForm2;

Соответственно нам теперь нужно создать два скрипта DelphiForm1.js и DelphiForm2.js в которых описать компоненты доступные по ссылкам form1 и form2 нашего модуля.

В DelphiForm2.js я запишу просто тестовую форму, чтобы она выводила хоть что-то (слово TEST).

/** Delphi Form v1.0.0
 * 
 *
 * Copyright (c) 2018-present, 
 * by Name Surname (admin@sergdudko.tk).
 *
 * LICENSE MIT.
 */
 
"use strict"

import _ from 'lodash';
import React from 'react';

module.exports = class DelphiForm2 extends React.PureComponent{
  
	constructor(props, context){
		super(props, context);
	}
	
  	render() {			
		return (
			<div className="DelphiForm2">
				TEST
			</div>
		);
	}
};

А DelphiForm1.js будет наш компонент формы. Визуально он будет представлять из себя:

Логика работы компонента:

Мы получаем данные в props. Допустимые данные

  • store — объект json, где ключами являются уникальные идентификаторы (далее uid), а значением наименование.
  • selected — массив uid-ов, которые будут изначально выбраны (нужно  для реализации сохранения состояния компонента).
  • name — имя компонента.
  • callback — функция обратного вызова, в которую будет возвращен массив selected при инициализации компонента и далее будет возвращаться массив выбранных элементов из правой формы.

Изначально мы загружаем в ObjSearch — весь props.store объект, при этом переводим в верхний регистр все значения (нужно для реализации регистронезависимого поиска). В SelectedArr — загружаем массив props.selected, далее будем работать с SelectedArr — хранилище правой формы, оно же выбрасывается в callback при обновлении. SearchStr — строка поиска при монтировании пуста. Т.к. стока поиска пуста, то в SearchArr — массив uid/хранилище левой формы будут включены все ключи props.store, за исключением тех что уже отобраны в SelectedArr. И эти же ключи будут включены в StoreArr. При клике по левой форме мы удаляем uid из SearchArr и StoreArr и добавляем его в SelectedArr. При клике по правой форме — с точностью наоборот, удаляем uid из SelectedArr и добавляем в SearchArr и StoreArr. При вводе строки поиска, если длинна строки меньше 2 символов в SearchArr клонируем данные из StoreArr. Если больше двух символов то проходим по значениям массива StoreArr. Эти значения подставляем в ключи объекта ObjSearch и получаем его значения. В случае, если в строке (значение объекта ObjSearch) встречается строка из «строки поиска», то добавляем значение массива StoreArr (соответствующий данному значению в объекте ObjSearch) в массив SearchArr. Для визуализации формируем внутренние кнопки из массивов SearchArr и SelectedArr соответственно для левой и правой формы, а подписи берем из объекта props.store (там значения в нормальном регистре).

Код компонента:

/** Delphi Form v1.0.3
 * 
 *
 * Copyright (c) 2018-present, 
 * by Name Surname (admin@sergdudko.tk).
 *
 * LICENSE MIT.
 */
 
"use strict"

import _ from 'lodash';
import React from 'react';

module.exports = class DelphiForm1 extends React.PureComponent{
  
	constructor(props, context){
		super(props, context);
		this.state = {
			StoreArr: [], //полученные uid элементов
			ObjSearch: {}, //полученный объект uid->name, где name в верхнем регистре
			SearchArr: [], //найденные (отображаемые в левом окне) элементы
			SelectedArr: [], //выбранные (отображаемые в правом окне) элементы
			SearchStr: "" //строка поиска
		};
	}
	
	componentWillMount(){ //загружаем первоначальное состояние компонента
		try{
			let self = this;
			if(typeof(self.props.store) === 'object'){
				let this_StoreArr = [],
				this_store = {SelectedArr:[], ObjSearch:{}},
				StoreArrobject = _.clone(self.props.store);				
				if(typeof(self.props.selected) === 'object'){
					for(let i = 0; i < self.props.selected.length; i++){
						if(typeof(self.props.store[self.props.selected[i]]) !== 'undefined'){
							this_store['SelectedArr'].push(_.clone(self.props.selected[i]));
						}
					}
				}				
				for(let key in StoreArrobject){
					this_store['ObjSearch'][key] = StoreArrobject[key].toUpperCase();
					if(this_store['SelectedArr'].indexOf(key) === -1){
						this_StoreArr.push(key);
					}
				}
				this_store['StoreArr'] = _.clone(this_StoreArr);
				this_store['SearchArr'] = _.clone(this_StoreArr);
				self.setState(this_store);
			}			
		} catch(e){
			window.console.log(e);
		};
	}
	
	componentWillReceiveProps(nextProps){ //при обновлении пропсов перезагружаем состояние компонента
		try{
			let self = this;
			if(typeof(nextProps.callback) === 'function'){
				self.callback = nextProps.callback;
			}
			if(typeof(nextProps.store) === 'object'){
				let this_StoreArr = [],
				this_store = {SelectedArr:[], ObjSearch:{}},
				StoreArrobject = _.clone(nextProps.store);				
				if(typeof(nextProps.selected) === 'object'){
					for(let i = 0; i < nextProps.selected.length; i++){
						if(typeof(nextProps.store[nextProps.selected[i]]) !== 'undefined'){
							this_store['SelectedArr'].push(_.clone(nextProps.selected[i]));
						}
					}
				}				
				for(let key in StoreArrobject){
					this_store['ObjSearch'][key] = StoreArrobject[key].toUpperCase();
					if(this_store['SelectedArr'].indexOf(key) === -1){
						this_StoreArr.push(key);
					}
				}
				this_store['StoreArr'] = _.clone(this_StoreArr);
				this_store['SearchArr'] = _.clone(this_StoreArr);
				self.setState(this_store);
			}			
		} catch(e){
			window.console.log(e);
		};
	}
      
	componentDidMount() { //при монтировании компонента привязываем функцию обратного вызова
		let self = this;
		if(typeof(self.props.callback) === 'function'){
			self.callback = self.props.callback;
		}
		self.callback(_.clone(self.state.SelectedArr));
	}
	
	componentWillUpdate(nextProps, nextState) { //при обновлении компонента выбрасываем отобраные uid в функцию обратного вызова
		let self = this;
		if (!_.isEqual(nextState.SelectedArr, self.state.SelectedArr)) {
			self.callback(_.clone(nextState.SelectedArr));
		}
	}
	
	onClickHandler(e){
		let self = this;
		let tempstore = JSON.parse(JSON.stringify(self.state));
		switch(e.target.id){
			case 'add':
				if(tempstore['SelectedArr'].indexOf(e.target.name) === -1){
					tempstore['SelectedArr'].push(e.target.name);
				}
				let index_Store = tempstore['StoreArr'].indexOf(e.target.name);
				if(index_Store !== -1){
					tempstore['StoreArr'].splice(index_Store, 1);
				}
				let index_Search = tempstore['SearchArr'].indexOf(e.target.name);
				if(index_Search !== -1){
					tempstore['SearchArr'].splice(index_Search, 1);
				}
				break;
			case 'del':
				let index_Selected = tempstore['SelectedArr'].indexOf(e.target.name);
				if(index_Selected !== -1){
					tempstore['SelectedArr'].splice(index_Selected, 1);
				}
				if(tempstore['StoreArr'].indexOf(e.target.name) === -1){
					tempstore['StoreArr'].push(e.target.name);
				}
				if(tempstore['SearchArr'].indexOf(e.target.name) === -1){
					tempstore['SearchArr'] = _.clone(self.SearchHandler(_.clone(tempstore['StoreArr']), tempstore['SearchStr']));
				}
				break;
		}
		if(!_.isEqual(self.state, tempstore)){
			self.setState(tempstore);
		}
	}
	
	onChangeHandler(e){
		let self = this;
		let tempstore = JSON.parse(JSON.stringify(self.state));
		switch(e.target.name){
			case 'SearchStr':
				tempstore['SearchStr'] = e.target.value;
				tempstore['SearchArr'] = _.clone(self.SearchHandler(_.clone(tempstore['StoreArr']), e.target.value));
				break;
		}
		if(!_.isEqual(self.state, tempstore)){
			self.setState(_.clone(tempstore));
		}
	}
	
	SearchHandler(store, string){  //формирование результатов поиска
		let self = this,
		returnArray = _.clone(store);		
		if(typeof(store) === 'object'){
			if(typeof(string) === 'string'){
				if(string.length > 1){
					returnArray = [];
					for(let i = 0; i < store.length; i++){
						if(self.state.ObjSearch[store[i]].indexOf(string.toUpperCase()) !== -1){
							returnArray.push(store[i]);
						}
					}
				}
			}
		}		
		return returnArray;		
	}
	
	callback(data){
		window.console.log(data);
	}
      
  	render() {
		let DelphiForm = new Array;
		let DelphiFormLeft = new Array;
		let DelphiFormRight= new Array;		
		for(let i = 0; i < this.state.SearchArr.length; i++){
			DelphiFormLeft.push(<div  title={this.props.store[this.state.SearchArr[i]]}><input type="button" className="DelphiForm1Button" onClick={this.onClickHandler.bind(this)} id='add' name={_.clone(this.state.SearchArr[i])} value={this.props.store[this.state.SearchArr[i]]} /></div>);
		}		
		for(let i = 0; i < this.state.SelectedArr.length; i++){
			DelphiFormRight.push(<div  title={this.props.store[this.state.SelectedArr[i]]}><input type="button" className="DelphiForm1Button" onClick={this.onClickHandler.bind(this)} id='del' name={_.clone(this.state.SelectedArr[i])} value={this.props.store[this.state.SelectedArr[i]]} /></div>);
		}
		
		let DelphiFormString = <input type="text" className="DelphiForm1SearchInp" name="SearchStr" onChange={this.onChangeHandler.bind(this)} value={this.state.SearchStr} />;		
		DelphiForm.push(<div className="DelphiForm1SearchString">{DelphiFormString}</div>);
		DelphiForm.push(<div className="DelphiForm1Search">Позиции для отбора<div className="DelphiForm1SearchDiv">{DelphiFormLeft}</div></div>);
		DelphiForm.push(<div className="DelphiForm1Filtr">Отобранные позиции<div className="DelphiForm1FiltrDiv">{DelphiFormRight}</div></div>);			
		return (
			<div className="DelphiForm1">
				<div className="DelphiForm1Name">{this.props.name}</div>
				{DelphiForm}
			</div>
		);
	}
};

Варианты подключения компонента:

  • npm i delphiform —save
    • import DelphiForm from ‘delphiform’; и далее DelphiForm.form1
    • var DelphiForm = require(‘delphiform’); и далее DelphiForm.form1
  • <script src=»./umd/delphiform.js»><script> и далее window.form1

Не забываем подключить CSS <link rel=»stylesheet» href=»./umd/delphiform.css»>

Пример инициализации компонента:

"use strict"

import React from 'react';
import ReactDOM from 'react-dom';
import DelphiForm from 'delphiform';

class TEST extends React.PureComponent{ 	

	constructor(props, context){
		super(props, context);
		this.state = {
			store:{
				'14014265-6A12-4AFE-A40F-E4CE6E27DE10': 'Аптека №1 ООО "Фитобел" (г. Минск, ул.Бурдейного 13-5)',
				'D7040BD4-0C3E-414B-A559-79791575A91C': 'Аптека №2 ООО "Фитобел" (г. Минск, ул. В. Хоружей-8, пав. 38)',
				'80060CB5-FCCD-4E47-A523-D45A866EE554': 'Аптека №3 ООО "Фитобел" (г. Минск, ул. Лобанка-26, пом. 4)',
				'BFD523DF-FB43-4BB2-A3CD-4017C6C7A533': 'Аптека №4 ООО "Фитобел" (г. Минск, пр. Дзержинского, 104, пом. 7)',
				'911C6676-7661-4A69-A7F1-C333E42D275E': 'Аптека №5 ООО "Фитобел" (г. Минск, ул. В.Хоружей 6Б, пом. 9)',
				'70032A36-D7E9-4BF2-A889-B02B6B278EC8': 'Аптека №6 ООО "Фитобел" (г. Минск, ул.Кунцевщина 2А, пом.6)',
				'ACB559D6-9E06-42BF-AC13-064B3D3381B8': 'Аптека №7 ООО "Фитобел" (г. Минск, ул.Сурганова 57А пом.4)',
				'93E1AA76-9E7D-42C5-A68D-9DF0C7980DA6': 'Аптека №8 ООО "Фитобел" (г. Минск, ул. 50лет Победы, 8 пом. 56  )',
				'65C98749-F7C4-49F4-BDE9-A36D907BEDF5': 'Аптека №9 ООО "Фитобел" (г. Минск, ул. Есенина 27)',
				'37BB4995-2EEF-4349-B5D3-16167FF0603D': 'Аптека №10 ООО "Фитобел" (г. Минск, ул.Наполеона Орды 23)',
				'682BB6CB-17AC-4B9B-8D6B-0E6CD57B9980': 'Аптека №11 ООО "Фитобел" (Минский р-н, АГ Лесной, ул. Н. Н. Александрова, д. 10, пом.81)',
				'46A50D91-758A-459F-9DA5-2BF2425BD887': 'Аптека №12 ООО "Фитобел" (г. Витебск, ул.Ленина 26А)',
				'29B20FC1-C334-49FA-9F67-CED0E9BF8568': 'Аптека №14 ООО "Фитобел" (г. Минск, ул. Кальварийская, 2-113)',
				'CBB782D0-05B2-42EB-9C1F-B3E60103085D': 'Аптека №15 ООО "Фитобел" (г. Минск, ул. Ангарская, 62А)',
				'74586A62-CECF-4F3B-B64B-5508CB874291': 'Аптека №16 ООО "Фитобел" (г. Минск ул. Калиновского, 3-7Н)',
				'F1278B22-34AF-4ABB-8DF4-4FCDAD8488A8': 'Аптека №17 ООО "Фитобел" (г. Минск, ул.Нововиленская, 10-104)',
				'4FA6F0CE-26C3-4BF3-B30A-69356E80FA5E': 'Аптека №18 ООО "Фитобел" (г. Минск, ул.Карла Либкнехта 108/32)',
				'F8BCC010-527F-45CE-9019-74FD9164A506': 'Аптека №19 ООО "Фитобел" (г.Минск, ул. В. Голубка,2-2)',
				'1FD3B0B8-7B0F-4640-80E5-FD0004E8F3AD': 'Аптека №20 ООО "Фитобел" (г. Минск, пр. Партизанский, 67)',
				'49C2A45A-33B5-4929-A64D-31F395100871': 'Аптека №21 ООО "Фитобел" (г. Жодино, ул. Рокоссовского, 9)',
				'3634A2B4-88FF-4C56-B6B1-15438EB37ED2': 'Аптека №22 ООО "Фитобел" (г. Минск, ул. Илимская, 27)',
				'621965EA-2ABC-4F4B-AABC-8F8C26AD3E15': 'Аптека №23 ООО "Фитобел" (г. Минск, ул. Налибокская, 39)',
				'CF9BF992-0A97-4C27-A7B9-8625C3499C5F': 'Аптека №24 ООО "Фитобел" (г. Барановичи, бульвар Штоккерау, 3-1)',
				'266805FC-E8A7-412D-A9F3-E5A4AF540BFA': 'Аптека №25 ООО "Фитобел" (г. Борисов, ул. Гагарина, 4А)',
				'7D9C669B-AC9A-4FE3-BB5D-747A4C2667E7': 'Аптека №26 ООО "Фитобел" ( г. Минск, ул. Слободская, 47)',
				'4E3521C9-638B-46DD-9027-DB5821DB0BB4': 'Аптека №27 ООО "Фитобел" (г. Минск, ул. Гуляма Якубова, 14, пом.4)',
				'8051E801-95E4-478B-8CDF-E2803743DDC0': 'Аптека №28 ООО "Фитобел" (г. Минск, ул. Уборевича, 75)',
				'CA9C9EC6-525D-474F-A7A1-3E0F6B054AA6': 'Аптека №29 ООО "Фитобел" (Минская область, Минский р-он, Боровлянский с/с, д.Копище, ул. Лопатина, 4А-1а)',
				'3C7E8A08-84E3-40AF-9069-714AAAF6BAFA': 'Аптека №30 ООО "Фитобел" (Минская область, Минский р-он, Боровлянский с/с, а.г. Лесной, 37-79)',
				'7548B828-1FE2-43F8-B6DE-844BB66A209B': 'Аптека №31 ООО "Фитобел" (г. Минск, ул. Колесникова П.Р., дом № 15, помещение 1)',
				'A1243A9A-E71B-4E8D-84F8-BACD9B1016B7': 'Аптека №32 ООО "Фитобел" (г. Минск, ул. Некрасова, д.3А)',
				'AB1A51E8-9E30-4317-B5BB-E19BD8ED068A': 'Аптека №33 ООО "Фитобел" (г.Барановичи, ул. Борисовская, 4-1)',
				'CA75ECE6-1741-46CC-AC28-1221E255EC97': 'Аптека №1  "Не Ска" ООО  (г. Минск, пр-т. Дзержинского 119-862)',
				'D75C5403-37C1-438F-84B3-8498DCFD078E': 'Аптека №2  "Не Ска" ООО (г. Минск, ул. Казимировская, д. 35)',
				'9EB42DA6-DF55-4E74-BABE-2A4CF560E7EF': 'Аптека №3  "Не Ска" ООО (Лида, ул. Коммунистическая, д. 50)',
				'33172B85-8E29-4BDB-B409-CFB59D165394': 'Аптека №4  "Не Ска" ООО (г. Минск, ул. Голубева И.П., д. 24)',
				'AC732CA0-D570-4A09-8906-0046B00D6CF9': 'Аптека №5  "Не Ска" ООО (г. Минск, ул. Рафиева Н., д. 56)',
				'7D582258-2564-4969-861F-5A9971B2FFC7': 'Аптека №6  "Не Ска" ООО (г. Минск, ул. Иосифа Жиновича, д. 7)',
				'CCFB2056-E9E0-4B1A-99A8-1A75AA07515A': 'Аптека №7  "Не Ска" ООО (г.Солигорск,ул.Октябрьская,38)',
				'BC8F7416-DAD4-4CA1-AE2F-6E4BDAFA8ADC': 'Аптека №8  "Не Ска" ООО (г. Минск, тр. Игуменский, д. 11 )',
				'31AA6C3E-9B12-4B7D-B4F6-257D662A8E38': 'Аптека №9 "Не Ска" ООО (Лида, ул. Победы д.43 пом 2)',
				'131542B2-6C61-43DC-BF89-4C3218CD4C62': 'Аптека №10  "Не Ска" ООО (г. Минск, ул.Кижеватова, д.7, корп. 2, пом.4)',
				'5C9EE302-6E12-472D-95B5-4C6ED17B8846': 'Аптека №11  "Не Ска" ООО (г. Молодечно, ул. В.Гостинец, дом № 143Б)',
				'8884D92E-AABF-4794-98A4-C5E64C48C0ED': 'Аптека №12  "Не Ска" ООО (г. Молодечно, ул. Ф.Скорины, дом № 16A-3)',
				'B99D466E-4C00-4F18-9621-A1C925047D24': 'Аптека №14 "Не Ска" ООО (г. Гомель, ул. М.Г.Ефремова 5-266)',
				'82A1261B-9951-4DAB-BBDC-8E5E2DAC43BE': 'Аптека №15 "Не Ска" ООО (г. Гомель, пр. Ленина 51)',
				'D38727B1-7CBA-44EC-991E-79FAC867B29E': 'Аптека №16 "Не Ска" ООО (г. Гомель, ул. Свердлова 3А-3)',
				'59A77C4F-46D9-47E3-B261-210BBA3908C7': 'Аптека №17 "Не Ска" ООО (г. Гомель, ул. Юбилейная 8-3)',
				'4F5B18AB-36C4-426C-B1EC-15B28A0F40C8': 'Аптека №18 "Не Ска" ООО (г. Гомель, ул. Барыкина ,100-109)',
				'F2BB9CF0-BA1A-4714-9E3C-9A8C0F44CB32': 'Аптека №19 "Не Ска" ООО (г. Гомель, ул. Хатаевича 9)',
				'67F01F35-ADE2-44F0-B5DE-E5B79913D7FC': 'Аптека №20 "Не Ска" ООО (г. Гомель, ул. Владимирова 29-144)',
				'7FB88CFF-F765-4472-8B29-7CA587F859F0': 'Аптека №21 "Не Ска" ООО (г. Гомель, ул. Интернациональная 48-154)',
				'ABA40F89-CA53-4860-A23B-6A0ABE970F20': 'Аптека №22 "Не Ска" ООО (г. Борисов, ул. Лопатина, дом № 44)',
				'CC1EF8C8-062E-4492-AA02-F171E6E0105F': 'Аптека №23 "Не Ска" ООО (г. Минск, ул. Лынькова Михася, дом № 99 )',
				'FAAD016A-BE14-456E-9128-9A6AD4DD4A15': 'Аптека №24 "Не Ска" ООО (г.Гомель, ул. Григория Денисенко, 4-3)',
				'567BA474-A800-439A-9A11-B6CAE850166B': 'Аптека №25 "Не Ска" ООО (г.Минск, ул. Космонавтов, 28)',
				'F6DD46DD-EAF4-4255-9719-13ECBC27412C': 'Аптека №26 "Не Ска" ООО (г. Жлобин, ул. Козлова, 17а)',
				'CC5128BF-2AF2-4E39-8950-B1EBDA837F0F': 'Аптека №27 "Не Ска" ООО (г. Речица, ул. Хлуса, 59а-1)',
				'AB966D72-7EC5-4C5E-9926-A85D6F04645B': 'Аптека 1 ООО "Комповид" (г.Гродно, пр-т Я.Купалы, 80/3)',
				'AB2B213B-71CF-492B-A052-AC072951C4F7': 'Аптека 2 ООО "Комповид" (Гродненская обл, аг. Озеры, Гродненского р-на)',
				'02154086-58E3-43CC-A8CA-FFA4ED704508': 'Аптека 3 ООО "Комповид" (Гродненская обл., г. Ошмяны, ул. Советская, д. 106, пом. 11)',
				'40C7D118-A1A1-4759-BEDC-4224AB86AF5F': 'Аптека 4 ООО "Комповид" (Гродненская обл., г. Сморгонь, ул. Ленина, дом 3-2)',
				'34BBE3F2-9935-4292-B844-E4E3E2FEB3FC': 'Аптека 6 ООО "Комповид" (Гродненская обл.,г. Волковыск, ул. Зенитчиков, 46)',
				'15766EE3-EA15-41F2-A620-2B9AFE2542DB': 'Аптека 7 ООО "Комповид" (Минская обл., г. Молодечно, ул. Буховщина, дом № 60)',
				'EC4CAE90-0AE0-49EF-8399-07C8B4D8282C': 'Аптека 8 ООО "Комповид" (Гродненская обл., г.п. Красносельский, Волковысского района, ул. Школьная, )',
				'B8C1808C-7F54-4A85-A542-F9229FA20E49': 'Аптека 11 ООО "Комповид" (Гродненская обл., г. Волковыск, ул. Ф. Скорины, 11, пом. 72)',
				'C0217296-36DC-423A-A58E-6FB3DEECA515': 'Аптека 9 ООО "Комповид" (г. Гродно, пер.Поповича, 10-101)',
				'A551C82C-16E4-4BE9-BA42-C529C6AC9D40': 'Аптека 12 ООО "Комповид" ( г.Гродно, ул.Кремко, 2-89)',
				'54CCDBDD-F9CE-4B11-9ACA-F9761FC6350A': 'Аптека №1 ООО "ПартнерМедитек" (г.Минск, ул. Червякова 8)',
				'6B507809-919F-4591-B1C2-41B8C2A8E615': 'Аптека №2 ООО "ПартнерМедитек" (г.Минск, ст.м."Пушкинска", подз. переход №1)',
				'5A6440A9-1608-46FD-BFC4-7779906B23CC': 'Аптека №3 ООО "ПартнерМедитек" (г.Минск, ул. Ландера, 36а)',
				'676FFA37-1327-4B79-BF75-BFF0153AB567': 'Аптека №4 ООО "ПартнерМедитек" (г.Минск, ул. Райниса, 17)',
				'B5A4057D-052B-4C69-8B43-00889F94E181': 'Аптека №5 ООО "ПартнерМедитек" (г.Минск, ул. Калиновского, 55Г)',
				'D43E1A51-7C09-4E22-80D8-09A28CC97CEE': 'Аптека №6 ООО "ПартнерМедитек" (г.Минск, ул. Сухаревская, 6)',
				'685805A7-E356-4161-ADD0-27F2B1213133': 'Аптека №7 ООО "ПартнерМедитек" (г.Минск, пр. Партизанский, 84А-2)',
				'16911021-DC46-4BD9-B842-00879278BE63': 'Аптека №8 ООО "ПартнерМедитек" (г.Минск, ул. Жуковского, д. 7, пом. 26Н)',
				'F4DE6CD3-C5FB-41C2-9E5F-30A701B9B609': 'Аптека №9 ООО "ПартнерМедитек" (г.Минск, ул. Плеханова, 40-11Н/4-5)',
				'D9163999-F575-428F-9209-EC1DA664DCC2': 'Аптека на Казинца (г. Минск, ул.Казинца, 49)'
			},
			selected:[
				'D9163999-F575-428F-9209-EC1DA664DCC2',
				'15766EE3-EA15-41F2-A620-2B9AFE2542DB',
				'14014265-6A12-4AFE-A40F-E4CE6E27DE10',
				'D7040BD4-0C3E-414B-A559-79791575A91C'
			],
			name:"Тестовая форма",
			callbackV: 0
		};
	}
	
	componentDidMount() { //выкину изменение состояния в window (global) и снимок состояния в консоль
		var self = this;
		let testNewState = function(newstate){
			self.setState(newstate);
		}
		console.log(self.state);
		window.testNewState = testNewState;
		let testNewCallback = function(newcallback){
			self.DelphiFormHandler = newcallback;
			self.setState({callbackV: self.state.callbackV + 1});
		}
		window.testNewCallback = testNewCallback;
	}
	
	DelphiFormHandler(e){
		window.alert(e);
	}
	
  	render() {
		return (
			<div className="TEST">
				<DelphiForm.form1 
					store={this.state.store} 
					selected={this.state.selected}
					name={this.state.name} 
					callback={this.DelphiFormHandler} />
			</div>
		);
	}
};

ReactDOM.render(
	<TEST />,
	document.getElementById('DelphiFormTest')
);

Публикация пакета в npm

В корневой директории создаем:

  • файл лицензии LICENSE
  • файл описания модуля README.md
  • файл настроек игнорирования .npmignore с указанием соответствующих файлов и папок:
    npm-debug.log
    .gitignore
    node_modules
    img

Синтаксис аналогичен GITHUB.

Если еще не зарегистрированы, регистрируемся (хотя удобней на сайте www.npmjs.com):

npm adduser

Далее авторизуемся:

npm login

(нужно будет ввести учетные данные)

Публикуем модуль:

npm publish

После публикации пакет будет доступен по ссылке https://www.npmjs.com/package/имя_пакета, в моем случае это https://www.npmjs.com/package/delphiform.

 

Исходники проекта