среда, 25 октября 2017 г.

Компонента Angular \ Angular2

Введение

Данная статься скорее для себя, здесь не будет ничего нового чего бы вы не смогли найти в интернете. Я заметил что когда пишу новую компоненту мне легче начинать с какого-то шаблона. В качестве шаблона я выбрал компоненту - текстовое поле с иконкой поиска и вызовом callback, которую писал сам.

Код

AngularJs
//fastSearch.tmpl.html


<div class="fast-search">
    <input type="text"
           class="fast-search__input"
           placeholder="{{fastSearchCtrl.hint}}"
           ng-model="fastSearchCtrl.value"
           ng-keydown="fastSearchCtrl.cancelSearch()"
           ng-keyup="fastSearchCtrl.search()"
    >
    </input>
    <div class="fast-search__icon"></div>
</div>



//fastSearchComponent.js
/**
 *  Creates FastSearchComponent.
 *  Main parameter is:
 *  - callback - it will be executed after user input.
 *  Also it contains secondary parameters:
 *  - min-letters-count - callback will be called only if input text length >= min-letters-count (0 by default)
 *  - delay - number of ms to wait after user finish input (500ms by default)
 *  - hint - text to be shown af hint ("" by default)
 *
 *  Use it like:
 *  <code><pre>
 *  &lt;div ng-controller="testFastSearchController">
 *      &lt;fast-search search-callback="doSearch(value);">&lt;/fast-search>
 *  &lt;/div>
 *  </pre></code>
 *  where doSearch is a function of testFastSearchController so controller
 *  may looks like:
 *  <code><pre>
 *  function testFastSearchController($scope) {
 *      $scope.doSearch = function (value) {
 *          alert("doSearch: value=" + value);
 *      };
 *  };
 *  </pre></code>
 *  Another example with secondary params:
 *  <code><pre>
 *  &lt;fast-search search-callback="doSearch(value);"
 *                      min-letters-count="3"
 *                      delay="500"
 *                      hint="Type to search"
 *  >&lt;/fast-search>
 *  </pre></code>
 */
function fastSearchComponent() {
    return {
        templateUrl: "ui-components/fastSearch/fastSearch.tmpl.html",
        controllerAs: "fastSearchCtrl",
        bindings: {
            searchCallback: "&",
            minLettersCount: '@',
            delay: '@',
            hint: '@'
        },
        controller: function ($timeout) {
            this.hint = this.hint || "";
            this.minLettersCount = this.minLettersCount || 0;
            this.delay = this.delay || 500;

            this.value = "";

            var timerId = null;
            var oldValue = null;

            var self = this;
            self.search = function () {
                if (debug) console.log(self.value);
                if (debug) console.log(self.minLettersCount);

                if (self.value.length >= self.minLettersCount && self.value != oldValue) {
                    $timeout.cancel(timerId);
                    timerId = $timeout(function () {
                        self.searchCallback({value: self.value});
                        $timeout.cancel(timerId);
                    }, self.delay);
                }

                oldValue = self.value;
            };
            self.cancelSearch = function () {
                $timeout.cancel(timerId);
            };
        },
    };
}



//fastSearch.less

/**
 * Show magnifier icon as circle.
 * On hover input appears slowly increasing width to the right.
 * Input has round left and right. Magnifier is above input.
 * On un hover (of icon and input input) input slowly decrease width and hides.
 * If input has focus than it is not hides without hover.
 * Input should be over other element which can be laying under it after increasing.
 */
.fast-search {
  display: flex;
  overflow: hidden;
  margin: 20px -20px;
  padding-bottom: 30px;
  padding-left: 20px;
  padding-right: 20px;
  white-space: nowrap;
  height: 40px;
  width: 80px;

  &:hover {
    width: 400px;
    transition: width 1s;
  }

  &:not(:hover) {
    transition: width 1s;
  }

  &:hover &__input {
    width: inherit;
    z-index: 1000;
  }

  &:not(:hover) &__input {
    transition: width 1s, z-index 0s 1s;
    animation: fast-search__z-index-animation 3s cubic-bezier(1, 0, 1, 0) //wa
  }
  &:not(:hover) &__icon {
    animation: fast-search__z-index-animation 3s cubic-bezier(1, 0, 1, 0) //wa
  }

  &:hover &__icon {
    z-index: 1001;
  }

  &__input {
    border-radius: 20px;
    height: 40px;
    width: 0px;
    padding-left: 40px;
    left: 0px;
    position: absolute;
    border: 1px solid #edf0f3
  }

  &__input:focus {
    width: 400px !important;
    z-index: 1000;
  }

  &__input:focus + &__icon {
    z-index: 1001;
  }

  &__icon {
    .icon_fast-search;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background-position: 13px;
    background-size: 15px 15px;
    background-color: #b5cbd1;
    z-index: 1;
  }
}

@keyframes fast-search__z-index-animation {
  from {
    z-index: 1000;
  }
  to {
    z-index: 0;
  }
}

.icon_fast-search() {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAATlBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////+QlxstAAAAGXRSTlMA5CT4YEIX+cO3pwPP39vZmIl8dFpXNiIGZA///QAAAHpJREFUCNdVjFcOwzAMQ2XL286evP9FqyguivKDBB8kElF1iWHmi16FBBXvWmtCXE57NMA/3SFq3hljlUxY+h/rAePsQxmbOGB7L1jFDY7eJzjxGe2mPhwkLkYOWlGU7wDnMgEw75IfIYrFfEH12+oCWQU/PWCgPzC0D353BsTzDxftAAAAAElFTkSuQmCC");
  background-repeat: no-repeat;
}



//testFastSearchController.tmpl.html
<div ng-controller="testFastSearchController">
<fast-search search-callback="doSearch(value);"
min-letters-count="3"
delay="500"
hint="Type to search"
></fast-search>
</div>
//testFastSearchController.tmpl.html
function testFastSearchController($scope) {
    $scope.doSearch = function (value) {
        alert("doSearch: value=" + value);
    };
};


Angular2
//ux-search-input.component.html


<div [ngClass]="calculateClasses()"
     *ngIf="!hidden"
>
    <ux-textbox class="ux-search-input__input"
              [placeholder]="placeholder"
              [disabled]="disabled"
              [defaultValue]="defaultValue"
              (valueChange)="handleChange($event)"
              (keyup)="handleKeyUp($event)"
    ></ux-textbox>
</div>

//ux-search-input.component.ts
import {Component, Input, Output, ElementRef, EventEmitter, OnInit} from "@angular/core";

@Component({
    selector: "ux-search-input",
    templateUrl: "ux-search-input.component.html"
})
export class UxSearchInputComponent implements OnInit {
    @Input() protected wide: boolean = false;
    @Input() protected searchMinSymbols: number = 0;
    @Input() protected searchDelay: number = 500;
    @Input() protected hidden: boolean = false;
    @Input() protected disabled: boolean = false;
    @Input() protected placeholder: string = "";
    @Input() protected styleClass: string = "";
    @Input() protected defaultValue: string = "";

    private readonly debug: boolean;

    public constructor(elementRef: ElementRef) {
        this.debug = false;
    }

    public ngOnInit(): void {
        if (this.debug) {
            const properties = {
                wide: this.wide,
                searchMinSymbols: this.searchMinSymbols,
                searchDelay: this.searchDelay,
                hidden: this.hidden,
                disabled: this.disabled,
                placeholder: this.placeholder,
                styleClass: this.styleClass,
                defaultValue: this.defaultValue
            };
            console.log("UxSearchInputComponent#ngOnInit: ", properties);
        }

        this.value = this.defaultValue;
    }

    private timerId: any;
    private value: string = "";
    private oldValue: string = "";
    @Output() protected searchCallback = new EventEmitter();

    protected handleChange(value: string): void {
        this.value = value;

        if (this.debug) {
            const params = {value: this.value, oldValue: this.oldValue, searchMinSymbols: this.searchMinSymbols};
            console.log("UxSearchInputComponent#handleChange: ", params);
        }

        if (this.value.length >= this.searchMinSymbols && this.value != this.oldValue) {
            clearTimeout(this.timerId);
            this.timerId = setTimeout(() => {
                this.searchCallback.emit(this.value);
                clearTimeout(this.timerId);
            }, this.searchDelay);
        }

        this.oldValue = this.value;
    }

    private readonly ENTER_KEY_CODE: number = 13;

    /**
     * Watch for Enter value to call searchCallback without wating for searchDelay or minimal letters.
     * @param keyboardEvent
     */
    protected handleKeyUp(keyboardEvent: any): void {
        if (this.debug) console.log("UxSearchInputComponent#handleKeyUp: ", keyboardEvent);
        if (keyboardEvent.keyCode === this.ENTER_KEY_CODE) {
            this.searchCallback.emit(this.value);
        }
    }

    /**
     * Calculate classes for component
     * @returns
     * search-input, [search-input__wide], [styleClass]
     * where [styleClass] will convert to string value from parameter customClass
     * or to none if styleClass is empty or null,
     * search-input__wide will be added only if wide==true
     */
    //http://stackoverflow.com/questions/34518235/multiple-classes-in-ngclass
    protected calculateClasses(): any {
        var ngClass = {
            'ux-search-input': true,
            'ux-search-input__wide': this.wide,
        };
        ngClass[this.styleClass] = this.styleClass;
        return ngClass;
    }
}



//ux-search-input.module.ts
import {NgModule} from "@angular/core";
import {UxSearchInputComponent} from "./ux-search-input.component";

@NgModule({
    exports: [UxSearchInputComponent],
    declarations: [UxSearchInputComponent]
})
export class UxSearchInputModule {
}




//ux-search-input.less

/**
 * Show magnifier icon as rectangle.
 * Show rectangle input on the right from magnifier.
 * Can be wide - fill all space by horizontal
 */

.ux-search-input {
  display: inline-block;
  padding: 2px;
  &__input {
    height: 45px;
    padding: 16px 0px 14px 45px;
    width: 100%;
    border: 1px solid #e1e1e1;
    .icon_search_big;
    background-color: #fff;
    background-position: 10px 11px;
    letter-spacing: 0.06em;
    .font-light;
  }

  .icon_search_big() {
    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAAAaVBMVEUAAABOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaFOeaH///9OeaHt8fVWf6WMqMLy9fjQ2+amvNCctMuZssnv8/fT3ujT3eeNqcPCXzTyAAAAFXRSTlMAl9YH8NBMm7ZFRPbq2sVuUTcjE9+rz+RgAAAAv0lEQVQoz4WS1xKCMBQFU0Cw12yQrv//kWKkZsywrztnbhU9OxlHRLHcCZ+NYkBtFkZL5iR6plIgq165ydsqA9JJJsDTmh/2+U2OtYDSTJTAUFN1KTOnS6q+eciahbMZbJ2TUJslNUjnYig8V0DsXATWcxYi5wDjA6zlwvXCfQbma9x84b2s79O/wxt4aO9+7XC/jtN9lAlzFHC8Bf7lDByuYmTb/5lr/rKH/UX853oAzgF5O4IUAe6nVATRWnwAzC8kQg7xV5wAAAAASUVORK5CYII=");
    background-repeat: no-repeat;
  }
}

.ux-search-input__wide {
  display: inline;
  .ux-search-input__input {
    height: 51px;
    padding: 0px 0px 0px 45px;
    display: inline-block;
  }
  .ux-search-input__input .ux-textbox {
    width: 100%;
  }
}


//test-ux-search-input.component.html
<ux-search-input (searchCallback)="handleSearch($event)"
defaultValue="Erase text for hint"
placeholder="3 symbols or enter"
[searchMinSymbols]="3"
></ux-search-input>
//test-ux-search-input.component.ts
import {Component} from "@angular/core";
import {UxSearchInputComponent} from "../../../ux-search-input.component";

@Component({
    selector: "test-ux-search-input-parent",
    templateUrl: "test-ux-search-input.component.html"
})
export class TestUxSearchInput {
    handleSearch(value: string): void {
        console.log("handleSearch: value=" + value);
    }
}


Стили

Отмечу что в 1 и 2 ангуляре сложные стили лучше помещать в функцию. Сложные стили это:
  1. если ты используешь {{}} в class и потом еще ng-class используешь
  2. если у тебя ключи объекта в ng-class генерируются
Это проявлеятся тем что стили не применяются а в консоли иногда видно сообщение "Multiple definitions of a property not allowed in strict mode"

Ангуляр2
<div [ngClass]="calculateClasses()" *ngIf="!hidden"></div>

//http://stackoverflow.com/questions/34518235/multiple-classes-in-ngclass
protected calculateClasses(): any {
var ngClass = {
'ux-search-input': true,
'ux-search-input__wide': this.wide,
};
ngClass[this.styleClass] = this.styleClass;
return ngClass;
}


АнгулярJs
/* This function simulate
 * ng-class="{
 *   'my-button_{{$ctrl.color}}': $ctrl.color,
 *   'my-button_{{$ctrl.size}}': $ctrl.size,
 *   '{{$ctrl.customClass}}': $ctrl.customClass
 * }"
 */
 // https://stackoverflow.com/questions/18172573/angular-ng-class-if-else-expression
this.getClasses = function() {
var classes = [];
if(self.color) {
classes.push("my-button_"+self.color);
}
if(self.size) {
classes.push("my-button_"+self.size);
}               
if(self.customClass) {
classes.push(self.customClass);
}
return classes.join(" ");
}

PS

Online syntax highlighting - https://tohtml.com/jScript/

четверг, 17 августа 2017 г.

Duck Hierarchy

В этой статье больше шутки нежели полезной информацией.
Под влиянием книги Head First Design Patterns я когда-то давно нарисовал такую картинку.

Слева мы видим типичное наследование и проблему двойного наследования когда хотим реализовать в одном классе два поведения.
А справа наследование от абстрактного класса и реализация интерфейсов. В этом случае нам конечно придется писать под для каждого интерфейса, зато это провоцирует нас писать классы с одной обязанностью как и ожидает от нас один из пунктов принципа SOLID.

А под конец картинка про фабрику

вторник, 8 августа 2017 г.

Мой самый сложный SQL запрос

Я работу по большей части во фронтэнде на как-то раза два у меня была задача написать сложный SQL-запрос. В принципе получилось довольно быстро при использовании интернет. Может кому и пригодиться.

Задача 1 - посчитать количество ордеров определенного типа


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
DROP TABLE IF EXISTS Objects;
DROP TABLE IF EXISTS ObjectTypes;
DROP TABLE IF EXISTS Attrs;
DROP TABLE IF EXISTS Params;

CREATE TABLE Objects (id int, objectTypeId int, name varchar(30));
INSERT INTO Objects (id, objectTypeId, name) VALUES (1, 1, 'Order-Tv#1');
INSERT INTO Objects (id, objectTypeId, name) VALUES (2, 1, 'Order-Tv#2');
INSERT INTO Objects (id, objectTypeId, name) VALUES (3, 1, 'Order-Tv#3');
INSERT INTO Objects (id, objectTypeId, name) VALUES (4, 1, 'Order-Internet#1');
INSERT INTO Objects (id, objectTypeId, name) VALUES (5, 1, 'Order-Internet#2');
INSERT INTO Objects (id, objectTypeId, name) VALUES (6, 1, 'Order-Radio#1');

CREATE TABLE ObjectTypes (id int, name varchar(30));
INSERT INTO ObjectTypes (id, name) VALUES (1, 'order');

CREATE TABLE Attrs (id int, name varchar(30));
INSERT INTO Attrs (id, name) VALUES (1, 'not started');
INSERT INTO Attrs (id, name) VALUES (2, 'failed');
INSERT INTO Attrs (id, name) VALUES (3, 'success');

CREATE TABLE Params (objectId int, attrId int, val varchar(30));
INSERT INTO Params (objectId, attrId, val) VALUES (1, 1, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (1, 2, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (1, 3, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (2, 1, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (3, 2, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (1, 3, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (4, 1, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (4, 4, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (5, 1, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (6, 1, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (6, 2, '+');
INSERT INTO Params (objectId, attrId, val) VALUES (6, 4, '+');

SELECT t0."type", t1."failed", t2."success", t3."not started" FROM
        (
                SELECT name AS "type" FROM ObjectTypes WHERE id IN (1)
        ) AS t0
FULL OUTER JOIN
        (
                SELECT COUNT(*) as "failed" FROM Params, Objects WHERE
                Params.objectId IN (SELECT id FROM Objects WHERE objectTypeId IN (1))
                AND Params.attrId = 1
                AND Params.value = 'failed'
                AND Params.objectId = Objects.id
                GROUP BY objectTypeId
        ) as t1
ON t0.objectTypeId = t1.ObjectTypeId
FULL OUTER JOIN
        (
                SELECT COUNT(*) as "success" FROM Params, Objects WHERE
                Params.objectId IN (SELECT id FROM Objects WHERE objectTypeId IN (1))
                AND Params.attrId = 2
                AND Params.objectId = Objects.id
                GROUP BY objectTypeId
        ) as t2
ON t0.objectTypeId = t1.ObjectTypeId
FULL OUTER JOIN
        (
                SELECT COUNT(*) as "not started" FROM Params, Objects WHERE
                Params.objectId IN (SELECT id FROM Objects WHERE objectTypeId IN (1))
                AND Params.attrId = 3
                AND Params.objectId = Objects.id
                GROUP BY objectTypeId
        ) as t3
ON t0.objectTypeId = t1.ObjectTypeId;

Cначала находим все типа объектов в строке 55 и называем такую таблицу t0. Затем находим все объекты у которых тип это Order, а атрибут используется номер 1 и записываем результат в t1 (строка 58-65). То же самое повторяем для всех типов и затем склеиваем через JOIN. Конечно здесь есть не оптимальности типа копипаста t1-t3, но я же не SQL девелопер.

Отмечу, что запрос переписан и может не работать, т.к. я убирал всю специфику и чуть упростил запрос - убрал табличу и уменьшил количество типов, а там запрос у меня занимал А4 шрифтом 10.

Задача 2 - пробежаться по иерархии

Здесь задача посложнее нужно пробежаться по иерархии Order и найти все Offering, причем Offering из родительского Order может быть заоверрайджен, тогда его показывать не нужно.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
(
  /*compatibility - get Offerings FROM Order which are children for Order Container*/
  SELECT
    OrderOffering.id AS objectId,
    OrderOffering.name AS value
  FROM
    Objects OrderContainerOffering,
    Objects OrderContainerOfferingFolder,
    Refs OrderContainer2Order,
    Objects OrderOfferingFolder,
    Objects OrderOffering
  WHERE
    OrderContainerOffering.id IN (#$objectids$#)
    AND OrderContainerOfferingFolder.id = OrderContainerOffering.parentId
    AND OrderContainer2Order.objectId = OrderContainerOfferingFolder.parentId
    AND OrderContainer2Order.attrId = 1 /*order*/
    AND OrderOfferingFolder.parentId = OrderContainer2Order.objectRefId
    AND OrderOfferingFolder.typeId = OrderContainerOfferingFolder.typeId
    AND OrderOffering.parentId = OrderOfferingFolder.id
    AND OrderOffering.typeId = 1 /*offering*/
)
UNION
(
  /*get Offerings FROM Order hierarchy*/
  SELECT
    ParentOrderOffering.id AS objectId,
    ParentOrderOffering.name AS value
  FROM
    Objects OrderOffering,
    Objects OrderOfferingFolder,
    Objects ParentOrderOfferingFolder,
    Objects ParentOrderOffering
  WHERE
    OrderOffering.id IN (#$objectids$#)
    AND OrderOfferingFolder.id = OrderOffering.parentId
    AND ParentOrderOfferingFolder.parentId IN (
      /*get all parent Orders*/
      SELECT reference FROM Refs ref
      START WITH ref.objectId = OrderOfferingFolder.parentId
      /*OrderOfferingFolder.parentId: parent of Offering Folder
        is Order AND this is Order to start find parents FROM*/
      CONNECT BY PRIOR ref.objectRefId = ref.objectId 
      AND PRIOR ref.attrId = 2 /*parent order*/
    )
    AND ParentOrderOffering.parentId = ParentOrderOfferingFolder.id
    AND ParentOrderOffering.typeId = 1 /*offering*/
    AND ParentOrderOffering.id not IN (
      /*exclude Offerings which are already overrided - 
        only not overrided Offerings available for override*/
      SELECT reference FROM Refs WHERE attr_id = 3 /*ancestor offering*/
      AND objectId IN (
        /*SELECT Offerings which are set IN attr ancestor offering*/
                SELECT /*get offerings FROM order hierarchy*/
          ParentOrderOffering.id AS objectId
                FROM
          Objects OrderOffering,
          Objects OrderOfferingFolder,
          Objects ParentOrderOfferingFolder,
          Objects ParentOrderOffering
                WHERE
          OrderOffering.id IN (#$objectids$#)
          AND OrderOfferingFolder.id = OrderOffering.parentId
          AND ParentOrderOfferingFolder.parentId IN (
            /*get all parent orders*/
            SELECT reference FROM Refs ref
            START WITH ref.objectId = OrderOfferingFolder.parentId
            /*OrderOfferingFolder.parentId: parent of Offering folder 
              is Order AND this is Order to start find parents FROM*/
            CONNECT BY PRIOR ref.reference = ref.object_id 
            AND PRIOR ref.attr_id = 2 /*attr parent order*/
            UNION ALL
            SELECT id FROM Objects 
            WHERE id = OrderOfferingFolder.parentId /*plus current Order*/
          )
          AND ParentOrderOffering.parentId = ParentOrderOfferingFolder.id
          AND ParentOrderOffering.typeId = 1 /*offering*/
      )
    )
)

Итог

  • я познакомился с иерархическими запросами - START WITH
  • первый раз использовал UNION
  • наконец в боевых условиях использовал JOIN
  • попрактиковался в сложных WHERE и GROUP BY
  • использовал вложенные запросы
  • сделал параметризацию запросов
  • попробовал AS для именования
SQL не такой уж и сложный поначалу, хотя конечно чем дальше тем дремучей лес.