├── .eslintignore
├── .gitignore
├── docs
└── images
│ └── pg.jpg
├── src
├── decorators
│ ├── index.ts
│ └── property-grid-control-decorator.ts
├── models
│ ├── property-grid-constants.ts
│ ├── property-grid-config.ts
│ ├── property-grid-group.ts
│ ├── index.ts
│ ├── property-grid-item.ts
│ ├── property-grid-config-item.ts
│ ├── property-grid-options.ts
│ └── types.ts
├── controls
│ ├── form-controls-map.ts
│ ├── property-grid-form-controls-map.ts
│ ├── textbox-form-control.ts
│ ├── color-picker-form-control.ts
│ ├── date-picker-form-control.ts
│ ├── number-form-control.ts
│ ├── checkbox-form-control.ts
│ ├── index.ts
│ ├── label-form-control.ts
│ ├── form-control-composite.ts
│ ├── input-form-control.ts
│ ├── select-form-control.ts
│ ├── property-grid-row.ts
│ ├── iform-control.ts
│ ├── property-grid-row-group.ts
│ ├── form-control.ts
│ └── property-grid-form.ts
├── property-grid-template.ts
├── services
│ ├── index.ts
│ ├── logger.service.ts
│ ├── property-grid-service.ts
│ ├── event-dispatcher-service.ts
│ ├── property-grid-factory.ts
│ ├── config-parser-service.ts
│ └── property-grid-groups-builder-service.ts
├── index.ts
├── index.html
├── utils
│ └── property-grid-utils.ts
├── styles
│ └── property-grid-base-style.ts
├── sample
│ └── index.js
└── property-grid.component.ts
├── .npmignore
├── .prettierrc
├── .editorconfig
├── tsconfig.json
├── LICENSE
├── .eslintrc
├── package.json
├── webpack.config.js
├── webpack.prod.config.js
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .DS_Store
3 | node_modules
4 |
5 | dist
6 | lib
7 | lib-esm
8 | _bundles
9 |
--------------------------------------------------------------------------------
/docs/images/pg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bdimitrijoski/clean-web-ui-property-grid/HEAD/docs/images/pg.jpg
--------------------------------------------------------------------------------
/src/decorators/index.ts:
--------------------------------------------------------------------------------
1 | export { propertyGridControlDecorator } from './property-grid-control-decorator';
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | **/tsconfig.json
3 | **/webpack.config.js
4 |
5 | node_modules
6 | src
7 | docs
8 | .vscode
9 |
--------------------------------------------------------------------------------
/src/models/property-grid-constants.ts:
--------------------------------------------------------------------------------
1 | export const PROPERTY_GRID_CONSTANTS = {
2 | OTHER_GROUP_NAME: 'Other',
3 | };
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 140
6 | }
7 |
--------------------------------------------------------------------------------
/src/controls/form-controls-map.ts:
--------------------------------------------------------------------------------
1 | import { IFormControl } from './iform-control';
2 |
3 | export interface FormControlsMap {
4 | [controlName: string]: IFormControl;
5 | }
6 |
--------------------------------------------------------------------------------
/src/models/property-grid-config.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridConfigItem } from './property-grid-config-item';
2 |
3 | export interface PropertyGridConfig {
4 | [name: string]: PropertyGridConfigItem;
5 | }
6 |
--------------------------------------------------------------------------------
/src/models/property-grid-group.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridItem } from './property-grid-item';
2 |
3 | export interface PropertyGridGroup {
4 | name: string;
5 | label: string;
6 | children: PropertyGridItem[];
7 | }
8 |
--------------------------------------------------------------------------------
/src/controls/property-grid-form-controls-map.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridRowGroup } from './property-grid-row-group';
2 |
3 | export interface PropertyGridFormControlsMap {
4 | [rowGroupName: string]: PropertyGridRowGroup;
5 | }
6 |
--------------------------------------------------------------------------------
/src/controls/textbox-form-control.ts:
--------------------------------------------------------------------------------
1 | import { InputFormControl } from './input-form-control';
2 |
3 | export class TextboxFormControl extends InputFormControl {
4 | getInputType(): string {
5 | return 'text';
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/controls/color-picker-form-control.ts:
--------------------------------------------------------------------------------
1 | import { InputFormControl } from './input-form-control';
2 |
3 | export class ColorPickerFormControl extends InputFormControl {
4 | getInputType(): string {
5 | return 'color';
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/property-grid-template.ts:
--------------------------------------------------------------------------------
1 | const propertyGridTemplate = document.createElement('template');
2 | const propertyGridTemplateContent = `
3 |
4 | `;
5 | propertyGridTemplate.innerHTML = propertyGridTemplateContent;
6 |
7 | export { propertyGridTemplate };
8 |
--------------------------------------------------------------------------------
/src/models/index.ts:
--------------------------------------------------------------------------------
1 | export { PropertyGridGroup } from './property-grid-group';
2 | export { PropertyGridItem } from './property-grid-item';
3 | export { PropertyGridConfig } from './property-grid-config';
4 | export { PropertyGridConfigItem } from './property-grid-config-item';
5 | export { PropertyGridOptions } from './property-grid-options';
6 |
--------------------------------------------------------------------------------
/src/models/property-grid-item.ts:
--------------------------------------------------------------------------------
1 | import { Attributes } from './types';
2 |
3 | export interface PropertyGridItem {
4 | id?: string;
5 | name: string;
6 | label?: string;
7 | type: string;
8 | description?: string;
9 | attributes: Attributes;
10 | options: {
11 | description?: string;
12 | showHelp?: boolean;
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
15 | [*.json]
16 | max_line_length = 20
17 |
--------------------------------------------------------------------------------
/src/controls/date-picker-form-control.ts:
--------------------------------------------------------------------------------
1 | import { InputFormControl } from './input-form-control';
2 |
3 | export class DatePickerFormControl extends InputFormControl {
4 | getValue(): string {
5 | return super.getValue();
6 | }
7 |
8 | setValue(value: string | Date): void {
9 | super.setValue(value);
10 | }
11 |
12 | getInputType(): string {
13 | return 'date';
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/controls/number-form-control.ts:
--------------------------------------------------------------------------------
1 | import { InputFormControl } from './input-form-control';
2 |
3 | export class NumberFormControl extends InputFormControl {
4 | getValue(): number {
5 | return +super.getValue();
6 | }
7 |
8 | setValue(value: string | number): void {
9 | super.setValue(+value.toString());
10 | }
11 |
12 | getInputType(): string {
13 | return 'number';
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export { ConfigParserService } from './config-parser-service';
2 | export { EventDispatcherService } from './event-dispatcher-service';
3 | export { Logger } from './logger.service';
4 | export { PropertyGridFactory } from './property-grid-factory';
5 | export { PropertyGridGroupsBuilderService } from './property-grid-groups-builder-service';
6 | export { PropertyGridService } from './property-grid-service';
7 |
--------------------------------------------------------------------------------
/src/models/property-grid-config-item.ts:
--------------------------------------------------------------------------------
1 | export interface PropertyGridConfigItem {
2 | id?: string;
3 | name: string;
4 | label: string;
5 | /** Type of control (text, checkbox...etc.) */
6 | type: string;
7 | /** Using for showing help */
8 | description?: string;
9 | /** The group where this control belongs */
10 | group?: string;
11 | /** Should be visible in the property grid */
12 | browsable?: boolean;
13 | /** Should show help for this item */
14 | showHelp?: boolean;
15 | items?: string[];
16 | options: {
17 | [option: string]: string | number | boolean;
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "./src",
4 | "baseUrl": "./src",
5 | "moduleResolution": "node",
6 | "module": "commonjs",
7 | "target": "es2015",
8 | "lib": ["es2017", "dom"],
9 | "outDir": "dist/lib",
10 | "allowSyntheticDefaultImports": true,
11 | "suppressImplicitAnyIndexErrors": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "sourceMap": true,
14 | "emitDecoratorMetadata": true,
15 | "experimentalDecorators": true,
16 | "allowJs": false,
17 | "declaration": true
18 | },
19 | "include": ["**/*.ts"],
20 | "exclude": ["node_modules", "dist", "**/*.spec.ts", "sample"],
21 | "compileOnSave": false,
22 | "buildOnSave": false
23 | }
24 |
--------------------------------------------------------------------------------
/src/controls/checkbox-form-control.ts:
--------------------------------------------------------------------------------
1 | import { FormEvent } from '../models/types';
2 |
3 | import { PropertyGridUtils } from '../utils/property-grid-utils';
4 |
5 | import { InputFormControl } from './input-form-control';
6 |
7 | export class CheckboxFormControl extends InputFormControl {
8 | getInputType(): string {
9 | return 'checkbox';
10 | }
11 |
12 | getValue(): boolean {
13 | return this.value as boolean;
14 | }
15 |
16 | setValue(value: string | boolean): void {
17 | const val = PropertyGridUtils.toBool(value);
18 | super.setValue(val);
19 | this.getNativeElement().checked = val;
20 | }
21 |
22 | protected onValueChange(event: FormEvent): void {
23 | this.setValue(event.target.checked);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { PropertyGrid } from './property-grid.component';
2 | export {
3 | IFormControl,
4 | FormControlsMap,
5 | FormControl,
6 | FormControlComposite,
7 | InputFormControl,
8 | PropertyGridForm,
9 | PropertyGridRow,
10 | PropertyGridRowGroup,
11 | } from './controls';
12 |
13 | export { PropertyGridConfig, PropertyGridConfigItem, PropertyGridGroup, PropertyGridItem, PropertyGridOptions } from './models';
14 |
15 | export { Attributes, DataObject, EventHandler, EventListnerHandle, FormEvent, FormEventHandler, PropertyGridEvents } from './models/types';
16 |
17 | export {
18 | ConfigParserService,
19 | EventDispatcherService,
20 | PropertyGridFactory,
21 | PropertyGridGroupsBuilderService,
22 | PropertyGridService,
23 | } from './services';
24 |
--------------------------------------------------------------------------------
/src/services/logger.service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 | export class Logger {
4 | private static instance: Logger;
5 | logEnabled = true;
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-empty-function
8 | private constructor() {}
9 |
10 | public static getInstance(): Logger {
11 | if (!Logger.instance) {
12 | Logger.instance = new Logger();
13 | }
14 |
15 | return Logger.instance;
16 | }
17 |
18 | error(msg: string, data?: any): void {
19 | // eslint-disable-next-line no-console
20 | console.error(msg, data);
21 | }
22 |
23 | log(msg: string, data?: any): void {
24 | // eslint-disable-next-line no-console
25 | if (!this.logEnabled) {
26 | return;
27 | }
28 | // eslint-disable-next-line no-console
29 | console.log(msg, data);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/controls/index.ts:
--------------------------------------------------------------------------------
1 | export { CheckboxFormControl } from './checkbox-form-control';
2 | export { ColorPickerFormControl } from './color-picker-form-control';
3 | export { DatePickerFormControl } from './date-picker-form-control';
4 | export { FormControl } from './form-control';
5 | export { FormControlComposite } from './form-control-composite';
6 | export { FormControlsMap } from './form-controls-map';
7 | export { IFormControl } from './iform-control';
8 | export { InputFormControl } from './input-form-control';
9 | export { NumberFormControl } from './number-form-control';
10 | export { PropertyGridForm } from './property-grid-form';
11 | export { PropertyGridRow } from './property-grid-row';
12 | export { PropertyGridRowGroup } from './property-grid-row-group';
13 | export { SelectFormControl } from './select-form-control';
14 | export { TextboxFormControl } from './textbox-form-control';
15 | export { LabelFormControl } from './label-form-control';
16 |
--------------------------------------------------------------------------------
/src/models/property-grid-options.ts:
--------------------------------------------------------------------------------
1 | import { FormControlsMap } from '../controls/form-controls-map';
2 |
3 | import { FormEvent } from './types';
4 |
5 | export interface PropertyGridOptions {
6 | /**
7 | * indicating whether the control can receive focus.
8 | */
9 | canFocus?: boolean;
10 |
11 | /** Set custom controls that will be used for rendering */
12 | controls?: FormControlsMap;
13 |
14 | /** Gets or sets a value indicating whether the Help text is visible. */
15 | helpVisible?: boolean;
16 |
17 | /** Gets or sets if PropertyGrid will sort properties by display name. */
18 | propertySort?: boolean | ((params: string[]) => string[]);
19 |
20 | /** Gets or sets a value indicating whether the toolbar is visible. */
21 | toolbarVisible?: boolean;
22 |
23 | /** Gets or sets a value indicating whether controls are groupped and if groups are visible. */
24 | hasGroups?: boolean;
25 |
26 | /** Hook that is called when value is change in controls inside property grid */
27 | onValueChange?: (event: FormEvent) => any;
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 brankodimitrijoski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/controls/label-form-control.ts:
--------------------------------------------------------------------------------
1 | import { FormEvent } from '../models/types';
2 |
3 | import { FormControl, FormControlParams } from './form-control';
4 |
5 | export class LabelFormControl extends FormControl {
6 | constructor(params: FormControlParams) {
7 | super(params);
8 | this.onValueChange = this.onValueChange.bind(this);
9 | }
10 |
11 | getNativeElementType(): string {
12 | return 'label';
13 | }
14 |
15 | getNativeElement(): HTMLLabelElement {
16 | return super.getNativeElement() as HTMLLabelElement;
17 | }
18 |
19 | getValue(): boolean {
20 | return this.value as boolean;
21 | }
22 |
23 | setValue(value: string): void {
24 | super.setValue(value);
25 | this.getNativeElement().innerHTML = value;
26 | }
27 |
28 | render(): HTMLLabelElement {
29 | this.getNativeElement().innerHTML = this.value;
30 | return this.getNativeElement();
31 | }
32 |
33 | protected onValueChange(event: FormEvent): void {
34 | this.setValue(event.target.innerText);
35 | }
36 |
37 | // eslint-disable-next-line @typescript-eslint/no-empty-function
38 | attachEventListeners(): void {}
39 | // eslint-disable-next-line @typescript-eslint/no-empty-function
40 | removeEventListeners(): void {}
41 | }
42 |
--------------------------------------------------------------------------------
/src/models/types.ts:
--------------------------------------------------------------------------------
1 | export interface Attributes {
2 | [attributeName: string]: string | number | boolean;
3 | }
4 |
5 | export interface Options {
6 | [attributeName: string]: string | number | boolean | string[];
7 | }
8 |
9 | export interface SortCallback {
10 | (properties: string[]): string[];
11 | }
12 |
13 | export type PrimitiveValue = string | number | boolean;
14 |
15 | export interface DataObject {
16 | [property: string]: string | number | Date | boolean | any;
17 | }
18 |
19 | export interface FormEvent extends Event {
20 | bubbles: boolean;
21 | currentTarget: EventTarget & T;
22 | cancelable: boolean;
23 | defaultPrevented: boolean;
24 | eventPhase: number;
25 | isTrusted: boolean;
26 | nativeEvent: Event;
27 | preventDefault(): void;
28 | isDefaultPrevented(): boolean;
29 | stopPropagation(): void;
30 | isPropagationStopped(): boolean;
31 | persist(): void;
32 | target: EventTarget & T;
33 | type: string;
34 | }
35 |
36 | export interface EventHandler> {
37 | (event: E): void;
38 | }
39 |
40 | export type FormEventHandler = EventHandler>;
41 |
42 | export interface EventListnerHandle {
43 | unsubscribe: () => any;
44 | }
45 |
46 | export interface OnValueChangeEvent {
47 | name: string;
48 | value: any;
49 | }
50 |
51 | export enum PropertyGridEvents {
52 | onValueChanged = 'onValueChanged',
53 | }
54 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint", "prettier", "no-loops", "import"],
5 | "extends": [
6 | "eslint:recommended",
7 | "plugin:@typescript-eslint/recommended",
8 | "prettier"
9 | ],
10 | "rules": {
11 | "no-console": 1,
12 | "prettier/prettier": 2,
13 | "sort-imports": [
14 | "error",
15 | {
16 | "ignoreDeclarationSort": true
17 | }
18 | ],
19 | "import/order": [
20 | "error",
21 | {
22 | "pathGroupsExcludedImportTypes": ["external"],
23 | "groups": [
24 | "builtin",
25 | "external",
26 | "internal",
27 | "parent",
28 | "sibling",
29 | "index"
30 | ],
31 | "alphabetize": {
32 | "order": "asc"
33 | },
34 | "newlines-between": "always-and-inside-groups"
35 | }
36 | ],
37 | "no-prototype-builtins": "off",
38 | "@typescript-eslint/no-explicit-any": "off"
39 | },
40 | "settings": {
41 | "import/parsers": {
42 | "@typescript-eslint/parser": [".ts"]
43 | },
44 | "import/resolver": {
45 | "typescript": {
46 | "alwaysTryTypes": true, // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist`
47 | "directory": "./"
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/decorators/property-grid-control-decorator.ts:
--------------------------------------------------------------------------------
1 | import { IFormControl } from '../controls';
2 | import { PropertyGridOptions } from '../models';
3 | import { EventDispatcherService } from '../services';
4 |
5 | export const propertyGridControlDecorator = (
6 | ctrl: IFormControl,
7 | pgOptions: PropertyGridOptions,
8 | eventDispatcher: EventDispatcherService,
9 | ): IFormControl => {
10 | const proxyMethod = (method, callback) => {
11 | return new Proxy(method, {
12 | apply: (target, that, args) => {
13 | callback(target, that, args);
14 | return target.apply(that, args);
15 | },
16 | });
17 | };
18 |
19 | if (typeof ctrl.onValueChange === 'function') {
20 | ctrl.onValueChange = proxyMethod(ctrl.onValueChange, (target, that, args) => {
21 | if (pgOptions.onValueChange) {
22 | const event = args[0];
23 | const result = pgOptions.onValueChange(event);
24 | if (result === false || event.defaultPrevented) {
25 | return false;
26 | }
27 | }
28 | });
29 | }
30 |
31 | if (typeof ctrl.setValue === 'function') {
32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
33 | ctrl.setValue = proxyMethod(ctrl.setValue, (target, that, args) => {
34 | const data = {
35 | name: that.getName(),
36 | value: that.getValue(),
37 | };
38 | eventDispatcher.send('onValueChanged', data);
39 | });
40 | }
41 |
42 | return ctrl;
43 | };
44 |
--------------------------------------------------------------------------------
/src/services/property-grid-service.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridForm, PropertyGridRow, PropertyGridRowGroup } from '../controls';
2 | import { PropertyGridGroup, PropertyGridItem, PropertyGridOptions } from '../models';
3 |
4 | import { PropertyGridFactory } from './property-grid-factory';
5 |
6 | export class PropertyGridService {
7 | constructor(private factory: PropertyGridFactory) {}
8 |
9 | build(config: PropertyGridGroup[], pgOptions: PropertyGridOptions): PropertyGridForm {
10 | const propGrid = new PropertyGridForm({
11 | id: 'pg',
12 | name: 'pg',
13 | label: '',
14 | description: '',
15 | attributes: {},
16 | options: {},
17 | });
18 | propGrid.init();
19 |
20 | config.forEach((group) => {
21 | propGrid.add(this.buildGridRowGroup(group, pgOptions));
22 | });
23 |
24 | return propGrid;
25 | }
26 |
27 | private buildGridRowGroup(item: PropertyGridGroup, pgOptions: PropertyGridOptions): PropertyGridRowGroup {
28 | const group = this.factory.create('row_group', item, pgOptions) as PropertyGridRowGroup;
29 |
30 | item.children.forEach((gridItem) => group.add(this.buildGridRow(gridItem, pgOptions)));
31 |
32 | return group;
33 | }
34 |
35 | private buildGridRow(gridItem: PropertyGridItem, pgOptions: PropertyGridOptions): PropertyGridRow {
36 | const row = this.factory.create('row', gridItem, pgOptions) as PropertyGridRow;
37 | row.add(this.factory.create(gridItem.type, gridItem, pgOptions));
38 | return row;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/services/event-dispatcher-service.ts:
--------------------------------------------------------------------------------
1 | import { EventListnerHandle } from '../models/types';
2 |
3 | export class EventDispatcherService {
4 | private subscribers = {};
5 |
6 | hasEventListener(event: string): boolean {
7 | return this.subscribers.hasOwnProperty(event) && this.subscribers[event].length > 0;
8 | }
9 |
10 | /**
11 | * Removes all subscribers
12 | */
13 | reset(): void {
14 | for (const eventName in this.subscribers) {
15 | //clear array and event listeners
16 | this.subscribers[eventName].length = 0;
17 | this.subscribers[eventName] = null;
18 | delete this.subscribers[eventName];
19 | }
20 | this.subscribers = {};
21 | }
22 |
23 | /** Register (subscribe) to event */
24 | register(event: string, callback: (...args) => any): EventListnerHandle {
25 | if (!this.subscribers.hasOwnProperty(event)) {
26 | this.subscribers[event] = [];
27 | }
28 | const index = this.subscribers[event].push(callback) - 1;
29 | const subscription = {
30 | unsubscribe: () => {
31 | this.subscribers[event].splice(index, 1);
32 |
33 | if (this.subscribers[event].length === 0) {
34 | delete this.subscribers[event];
35 | }
36 | },
37 | };
38 | return subscription;
39 | }
40 |
41 | /** Broadcast event to all registered subscribers */
42 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
43 | send(event: string, data?: any): void {
44 | if (!this.subscribers[event]) return;
45 |
46 | this.subscribers[event].forEach((callback) => callback(data));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Clean Web UI Property Grid
8 |
21 |
22 |
23 |
24 |
25 |
Property Grid #1
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Property Grid #2
34 |
35 |
36 |
37 |
38 |
39 |
Options
40 |
41 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clean-web-ui-property-grid",
3 | "version": "1.1.1",
4 | "description": ".NET style property grid, written in Plain JavaScript as Web component",
5 | "main": "esm2015/index.js",
6 | "types": "esm2015/index.d.ts",
7 | "repository": "https://github.com/brankodimitrijoski/clean-web-ui-property-grid",
8 | "author": "Branko Dimitrijoski",
9 | "license": "MIT",
10 | "scripts": {
11 | "start": "webpack-dev-server",
12 | "clean": "shx rm -rf dist",
13 | "build": "npm run clean && tsc && tsc -m es2015 --target es2015 --outDir dist/esm2015 && webpack --config ./webpack.prod.config.js --mode=production",
14 | "lint": "eslint . --ext .ts",
15 | "lint-and-fix": "eslint . --ext .ts --fix",
16 | "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write"
17 | },
18 | "dependencies": {
19 | "shx": "^0.2.2",
20 | "typescript": "^3.8.3"
21 | },
22 | "devDependencies": {
23 | "@typescript-eslint/eslint-plugin": "^3.7.1",
24 | "@typescript-eslint/eslint-plugin-tslint": "^3.7.1",
25 | "@typescript-eslint/parser": "^3.7.1",
26 | "awesome-typescript-loader": "^5.0.0-1",
27 | "copy-webpack-plugin": "^6.0.3",
28 | "eslint": "^7.5.0",
29 | "eslint-config-prettier": "^6.11.0",
30 | "eslint-import-resolver-typescript": "^2.2.0",
31 | "eslint-plugin-import": "^2.22.0",
32 | "eslint-plugin-no-loops": "^0.3.0",
33 | "eslint-plugin-prettier": "^3.1.4",
34 | "prettier": "^2.0.5",
35 | "terser-webpack-plugin": "^4.1.0",
36 | "uglifyjs-webpack-plugin": "^1.1.2",
37 | "webpack": "^4.43.0",
38 | "webpack-cli": "^3.3.11",
39 | "webpack-dev-server": "^3.1.11"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/controls/form-control-composite.ts:
--------------------------------------------------------------------------------
1 | import { DataObject } from '../models/types';
2 |
3 | import { FormControl } from './form-control';
4 | import { FormControlsMap } from './form-controls-map';
5 | import { IFormControl } from './iform-control';
6 |
7 | export abstract class FormControlComposite extends FormControl {
8 | protected controls: FormControlsMap = {};
9 |
10 | add(field: IFormControl): void {
11 | const name = field.getName();
12 | this.controls[name] = field;
13 | }
14 |
15 | remove(component: IFormControl): void {
16 | delete this.controls[component.getName()];
17 | }
18 |
19 | setValue(data: DataObject): void {
20 | const setDataCallback = (control: IFormControl) => {
21 | const val = data.hasOwnProperty(control.getName()) ? data[control.getName()] : data;
22 | control.setValue(val);
23 | };
24 | this.iterateControls(setDataCallback);
25 | }
26 |
27 | getData(): any {
28 | const data = {};
29 | const getDataCallback = (control: IFormControl) => {
30 | data[control.getName()] = control.getValue();
31 | };
32 | this.iterateControls(getDataCallback);
33 |
34 | return data;
35 | }
36 |
37 | render(): any {
38 | const output = document.createDocumentFragment();
39 | const renderCallback = (control: IFormControl) => output.appendChild(control.render());
40 | this.iterateControls(renderCallback);
41 | return output;
42 | }
43 |
44 | destroy(): void {
45 | const destroyCallback = (control: IFormControl) => control.destroy();
46 | this.iterateControls(destroyCallback);
47 |
48 | this.controls = {};
49 | super.destroy();
50 | }
51 |
52 | private iterateControls(callback: (control: IFormControl) => void): void {
53 | Object.keys(this.controls).forEach((controlName) => {
54 | callback(this.controls[controlName]);
55 | });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/utils/property-grid-utils.ts:
--------------------------------------------------------------------------------
1 | import { DataObject } from '../models/types';
2 |
3 | export class PropertyGridUtils {
4 | static toBool(value: string | boolean): boolean {
5 | return value.toString() === 'true';
6 | }
7 |
8 | static camelize(str: string): string {
9 | return str
10 | .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
11 | return index === 0 ? word.toLowerCase() : word.toUpperCase();
12 | })
13 | .replace(/\s+/g, '');
14 | }
15 |
16 | static isColor(str: string): boolean {
17 | return str.toString().match(/^#[a-f0-9]{6}$/i) !== null;
18 | }
19 |
20 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
21 | static getDefaultVal(item: DataObject, property: string, defaultValue: any): any {
22 | return item.hasOwnProperty(property) ? item[property] : defaultValue;
23 | }
24 |
25 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
26 | static deepCopy(object: any): any {
27 | return JSON.parse(JSON.stringify(object));
28 | }
29 |
30 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
31 | static valueToControlType(value: any): string {
32 | let valueType = typeof value as string;
33 |
34 | if (valueType === 'undefined') {
35 | return;
36 | }
37 |
38 | if (PropertyGridUtils.isColor(value)) {
39 | valueType = 'color';
40 | }
41 | let controlType;
42 | switch (valueType) {
43 | case 'bigint':
44 | case 'number':
45 | controlType = 'number';
46 | break;
47 | case 'string':
48 | controlType = 'text';
49 | break;
50 | case 'boolean':
51 | controlType = 'boolean';
52 | break;
53 | case 'color':
54 | controlType = 'color';
55 | break;
56 | default:
57 | controlType = 'label';
58 | }
59 |
60 | return controlType;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/services/property-grid-factory.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CheckboxFormControl,
3 | ColorPickerFormControl,
4 | DatePickerFormControl,
5 | FormControlsMap,
6 | IFormControl,
7 | LabelFormControl,
8 | NumberFormControl,
9 | PropertyGridForm,
10 | PropertyGridRow,
11 | PropertyGridRowGroup,
12 | SelectFormControl,
13 | TextboxFormControl,
14 | } from '../controls';
15 | import { propertyGridControlDecorator } from '../decorators';
16 | import { PropertyGridOptions } from '../models';
17 | import { PropertyGridGroup } from '../models/property-grid-group';
18 | import { PropertyGridItem } from '../models/property-grid-item';
19 |
20 | import { EventDispatcherService } from './event-dispatcher-service';
21 |
22 | export const COMPONENTS_MAP = {
23 | text: TextboxFormControl,
24 | number: NumberFormControl,
25 | boolean: CheckboxFormControl,
26 | row: PropertyGridRow,
27 | row_group: PropertyGridRowGroup,
28 | form: PropertyGridForm,
29 | options: SelectFormControl,
30 | color: ColorPickerFormControl,
31 | date: DatePickerFormControl,
32 | label: LabelFormControl,
33 | };
34 |
35 | export class PropertyGridFactory {
36 | private customControls: FormControlsMap;
37 | constructor(private eventDispatcher: EventDispatcherService) {}
38 |
39 | registerControls(ctrls: FormControlsMap): void {
40 | this.customControls = {};
41 | this.customControls = ctrls;
42 | }
43 |
44 | create(type: string, data: PropertyGridGroup | PropertyGridItem, pgOptions: PropertyGridOptions): IFormControl {
45 | const gridItem = data as PropertyGridItem;
46 | const controls: any = Object.assign(COMPONENTS_MAP, this.customControls);
47 | const ctrl = propertyGridControlDecorator(
48 | new controls[type]({
49 | id: gridItem.id,
50 | name: gridItem.name,
51 | label: gridItem.label,
52 | description: gridItem.description,
53 | options: gridItem.options,
54 | }),
55 | pgOptions,
56 | this.eventDispatcher,
57 | );
58 |
59 | ctrl.init();
60 | return ctrl;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/controls/input-form-control.ts:
--------------------------------------------------------------------------------
1 | import { FormEvent } from '../models/types';
2 |
3 | import { FormControl, FormControlParams } from './form-control';
4 |
5 | export abstract class InputFormControl extends FormControl {
6 | constructor(params: FormControlParams) {
7 | super(params);
8 | this.onValueChange = this.onValueChange.bind(this);
9 | }
10 |
11 | /**
12 | * Set input type (checkbox, text, number)
13 | * @param type
14 | */
15 | abstract getInputType(): string;
16 |
17 | getNativeElementType(): string {
18 | return 'input';
19 | }
20 |
21 | getNativeElement(): HTMLInputElement {
22 | return super.getNativeElement() as HTMLInputElement;
23 | }
24 |
25 | renderValue(): void {
26 | if (!this.value) {
27 | return;
28 | }
29 | this.getNativeElement().value = this.value.toString();
30 | }
31 |
32 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
33 | setValue(value: any): void {
34 | super.setValue(value);
35 | this.renderValue();
36 | }
37 |
38 | render(): HTMLInputElement {
39 | this.assignAttributes();
40 | this.renderValue();
41 | return this.getNativeElement();
42 | }
43 |
44 | createNatveElement(): void {
45 | super.createNatveElement();
46 | this.getNativeElement().type = this.getInputType();
47 | this.getNativeElement().name = this.getName();
48 | }
49 |
50 | private assignAttributes() {
51 | if (!this.attributes) {
52 | return;
53 | }
54 |
55 | Object.keys(this.attributes).forEach((attr) => {
56 | this.getNativeElement().setAttribute(attr, this.attributes[attr].toString());
57 | });
58 | }
59 |
60 | attachEventListeners(): void {
61 | this.getNativeElement().addEventListener('change', this.onValueChange);
62 | }
63 |
64 | removeEventListeners(): void {
65 | this.getNativeElement().removeEventListener('change', this.onValueChange);
66 | }
67 |
68 | protected onValueChange(event: FormEvent): void {
69 | this.setValue(event.target.value);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/controls/select-form-control.ts:
--------------------------------------------------------------------------------
1 | import { FormEvent, Options } from '../models/types';
2 |
3 | import { FormControl, FormControlParams } from './form-control';
4 |
5 | export interface SelectFormControlOptions extends Options {
6 | items: string[];
7 | }
8 | export interface SelectFormControlParams extends FormControlParams {
9 | options: SelectFormControlOptions;
10 | }
11 |
12 | export class SelectFormControl extends FormControl {
13 | private items: string[];
14 |
15 | constructor(params: SelectFormControlParams) {
16 | super(params);
17 | this.items = params.options.items;
18 |
19 | this.onValueChange = this.onValueChange.bind(this);
20 | }
21 |
22 | getNativeElementType(): string {
23 | return 'select';
24 | }
25 |
26 | getNativeElement(): HTMLSelectElement {
27 | return super.getNativeElement() as HTMLSelectElement;
28 | }
29 |
30 | setValue(value: string | number): void {
31 | super.setValue(value);
32 | this.renderValue();
33 | }
34 |
35 | renderValue(): void {
36 | if (!this.value) {
37 | return;
38 | }
39 | this.getNativeElement().value = this.value.toString();
40 | }
41 |
42 | createNatveElement(): void {
43 | super.createNatveElement();
44 | this.getNativeElement().name = this.name;
45 | }
46 |
47 | render(): HTMLSelectElement {
48 | this.createOptions();
49 |
50 | return this.getNativeElement();
51 | }
52 |
53 | createOptions(): void {
54 | if (!this.options) {
55 | return;
56 | }
57 |
58 | this.items.forEach((item) => {
59 | const option = document.createElement('option') as HTMLOptionElement;
60 | option.innerHTML = item;
61 | option.value = item;
62 | this.getNativeElement().options.add(option);
63 | });
64 | }
65 |
66 | attachEventListeners(): void {
67 | this.nativeElement.addEventListener('change', this.onValueChange);
68 | }
69 | removeEventListeners(): void {
70 | this.nativeElement.removeEventListener('change', this.onValueChange);
71 | }
72 |
73 | protected onValueChange(event: FormEvent): void {
74 | this.setValue(event.target.value);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/services/config-parser-service.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridConfig, PropertyGridConfigItem } from '../models';
2 | import { PROPERTY_GRID_CONSTANTS } from '../models/property-grid-constants';
3 | import { DataObject } from '../models/types';
4 | import { PropertyGridUtils } from '../utils/property-grid-utils';
5 |
6 | export class ConfigParserService {
7 | parse(selectedObject: DataObject, config?: PropertyGridConfig): PropertyGridConfig {
8 | config = config || {};
9 | return this.createConfigItem(selectedObject, config);
10 | }
11 |
12 | /** Assigns property grid config items to same group */
13 | assignToGroup(config: PropertyGridConfig, groupName = PROPERTY_GRID_CONSTANTS.OTHER_GROUP_NAME): void {
14 | Object.keys(config).forEach((key) => {
15 | config[key].group = groupName;
16 | });
17 | }
18 |
19 | private createConfigItem(selectedObject: DataObject, configObject: PropertyGridConfig): PropertyGridConfig {
20 | const pgConfig = Object.keys(configObject).length ? configObject : selectedObject;
21 | let ID_COUNTER = 1;
22 | const config = {};
23 | Object.keys(pgConfig).forEach((propertyName) => {
24 | const item = pgConfig[propertyName];
25 | config[propertyName] = Object.assign(item, {
26 | id: `pg${PropertyGridUtils.camelize(propertyName)}${ID_COUNTER}`,
27 | name: PropertyGridUtils.camelize(propertyName),
28 | type: this.defaultVal(item, 'type', PropertyGridUtils.valueToControlType(selectedObject[propertyName])),
29 | label: this.defaultVal(item, 'name', propertyName),
30 | browsable: this.defaultVal(item, 'browsable', true),
31 | description: this.defaultVal(item, 'description', ''),
32 | group: this.defaultVal(item, 'group', PROPERTY_GRID_CONSTANTS.OTHER_GROUP_NAME),
33 | showHelp: this.defaultVal(item, 'showHelp', false),
34 | options: this.defaultVal(item, 'options', {}),
35 | });
36 | ID_COUNTER++;
37 | });
38 |
39 | return config;
40 | }
41 |
42 | private defaultVal(item: DataObject, property: string, defaultValue: any): any {
43 | return PropertyGridUtils.getDefaultVal(item, property, defaultValue);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/controls/property-grid-row.ts:
--------------------------------------------------------------------------------
1 | import { FormControlComposite } from './form-control-composite';
2 | import { IFormControl } from './iform-control';
3 |
4 | export class PropertyGridRow extends FormControlComposite {
5 | private propertyGridRowCssClass = 'property-grid-row';
6 | private propertyGridCellCssClass = 'property-grid-cell';
7 | public render(): HTMLTableRowElement {
8 | const el = this.getNativeElement();
9 | el.innerHTML = '';
10 |
11 | Object.keys(this.controls).forEach((controlName) => {
12 | el.appendChild(this.createLabelCell(this.controls[controlName]));
13 | el.appendChild(this.createControls(this.controls[controlName]));
14 | });
15 |
16 | return el;
17 | }
18 |
19 | getNativeElement(): HTMLTableRowElement {
20 | return super.getNativeElement() as HTMLTableRowElement;
21 | }
22 |
23 | getNativeElementType(): string {
24 | return 'tr';
25 | }
26 |
27 | createNatveElement(): void {
28 | super.createNatveElement();
29 | this.nativeElement.id = this.nativeElement.id + 'Row';
30 | this.getNativeElement().classList.add(this.propertyGridRowCssClass);
31 | }
32 | // eslint-disable-next-line @typescript-eslint/no-empty-function
33 | attachEventListeners(): void {}
34 | // eslint-disable-next-line @typescript-eslint/no-empty-function
35 | removeEventListeners(): void {}
36 |
37 | private createLabelCell(control: IFormControl): HTMLTableCellElement {
38 | const cell = this.createGridCell();
39 | cell.appendChild(document.createTextNode(control.getLabel()));
40 |
41 | if (control.getDescription() && control.getOptions().showHelp) {
42 | const helpTooltip = document.createElement('span');
43 | helpTooltip.innerHTML = ' [?]';
44 | helpTooltip.title = control.getDescription();
45 |
46 | cell.appendChild(helpTooltip);
47 | }
48 |
49 | return cell;
50 | }
51 |
52 | private createControls(control: IFormControl): HTMLTableCellElement {
53 | const cell = this.createGridCell();
54 |
55 | cell.appendChild(control.render());
56 | return cell;
57 | }
58 |
59 | private createGridCell(): HTMLTableCellElement {
60 | const el = document.createElement('td');
61 | el.classList.add(this.propertyGridCellCssClass);
62 | return el;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/controls/iform-control.ts:
--------------------------------------------------------------------------------
1 | import { Attributes, FormEvent, Options } from '../models/types';
2 |
3 | export interface IFormControl {
4 | /**
5 | * Method called from init to attach event listeners like click, change..etc.
6 | */
7 | attachEventListeners(): void;
8 |
9 | /**
10 | * A callback method that is called only once during init.
11 | * Return the DOM element of your component, this is what the property grid puts into the DOM.
12 | * Creates the native element based on props.
13 | */
14 | createNatveElement(): void;
15 |
16 | /**
17 | * A callback method that performs custom clean-up, invoked immediately
18 | * before a component instance is destroyed.
19 | */
20 | destroy(): void;
21 |
22 | /**
23 | * Returns Component Attributes
24 | */
25 | getAttributes(): Attributes;
26 |
27 | /**
28 | * Returns Component Id
29 | */
30 | getId(): string;
31 |
32 | /**
33 | * Returns component name
34 | */
35 | getName(): string;
36 |
37 | /**
38 | * Returns the Native Element
39 | */
40 | getNativeElement(): HTMLElement;
41 |
42 | /**
43 | * Returns Native Element type. For ex: input, select...etc.
44 | */
45 | getNativeElementType(): string;
46 |
47 | /**
48 | * Returns component label (title).
49 | */
50 | getLabel(): string;
51 | /**
52 | * Returns component label (title).
53 | */
54 | getDescription(): string;
55 |
56 | /**
57 | * Returns component options
58 | */
59 | getOptions(): Options;
60 |
61 | /**
62 | * Returns Component Value
63 | */
64 | getValue(): any;
65 |
66 | /**
67 | * A callback method that is invoked only once when the component is instantiated.
68 | */
69 | init(): void;
70 |
71 | /**
72 | * A callback method that is called before destory to remove all attached event listeners.
73 | */
74 | removeEventListeners(): void;
75 |
76 | /**
77 | * A callback function that is called when the native element should be rendered on UI.
78 | * Could be called multiple times to reflect component state.
79 | */
80 | render(): HTMLElement;
81 |
82 | /**
83 | * A callback function that is called to set the component value
84 | * @param value
85 | */
86 | setValue(value: any): void;
87 |
88 | /**
89 | * Optional on Value change event handler, that will be used by hooks and handlers.
90 | */
91 | onValueChange?: (event: FormEvent) => void;
92 | }
93 |
--------------------------------------------------------------------------------
/src/controls/property-grid-row-group.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridFormControlsMap } from '../controls/property-grid-form-controls-map';
2 | import { DataObject } from '../models/types';
3 |
4 | import { FormControlParams } from './form-control';
5 | import { FormControlComposite } from './form-control-composite';
6 |
7 | export class PropertyGridRowGroup extends FormControlComposite {
8 | protected controls: PropertyGridFormControlsMap = {};
9 |
10 | constructor(params: FormControlParams) {
11 | super(params);
12 | this.onToggleGroupVisibility = this.onToggleGroupVisibility.bind(this);
13 | }
14 |
15 | public render(): any {
16 | const el = this.getNativeElement();
17 |
18 | el.querySelector('.property-grid-group-content table').appendChild(super.render());
19 | return el;
20 | }
21 |
22 | public getData(): DataObject {
23 | let result = {};
24 | for (const name in this.controls) {
25 | if (this.controls[name]) {
26 | result = Object.assign(result, this.controls[name].getData());
27 | }
28 | }
29 |
30 | return result;
31 | }
32 |
33 | getNativeElementType(): string {
34 | return 'tr';
35 | }
36 |
37 | createNatveElement(): void {
38 | const el = document.createElement(this.getNativeElementType());
39 | el.classList.add('property-grid-row-group');
40 |
41 | const innerHTML = `
42 |
43 |
44 | | ${this.getLabel()} |
45 | |
46 |
47 | |
48 |
49 | |
50 | `;
51 | el.innerHTML = innerHTML;
52 | this.nativeElement = el;
53 | }
54 | attachEventListeners(): void {
55 | this.getNativeElement()
56 | .querySelector('.property-grid-row-group-wrapper .property-grid-group')
57 | .addEventListener('click', this.onToggleGroupVisibility, false);
58 | }
59 | removeEventListeners(): void {
60 | this.getNativeElement()
61 | .querySelector('.property-grid-row-group-wrapper .property-grid-group')
62 | .removeEventListener('click', this.onToggleGroupVisibility);
63 | }
64 |
65 | protected onToggleGroupVisibility(): void {
66 | this.getNativeElement().querySelector('.property-grid-group-content').classList.toggle('collapsed');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/services/property-grid-groups-builder-service.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridGroup, PropertyGridItem } from '../models';
2 | import { PropertyGridConfig, PropertyGridConfigItem } from '../models';
3 | import { PropertyGridUtils } from '../utils/property-grid-utils';
4 |
5 | export class PropertyGridGroupsBuilderService {
6 | private OTHER_GROUP_KEY = 'other';
7 | private OTHER_GROUP_NAME = 'Other';
8 |
9 | buildGroups(config: PropertyGridConfig, sortProperties: boolean | ((params: string[]) => string[])): PropertyGridGroup[] {
10 | const groupsMap = {};
11 | const otherGroup = {
12 | label: this.OTHER_GROUP_NAME,
13 | name: this.OTHER_GROUP_KEY,
14 | children: [],
15 | };
16 |
17 | const properties = this.sortProperties(Object.keys(config), sortProperties);
18 |
19 | properties.forEach((prop) => {
20 | const item = config[prop];
21 | if (this.isNotBrowsable(item)) {
22 | return;
23 | }
24 |
25 | const itemGroupLabel = item.group;
26 | const itemGroupKey = PropertyGridUtils.camelize(itemGroupLabel);
27 |
28 | if (!groupsMap.hasOwnProperty(itemGroupKey)) {
29 | groupsMap[itemGroupKey] = this.createGroup(itemGroupLabel, itemGroupKey);
30 | }
31 |
32 | if (itemGroupKey === 'other') {
33 | otherGroup.children.push(this.createGridItem(item));
34 | } else {
35 | groupsMap[itemGroupKey].children.push(item);
36 | }
37 | });
38 | groupsMap[this.OTHER_GROUP_KEY] = otherGroup;
39 | const groups = this.sortProperties(Object.keys(groupsMap), sortProperties).map((key) => groupsMap[key]);
40 |
41 | return groups;
42 | }
43 |
44 | private createGridItem(configItem: PropertyGridConfigItem): PropertyGridItem {
45 | return {
46 | id: configItem.id,
47 | name: configItem.name,
48 | type: configItem.type,
49 | description: configItem.description,
50 | label: configItem.label,
51 | attributes: configItem.options,
52 | options: {
53 | group: configItem.group,
54 | browsable: configItem.browsable,
55 | showHelp: configItem.showHelp,
56 | items: configItem.items || [],
57 | },
58 | };
59 | }
60 |
61 | private createGroup(groupLabel, groupKey): PropertyGridGroup {
62 | return {
63 | name: groupKey,
64 | label: groupLabel,
65 | children: [],
66 | };
67 | }
68 |
69 | private isNotBrowsable(item: PropertyGridConfigItem): boolean {
70 | return item.hasOwnProperty('browsable') && item.browsable === false;
71 | }
72 |
73 | private sortProperties(properties: string[], sortMethod?: boolean | ((params: string[]) => string[])): string[] {
74 | if (sortMethod === false) {
75 | return properties;
76 | }
77 | return sortMethod instanceof Function ? sortMethod(properties) : properties.sort();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable */
2 | var path = require('path');
3 |
4 | var webpack = require('webpack');
5 |
6 | var PATHS = {
7 | entryPoint: path.resolve(__dirname, 'src/index.ts'),
8 | bundles: path.resolve(__dirname, 'dist/bundles'),
9 | };
10 |
11 | var config = {
12 | // These are the entry point of our library. We tell webpack to use
13 | // the name we assign later, when creating the bundle. We also use
14 | // the name to filter the second entry point for applying code
15 | // minification via UglifyJS
16 | entry: {
17 | 'clean-web-ui-property-grid': [PATHS.entryPoint],
18 | 'clean-web-ui-property-grid.min': [PATHS.entryPoint],
19 | },
20 | // The output defines how and where we want the bundles. The special
21 | // value `[name]` in `filename` tell Webpack to use the name we defined above.
22 | // We target a UMD and name it MyLib. When including the bundle in the browser
23 | // it will be accessible at `window.MyLib`
24 | output: {
25 | path: PATHS.bundles,
26 | filename: '[name].js',
27 | libraryTarget: 'umd',
28 | library: 'CleanWebUIPropertyGrid',
29 | umdNamedDefine: true,
30 | globalObject: 'this',
31 | },
32 | // Add resolve for `tsx` and `ts` files, otherwise Webpack would
33 | // only look for common JavaScript file extension (.js)
34 | resolve: {
35 | extensions: ['.ts', '.js'],
36 | },
37 | // Activate source maps for the bundles in order to preserve the original
38 | // source when the user debugs the application
39 | devtool: 'source-map',
40 | plugins: [new webpack.HotModuleReplacementPlugin()],
41 | module: {
42 | // Webpack doesn't understand TypeScript files and a loader is needed.
43 | // `node_modules` folder is excluded in order to prevent problems with
44 | // the library dependencies, as well as `__tests__` folders that
45 | // contain the tests for the library
46 | rules: [
47 | {
48 | test: /\.ts?$/,
49 | loader: 'awesome-typescript-loader',
50 | exclude: /node_modules/,
51 | query: {
52 | // we don't want any declaration file in the bundles
53 | // folder since it wouldn't be of any use ans the source
54 | // map already include everything for debugging
55 | declaration: false,
56 | },
57 | },
58 | ],
59 | },
60 | stats: {
61 | colors: {
62 | green: '\u001b[32m',
63 | },
64 | },
65 |
66 | devServer: {
67 | contentBase: './src',
68 | historyApiFallback: true,
69 | port: 3000,
70 | compress: false,
71 | inline: true,
72 | hot: true,
73 | stats: {
74 | assets: true,
75 | children: false,
76 | chunks: false,
77 | hash: false,
78 | modules: false,
79 | publicPath: false,
80 | timings: true,
81 | version: false,
82 | warnings: true,
83 | colors: {
84 | green: '\u001b[32m',
85 | },
86 | },
87 | },
88 | };
89 |
90 | module.exports = config;
91 |
--------------------------------------------------------------------------------
/src/controls/form-control.ts:
--------------------------------------------------------------------------------
1 | import { Attributes, Options } from '../models/types';
2 |
3 | import { IFormControl } from './iform-control';
4 |
5 | export interface FormControlParams {
6 | id: string;
7 | name: string;
8 | label: string;
9 | description: string;
10 | attributes: Attributes;
11 | options: Options;
12 | }
13 |
14 | /**
15 | * The base Component class declares an interface for all concrete components,
16 | * both simple and complex.
17 | *
18 | */
19 | export abstract class FormControl implements IFormControl {
20 | protected id: string;
21 | protected name: string;
22 | protected label: string;
23 | protected description: string;
24 | protected value: any;
25 | protected attributes: Attributes;
26 | protected options: Options;
27 | protected nativeElement: HTMLElement;
28 |
29 | constructor(params: FormControlParams) {
30 | Object.assign(this, params);
31 | this.options = params.options || { showHelp: true };
32 | this.attributes = params.attributes || {};
33 | }
34 |
35 | /**
36 | * Use to attach event listeners like click, change...etc.
37 | * It is called inside createNativeElement
38 | */
39 | abstract attachEventListeners(): void;
40 |
41 | /**
42 | * A callback method that is called before destory to remove all attached event listeners.
43 | */
44 | abstract removeEventListeners(): void;
45 |
46 | /**
47 | * Each concrete DOM element must provide its rendering implementation, but
48 | * we can safely assume that all of them are returning strings.
49 | */
50 | abstract render(): HTMLElement;
51 |
52 | /** Returns native element type. Ex: input, select */
53 | abstract getNativeElementType(): string;
54 |
55 | /**
56 | * Initialize component, creates native element, attach event listners
57 | */
58 | init(): void {
59 | if (this.nativeElement) {
60 | return;
61 | }
62 | this.createNatveElement();
63 | this.attachEventListeners();
64 | }
65 |
66 | /**
67 | * Cleanup form control and removes all event listeners
68 | */
69 | destroy(): void {
70 | if (!this.getNativeElement()) {
71 | return;
72 | }
73 |
74 | this.removeEventListeners();
75 | this.nativeElement = null;
76 | }
77 |
78 | /**
79 | * Creates native element and attach event listeners
80 | */
81 | createNatveElement(): void {
82 | if (this.nativeElement) {
83 | return;
84 | }
85 |
86 | this.nativeElement = document.createElement(this.getNativeElementType());
87 |
88 | this.nativeElement.id = this.id;
89 | }
90 |
91 | getAttributes(): Attributes {
92 | return this.attributes;
93 | }
94 |
95 | getId(): string {
96 | return this.id;
97 | }
98 |
99 | getName(): string {
100 | return this.name;
101 | }
102 |
103 | getDescription(): string {
104 | return this.description;
105 | }
106 |
107 | getNativeElement(): HTMLElement {
108 | if (!this.nativeElement) {
109 | throw Error('You can not use native element before is being initialized!');
110 | }
111 |
112 | return this.nativeElement;
113 | }
114 |
115 | getLabel(): string {
116 | return this.label;
117 | }
118 |
119 | getOptions(): Options {
120 | return this.options;
121 | }
122 |
123 | getValue(): any {
124 | return this.value;
125 | }
126 |
127 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
128 | setValue(value: any): void {
129 | this.value = value;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable */
2 | var path = require('path');
3 |
4 | var TerserPlugin = require('terser-webpack-plugin');
5 | const CopyPlugin = require('copy-webpack-plugin');
6 | var webpack = require('webpack');
7 |
8 | var PATHS = {
9 | entryPoint: path.resolve(__dirname, 'src/index.ts'),
10 | bundles: path.resolve(__dirname, 'dist/bundles'),
11 | };
12 |
13 | console.log('Prod config');
14 |
15 | var config = {
16 | // These are the entry point of our library. We tell webpack to use
17 | // the name we assign later, when creating the bundle. We also use
18 | // the name to filter the second entry point for applying code
19 | // minification via UglifyJS
20 | entry: {
21 | 'clean-web-ui-property-grid': [PATHS.entryPoint],
22 | 'clean-web-ui-property-grid.min': [PATHS.entryPoint],
23 | },
24 | // The output defines how and where we want the bundles. The special
25 | // value `[name]` in `filename` tell Webpack to use the name we defined above.
26 | // We target a UMD and name it MyLib. When including the bundle in the browser
27 | // it will be accessible at `window.MyLib`
28 | output: {
29 | path: PATHS.bundles,
30 | filename: '[name].js',
31 | libraryTarget: 'umd',
32 | library: 'CleanWebUIPropertyGrid',
33 | umdNamedDefine: true,
34 | globalObject: 'this',
35 | },
36 | // Add resolve for `tsx` and `ts` files, otherwise Webpack would
37 | // only look for common JavaScript file extension (.js)
38 | resolve: {
39 | extensions: ['.ts', '.js'],
40 | },
41 | // Activate source maps for the bundles in order to preserve the original
42 | // source when the user debugs the application
43 | devtool: 'source-map',
44 | plugins: [
45 | new CopyPlugin({
46 | patterns: [
47 | {
48 | from: path.resolve(__dirname, 'README.md'),
49 | to: path.resolve(__dirname, 'dist'),
50 | },
51 | {
52 | from: path.resolve(__dirname, 'package.json'),
53 | to: path.resolve(__dirname, 'dist'),
54 | },
55 | {
56 | from: path.resolve(__dirname, 'LICENSE'),
57 | to: path.resolve(__dirname, 'dist'),
58 | },
59 | {
60 | from: path.resolve(__dirname, '.npmignore'),
61 | to: path.resolve(__dirname, 'dist'),
62 | },
63 | ],
64 | }),
65 | ],
66 | module: {
67 | // Webpack doesn't understand TypeScript files and a loader is needed.
68 | // `node_modules` folder is excluded in order to prevent problems with
69 | // the library dependencies, as well as `__tests__` folders that
70 | // contain the tests for the library
71 | rules: [
72 | {
73 | test: /\.ts?$/,
74 | loader: 'awesome-typescript-loader',
75 | exclude: /node_modules/,
76 | query: {
77 | // we don't want any declaration file in the bundles
78 | // folder since it wouldn't be of any use ans the source
79 | // map already include everything for debugging
80 | declaration: false,
81 | },
82 | },
83 | ],
84 | },
85 | optimization: {
86 | minimizer: [
87 | // we specify a custom UglifyJsPlugin here to get source maps in production
88 | new TerserPlugin({
89 | parallel: true,
90 | include: /\.min\.js$/,
91 | terserOptions: {
92 | ecma: 6,
93 | },
94 | }),
95 | ],
96 | },
97 | stats: {
98 | colors: {
99 | green: '\u001b[32m',
100 | },
101 | },
102 | };
103 |
104 | module.exports = config;
105 |
--------------------------------------------------------------------------------
/src/styles/property-grid-base-style.ts:
--------------------------------------------------------------------------------
1 | const propertyGridStyles = document.createElement('template');
2 | propertyGridStyles.innerHTML = `
3 |
150 | `;
151 |
152 | export { propertyGridStyles };
153 |
--------------------------------------------------------------------------------
/src/sample/index.js:
--------------------------------------------------------------------------------
1 | function createPropertyGrid(id, config, options, data) {
2 | var propertyGridWrapper = document.getElementById(id);
3 | let propertyGrid = document.createElement('property-grid');
4 | propertyGrid.id = 'pg' + id;
5 | propertyGrid.config = config;
6 | propertyGrid.options = options;
7 | propertyGrid.selectedObject = data;
8 | propertyGridWrapper.innerHTML = '';
9 | propertyGridWrapper.appendChild(propertyGrid);
10 | }
11 |
12 | function getValues(target) {
13 | var grid = document.getElementById(target);
14 | document.getElementById('txtValues').value = JSON.stringify(grid.getValues(), null, 2);
15 | }
16 |
17 | function updateOptions() {
18 | var options = document.getElementById('pgpropGridOptions').getValues();
19 | document.getElementById('pgpropertyGridWithConfig').options = options;
20 | }
21 |
22 | setTimeout(() => {
23 | var pgOptions = {
24 | preferredFormat: 'hex',
25 | showInput: true,
26 | showInitial: true,
27 | hasGroups: true,
28 | propertySort: true,
29 | toolbarVisible: true,
30 | onValueChange: (e) => {
31 | console.log(e);
32 | if (e.target.id == 'pgfilter2') {
33 | e.preventDefault();
34 | e.stopPropagation();
35 | return false;
36 | }
37 | },
38 | };
39 |
40 | var pgConfig = {
41 | dontShowMe: { browsable: false },
42 | filter: { group: 'Behavior', name: 'Filter', type: 'boolean' },
43 | filterSize: {
44 | group: 'Behavior',
45 | name: 'Filter size',
46 | type: 'number',
47 | options: { min: 0, max: 500, step: 10 },
48 | },
49 | accumulateTicks: {
50 | group: 'Behavior',
51 | name: 'Accumulate ticks',
52 | type: 'boolean',
53 | },
54 | buyColor: {
55 | group: 'Appearance',
56 | name: 'Buy color',
57 | type: 'color',
58 | options: {},
59 | },
60 | sellColor: {
61 | group: 'Appearance',
62 | name: 'Sell color',
63 | type: 'color',
64 | options: {
65 | preferredFormat: 'hex',
66 | showInput: true,
67 | showInitial: true,
68 | },
69 | },
70 | someOption: {
71 | name: 'Some option',
72 | type: 'options',
73 | items: ['Yes', 'No'],
74 | },
75 | readOnly: {
76 | name: 'Read Only',
77 | type: 'label',
78 | description: 'This is a label',
79 | showHelp: true,
80 | },
81 | };
82 |
83 | // var opts = {
84 | // propertySort: (props) => props.sort().reverse(),
85 | // };
86 |
87 | var pgData = {
88 | accumulateTicks: true,
89 | filter: false,
90 | filterSize: 200,
91 | buyColor: '#00ff00',
92 | sellColor: '#ff0000',
93 | someOption: 'No',
94 | noGroup: 'I have no group',
95 | dontShowMe: 'please',
96 | readOnly: 'I am read only',
97 | };
98 |
99 | createPropertyGrid('propertyGridWithConfig', pgConfig, pgOptions, pgData);
100 |
101 | document.getElementById('pgpropertyGridWithConfig').addEventListener('valueChanged', (v) => console.log(v));
102 |
103 | const pg1 = document.getElementById('pg1');
104 | pg1.selectedObject = pgData;
105 |
106 | // document.addEventListener('onValueChange', (v) => console.log(v));
107 |
108 | var pgOptionsConfig = {
109 | hasGroups: {
110 | group: 'Groups',
111 | name: 'Has Groups',
112 | type: 'boolean',
113 | description: 'Gets or sets a value indicating whether controls are groupped and if groups are visible.',
114 | },
115 | propertySort: {
116 | group: 'Controls',
117 | name: 'Properties sort',
118 | type: 'boolean',
119 | description: 'Gets or sets if PropertyGrid will sort properties by display name.',
120 | },
121 | toolbarVisible: {
122 | group: 'Controls',
123 | name: 'Is Toolbar Visible',
124 | type: 'boolean',
125 | description: 'Gets or sets a value indicating whether the toolbar is visible.',
126 | },
127 | };
128 | createPropertyGrid('propGridOptions', pgOptionsConfig, pgOptions, pgOptions);
129 | }, 1000);
130 |
--------------------------------------------------------------------------------
/src/controls/property-grid-form.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridFormControlsMap } from '../controls/property-grid-form-controls-map';
2 | import { DataObject, FormEvent } from '../models/types';
3 |
4 | import { FormControlParams } from './form-control';
5 | import { FormControlComposite } from './form-control-composite';
6 | import { PropertyGridRowGroup } from './property-grid-row-group';
7 |
8 | /**
9 | * The fieldset element is a Concrete Composite.
10 | */
11 | export class PropertyGridForm extends FormControlComposite {
12 | private propertyGridFormCssClass = 'property-grid-form';
13 | private propertyGridTableCssClass = 'property-grid';
14 | protected controls: PropertyGridFormControlsMap = {};
15 |
16 | constructor(params: FormControlParams) {
17 | super(params);
18 | this.onFormSubmit = this.onFormSubmit.bind(this);
19 | this.onSearch = this.onSearch.bind(this);
20 | }
21 |
22 | createNatveElement(): void {
23 | super.createNatveElement();
24 | const el = this.getNativeElement();
25 | el.classList.add(this.propertyGridFormCssClass);
26 |
27 | const toolbar = document.createElement('header');
28 | toolbar.classList.add('property-grid-header');
29 | toolbar.innerHTML = `
30 | `;
31 | el.appendChild(toolbar);
32 |
33 | const fieldset = document.createElement('fieldset');
34 | const pgTable = document.createElement('table');
35 | pgTable.classList.add(this.propertyGridTableCssClass);
36 |
37 | fieldset.appendChild(pgTable);
38 |
39 | el.appendChild(fieldset);
40 | }
41 |
42 | public render(): any {
43 | const el = this.getNativeElement();
44 | const table = el.querySelector('table');
45 | table.appendChild(super.render());
46 | return el;
47 | }
48 |
49 | public setData(data: DataObject): void {
50 | for (const name in this.controls) {
51 | if (this.controls[name] && this.isRowGroupCtrl(this.controls[name])) {
52 | this.controls[name].setValue(data);
53 | }
54 | }
55 | }
56 |
57 | public getData(): any {
58 | let response = {};
59 | for (const name in this.controls) {
60 | if (this.controls[name] && this.isRowGroupCtrl(this.controls[name])) {
61 | response = Object.assign(response, this.controls[name].getData());
62 | }
63 | }
64 |
65 | return response;
66 | }
67 |
68 | private isRowGroupCtrl(ctrl): boolean {
69 | return ctrl.constructor.name.toString() === PropertyGridRowGroup.name.toString();
70 | }
71 |
72 | getNativeElementType(): string {
73 | return 'form';
74 | }
75 |
76 | setDisabled(isDisabled: boolean): void {
77 | this.getNativeElement().querySelector('fieldset').disabled = isDisabled;
78 | }
79 |
80 | toggleToolbar(isToolbarVisible: boolean): void {
81 | if (!isToolbarVisible) {
82 | this.getNativeElement().querySelector('.property-grid-header').classList.toggle('hidden');
83 | } else {
84 | this.getNativeElement().querySelector('.property-grid-header').classList.remove('hidden');
85 | }
86 | }
87 |
88 | attachEventListeners(): void {
89 | this.getNativeElement().addEventListener('submit', this.onFormSubmit);
90 | this.getNativeElement().querySelector('.property-grid-header input').addEventListener('input', this.onSearch);
91 | }
92 | removeEventListeners(): void {
93 | this.getNativeElement().removeEventListener('submit', this.onFormSubmit);
94 | this.getNativeElement().querySelector('.property-grid-header input').removeEventListener('input', this.onSearch);
95 | }
96 |
97 | onFormSubmit(event: FormEvent): void {
98 | event.preventDefault();
99 | }
100 |
101 | onSearch(event: FormEvent): void {
102 | event.preventDefault();
103 | const value = event.target.value.toLowerCase();
104 |
105 | value ? this.getNativeElement().classList.add('no-groups') : this.getNativeElement().classList.remove('no-groups');
106 | this.getNativeElement()
107 | .querySelectorAll('.property-grid-row')
108 | .forEach((row) => row.classList.remove('hidden'));
109 |
110 | this.getNativeElement()
111 | .querySelectorAll('.property-grid-row td:first-of-type')
112 | .forEach((cell) => {
113 | if (!cell.textContent.toLowerCase().startsWith(value)) {
114 | cell.parentElement.classList.add('hidden');
115 | }
116 | });
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/property-grid.component.ts:
--------------------------------------------------------------------------------
1 | import { PropertyGridForm } from './controls';
2 | import { PropertyGridConfig, PropertyGridOptions } from './models';
3 | import { DataObject, PropertyGridEvents } from './models/types';
4 | import { propertyGridTemplate } from './property-grid-template';
5 | import { EventDispatcherService, Logger, PropertyGridGroupsBuilderService, PropertyGridService } from './services';
6 | import { ConfigParserService } from './services/config-parser-service';
7 | import { PropertyGridFactory } from './services/property-grid-factory';
8 | import { propertyGridStyles } from './styles/property-grid-base-style';
9 | import { PropertyGridUtils } from './utils/property-grid-utils';
10 |
11 | export class PropertyGrid extends HTMLElement {
12 | private _defaultOptions: PropertyGridOptions = {
13 | canFocus: true,
14 | controls: {},
15 | helpVisible: true,
16 | propertySort: true,
17 | toolbarVisible: true,
18 | };
19 |
20 | private _options: PropertyGridOptions;
21 | private _config: PropertyGridConfig;
22 | private _root;
23 | private _propertyGridEl: HTMLDivElement;
24 | private _gridReady = false;
25 | private _selectedObject;
26 | private readonly _selectedGridItem;
27 | private propertyGridForm: PropertyGridForm;
28 | private eventListeners = {};
29 | private groupsBuilder: PropertyGridGroupsBuilderService;
30 | private configParser: ConfigParserService;
31 | private eventDispatcher: EventDispatcherService;
32 | private factory: PropertyGridFactory;
33 | private pgBuilder: PropertyGridService;
34 |
35 | static get observedAttributes(): string[] {
36 | return ['disabled'];
37 | }
38 |
39 | constructor() {
40 | super();
41 |
42 | // Create a new shadow dom root.
43 | // The mode describes, if the node can be accessed from the outside.
44 | this.attachShadow({ mode: 'open' });
45 |
46 | this._root = this.shadowRoot;
47 |
48 | // Fill the shadow dom with the template by a deep clone.
49 | this._root.appendChild(propertyGridStyles.content.cloneNode(true));
50 | this._root.appendChild(propertyGridTemplate.content.cloneNode(true));
51 |
52 | this._options = this._defaultOptions;
53 |
54 | this._propertyGridEl = this.shadowRoot.querySelector('#propertyGrid');
55 | this.groupsBuilder = new PropertyGridGroupsBuilderService();
56 | this.configParser = new ConfigParserService();
57 | this.eventDispatcher = new EventDispatcherService();
58 | this.factory = new PropertyGridFactory(this.eventDispatcher);
59 | this.pgBuilder = new PropertyGridService(this.factory);
60 |
61 | this.onValueChanged = this.onValueChanged.bind(this);
62 |
63 | this.eventListeners['onValueChanged'] = this.eventDispatcher.register(PropertyGridEvents.onValueChanged, this.onValueChanged);
64 | }
65 |
66 | set config(opts: PropertyGridConfig) {
67 | this._config = Object.assign({}, opts);
68 | }
69 |
70 | get config(): PropertyGridConfig {
71 | return this._config;
72 | }
73 |
74 | get disabled(): boolean {
75 | return this.hasAttribute('disabled');
76 | }
77 |
78 | set disabled(val: boolean) {
79 | if (val) {
80 | this.setAttribute('disabled', '');
81 | } else {
82 | this.removeAttribute('disabled');
83 | }
84 | }
85 |
86 | set options(opts: PropertyGridOptions) {
87 | this._options = Object.assign(this._defaultOptions, opts);
88 |
89 | this._options = opts;
90 |
91 | if (this._gridReady) {
92 | this.render();
93 | }
94 | }
95 |
96 | get options(): PropertyGridOptions {
97 | return this._options;
98 | }
99 |
100 | set selectedObject(obj: DataObject) {
101 | if (typeof obj === 'string') {
102 | Logger.getInstance().error('PropertyGrid got invalid option:', obj);
103 | return;
104 | } else if (typeof obj !== 'object' || obj === null) {
105 | Logger.getInstance().error('PropertyGrid must get an object in order to initialize the grid.');
106 | return;
107 | }
108 | this._selectedObject = obj;
109 | this.render();
110 | }
111 |
112 | get selectedObject(): DataObject {
113 | return this.selectedObject;
114 | }
115 |
116 | get selectedGridItem(): any {
117 | return this._selectedGridItem;
118 | }
119 |
120 | connectedCallback(): void {
121 | this.render();
122 | }
123 | disconnectedCallback(): void {
124 | this.destroy();
125 | }
126 |
127 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
128 | attributeChangedCallback(name: string, oldValue: any, newValue: any): void {
129 | if (oldValue === newValue) {
130 | return;
131 | }
132 | if (name === 'disabled') {
133 | if (this.disabled) {
134 | this.setAttribute('tabindex', '-1');
135 | this.setAttribute('aria-disabled', 'true');
136 | } else {
137 | this.setAttribute('tabindex', '0');
138 | this.setAttribute('aria-disabled', 'false');
139 | }
140 |
141 | if (this._gridReady) {
142 | this.propertyGridForm.setDisabled(this.disabled);
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * Forces the control to invalidate its client area and immediately redraw itself and any child controls.
149 | */
150 | render(): void {
151 | if (!this._selectedObject) {
152 | return;
153 | }
154 |
155 | if (this._options && this._options.controls) {
156 | this.factory.registerControls(this._options.controls);
157 | }
158 |
159 | const origConfig = this._config ? PropertyGridUtils.deepCopy(this._config) : null;
160 | const config = this.configParser.parse(this._selectedObject, origConfig);
161 |
162 | if (this.options.hasGroups === false) {
163 | this.configParser.assignToGroup(config);
164 | }
165 |
166 | const groups = this.groupsBuilder.buildGroups(config, this.options.propertySort);
167 | this.propertyGridForm = this.pgBuilder.build(groups, this._options);
168 | this._propertyGridEl.innerHTML = '';
169 | this._propertyGridEl.appendChild(this.propertyGridForm.render());
170 |
171 | this.propertyGridForm.setData(this._selectedObject);
172 |
173 | this.propertyGridForm.setDisabled(this.disabled);
174 | this.propertyGridForm.toggleToolbar(this.options.toolbarVisible);
175 | if (this.options.hasGroups === false) {
176 | this.propertyGridForm.getNativeElement().classList.add('no-groups');
177 | }
178 |
179 | this._gridReady = true;
180 | }
181 |
182 | getValues(): DataObject {
183 | return Object.assign(this._selectedObject, this.propertyGridForm.getData());
184 | }
185 |
186 | destroy(): void {
187 | for (const key in this.eventListeners) {
188 | this.eventListeners[key].unsubscribe();
189 | }
190 | this.eventDispatcher.reset();
191 | this.propertyGridForm.destroy();
192 | }
193 |
194 | private onValueChanged(data) {
195 | if (!this._gridReady) {
196 | return;
197 | }
198 |
199 | this.dispatchEvent(new CustomEvent('valueChanged', { detail: data }));
200 | }
201 | }
202 | customElements.define('property-grid', PropertyGrid);
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Property Grid
2 |
3 | - [Overview](#Overview)
4 | - [Installation](#Installation)
5 | - [Usage](#Usage)
6 | - [With Typescript](#with-typescript)
7 | - [With JavaScript](#with-javascript)
8 | - [Advanced](#advanced)
9 | - [API](#API)
10 | - [Properties](#Properties)
11 | - [Methods](#Methods)
12 | - [Events](#Events)
13 | - [Hooks](#Hooks)
14 | - [Styling](#Styling)
15 |
16 | ## Overview
17 |
18 | A small and simple property grid, written in pure Vanilla JavaScript (web components), inspired by [jqPropertyGrid](https://github.com/ValYouW/jqPropertyGrid).
19 |
20 | With this PropertyGrid control you can pass object and allow users to edit the properties with the data type-specific editors. The component offers the ability to group and sort its items, use custom editors per data type, work with and without configuration...etc.
21 |
22 | 
23 |
24 | ## Installation
25 |
26 | Install with npm:
27 |
28 | `npm i clean-web-ui-property-grid --save`
29 |
30 | ## Usage
31 |
32 | ### With Typescript
33 |
34 | If you are using typescript:
35 | ```JS
36 | // In your main file
37 | import 'clean-web-ui-property-grid';
38 |
39 | const pgOptions = {
40 | hasGroups: true,
41 | propertySort: true,
42 | toolbarVisible: true,
43 | } as PropertyGridOptions;
44 |
45 | const pgData = {
46 | filter: true,
47 | filterSize: 200,
48 | };
49 |
50 | const pg1: PropertyGrid = document.getElementById('pg1') as PropertyGrid;
51 | pg1.options = pgOptions;
52 | pg1.selectedObject = pgData;
53 |
54 | // To listen for value changes
55 | pg1.addEventListener('valueChanged', (v) => console.log(v));
56 |
57 | // To get values from property grid
58 | document.querySelector('#getValuesBtn').addEventListener('click', () => {
59 | console.log(pg1.getValues());
60 | });
61 |
62 | ```
63 |
64 | Html markup
65 | ```HTML
66 |
67 | ```
68 |
69 | ### With JavaScript
70 |
71 | With JavaScript, just import the script in the html body:
72 |
73 | ``
74 |
75 |
76 | You can use by inserting web component in html:
77 |
78 | ```html
79 |
80 | ```
81 |
82 | With JS:
83 |
84 | ```JS
85 | const pg1 = document.getElementById('pg1');
86 | pg1.selectedObject = pgData;
87 | ```
88 |
89 | ### Advanced
90 |
91 | More complex example would be to pass config and options to the grid.
92 |
93 | In HTML:
94 |
95 | ```html
96 |
97 | ```
98 |
99 | In Js/Typescript:
100 |
101 | ```JS
102 | //If using Typescript
103 | import {createPropertyGrid} from 'clean-web-ui-property-grid';
104 |
105 | var pgOptions = {
106 | hasGroups: true,
107 | propertySort: true
108 | };
109 |
110 | var pgConfig = {
111 | filter: { group: 'Behavior', name: 'Filter', type: 'boolean' },
112 | filterSize: {
113 | group: 'Behavior',
114 | name: 'Filter size',
115 | type: 'number',
116 | options: { min: 0, max: 500, step: 10 },
117 | }
118 | };
119 |
120 | var pgData = {
121 | filter: true,
122 | filterSize: 200
123 | };
124 |
125 | function createPropertyGrid(id, config, options, data) {
126 | var propertyGridWrapper = document.getElementById(id);
127 | let propertyGrid = document.createElement('property-grid');
128 | propertyGrid.id = 'pg' + id;
129 | propertyGrid.config = config;
130 | propertyGrid.options = options;
131 | propertyGrid.selectedObject = data;
132 | propertyGridWrapper.innerHTML = '';
133 | propertyGridWrapper.appendChild(propertyGrid);
134 | }
135 |
136 | createPropertyGrid('propertyGridWithConfig', pgConfig, pgOptions, pgData);
137 |
138 | // To listen for events emmited from grid
139 | document.getElementById('pgpropertyGridWithConfig').addEventListener('valueChanged', (v) => console.log(v));
140 | ```
141 |
142 | ## API
143 |
144 | ### Properties
145 |
146 | **_config_** - PropertyGridConfig | null
147 | This is the metadata object that describes the target object properties.
148 | Here you can pass and configure how the property grid editors will work like: control type, min/max values...etc.
149 |
150 | ```JS
151 | {
152 | "browsable": boolean // Whether this property should be included in the grid, default is true (can be omitted),
153 | "group": string // The group this property belongs to
154 | "name": string // The display name of the property in the grid
155 | "type": 'text' | 'number' | 'boolean' | 'color' | 'options' | 'label' // The type of the property
156 | "description": string // A description of the property, will be used as tooltip on an hint element (a span with text "[?]")
157 | "showHelp": boolean // If set to false, will disable showing description's span with text "[?]" on property name.
158 | "items": string[] // List of Dropdown Items used for select control
159 | "options": Object // An extra options object per type (attributes per control)
160 | }
161 | ```
162 |
163 | **_options_** - PropertyGridOptions | null
164 | The options that are passed to the property grid control to configure how the grid will work. Here you pass options that configure the grid itself, not the individual controls. You can pass options like: show/hide groups, sort method...etc.
165 |
166 | ```JS
167 | {
168 | "hasGroups": boolean //Gets or sets a value indicating whether controls are groupped and if groups are visible
169 | "propertySort": boolean | Function //If and how to sort properties in the grid. Optionally you can pass callback functiom
170 | "onValueChange": FormControl // Hook that is called before value is changed
171 | "controls": Function // Set custom controls that will be used for rendering
172 | "enabled": boolean // Gets or sets a value indicating whether the control can respond to user interaction.
173 | "toolbarVisible": boolean // Gets or sets a value indicating whether the toolbar is visible.
174 | }
175 | ```
176 |
177 | **_selectedObject_** - Object
178 | The is the object that you want to edit in the grid. If no config is passed to the grid, the grid will try to create config based on object properties and value type.
179 |
180 | **_disabled_** - boolean
181 | Gets or sets a value indicating whether the control can respond to user interaction.
182 |
183 | ### Methods
184 |
185 | **_render()_** - void
186 | Build the config for the grid, render controls on UI and attach event listeners. Called internally by the grid when selectedObject is changed.
187 |
188 | **_getValues()_** - Object
189 | Returns the current values for all properties in the grid.
190 |
191 | **_destroy()_** - void
192 | Destroys all elements and removes all event listners from elements.
193 | Called internally by the grid when the component is about to be destroyed.
194 |
195 | ## Events
196 |
197 | **_valueChanged_** - CustomEvent
198 | Fired when the value is changed for some of controls in the property grid.
199 |
200 | Event payload:
201 |
202 | `{name: 'controlname', value: 'new value'}`
203 |
204 | ## Hooks
205 |
206 | **_onValueChange(event)_** - Object | boolean | void
207 | Hook that is called before value is changed. Usefull if you want to intercept the original valueChange event.
208 |
209 | ## Styling
210 |
211 | Available CSS variables
212 |
213 | ```JS
214 | --property-grid-header-background: #E0ECFF;
215 | --property-grid-header-border: 1px dotted #95B8E7;
216 | --property-grid-table-row-group-background: #E0ECFF;
217 | --property-grid-table-row-group-font-weight: bold;
218 | --property-grid-table-row-hover: #f0f5fd;
219 | --property-grid-cell-border: 1px dotted #ccc;
220 | ```
221 |
--------------------------------------------------------------------------------