複数行をTruncate

f:id:ryotah:20170718210942p:plain

  • ブラウザが限られている(Safari, Chrome)
  • そのうち使えなくなるかもしれない
  • heightで制限しているので、対象ブラウザ以外でもレイアウトが崩れることはない、はず
.note {
  font-size: 16px;
  line-height: 1.25;
  width: 100px;
  border: 1px solid #000;
  overflow: hidden;
  word-wrap: break-word;
  
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  display: -webkit-box;

  height: calc(16px * 1.25 * 3);
}

ユーザーからのフィードバックをSlackで受け取る

f:id:ryotah:20170713131031p:plain

f:id:ryotah:20170713131027p:plain

こういうやつを作りました。


Node.js用のSlack SDKを利用したら簡単にできました。

GitHub - slackapi/node-slack-sdk: Slack Developer Kit for Node.js

以下、Webhook URL発行前提の話です。

const IncomingWebhook = require('@slack/client').IncomingWebhook;
const webhook = new IncomingWebhook(SLACK_WEBHOOK_URL);

router.get('/send', (req, res) => {

  // ...

  new Promise((resolve, reject) => {
    webhook.send({
      attachments: createAttachments({
        id: '00001',
        name: '名前',
        text: '本文が入ります。本文が入ります。本文が入ります。本文が入ります。本文が入ります。',
        title: 'タイトル',
      })
    }, (err, header, statusCode, body) => {
      if (err) {
        reject(err);
      } else {
        resolve('ok');
      }
    })
  })
  .then((result) => {
    res.json(result);
  })
  .catch((err) => {
    // エラー処理
  });
});

function createAttachments(params) {
  return [{
      color: '#03A9F4',
      pretext: 'ユーザーからのフィードバックです',
      author_name: params.name,
      title: params.title,
      text: params.text,
      fields: [{
        title: 'User ID',
        value: params.id
      }],
      ts: Math.floor(Date.now() / 1000)
    }
  ];
}

参考URL

カラーコードの変換 (Hex -> RGB)

例えば#ff0000rgb(255,0,0)に変換したい場合。

  1. カラーコード(文字列)を2文字ずつに分ける
  2. 16進法を10進法に変換する
import compose from 'lodash/fp/compose';
import map from 'lodash/fp/map';

function hexToRgb(hex: string): string {
  return compose(
    (decimals) => `rgb(${decimals.join(',')})`,

    // hex to decimal
    map((hex) => parseInt(hex, 16)),

    // split string every 2 characters
    //   e.g. 'ff0000' -> ['ff', '00', '00']
    (hex) => hex.match(/.{1,2}/g),

    // remove `#`
    (hex) => hex.substr(1),

  )(hex);
}

console.error(hexToRgb('#ff0000'));
// -> 'rgb(255,0,0)'

split正規表現が使えた。文字を分割する処理に関してはsplitでもできそう。

String.prototype.split() - JavaScript | MDN

MouseEvent, KeyboardEventを利用してHTMLElementを操作

/**
 * ボタンのdisabled属性を変更
 */
public onMouseEvent($event: MouseEvent) {
  const el = <HTMLButtonElement>$event.target;

  /* disabled */
  el.disabled = true;
  
  /* enabled */
  // el.disabled = false;

  /**
   * 以下でも可
   */
  /* disabled */
  // el.setAttribute('disabled', '');
  /* enabled */
  // el.removeAttribute('disabled');
}

/**
 * フォーカスを外す
 */
public onKeyEvent($event: KeyboardEvent) {
  const el = <HTMLElement>$event.target;
  el.blur();
}

NgModulesのドキュメント読む

前回のスタイルガイドに続き、ドキュメント読んで気になる箇所を抜粋。

NgModules

Angular modularity

  • @NgModuleのmetadata
    • components, directives, pipesの宣言
    • 必要ならば、宣言したものをpublicにする (他のモジュールのテンプレートで利用可能になる)
    • 自身で必要な他のモジュールをインポートする
    • servicesを提供し、どのコンポーネントでも利用可能する
  • モジュールクラスは必ず1つある (root module)

The root AppModule

Bootstrapping in main.ts

Declare directives and components

Service providers

Modules are a great way to provide services for all of the module’s components.

The Dependency Injection page describes the Angular hierarchical dependency-injection system and how to configure that system with providers at different levels of the application’s component tree.

Dependency Injectionも後で読む)

  • モジュールはアプリケーションのroot dependency injectorにprovidersを追加し、servicesをアプリケーションのどこからでも利用可能にする

Import supporting modules

  • BrowserModuleをimportするとCommonModuleもimportされる
  • CommonModulengIf, ngForなどを宣言している

Application-scoped providers

  • serviceをmoduleに登録すると application-scoped になる (どこからでもinjectできるようになる)
  • アーキテクチャ的にはContactServiceは他のドメインで利用することはないので、injectできないようにしたい
  • Angularは意図的に module-scoping mechanism を提供していない

Read more in the How do I restrict service scope to a module? section of the NgModule FAQs page.

Resolve directive conflicts

Feature modules

  • モジュールは他のモジュールで宣言されているcomponents, directives, またはpipesへのアクセスを継承しない

Lazy-loading modules with the router

src/app/app-routing.module.ts

export const routes: Routes = [
  { path: '', redirectTo: 'contact', pathMatch: 'full'},
  { path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' },
  { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' }
];
  • routerについてはRouting & Navigation
  • cotact routeは Contact featureのrouting moduleで設定
  • routerによりContactComponentに遷移する場合、publicにする必要はない (exportする必要はない)
    • ContactComponentはselectorも必要としない

Shared modules

src/app/src/app/shared/shared.module.ts

@NgModule({
  imports:      [ CommonModule ],
  declarations: [ AwesomePipe, HighlightDirective ],
  exports:      [ AwesomePipe, HighlightDirective,
  
    // re-exports the CommonModule and FormsModule
    CommonModule, FormsModule ]
})
  • SharedModuleをimportすればCommonModule, FormsModuleもimportされるようになる
    • CommonModuleがimportされているのは、SharedModuleが必要としているから

Do not specify app-wide singleton providers in a shared module. A lazy-loaded module that imports that shared module makes its own copy of the service.

The Core module

  • CoreModuleは一度だけimportされる
    • AppRoot moduleがimportする

Cleanup

Configure core services with CoreModule.forRoot

  • 慣例で、forRootのstatic methodはserviceの提供と設定を同時に実行する
    • serviceの設定オブジェクトを受け取り、ModuleWithProvidersを返す

More precisely, Angular accumulates all imported providers before appending the items listed in @NgModule.providers. This sequence ensures that whatever you add explicitly to the AppModule providers takes precedence over the providers of imported modules.

src/app/core/user.service.ts (constructor)

constructor(@Optional() config: UserServiceConfig) {
  if (config) { this._userName = config.userName; }
}

src/app/core/core.module.ts (forRoot)

static forRoot(config: UserServiceConfig): ModuleWithProviders {
  return {
    ngModule: CoreModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}

src/app//app.module.ts (imports)

imports: [
  BrowserModule,
  ContactModule,
  CoreModule.forRoot({userName: 'Miss Marple'}),
  AppRoutingModule
],

Prevent reimport of the CoreModule

src/app/core/core.module.ts

constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
  if (parentModule) {
    throw new Error(
      'CoreModule is already loaded. Import it in the AppModule only');
  }
}

Conclusion

Angular4スタイルガイド読む

Angular公式のStyle Guideから気になった箇所を抜粋したものです。

Style GuideとNgModulesについての公式ドキュメントを読んだら、実際のアプリを作るときの大枠がある程度整理できました。

ドキュメントはそれなりに長いですが、命名規則など、Angular CLIを利用して入れば特に意識しなくても実現できている箇所も多いです。

Style Guide

Single responsibility

Consider limiting files to 400 lines of code.

Consider limiting to no more than 75 lines.

Naming

Coding conventions

Constants

Do declare variables with const if their values should not change during the application lifetime.

Interfaces

Do name an interface using upper camel case.

Consider naming an interface without an I prefix.

Consider using a class instead of an interface.

Application structure and Angular modules

All of the app’s code goes in a folder named src. All feature areas are in their own folder, with their own Angular module.

All third party vendor scripts are stored in another folder and not in the src folder.

LIFT

Locate

Identify

Flat

Consider creating sub-folders when a folder reaches seven or more files.

T-DRY

Why? Being DRY is important, but not crucial if it sacrifices the other elements of LIFT. That’s why it’s called T-DRY.

Overall structural guidelines

https://angular.io/guide/styleguide#file-tree

Folders-by-feature structure

Do create an Angular module for each feature area.

Shared feature module

Avoid providing services in shared modules. Services are usually singletons that are provided once for the entire application or in a particular feature module.

Do import all modules required by the assets in the SharedModule; for example, CommonModule and FormsModule.

Avoid specifying app-wide singleton providers in a SharedModule. Intentional singletons are OK. Take care.

Why? A lazy loaded feature module that imports that shared module will make its own copy of the service and likely have undesireable results.

Core feature module

Consider collecting numerous, auxiliary, single-use classes inside a core module to simplify the apparent structure of a feature module.

Do gather application-wide, single use components in the CoreModule. Import it once (in the AppModule) when the app starts and never import it anywhere else. (e.g. NavComponent and SpinnerComponent).

Prevent re-import of the core module

Do guard against reimporting of CoreModule and fail fast by adding guard logic.

export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
  if (parentModule) {
    throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
  }
}

Components

Member sequence

Do place properties up top followed by methods.

Do place private members after public members, alphabetized.

Delegate complex component logic to services

Do limit logic in a component to only that required for the view. All other logic should be delegated to services.

/* avoid */
private hideSpinner() {
  // hide the spinner
}

Directives

HostListener/HostBinding decorators versus host metadata

Consider preferring the @HostListener and @HostBinding to the host property of the @Directive and @Component decorators.

Services

Providing a service

Do provide services to the Angular injector at the top-most component where they will be shared.

Why? When providing the service to a top level component, that instance is shared and available to all child components of that top level component.

Data Services

Lifecycle hooks

Appendix