diff --git a/app/app.component.ts b/app/app.component.ts deleted file mode 100644 index 00fd09b..0000000 --- a/app/app.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'my-app', - template: ` -

Angular 2 Systemjs start

- ` -}) -export class AppComponent { } \ No newline at end of file diff --git a/app/app.module.ts b/app/app.module.ts deleted file mode 100644 index e096758..0000000 --- a/app/app.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; - -import { AppComponent } from './app.component'; - -@NgModule({ - imports: [ BrowserModule ], - declarations: [ AppComponent ], - bootstrap: [ AppComponent ] -}) -export class AppModule { } \ No newline at end of file diff --git a/index.html b/index.html index 3769dc7..fb4640b 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ Angular 2 QuickStart + @@ -13,13 +14,18 @@ - Loading... + +
+ +
+
+ \ No newline at end of file diff --git a/loader.svg b/loader.svg new file mode 100644 index 0000000..91ad1a0 --- /dev/null +++ b/loader.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 1ea5873..efb939b 100644 --- a/package.json +++ b/package.json @@ -9,26 +9,24 @@ }, "license": "ISC", "dependencies": { - "@angular/common": "4.0.0-rc.3", - "@angular/compiler": "4.0.0-rc.3", - "@angular/core": "4.0.0-rc.3", - "@angular/forms": "4.0.0-rc.3", - "@angular/http": "4.0.0-rc.3", - "@angular/platform-browser": "4.0.0-rc.3", - "@angular/platform-browser-dynamic": "4.0.0-rc.3", - "@angular/router": "4.0.0-rc.3", + "@angular/common": "4.1.0-beta.1", + "@angular/compiler": "4.1.0-beta.1", + "@angular/core": "4.1.0-beta.1", + "@angular/forms": "4.1.0-beta.1", + "@angular/http": "4.1.0-beta.1", + "@angular/platform-browser": "4.1.0-beta.1", + "@angular/platform-browser-dynamic": "4.1.0-beta.1", + "@angular/router": "4.1.0-beta.1", "core-js": "^2.4.1", "rxjs": "^5.0.1", "systemjs": "0.19.41", - "zone.js": "^0.7.8" + "zone.js": "^0.8.4" }, "devDependencies": { "concurrently": "^3.2.0", "lite-server": "^2.2.2", - "@types/node": "^6.0.46", - "rimraf": "^2.5.4", "typescript": "^2.1.6" } diff --git a/src/app-routing.module.ts b/src/app-routing.module.ts new file mode 100644 index 0000000..1b26730 --- /dev/null +++ b/src/app-routing.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { ChipsComponent } from './chips/chips.component'; + +export const routes: Routes = [ + { path: 'chips', component: ChipsComponent }, + { path: '', redirectTo: '/chips', pathMatch: 'full' } +]; + +@NgModule({ + imports: [ + RouterModule.forRoot(routes) + ], + exports: [ + RouterModule + ] +}) +export class AppRoutingModule { } \ No newline at end of file diff --git a/src/app.component.html b/src/app.component.html new file mode 100644 index 0000000..627205e --- /dev/null +++ b/src/app.component.html @@ -0,0 +1,15 @@ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/app.component.ts b/src/app.component.ts new file mode 100644 index 0000000..cf1f250 --- /dev/null +++ b/src/app.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + + +@Component({ + selector: 'my-app', + templateUrl: `./app.component.html` +}) +export class AppComponent { } diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..f7e7be6 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,44 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpModule } from '@angular/http'; +import { LocationStrategy, HashLocationStrategy } from '@angular/common'; + +import { CoreModule } from './core/core.module'; +import { AppRoutingModule } from './app-routing.module'; +import { TreeModule } from './tree/tree.module'; +import { TableModule } from './table/table.module'; +import { EditorModule } from './editor/editor.module'; +import { TemplateDrivenFormsModule } from './template-driven-forms/template-driven-forms.module'; +import { ReactiveDrivenFormsModule } from './reactive-driven-forms/reactive-driven-forms.module'; + +import { AppComponent } from './app.component'; + +import { ChipsComponent } from './chips/chips.component'; +import { PageNotFoundComponent } from './page-not-found.component'; + +@NgModule({ + imports: [ + BrowserModule, + HttpModule, + CoreModule, + AppRoutingModule, + TreeModule, + TableModule, + EditorModule, + TemplateDrivenFormsModule, + ReactiveDrivenFormsModule + ], + declarations: [ + AppComponent, + ChipsComponent, + PageNotFoundComponent + ], + providers: [ + { + provide: LocationStrategy, useClass: HashLocationStrategy + } + ], + bootstrap: [AppComponent] +}) +export class AppModule { +} \ No newline at end of file diff --git a/src/chips/chips.component.html b/src/chips/chips.component.html new file mode 100644 index 0000000..08df03a --- /dev/null +++ b/src/chips/chips.component.html @@ -0,0 +1,20 @@ +

Chips example

+

Type something and press Enter

+
+ + +
+ +
{{ tags | json }}
\ No newline at end of file diff --git a/src/chips/chips.component.ts b/src/chips/chips.component.ts new file mode 100644 index 0000000..ff82a79 --- /dev/null +++ b/src/chips/chips.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + + +@Component({ + selector: 'my-chips', + templateUrl: `./chips.component.html` +}) +export class ChipsComponent { + tags: string[] = []; + + focused: boolean = false; + + onKeydown(tokenInput: HTMLInputElement): void { + let value = tokenInput.value; + if (value && value.trim().length && !this.tags.find(x => x == value)) { + this.tags.push(value); + } + tokenInput.value = ''; + } + + removeTag(i: any) { + this.tags.splice(i, 1); + } +} \ No newline at end of file diff --git a/src/core/core.module.ts b/src/core/core.module.ts new file mode 100644 index 0000000..2bebfc7 --- /dev/null +++ b/src/core/core.module.ts @@ -0,0 +1,17 @@ +import { NgModule, Optional, SkipSelf } from '@angular/core'; + +import { Logger } from './logger'; + +@NgModule({ + providers: [ + Logger + ] +}) +export class CoreModule { + constructor( @Optional() @SkipSelf() parentModule: CoreModule) { + if (parentModule) { + throw new Error( + 'CoreModule is already loaded. Import it in the AppModule only'); + } + } +} \ No newline at end of file diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 0000000..af2a9a0 --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class Logger { + stack: string[] = []; + + log(message: string) { + this.stack.push(message); + } + + clean() { + this.stack = []; + } +} \ No newline at end of file diff --git a/src/editor/editor-layout.component.ts b/src/editor/editor-layout.component.ts new file mode 100644 index 0000000..4085b19 --- /dev/null +++ b/src/editor/editor-layout.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-editor-layout', + template: ` +

WYSIWYG example

+ +

+ {{ content }} +

+ ` +}) +export class EditorLayoutComponent { + content = '

hello

'; +} \ No newline at end of file diff --git a/src/editor/editor.component.html b/src/editor/editor.component.html new file mode 100644 index 0000000..ccb0936 --- /dev/null +++ b/src/editor/editor.component.html @@ -0,0 +1,12 @@ + + +
+
+ +
+
+
\ No newline at end of file diff --git a/src/editor/editor.component.ts b/src/editor/editor.component.ts new file mode 100644 index 0000000..e1d50a1 --- /dev/null +++ b/src/editor/editor.component.ts @@ -0,0 +1,144 @@ +import { + Component, + OnInit, + ElementRef, + ViewChild, + forwardRef,Renderer2 +} from '@angular/core'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; + +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/debounceTime'; + +export const EDITOR_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EditorComponent), + multi: true +}; + +export interface ToolbarButton { + title: string; + command: string; + tag: string; + options?: any; + active?: boolean; +} + +@Component({ + selector: 'wysiwyg-editor', + templateUrl: './editor.component.html', + providers: [EDITOR_VALUE_ACCESSOR] +}) +export class EditorComponent implements OnInit, ControlValueAccessor { + + content: string; + + @ViewChild('editor') container: ElementRef; + + toolbarButtons: ToolbarButton[] = [ + { + title: 'bold', + command: 'bold', + tag: 'b' + }, + { + title: 'italic', + command: 'italic', + tag: 'i' + }, + { + title: 'h1', + command: 'formatBlock', + options: '

', + tag: 'h1' + }, + { + title: 'h2', + command: 'formatBlock', + options: '

', + tag: 'h2' + }, + { + title: 'link', + command: 'createlink', + tag: 'a' + } + ]; + + constructor(private renderer: Renderer2) { } + + editMode: boolean = false; + + subscriptions: Subscription[] = [] + + ngOnInit() { + document.execCommand('defaultParagraphSeparator', false, 'p'); + ['mouseup', 'keydown', 'keyup'].forEach(event => { + this.subscriptions.push(Observable + .fromEvent(this.container.nativeElement, event) + .debounceTime(60) + .subscribe(e => { + this.refreshActiveButtons(); + })); + }); + } + + onContentChanged() { + this.content = this.container.nativeElement.innerHTML; + this.propagateChange(this.content); + } + + refreshActiveButtons() { + const tags = this.getTagsRecursive(document.getSelection().focusNode); + this.toolbarButtons.forEach(x => x.active = tags.indexOf(x.tag.toUpperCase()) > - 1); + } + + getTagsRecursive(element, tags?: any[]) { + tags = tags || (element && element.tagName ? [element.tagName] : []); + + if (element && element.parentNode) { + element = element.parentNode; + } else { + return tags; + } + + const tag = element.tagName; + if (tag === 'DIV') { + return tags; + } + + tags.push(tag); + + return this.getTagsRecursive(element, tags); + } + + onBlur() { + this.toolbarButtons.forEach(x => x.active = false); + } + + onCommandExecuted() { + this.onContentChanged(); + this.refreshActiveButtons() + } + + writeValue(value: any) { + if (value) { + this.content = value; + this.renderer.setProperty(this.container.nativeElement, 'innerHTML', this.content); + } + } + + propagateChange: any = (_: any) => { }; + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched() { } + + ngOnDestroy() { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } +} \ No newline at end of file diff --git a/src/editor/editor.module.ts b/src/editor/editor.module.ts new file mode 100644 index 0000000..02b2323 --- /dev/null +++ b/src/editor/editor.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { EditorComponent } from './editor.component'; +import { ToolbarComponent } from './toolbar.component'; +import { EditorLayoutComponent } from './editor-layout.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + RouterModule.forChild([ + { path: 'editor', component: EditorLayoutComponent } + ]) + ], + declarations: [ + EditorLayoutComponent, + EditorComponent, + ToolbarComponent + ], + exports: [ + EditorComponent + ] +}) +export class EditorModule { } \ No newline at end of file diff --git a/src/editor/toolbar.component.html b/src/editor/toolbar.component.html new file mode 100644 index 0000000..31cdb28 --- /dev/null +++ b/src/editor/toolbar.component.html @@ -0,0 +1,14 @@ +
+
+ +
+
+ +
+ +
\ No newline at end of file diff --git a/src/editor/toolbar.component.ts b/src/editor/toolbar.component.ts new file mode 100644 index 0000000..d7ed4de --- /dev/null +++ b/src/editor/toolbar.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'wysiwyg-toolbar', + templateUrl: './toolbar.component.html' +}) +export class ToolbarComponent { + + @Input() buttons: any[]; + + @Input() editMode: boolean; + + @Output() editModeChange: EventEmitter = new EventEmitter(); + + @Output() commandExecuted: EventEmitter = new EventEmitter(); + + execCommand(command: string, options: string) { + if (this.editMode) { + return false; + } + + if (command === 'createlink') { + options = window.prompt('Please enter the URL', 'http://'); + if (!options) { + return; + } + } + + let selection = document.getSelection().toString(); + + if (command === 'createlink' && selection === '') { + document.execCommand('insertHtml', false, '' + options + ''); + } + else { + document.execCommand(command, false, options); + } + + this.commandExecuted.emit(); + } + + + isActive(command) { + return !!command && document.queryCommandState(command); + } + + toggleEditMode() { + this.editMode = !this.editMode; + this.editModeChange.emit(this.editMode); + } + +} \ No newline at end of file diff --git a/app/main.ts b/src/main.ts similarity index 65% rename from app/main.ts rename to src/main.ts index 3d4db6d..bbb697a 100644 --- a/app/main.ts +++ b/src/main.ts @@ -2,4 +2,4 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; -platformBrowserDynamic().bootstrapModule(AppModule); \ No newline at end of file +platformBrowserDynamic().bootstrapModule(AppModule).then(); \ No newline at end of file diff --git a/src/page-not-found.component.ts b/src/page-not-found.component.ts new file mode 100644 index 0000000..37ec218 --- /dev/null +++ b/src/page-not-found.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + + +@Component({ + selector: 'my-page-not-found', + template: 'my-page-not-found' +}) +export class PageNotFoundComponent {} \ No newline at end of file diff --git a/src/reactive-driven-forms/dynamic-forms/components/form-select.component.ts b/src/reactive-driven-forms/dynamic-forms/components/form-select.component.ts new file mode 100644 index 0000000..09855a6 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/components/form-select.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { FormBaseControlComponent } from '../directives/form-base.component'; + +@Component({ + selector: 'form-text', + template: ` +
+ +
+ +
+
+ `, + host: { + '[class]': 'config.css.container' + } +}) +export class FormSelectComponent extends FormBaseControlComponent { + +} diff --git a/src/reactive-driven-forms/dynamic-forms/components/form-text.component.ts b/src/reactive-driven-forms/dynamic-forms/components/form-text.component.ts new file mode 100644 index 0000000..f02b7c5 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/components/form-text.component.ts @@ -0,0 +1,21 @@ +import { Component, HostBinding } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { FormBaseControlComponent } from '../directives/form-base.component'; + +@Component({ + selector: 'form-text', + template: ` +
+ +
+ +
+
+ `, + host: { + '[class]': 'config.css.container' + } +}) +export class FormTextComponent extends FormBaseControlComponent { +} diff --git a/src/reactive-driven-forms/dynamic-forms/directives/dynamic-control.directive.ts b/src/reactive-driven-forms/dynamic-forms/directives/dynamic-control.directive.ts new file mode 100644 index 0000000..658ddc6 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/directives/dynamic-control.directive.ts @@ -0,0 +1,67 @@ +import { + Directive, + Input, + OnChanges, + OnDestroy, + ViewContainerRef, + Type, + ComponentRef, + OnInit, + ComponentFactoryResolver +} from '@angular/core'; + +import { FormGroup } from '@angular/forms'; + +import { Control } from '../models/control.interface'; +import { FormControlModel } from '../models/form-control.model'; + + +import { FormTextComponent } from '../components/form-text.component'; +import { FormSelectComponent } from '../components/form-select.component'; + +const components: {[type: string]: Type} = { + text: FormTextComponent, + select: FormSelectComponent +}; + + +@Directive({ + selector: '[dynamicControl]' +}) +export class DynamicControlDirective implements Control, OnChanges, OnInit, OnDestroy { + + @Input() config: FormControlModel; + + @Input() group: FormGroup; + + component: ComponentRef; + + constructor(private resolver: ComponentFactoryResolver, private vcRef: ViewContainerRef) {} + + ngOnChanges() { + if (this.component) { + this.component.instance.config = this.config; + this.component.instance.group = this.group; + } + } + + ngOnInit() { + if (!components[this.config.type]) { + const supportedTypes = Object.keys(components).join(', '); + throw new Error( + `Trying to use an unsupported type (${this.config.type}). + Supported types: ${supportedTypes}` + ); + } + const component = this.resolver.resolveComponentFactory(components[this.config.type]); + this.component = this.vcRef.createComponent(component); + this.component.instance.config = this.config; + this.component.instance.group = this.group; + } + + ngOnDestroy() { + if (this.component) { + this.component.destroy(); + } + } +} diff --git a/src/reactive-driven-forms/dynamic-forms/directives/dynamic-form.component.ts b/src/reactive-driven-forms/dynamic-forms/directives/dynamic-form.component.ts new file mode 100644 index 0000000..fa658d5 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/directives/dynamic-form.component.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { DynamicFormService } from '../services/dynamic-form.service'; +import { FormControlModel } from '../models/form-control.model'; +//
{{ form.getRawValue() | json }}
+@Component({ + selector: 'dynamic-form', + template: ` +
+ +
{{ config | json}}
+
+ ` +}) +export class DynamicFormComponent { + + @Input() config: FormControlModel[]; + + form: FormGroup; + + constructor(private formService: DynamicFormService) {} + + ngOnInit() { + this.form = this.formService.buildFormGroup(this.config); + } +} \ No newline at end of file diff --git a/src/reactive-driven-forms/dynamic-forms/directives/form-base.component.ts b/src/reactive-driven-forms/dynamic-forms/directives/form-base.component.ts new file mode 100644 index 0000000..f140f49 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/directives/form-base.component.ts @@ -0,0 +1,63 @@ +import { OnInit, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; +import { Subscription } from 'rxjs/Subscription'; + +import { Control } from '../models/control.interface'; +import { FormControlModel } from '../models/form-control.model'; +import { FormValueControlModel, FormControlValue } from '../models/form-value-control.model'; + +export abstract class FormBaseControlComponent implements Control, OnInit, OnDestroy { + group: FormGroup; + + config: FormControlModel; + + control: FormControl; + + private _subscriptions: Subscription[] = []; + + ngOnInit() { + this.control = this.group.get(this.config.key) as FormControl; + + this._subscribeToValueChanges(); + this._setUpConditions(); + } + + _subscribeToValueChanges(): void { + if (!(this.config instanceof FormValueControlModel)) { + return; + } + + this._subscriptions.push(this.control.valueChanges.subscribe((value: FormControlValue) => { + let model = this.config as FormValueControlModel; + + if (model.value !== value) { + model.value = value; + } + })); + } + + _setUpConditions() { + if(!this.config.showWhen) { + return; + } + + let relatedControl = this.group.get(this.config.showWhen.key); + if(!relatedControl) { + return; + } + + this._updateDisabled(); + this._subscriptions.push(relatedControl.valueChanges.subscribe(x => this._updateDisabled())); + } + + _updateDisabled(): void { + let relatedControl = this.group.get(this.config.showWhen.key); + const disabled = relatedControl.value !== this.config.showWhen.value; + this.config.disabledUpdates.next(disabled); + disabled ? this.control.disable() : this.control.enable(); + } + + ngOnDestroy(): void { + this._subscriptions.forEach(x => x.unsubscribe()); + } +} \ No newline at end of file diff --git a/src/reactive-driven-forms/dynamic-forms/dynamic-form.module.ts b/src/reactive-driven-forms/dynamic-forms/dynamic-form.module.ts new file mode 100644 index 0000000..53b54ab --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/dynamic-form.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { DynamicFormComponent } from './directives/dynamic-form.component'; +import { DynamicControlDirective } from './directives/dynamic-control.directive'; + + +import { FormTextComponent } from './components/form-text.component'; +import { FormSelectComponent } from './components/form-select.component'; + +import { DynamicFormService } from './services/dynamic-form.service'; + + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule + ], + declarations: [ + DynamicFormComponent, + DynamicControlDirective, + + FormTextComponent, + FormSelectComponent + ], + exports: [ + DynamicFormComponent, + ReactiveFormsModule + ], + entryComponents: [ + FormTextComponent, + FormSelectComponent + ], + providers: [DynamicFormService] +}) +export class DynamicFormModule { } diff --git a/src/reactive-driven-forms/dynamic-forms/models/control.interface.ts b/src/reactive-driven-forms/dynamic-forms/models/control.interface.ts new file mode 100644 index 0000000..a954bb4 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/control.interface.ts @@ -0,0 +1,7 @@ +import { FormGroup } from '@angular/forms'; +import { FormControlModel } from './form-control.model'; + +export interface Control { + config: FormControlModel, + group: FormGroup +} diff --git a/src/reactive-driven-forms/dynamic-forms/models/control.types.ts b/src/reactive-driven-forms/dynamic-forms/models/control.types.ts new file mode 100644 index 0000000..b7af1db --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/control.types.ts @@ -0,0 +1,8 @@ +export const CONTROL_TYPES = { + ARRAY: 'array', + GROUP: 'group', + TEXT: 'text', + SELECT: 'select', + CHECKBOX: 'checkbox', + RADIO: 'radio' +} diff --git a/src/reactive-driven-forms/dynamic-forms/models/form-control.model.ts b/src/reactive-driven-forms/dynamic-forms/models/form-control.model.ts new file mode 100644 index 0000000..3454e41 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/form-control.model.ts @@ -0,0 +1,61 @@ +import { Subject } from 'rxjs/Subject'; +import { CONTROL_TYPES } from './control.types'; + +import { serializable, serialize } from '../services/serializable.decorator'; + +export class FormControlCondition { + key: string; + value: string; +} + +export interface CssClassConfig { + container?: string; + + control?: string; + + label?: string; +} + +export interface FormControlModelConfig { + key: string; + + label?: string; + + disabled?: boolean; + + showWhen?: FormControlCondition; + + css?: CssClassConfig +} + + +export abstract class FormControlModel { + + @serializable() key: string; + + abstract readonly type: string; + + @serializable() label?: string; + + @serializable() disabled: boolean; + + @serializable() showWhen: FormControlCondition; + + @serializable() css: CssClassConfig; + + disabledUpdates: Subject = new Subject();; + + constructor(config: FormControlModelConfig) { + this.key = config.key; + this.label = config.label || config.key; + this.css = config.css || { container: 'col-1-1'}; + this.disabled = this.disabled || false; + this.showWhen = config.showWhen; + this.disabledUpdates.subscribe((value: boolean) => this.disabled = value); + } + + toJSON() { + return serialize(this); + } +} + diff --git a/src/reactive-driven-forms/dynamic-forms/models/form-group.model.ts b/src/reactive-driven-forms/dynamic-forms/models/form-group.model.ts new file mode 100644 index 0000000..9166a70 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/form-group.model.ts @@ -0,0 +1,22 @@ +import { FormControlModel, FormControlModelConfig } from './form-control.model'; +import { CONTROL_TYPES } from './control.types'; + +import { serializable } from '../services/serializable.decorator'; + +export interface FormGroupModelConfig extends FormControlModelConfig { + controls: FormControlModel[]; +} + + +export class FormGroupModel extends FormControlModel { + @serializable() controls: FormControlModel[]; + + @serializable() type = CONTROL_TYPES.GROUP; + + constructor(private config: FormGroupModelConfig) { + super(config); + + this.controls = config.controls; + } +} + diff --git a/src/reactive-driven-forms/dynamic-forms/models/form-select.model.ts b/src/reactive-driven-forms/dynamic-forms/models/form-select.model.ts new file mode 100644 index 0000000..779c9f5 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/form-select.model.ts @@ -0,0 +1,20 @@ +import { FormValueControlModel, FormValueControlModelConfig } from './form-value-control.model'; +import { CONTROL_TYPES } from "./control.types"; +import { serializable } from '../services/serializable.decorator'; + +export interface FormGroupModelConfig extends FormValueControlModelConfig { + options: any[]; +} + + +export class FormSelectModel extends FormValueControlModel { + @serializable() type = CONTROL_TYPES.SELECT; + + @serializable() options: any[]; + + constructor(private config: FormGroupModelConfig) { + super(config); + this.options = config.options; + } +} + diff --git a/src/reactive-driven-forms/dynamic-forms/models/form-text.model.ts b/src/reactive-driven-forms/dynamic-forms/models/form-text.model.ts new file mode 100644 index 0000000..ff6f70f --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/form-text.model.ts @@ -0,0 +1,12 @@ +import { FormValueControlModel, FormValueControlModelConfig } from './form-value-control.model'; +import { CONTROL_TYPES } from './control.types'; +import { serializable } from '../services/serializable.decorator'; + +export class FormTextModel extends FormValueControlModel { + @serializable() type = CONTROL_TYPES.TEXT; + + constructor(private config: FormValueControlModelConfig) { + super(config); + } +} + diff --git a/src/reactive-driven-forms/dynamic-forms/models/form-value-control.model.ts b/src/reactive-driven-forms/dynamic-forms/models/form-value-control.model.ts new file mode 100644 index 0000000..4762586 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/models/form-value-control.model.ts @@ -0,0 +1,21 @@ +import { FormControlModel, FormControlModelConfig } from './form-control.model'; +import { CONTROL_TYPES } from './control.types'; +import { serializable } from '../services/serializable.decorator'; + +export type FormControlValue = boolean | number | string | Array; + +export interface FormValueControlModelConfig extends FormControlModelConfig { + value?: T; +} + + +export abstract class FormValueControlModel extends FormControlModel { + + @serializable() value?: T | null; + + constructor(config: FormValueControlModelConfig) { + super(config); + this.value = config.value; + } + +} diff --git a/src/reactive-driven-forms/dynamic-forms/services/dynamic-form.service.ts b/src/reactive-driven-forms/dynamic-forms/services/dynamic-form.service.ts new file mode 100644 index 0000000..b661a37 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/services/dynamic-form.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { AbstractControl, FormControl, FormBuilder, FormGroup, FormArray } from '@angular/forms'; + +import { CONTROL_TYPES } from '../models/control.types'; + +import { FormControlModel } from '../models/form-control.model'; +import { FormValueControlModel, FormControlValue } from '../models/form-value-control.model'; +import { FormGroupModel } from '../models/form-group.model'; + +@Injectable() +export class DynamicFormService { + + constructor(private formBuilder: FormBuilder) { } + + + buildFormGroup(controls: FormControlModel[]): FormGroup { + + let formGroup: { [id: string]: AbstractControl; } = {}; + + controls.forEach(model => { + switch (model.type) { + case CONTROL_TYPES.GROUP: + formGroup[model.key] = this.buildFormGroup((model as FormGroupModel).controls); + break; + default: + formGroup[model.key] = new FormControl({ + value: (model as FormValueControlModel).value, + disabled: model.disabled + }); + } + }); + + return this.formBuilder.group(formGroup); + } +} diff --git a/src/reactive-driven-forms/dynamic-forms/services/serializable.decorator.ts b/src/reactive-driven-forms/dynamic-forms/services/serializable.decorator.ts new file mode 100644 index 0000000..425ed86 --- /dev/null +++ b/src/reactive-driven-forms/dynamic-forms/services/serializable.decorator.ts @@ -0,0 +1,40 @@ +declare let Reflect: any; + +export const METADATA_KEY_SERIALIZABLE = "SERIALIZABLE"; + +export interface SerializableProperty { + key: string; + name: string; +} + +export function serializable(name?: string): (target: any, key: string) => void { + + return function (target, key) { + Reflect.defineMetadata(METADATA_KEY_SERIALIZABLE, { key: key, name: name || key }, target, key); + }; +} + +export function getSerializables(target: any): SerializableProperty[] { + let serializables = []; + + for (let key in target) { + + let metadata = Reflect.getMetadata(METADATA_KEY_SERIALIZABLE, target, key); + + if (metadata) { + serializables.push(metadata); + } + } + + return serializables; +} + +export function serialize(target: any, prototype?: any): Object { + return getSerializables(prototype || target).reduce((prev: any, prop: SerializableProperty) => { + + prev[prop.name] = target[prop.key]; + + return prev; + + }, {}); +} \ No newline at end of file diff --git a/src/reactive-driven-forms/reactive-driven-forms.component.ts b/src/reactive-driven-forms/reactive-driven-forms.component.ts new file mode 100644 index 0000000..66ac7c8 --- /dev/null +++ b/src/reactive-driven-forms/reactive-driven-forms.component.ts @@ -0,0 +1,73 @@ +import { Component } from '@angular/core'; + +import { FormControlModel } from './dynamic-forms/models/form-control.model'; +import { FormTextModel } from './dynamic-forms/models/form-text.model'; +import { FormSelectModel } from './dynamic-forms/models/form-select.model'; + +let model = []; + +model.push(new FormTextModel( + { + key: 'hello', + label: 'Name', + value: '', + showWhen: { + key: 'name4', + value: 'Hello' + } + } +)) +for (var i = 1, len = 9; i <= len; i++) { + model.push(new FormTextModel( + { + key: 'name' + i, + label: 'Field ' + i, + value: 'value' + i, + css: { + container: 'col-1-3' + } + } + )) +} + +for (var i = 1, len = 10; i <= len; i++) { + model.push(new FormSelectModel( + { + key: 'select' + i, + label: 'Select ' + i, + options: ['A', 'B', 'C'], + value: 'A', + showWhen: { + key: i > 1 ? 'select' + (i - 1) : '', + value: 'B' + }, + css: { + container: 'col-1-5' + } + } + )) +} + +for (var i = 1, len = 4; i <= len; i++) { + model.push(new FormTextModel( + { + key: 'name' + i, + label: 'Field ' + i, + value: 'value' + i, + css: { + container: 'col-1-2' + } + } + )) +} + +@Component({ + selector: 'my-reactive-driven-forms', + template: ` +

Dynamic forms

+ + `, +}) +export class ReactiveDrivenFormsComponent { + model: FormControlModel[] = model; +} \ No newline at end of file diff --git a/src/reactive-driven-forms/reactive-driven-forms.module.ts b/src/reactive-driven-forms/reactive-driven-forms.module.ts new file mode 100644 index 0000000..934ba4f --- /dev/null +++ b/src/reactive-driven-forms/reactive-driven-forms.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { ReactiveDrivenFormsComponent } from './reactive-driven-forms.component'; +import { DynamicFormModule } from './dynamic-forms/dynamic-form.module'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([{ + path: 'reactive-driven-forms', component: ReactiveDrivenFormsComponent + }]), + DynamicFormModule + ], + declarations: [ + ReactiveDrivenFormsComponent + ] +}) +export class ReactiveDrivenFormsModule { } \ No newline at end of file diff --git a/src/shared/pipes/pager.pipe.ts b/src/shared/pipes/pager.pipe.ts new file mode 100644 index 0000000..00d2ee6 --- /dev/null +++ b/src/shared/pipes/pager.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'pager' +}) +export class PagerPipe implements PipeTransform { + transform(arr: any[], page: number, pageSize: number) { + if(!arr) { + return []; + } + --page; + return arr.slice(page * pageSize, (page + 1) * pageSize); + } +} \ No newline at end of file diff --git a/src/shared/shared.module.ts b/src/shared/shared.module.ts new file mode 100644 index 0000000..c05d8d5 --- /dev/null +++ b/src/shared/shared.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; + +import { PagerPipe } from './pipes/pager.pipe'; + +@NgModule({ + declarations: [ + PagerPipe + ], + exports: [ + PagerPipe + ] +}) +export class SharedModule {} \ No newline at end of file diff --git a/src/table/components/column.component.ts b/src/table/components/column.component.ts new file mode 100644 index 0000000..0259f7e --- /dev/null +++ b/src/table/components/column.component.ts @@ -0,0 +1,23 @@ +import { Component, TemplateRef, ContentChild, Input } from '@angular/core'; +import { DataTableComponent } from './datatable.component'; + +@Component({ + selector: 'column', + template: `` +}) +export class ColumnComponent { + + @Input() header: string; + + @Input() value: string; + + @Input() editable: boolean; + + @ContentChild('tableHeaderTemplate') headerTemplate: TemplateRef; + + @ContentChild('tableBodyTemplate') bodyTemplate: TemplateRef; + + constructor(public table: DataTableComponent) { + table.addColumn(this); + } +} \ No newline at end of file diff --git a/src/table/components/datatable.component.html b/src/table/components/datatable.component.html new file mode 100644 index 0000000..13af0b0 --- /dev/null +++ b/src/table/components/datatable.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + +
+ + + {{column.header}} + +
+
+ + + {{row[column.value]}} + +
+
+ +
+
\ No newline at end of file diff --git a/src/table/components/datatable.component.ts b/src/table/components/datatable.component.ts new file mode 100644 index 0000000..a94ef59 --- /dev/null +++ b/src/table/components/datatable.component.ts @@ -0,0 +1,33 @@ +import { Component, Input } from '@angular/core'; +import { ColumnComponent } from './column.component'; + +@Component({ + selector: 'datatable', + templateUrl: `./datatable.component.html`, + host: { + class: 'scroll' + } +}) +export class DataTableComponent { + + @Input() dataset: any; + + columns: ColumnComponent[] = []; + + editingCell: any; + + addColumn(column) { + this.columns.push(column); + } + + toggleEditing(cell: HTMLElement, column: ColumnComponent) { + if(column.editable) { + this.editingCell = cell; + setTimeout(() => { + cell.querySelector('input').focus(); + }, 100); + } + + } + +} \ No newline at end of file diff --git a/src/table/components/pagination.component.html b/src/table/components/pagination.component.html new file mode 100644 index 0000000..fd62bf6 --- /dev/null +++ b/src/table/components/pagination.component.html @@ -0,0 +1,30 @@ +
    +
  • + First +
  • + +
  • + Previous +
  • + +
  • + {{page}} +
  • + +
  • + Next +
  • + +
  • + Last +
  • +
  • + Items per page + +
  • +
  • + Page {{ currentPage }} of {{ total }} +
  • +
\ No newline at end of file diff --git a/src/table/components/pagination.component.ts b/src/table/components/pagination.component.ts new file mode 100644 index 0000000..578c3c6 --- /dev/null +++ b/src/table/components/pagination.component.ts @@ -0,0 +1,92 @@ +import { Component, OnChanges, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'my-pagination', + templateUrl: `./pagination.component.html` +}) +export class PaginationComponent implements OnChanges { + + @Input() pageSize: number; + + @Input() total: number; + + @Input() maxSize:number = 3; + + @Input() boundaryLinks:boolean = true; + + @Input() directionLinks:boolean = true; + + @Output() currentPageChange:EventEmitter = new EventEmitter(); + + @Output() pageSizeChange:EventEmitter = new EventEmitter(); + + @Input() + public set currentPage(value) { + const _previous = this._currentPage; + this._currentPage = (value > this.totalPages) ? this.totalPages : (value || 1); + + if (_previous === this._currentPage || typeof _previous === 'undefined') { + return; + } + + this.currentPageChange.emit(this._currentPage); + } + + public get currentPage() { + return this._currentPage; + } + + totalPages: number; + + pages: Array; + + private _currentPage:number; + + ngOnChanges() { + this.totalPages = this.calculateTotalPages(); + this.pages = this.getPages(this.totalPages); + } + + pageSizeChanged(val) { + this.pageSizeChange.emit(val); + } + + private selectPage(page:number, event?: MouseEvent) { + if (event) { + event.preventDefault(); + } + + this.currentPage = page; + this.pages = this.getPages(this.totalPages); + } + + private getPages(totalPages:number):Array { + let pages:any[] = []; + + let startPage = 1; + let endPage = totalPages; + let isMaxSized = this.maxSize && this.maxSize < totalPages; + + if (isMaxSized) { + startPage = Math.max(this.currentPage - Math.floor(this.maxSize / 2), 1); + endPage = startPage + this.maxSize - 1; + + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - this.maxSize + 1; + } + } + + for (let number = startPage; number <= endPage; number++) { + pages.push(number); + } + + return pages; + } + + private calculateTotalPages():number { + let totalPages = this.pageSize < 1 ? 1 : Math.ceil(this.total / this.pageSize); + return Math.max(totalPages || 0, 1); + } + +} diff --git a/src/table/components/table-layout.component.html b/src/table/components/table-layout.component.html new file mode 100644 index 0000000..80a565e --- /dev/null +++ b/src/table/components/table-layout.component.html @@ -0,0 +1,23 @@ +

A Table of Data2

+ + + + + {{ column.header }} + + + + +
+ +
+
+
+ + + + + +
+ diff --git a/src/table/components/table-layout.component.ts b/src/table/components/table-layout.component.ts new file mode 100644 index 0000000..2655eea --- /dev/null +++ b/src/table/components/table-layout.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; + + +import { Photo } from '../models/photo'; +import { PhotoService } from '../services/photos.service'; + + +@Component({ + selector: 'my-table', + templateUrl: `./table-layout.component.html`, + providers: [PhotoService] +}) +export class TableLayoutComponent { + photoData: Photo[]; + + currentPage: number = 1; + + pageSize: number = 3; + + constructor(private photoService: PhotoService) { + + } + + ngOnInit() { + this.photoService.getPhotos().subscribe(data => { + this.photoData = data; + }); + } + + remove(id) { + this.photoData = this.photoData.filter(x => x.id != id); + } +} \ No newline at end of file diff --git a/src/table/models/photo.ts b/src/table/models/photo.ts new file mode 100644 index 0000000..7c999ad --- /dev/null +++ b/src/table/models/photo.ts @@ -0,0 +1,8 @@ + +export interface Photo { + id: number; + + title: string; + + url: string; +} \ No newline at end of file diff --git a/src/table/models/photos.json b/src/table/models/photos.json new file mode 100644 index 0000000..5a79444 --- /dev/null +++ b/src/table/models/photos.json @@ -0,0 +1,36 @@ +[ + { + "id": "1", + "title": "Title1", + "url": "/gallery/oil/portret-admirala.jpg" + }, + { + "id": "2", + "title": "Title2", + "url": "/gallery/oil/zadum4ivaja-devushka.jpg" + }, { + "id": "3", + "title": "Title3", + "url": "/gallery/CHerno_belyj_portret_luchnika.jpg" + }, + { + "id": "4", + "title": "Title4", + "url": "/gallery/oil/portret-muzchini.jpg" + }, + { + "id": "5", + "title": "Title5", + "url": "/gallery/Monohromnyj_portret_maslom.jpg" + }, + { + "id": "6", + "title": "Title6", + "url": "/gallery/pencil/portret-devushki-v-shljape-karandashom.jpg" + }, + { + "id": "7", + "title": "Title7", + "url": "/gallery/pencil/portret-devushki-s-ogolennim-plechom.jpg" + } +] \ No newline at end of file diff --git a/src/table/services/photos.service.ts b/src/table/services/photos.service.ts new file mode 100644 index 0000000..fe39e93 --- /dev/null +++ b/src/table/services/photos.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; + +import { Photo } from '../models/photo'; + +@Injectable() +export class PhotoService { + + constructor(private http: Http) { } + + getPhotos(): Observable { + return this.http.get('src/table/models/photos.json') + .map((res: Response) => { + return res.json() || []; + }) + } + +} \ No newline at end of file diff --git a/src/table/table-routing.module.ts b/src/table/table-routing.module.ts new file mode 100644 index 0000000..49702e6 --- /dev/null +++ b/src/table/table-routing.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TableLayoutComponent } from './components/table-layout.component'; + + +export const routes: Routes = [ + { path: 'table', component: TableLayoutComponent } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class TableRoutingModule { } \ No newline at end of file diff --git a/src/table/table.module.ts b/src/table/table.module.ts new file mode 100644 index 0000000..54edd96 --- /dev/null +++ b/src/table/table.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TableRoutingModule } from './table-routing.module'; +import { SharedModule } from '../shared/shared.module'; + +import { TableLayoutComponent } from './components/table-layout.component'; +import { DataTableComponent } from './components/datatable.component'; +import { ColumnComponent } from './components/column.component'; +import { PaginationComponent } from './components/pagination.component'; + + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + TableRoutingModule + ], + declarations: [ + TableLayoutComponent, + DataTableComponent, + ColumnComponent, + PaginationComponent + ] +}) +export class TableModule { } \ No newline at end of file diff --git a/src/template-driven-forms/custom-controls/my-input.component.ts b/src/template-driven-forms/custom-controls/my-input.component.ts new file mode 100644 index 0000000..39ea3ba --- /dev/null +++ b/src/template-driven-forms/custom-controls/my-input.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'my-custom-input', + template: ` + + ` +}) +export class MyInputComponent { + + @Input() value: number = 0; + + @Output() valueChange: EventEmitter = new EventEmitter() + + onChange(event) { + this.valueChange.emit(this.value); + } + +} \ No newline at end of file diff --git a/src/template-driven-forms/models/address.ts b/src/template-driven-forms/models/address.ts new file mode 100644 index 0000000..0fecc87 --- /dev/null +++ b/src/template-driven-forms/models/address.ts @@ -0,0 +1,5 @@ +export class Address { + street: string; + zipCode: number; + country: string; +} \ No newline at end of file diff --git a/src/template-driven-forms/models/index.ts b/src/template-driven-forms/models/index.ts new file mode 100644 index 0000000..a0df6d5 --- /dev/null +++ b/src/template-driven-forms/models/index.ts @@ -0,0 +1,2 @@ +export * from './address'; +export * from './user'; \ No newline at end of file diff --git a/src/template-driven-forms/models/user.ts b/src/template-driven-forms/models/user.ts new file mode 100644 index 0000000..661fb55 --- /dev/null +++ b/src/template-driven-forms/models/user.ts @@ -0,0 +1,6 @@ +import { Address } from './address'; + +export class User { + name: string; + address: Address; +} diff --git a/src/template-driven-forms/template-driven-forms.component.html b/src/template-driven-forms/template-driven-forms.component.html new file mode 100644 index 0000000..22adbb6 --- /dev/null +++ b/src/template-driven-forms/template-driven-forms.component.html @@ -0,0 +1,48 @@ +

Add user

+
+
+ + + + Name is required (minimum 5 characters). + +
+
+ Address +
+ + + + Country is required. + +
+ +
+ + + + Street is required. + +
+
+ + +
+
+ + +
+ +
+
+

Form valid: {{form.valid | json}}

+ +

Form value:

+
{{form.value | json}}
+ +

User value:

+
{{user | json}}
+
\ No newline at end of file diff --git a/src/template-driven-forms/template-driven-forms.component.ts b/src/template-driven-forms/template-driven-forms.component.ts new file mode 100644 index 0000000..5a5a6b5 --- /dev/null +++ b/src/template-driven-forms/template-driven-forms.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { User } from './models' + +@Component({ + selector: 'my-templater-driven-forms', + templateUrl: `src/template-driven-forms/template-driven-forms.component.html` +}) +export class TemplateDrivenFormsComponent { + + public user: User; + + countries: string[] = [ + 'Russian', + 'Argentina', + 'Australia', + 'USA' + ]; + + ngOnInit() { + this.user = { + name: '', + address: { + street: '', + zipCode: 230445, + country: '' + } + }; + } + + save(model: User, isValid: boolean) { + console.log(model, isValid); + } +} diff --git a/src/template-driven-forms/template-driven-forms.module.ts b/src/template-driven-forms/template-driven-forms.module.ts new file mode 100644 index 0000000..154498e --- /dev/null +++ b/src/template-driven-forms/template-driven-forms.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TemplateDrivenFormsComponent } from './template-driven-forms.component'; +import { MyInputComponent } from './custom-controls/my-input.component'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + RouterModule.forChild([{ + path: 'template-driven-forms', component: TemplateDrivenFormsComponent + }]) + ], + declarations: [ + TemplateDrivenFormsComponent, + MyInputComponent + ] +}) +export class TemplateDrivenFormsModule { } \ No newline at end of file diff --git a/src/tree/components.ts b/src/tree/components.ts new file mode 100644 index 0000000..b8ccd90 --- /dev/null +++ b/src/tree/components.ts @@ -0,0 +1,251 @@ +import { Component, ChangeDetectionStrategy, ElementRef, ChangeDetectorRef, NgZone, DoCheck } from '@angular/core'; + +import { toggleClass } from './shared/utils'; +import { Logger } from 'src/core/logger'; + + +export abstract class SomeComponent implements DoCheck { + constructor(private elRef: ElementRef, private zone: NgZone) { } + + ngDoCheck() { + toggleClass(this.elRef, this.zone); + } +} + +@Component({ + selector: 'comp1', + template: ` + 1 +
    +
  • +
  • +
  • +
+ ` +}) +export class Comp1Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp2', + template: ` + 2 +
    +
  • +
  • +
+ ` +}) +export class Comp2Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp3', + template: ` + 3 +
    +
  • +
  • +
+ `, + // changeDetection: ChangeDetectionStrategy.OnPush +}) +export class Comp3Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp4', + template: ` + 4 +
    +
  • +
  • +
  • +
+ ` +}) +export class Comp4Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp5', + template: `5` +}) +export class Comp5Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp6', + template: `6` +}) +export class Comp6Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp7', + template: ` + 7 +
    +
  • +
  • +
+ `, + //changeDetection: ChangeDetectionStrategy.OnPush +}) +export class Comp7Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp8', + template: `8` +}) +export class Comp8Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp9', + template: `9 +
    +
  • +
  • +
+ ` +}) +export class Comp9Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp10', + template: `10` +}) +export class Comp10Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp11', + template: `11` +}) +export class Comp11Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp12', + template: `12` +}) +export class Comp12Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + + +@Component({ + selector: 'comp13', + template: `13` +}) +export class Comp13Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp14', + template: `14` +}) +export class Comp14Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + + +@Component({ + selector: 'comp15', + template: ` + 15 +
    +
  • +
  • +
+ ` +}) +export class Comp15Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +@Component({ + selector: 'comp16', + template: `16` +}) +export class Comp16Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + + +@Component({ + selector: 'comp17', + template: `17` +}) +export class Comp17Component extends SomeComponent { + constructor(elRef: ElementRef, zone: NgZone) { + super(elRef, zone); + } +} + +export const components = [ + Comp1Component, + Comp2Component, + Comp3Component, + Comp4Component, + Comp5Component, + Comp6Component, + Comp7Component, + Comp8Component, + Comp9Component, + Comp10Component, + Comp11Component, + Comp12Component, + Comp13Component, + Comp14Component, + Comp15Component, + Comp16Component, + Comp17Component +]; \ No newline at end of file diff --git a/src/tree/shared/utils.ts b/src/tree/shared/utils.ts new file mode 100644 index 0000000..03310fc --- /dev/null +++ b/src/tree/shared/utils.ts @@ -0,0 +1,35 @@ +import { NgZone, ElementRef } from '@angular/core'; + +let delay = 0; +export function toggleClass(el: ElementRef, zone: NgZone, className = 'checked') { + const a = el.nativeElement.querySelector('span'); + a.classList.add(className); + + zone.runOutsideAngular(() => { + setTimeout(() => { + a.classList.remove(className); + }, 200); + }); +} + +// let delay = 0; +// export function toggleClass(el: ElementRef, zone: NgZone, className = 'checked') { +// const a = el.nativeElement.querySelector('span'); +// // a.classList.add(className); +// let t = delay; +// delay += 150; +// zone.runOutsideAngular(() => { +// setTimeout(() => { + +// a.classList.add(className); +// setTimeout(() => { + +// a.classList.remove(className); +// }, t + 150); +// }, t); +// }); + +// zone.onMicrotaskEmpty.subscribe(x => { +// delay = 0; +// }) +// } \ No newline at end of file diff --git a/src/tree/tree-routing.module.ts b/src/tree/tree-routing.module.ts new file mode 100644 index 0000000..24713df --- /dev/null +++ b/src/tree/tree-routing.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TreeComponent } from "./tree.component"; + + +export const routes: Routes = [ + { path: 'tree', component: TreeComponent } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class TreeRoutingModule { } \ No newline at end of file diff --git a/src/tree/tree.component.ts b/src/tree/tree.component.ts new file mode 100644 index 0000000..ef74bc2 --- /dev/null +++ b/src/tree/tree.component.ts @@ -0,0 +1,17 @@ +import { Component, ViewChild, NgZone, ChangeDetectorRef } from '@angular/core'; + + +@Component({ + selector: 'my-tree', + template: ` +

Some tree

+
    +
  • + +
  • +
+ ` +}) +export class TreeComponent { + +} \ No newline at end of file diff --git a/src/tree/tree.module.ts b/src/tree/tree.module.ts new file mode 100644 index 0000000..3b15e9c --- /dev/null +++ b/src/tree/tree.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TreeRoutingModule } from './tree-routing.module'; + +import { TreeComponent } from './tree.component'; + +import { components } from './components'; + +@NgModule({ + imports: [ + CommonModule, + TreeRoutingModule + ], + declarations: [ + TreeComponent, + ...components + ] +}) +export class TreeModule { } \ No newline at end of file diff --git a/style.css b/style.css index e69de29..6e5b4ca 100644 --- a/style.css +++ b/style.css @@ -0,0 +1,558 @@ +body { + margin: 0; + background: #dedede; + font-family: 'Roboto', sans-serif; +} + +.page { + position: absolute; + top: 20px; + right: 80px; + left: 80px; + bottom: 20px; + background: #fff; + display: flex; + flex-direction: column; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); +} + +.body { + flex: 1; + padding: 20px 30px; +} + +h2 { + margin: 0 0 10px 0; +} + +pre { + background: #4e5054; + color: #fff; + padding: 10px; +} + +.center { + text-align: center; +} +.scroll { + overflow-y: scroll; + height: 100%; +} + +.scroll:hover::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,.2); +} + +.scroll::-webkit-scrollbar-thumb:hover { + background-color: rgba(0,0,0,.3); + +} + +.scroll::-webkit-scrollbar{ + width: 10px; +} + +.scroll::-webkit-scrollbar-thumb { + transition: background-color 2s ease-out; + border-radius:6px; +} + + +nav { + display: flex; + background: #2a2d33; +} + +nav a { + line-height: 57px; + text-decoration: none; + padding: 0 30px; + color: #0C0D0E; + border-top: 3px solid #2a2d33; + color: #fff; +} + +nav a.active { + border-top: 3px solid #F48024; + background: #eff0f1; + color: #242729; +} + +nav a:hover, nav a:focus { + background: #eff0f1; + color: #242729; + border-top: 3px solid #F48024; +} +my-page-not-found { + display: block; + height: 1000px; +} + + + +/* Tree */ + +my-tree { + display: block; + position: relative; +} + +my-tree ul { + padding-top: 20px; + position: relative; + margin: 0 -9px; +} + +my-tree * { + margin: 0; + padding: 0; +} + +my-tree li { + float: left; + text-align: center; + list-style-type: none; + position: relative; + padding: 20px 5px 0 5px; +} + +my-tree li:only-child { + padding-top: 0; +} + +my-tree span { + display: inline-block; + padding: 10px; + background: #4f515a; + color: #fff; + width: 100px; + border-radius: 50%; + line-height: 100px; + cursor: pointer; + z-index: 1; + position: relative; + box-shadow: 0 6px 10px 0 rgba(0,0,0,0.3); +} + +my-tree span.checked { + background: #ff9800; +} + +my-tree span.on-push:before { + content: "onPush"; + position: absolute; + top: 0; + left: 100%; + transform: translateX(-30px); + background: #4caf50; + border-radius: 5px; + line-height: 26px; + padding: 0 5px; +} + + +my-tree li::before, my-tree li::after{ + content: ''; + position: absolute; top: 0; right: 50%; + border-top: 2px solid #c3c1c1; + width: 50%; height: 20px; +} +my-tree li::after{ + right: auto; left: 50%; + border-left: 2px solid #c3c1c1; +} + + +my-tree li:first-child::before, my-tree li:last-child::after{ + border: 0 none; +} + +my-tree li:last-child::before{ + border-right: 2px solid #c3c1c1; + border-radius: 0 5px 0 0; +} +my-tree li:first-child::after{ + border-radius: 5px 0 0 0; +} + +my-tree ul::before{ + content: ''; + position: absolute; top: 0; + left: 50%; + border-left: 2px solid #c3c1c1; + width: 0; + height: 20px; +} + +my-tree > ul:before, +my-tree > ul > li:before { + display: none; +} + +/* Tree end */ + +.log { + position: fixed; + top: 0; + right: 0; +} + +.log-item { + padding: 0 10px; + line-height: 20px; + text-align: center; +} + +.btn { + color: #FFF; + line-height: 40px; + padding: 0 10px; + background-color: #369; + border: none; + cursor: pointer; +} + +.btn-run { + position: absolute; + top: 0; + right: 0; +} + +/* Table */ + +my-table { + display: flex; + flex-direction: column; + height: 100%; +} + +table { + table-layout: fixed; + background: #fff none repeat scroll 0 0; + border-collapse: separate; + border-radius: 5px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + height: 100%; + width: 100%; +} + +thead { + border-radius: 5px; +} +thead th { + background-image: linear-gradient(#636569, #2a2d33); + background-size: 100% auto; + border-top: 1px solid #858d99; + color: #fff; + font-size: 16px; + font-weight: 400; + padding: 20px; + text-align: left; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); +} +thead th:first-child { + border-top-left-radius: 5px; +} +thead th:last-child { + border-top-right-radius: 5px; +} +tbody tr td { + border-bottom: 1px solid #e0e0e0; + padding: 20px; +} +tbody tr:nth-child(2n) { + background: #f0f3f5 none repeat scroll 0 0; +} +tbody tr:last-child td { + border-bottom: medium none; +} +tbody tr:last-child td:first-child { + border-bottom-left-radius: 5px; +} +tbody tr:last-child td:last-child { + border-bottom-right-radius: 5px; +} + +.pagination { + list-style: none; + margin: 0; + padding: 0; +} +.pagination li { + display: inline-block; +} + +.page-link { + display: block; + line-height: 40px; + padding: 0 15px; + background: #2a2d33; + color: #fff; + text-decoration: none; +} + +.page-link.active { + color: #2a2d33; + background: #eff0f1; +} + +.pagination li.disabled .page-link { + background: #b7b4ae; +} + +.pagination-current-page { + float: right; + margin-top: 10px; +} + +.cell-editor { + display: none; +} +.editing .cell-editor { + display: block; +} +.editing .cell-data { + display: none; +} + +/* Forms */ + +.form-group { + margin: 10px 0; +} + +input[type="text"] { + line-height: 24px; + +} + +.form-control { + margin-bottom: 15px; +} + +.form-control label { + margin-right: 10px; +} + +.form-control:last-of-type { + margin-bottom: 0; +} + +.text-danger { + color: red; +} + +/* Reactive forms */ + + +input[type="text"] { + width: 100%; + padding: 0 .5em; + line-height: 40px; + height: 40px; + border: 1px solid #cbcbcb; + box-sizing: border-box; + font-size: 16px; +} + +select { + height: 40px; + line-height: 40px; + width: 100%; +} + +.form-wrapper { + padding: 20px 0 0 20px; + margin: 0 -20px; +} +fieldset.form-wrapper { + margin: 0 0 0 -20px; + border: none; +} + +legend { + border: 1px solid; + display: block; + padding: 5px 10px; + background: #f4f4f4; +} + +[class*='col-1-'] { + display: inline-block; + vertical-align: top; + padding-right: 20px; + margin-bottom: 10px; + box-sizing: border-box; +} + +.col-1-1 { + width: 100%; +} + +.col-1-2 { + width: 50%; +} + +.col-1-3 { + width: 33.33%; +} + +.col-1-4 { + width: 25%; +} + +.col-1-5 { + width: 20%; +} + +.form-control { + display: block; + margin-bottom: 10px; +} +.form-control--label { + display: inline-block; + vertical-align: top; + font-size: 18px; + line-height: 20px; + padding-bottom: 5px; + padding-top: 3px; + color: #4f4f4f; + text-transform: capitalize; + font-weight: bold; +} + + +/* Chips */ + +.control { + display: flex; +} + +.control-label { + line-height: 50px; + font-size: 18px; + font-weight: bold; +} +.chips-list { + list-style: none; + margin: 0 0 0 10px; + padding: 0 5px; + cursor: text; + border: 1px solid #b3b0b0; + flex: 1; +} + +.chips-list.focus { + box-shadow: 0px 0px 5px #51a6de; +} +.chip-input-control { + border: none !important; + width: 10em !important; + line-height: 50px !important; + outline: none !important; + background-color: transparent; + margin: 0; + padding: 0; + height: auto !important; +} + +.chip { + cursor: default; + display: inline-block; + margin-right: 5px; + background: #f1f1f1; + height: 50px; + line-height: 50px; + padding: 0 20px 0 60px; + border-radius: 25px; + position: relative; +} + +.chip__icon { + position: absolute; + left: 0; + top: 0; + background: url('https://www.w3schools.com/howto/img_avatar.png') no-repeat; + background-size: contain; + height: 50px; + width: 50px; + border-radius: 50%; +} + +.chip__close { + padding: 5px 5px 5px 10px; + cursor: pointer; +} + +.chip--input { + display: inline-block; +} + +/* WYSIWYG */ + +wysiwyg-editor { + display: block; +} + +wysiwyg-toolbar { + display: flex; + font-size: 12px; + background: #f0f0f0; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 0 0; +} + +.wysiwyg-toolbar__item { + border-right: 1px solid #DEDEDE; +} + + +.wysiwyg-toolbar__button { + padding: 0 15px; + font-weight: bold; + background: transparent; + border: none; + outline: none; + cursor: pointer; + line-height: 34px; +} + +.wysiwyg-toolbar__button--active { + background: #fff; +} + + + +.wysiwyg-editor__container { + border: 1px solid #CCCCCC; + border-top: none; + border-radius: 0 0 3px 3px; + position: relative; +} + +.wysiwyg-editor__content { + height: 400px; + background: #fff; + cursor: text; + outline: none; + padding: 15px +} + + + +.wysiwyg-editor__src-container { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} + +.wysiwyg-editor__src { + height: 100%; + width: 100%; + resize: none; + padding: 20px 30px; + outline: none; + border: none; +} \ No newline at end of file diff --git a/systemjs.config.js b/systemjs.config.js index 52127a8..0bb63c6 100644 --- a/systemjs.config.js +++ b/systemjs.config.js @@ -21,7 +21,7 @@ '@angular/http': 'npm:@angular/http/bundles/http.umd.js', '@angular/router': 'npm:@angular/router/bundles/router.umd.js', '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', - + // other libraries 'rxjs': 'npm:rxjs' }, @@ -29,10 +29,15 @@ packages: { app: { main: './main.js', - defaultExtension: 'js' + defaultExtension: 'js', + meta: { + './*.js': { + loader: 'systemjs-angular-loader.js' + } + } }, rxjs: { - defaultExtension: 'js' + defaultExtension: 'js' } } }); diff --git a/tsconfig.json b/tsconfig.json index d8d61b2..dbdf841 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "baseUrl": ".", "target": "es5", "module": "commonjs", "moduleResolution": "node",