{"id":25020905,"url":"https://github.com/frontainer/practice-angular-animations","last_synced_at":"2025-07-27T04:03:08.983Z","repository":{"id":80869488,"uuid":"97546115","full_name":"frontainer/practice-angular-animations","owner":"frontainer","description":"Angular4.0アニメーションワークショップ用サンプル","archived":false,"fork":false,"pushed_at":"2017-09-04T09:48:21.000Z","size":194,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-30T10:29:53.003Z","etag":null,"topics":["angular","animations","handson","workshop"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/frontainer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-07-18T03:00:00.000Z","updated_at":"2017-09-04T03:00:48.000Z","dependencies_parsed_at":"2023-04-15T13:31:09.638Z","dependency_job_id":null,"html_url":"https://github.com/frontainer/practice-angular-animations","commit_stats":null,"previous_names":["frontainer/practice-angular-animations"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/frontainer/practice-angular-animations","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frontainer%2Fpractice-angular-animations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frontainer%2Fpractice-angular-animations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frontainer%2Fpractice-angular-animations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frontainer%2Fpractice-angular-animations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/frontainer","download_url":"https://codeload.github.com/frontainer/practice-angular-animations/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frontainer%2Fpractice-angular-animations/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267298274,"owners_count":24065879,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-27T02:00:11.917Z","response_time":82,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","animations","handson","workshop"],"created_at":"2025-02-05T12:18:08.356Z","updated_at":"2025-07-27T04:03:08.967Z","avatar_url":"https://github.com/frontainer.png","language":"TypeScript","readme":"## 前提\nNode.js 6 or 8をローカルにインストールしていること\n\n## 環境構築\n\nAngular CLIをインストールしましょう\n\n```\nnpm i @angular/cli -g\n```\n\nプロジェクトを作成するディレクトリで次のコマンドを実行\n\n```\nng new animation-workshop --routing --style=scss\n```\n\n- --routing ルーティングを使うアプリケーションを構築する宣言（`app-routing.module.ts`ができる）\n- --style コンポーネントで使うcssの種類（css/scss/sassなど）\n\n以上で準備完了！\n\n## サーバー起動\n\n次のコマンド実行後に`localhost:4200`にアクセス\n\n```\nng serve\n```\n\n※ 環境によって関連ファイルのインストールに失敗していることがあるので、もしエラーがでたら `npm i` を実行してから改めてserveしてみよう。\n\n![AnimationWorkshop - http___localhost_4200_.png (162.7 kB)](https://img-esa-emotion-tech.s3-ap-northeast-1.amazonaws.com/uploads/production/attachments/2271/2017/07/18/17433/73df7f3c-b96f-4e35-9d11-0efb0abca909.png)\n\nこの状態でコードを変更すると勝手にビルドして更新してくれるよ！\n\n## 新しいコンポーネントを作る\n\nhomeコンポーネントを作るよ！\nng generate（エイリアス: `ng g`）を使うと簡単。\n\n```\nng g component home\n```\n\nファイルを作ってModuleにも参照を追加してくれるよ！\n```\ncreate src/app/home/home.component.scss\ncreate src/app/home/home.component.html\ncreate src/app/home/home.component.spec.ts\ncreate src/app/home/home.component.ts\nupdate src/app/app.module.ts\n```\n\n## ルーティング\n\nこのままだとhomeにアクセスできないのでルーティングを足しますよ！\n\n``` app-routing.module.ts\nimport { NgModule } from '@angular/core';\nimport { Routes, RouterModule } from '@angular/router';\nimport { HomeComponent } from './home/home.component'; // \u003c- 追加\n\nconst routes: Routes = [\n  {\n    path: '',\n    pathMatch: 'full', // \u003c- 追加\n    children: [\n      { // 追加ここから\n        path: '',\n        pathMatch: 'full',\n        component: HomeComponent\n      } // 追加ここまで\n    ]\n  }\n];\n\n@NgModule({\n  imports: [RouterModule.forRoot(routes)],\n  exports: [RouterModule]\n})\nexport class AppRoutingModule { }\n```\n\nトップページにいらないテンプレが書いてあるので次のものだけにしてしまいます。\n\n```app.component.html\n\u003crouter-outlet\u003e\u003c/router-outlet\u003e\n```\n\n![AnimationWorkshop - http___localhost_4200_2.png (82.8 kB)](https://img-esa-emotion-tech.s3-ap-northeast-1.amazonaws.com/uploads/production/attachments/2271/2017/07/18/17433/3097b8d7-cc23-4548-82d1-5504ddf22d13.png)\n\nできた！\n\n## @angular/materialいれよう\n\n```\nnpm i -S @angular/material @angular/cdk\n```\n\n※cdk - component dev kit. もともと@angular/coreにあったものが切り出されたもの\n\nスタイルテーマを追加\n```styles.scss\n@import \"~@angular/material/prebuilt-themes/indigo-pink.css\";\nmd-list-item {\n  overflow: hidden;\n}\n```\n\napp.moduleに使いたいモジュールをimportしておくよ！\n```app.module.ts\nimport { BrowserModule } from '@angular/platform-browser';\nimport { NgModule } from '@angular/core';\nimport { FormsModule } from '@angular/forms'; // \u003c-追加\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // \u003c-追加\n\n// 追加ここから\nimport {\n  MdButtonModule,\n  MdInputModule,\n  MdRadioModule,\n  MdListModule,\n  MdCheckboxModule,\n  MdIconModule\n} from '@angular/material';\n// 追加ここまで\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { HomeComponent } from './home/home.component';\n\n@NgModule({\n  declarations: [\n    AppComponent,\n    HomeComponent\n  ],\n  imports: [\n    BrowserModule,\n    AppRoutingModule,\n    // 追加ここから\n    BrowserAnimationsModule,\n    FormsModule,\n    MdButtonModule,\n    MdInputModule,\n    MdRadioModule,\n    MdListModule,\n    MdCheckboxModule,\n    MdIconModule\n    // 追加ここまで\n  ],\n  providers: [\n  ],\n  bootstrap: [AppComponent]\n})\nexport class AppModule { }\n```\n\n## ToDo保持のService作成\n\nToDoを保持するServiceを作りますよ！\n```\nng g service shared/services/todo/todo\n```\n\n\u003e WARNING Service is generated but not provided, it must be provided to be used\n\nServiceはModuleに勝手に読み込まれないので注意です。\n\nServiceで使う型定義書くファイルも一緒に作りましょう。\n```\nng g interface shared/services/todo/todo model\n```\n\n```shared/services/todo/todo.model.ts\nexport interface TodoItem {\n  id: number;\n  state: string;\n  value: string;\n}\n```\n\n作った型定義を読み込むよ！\n\n```shared/services/todo.service.ts\nimport { Injectable } from '@angular/core';\nimport { TodoItem } from './todo.model'; // \u003c-追加\n\n@Injectable()\nexport class TodoService {\n  constructor() { }\n}\n```\n\n### Service作り込み\n\n``` shared/services/todo.service.ts\nimport { Injectable } from '@angular/core';\nimport { Observable } from 'rxjs/Observable';\nimport { BehaviorSubject } from 'rxjs/BehaviorSubject';\nimport { TodoItem } from './todo.model';\n\n@Injectable()\nexport class TodoService {\n  private subject: BehaviorSubject\u003cTodoItem[]\u003e = new BehaviorSubject([]);\n  private _list: TodoItem[] = [];\n  private _type: string = 'all';\n\n  constructor() {\n    const data: string = \u003cstring\u003ewindow.localStorage.getItem('list');\n    if (data) {\n      this._list = JSON.parse(data);\n    }\n\n    this.subject.subscribe(() =\u003e {\n      this._update();\n    });\n  }\n\n  get items(): Observable\u003cTodoItem[]\u003e {\n    return this.subject.asObservable();\n  }\n\n  _findById(id: number): TodoItem | undefined {\n    return this._list.find((item: TodoItem) =\u003e {\n      return (item.id === id);\n    });\n  }\n  _findIndexById(id: number): number {\n    return this._list.findIndex((item: TodoItem) =\u003e {\n      return (item.id === id);\n    });\n  }\n  _filterByState(state: string): TodoItem[] {\n    switch (state) {\n      case 'all':\n        return this._list;\n      case 'complete':\n        return this._list.filter((item) =\u003e {\n          return (item.state === state);\n        });\n      case 'todo':\n        return this._list.filter((item) =\u003e {\n          return (item.state === state);\n        });\n      default:\n        break;\n    }\n    return [];\n  }\n  _update() {\n    window.localStorage.setItem('list', JSON.stringify(this._list));\n  }\n\n  add(value: string) {\n    this._list.unshift({\n      id: Date.now(),\n      state: 'todo',\n      value\n    });\n    this.subject.next(this._list.slice());\n  }\n  changeState(id: number) {\n    const hit: TodoItem | undefined = this._findById(id);\n    if (hit) {\n      if (hit.state === 'complete') {\n        hit.state = 'todo';\n      } else {\n        hit.state = 'complete';\n      }\n    }\n    this.subject.next(this._filterByState(this._type).slice());\n  }\n  delete(id: number) {\n    const hit: number = this._findIndexById(id);\n    if (hit !== -1) {\n      this._list.splice(hit, 1);\n    }\n    this.subject.next(this._filterByState(this._type).slice());\n  }\n  refresh() {\n    this._update();\n    this.subject.next(this._list.slice());\n  }\n  filter(type: string) {\n    this._type = type;\n    this.subject.next(this._filterByState(this._type).slice());\n  }\n  clearComplete() {\n    this._list = this._filterByState('todo');\n    this.subject.next(this._list.slice());\n  }\n}\n```\n\n```app.module.ts\nimport { TodoService } from './shared/services/todo/todo.service';\n\n@NgModule({\n // ...略\n  providers: [\n    TodoService\n  ],\n  // ...略\n})\n```\n\n## Pipe作る\n\nToDoをテキストで絞り込むためのPipeを作るよ！\n\n```\nng g pipe shared/pipes/todoSearch \n```\n\n\n```shared/pipes/todo-search.ts\nimport { Pipe, PipeTransform } from '@angular/core';\nimport { TodoItem } from '../services/todo/todo.model';\n\n@Pipe({\n  name: 'todoSearch'\n})\nexport class TodoSearchPipe implements PipeTransform {\n  transform(items: TodoItem[], word: string): any {\n    return items.filter((item: TodoItem) =\u003e {\n      return (item.value.indexOf(word) !== -1);\n    });\n  }\n}\n```\n\n## これまで作ったものをコンポーネントに\n\n```home/home.component.ts\nimport { Component, OnInit } from '@angular/core';\nimport { Observable } from 'rxjs/Observable';\nimport { TodoService } from '../shared/services/todo/todo.service';\nimport { TodoItem } from '../shared/services/todo/todo.model';\n\nimport { MdRadioChange } from '@angular/material';\n\n@Component({\n  selector: 'app-home',\n  templateUrl: './home.component.html',\n  styleUrls: ['./home.component.scss']\n})\nexport class HomeComponent implements OnInit {\n  public list: Observable\u003cTodoItem[]\u003e;\n  public searchWord = '';\n  public filterState = 'all';\n  public filterItems = [\n    {\n      name: 'all',\n      label: 'すべて'\n    },\n    {\n      name: 'complete',\n      label: '完了'\n    },\n    {\n      name: 'todo',\n      label: '未完了'\n    }\n  ];\n  constructor(\n    private todoService: TodoService\n  ) { }\n\n  ngOnInit() {\n    this.list = this.todoService.items;\n    this.todoService.refresh();\n  }\n\n  add(event: Event, input: HTMLInputElement) {\n    event.preventDefault();\n    if (input.value) {\n      this.reset();\n      this.todoService.add(input.value);\n      input.value = '';\n    }\n  }\n  reset() {\n    this.filterState = 'all';\n    this.todoService.filter(this.filterState);\n  }\n  changeState(id: number) {\n    this.todoService.changeState(id);\n  }\n  changeFilter(event: MdRadioChange) {\n    this.todoService.filter(event.value);\n  }\n  delete(id: number) {\n    this.todoService.delete(id);\n  }\n  clearComplete() {\n    if (window.confirm('完了したタスクをすべて削除してよろしいですか？')) {\n      this.todoService.clearComplete();\n    }\n  }\n}\n\n```\n\n```home/home.component.html\n\u003cform action=\"\" (submit)=\"add($event, input)\"\u003e\n  \u003cmd-input-container\u003e\n    \u003cinput mdInput placeholder=\"TODO\" #input\u003e\n  \u003c/md-input-container\u003e\n  \u003cbutton md-raised-button\u003e追加\u003c/button\u003e\n\u003c/form\u003e\n\n\u003cform action=\"\"\u003e\n  \u003cmd-input-container\u003e\n    \u003cinput mdInput name=\"search\" [(ngModel)]=\"searchWord\" placeholder=\"検索ワード\"\u003e\n  \u003c/md-input-container\u003e\n  \u003cmd-radio-group class=\"example-radio-group\" (change)=\"changeFilter($event)\" [(ngModel)]=\"filterState\" name=\"filter\"\u003e\n    \u003cmd-radio-button class=\"example-radio-button\" *ngFor=\"let item of filterItems\" [value]=\"item.name\"\u003e{{item.label}}\n    \u003c/md-radio-button\u003e\n  \u003c/md-radio-group\u003e\n\u003c/form\u003e\n\n\u003cbutton md-raised-button color=\"warn\" (click)=\"clearComplete()\"\u003e完了したタスクを削除\u003c/button\u003e\n\n\u003cmd-list *ngIf=\"(list | async | todoSearch : searchWord) as items\"\u003e\n  \u003cmd-list-item *ngFor=\"let item of items\" (click)=\"changeState(item.id)\"\u003e\n    \u003cmd-checkbox md-list-icon [checked]=\"item.state === 'complete'\" (click)=\"$event.preventDefault()\"\u003e\u003c/md-checkbox\u003e\n    \u003ch4 md-line\u003e{{item.value}}\u003c/h4\u003e\n    \u003cp md-line\u003e{{item.id | date : 'yyyy/MM/dd hh:mm'}}\u003c/p\u003e\n    \u003cp\u003e\n      \u003cmd-icon (click)=\"delete(item.id)\"\u003edelete\u003c/md-icon\u003e\n    \u003c/p\u003e\n  \u003c/md-list-item\u003e\n\u003c/md-list\u003e\n```\n\n---\n\n## アニメーション基本\n\n```home/home.component.ts\nimport {\n  transition,\n  trigger,\n  state,\n  style,\n  animate\n} from '@angular/animations';\n\n// ...略\n\n@Component({\n  selector: 'et-home',\n  templateUrl: './home.component.html',\n  styleUrls: ['./home.component.scss'], // \u003c- 「,」忘れずに\n  // 追加ここから\n  animations: [\n    trigger('stateEffect', [\n      state('complete', style({\n        backgroundColor: '#eee'\n      })),\n      transition('* =\u003e complete', [\n        style({\n          backgroundColor: '#fff'\n        }),\n        animate('200ms ease-out', style({\n          backgroundColor: '#eee'\n        }))\n      ]),\n      transition('* =\u003e todo', [\n        style({\n          backgroundColor: '#eee'\n        }),\n        animate('200ms ease-in', style({\n          backgroundColor: '#fff'\n        }))\n      ])\n    ])\n  ]\n  // 追加ここまで\n})\n```\n\n```home/home.component.html 21行目\n\u003cmd-list-item *ngFor=\"let item of items\" (click)=\"changeState(item.id)\" [@stateEffect]=\"item.state\"\u003e\n```\n\n`[@stateEffect]=\"item.state\"`を追加しました。\nそうするとチェックをつけるとアニメーションして背景色が変わりますよ！\n\n### ポイント\n1. tirggerでアニメーションする対象と状態を受け取る\n2. transitionを用いて変化によって処理を振り分け\n3. styleやanimateを使ってスタイルを変えたりアニメーションさせたり\n4. 最終的なスタイルはstateとstyleで定義しよう（しないと元に戻るぞ）\n\n\n### transition\n\n`FROM =\u003e TO` の形で記述します。\n双方向もできますよ！ `A \u003c=\u003e B`\n\n何もない状態から変化するときは`void`を使います。\n\nエイリアスとして`:enter`と`:leave`があります。\n`:enter` は `void =\u003e *` と同じ意味で`:leave`は `* =\u003e void`と同じです。\n\n## アニメーションテンプレート\n\n4.2で追加されたanimation関数を使うことで共通に使うアニメーションの定義をすることができます。パラメータで実行時間などは変更できるので、ある程度汎用的に作れる様になりますよ！\n\n```\nng g class app.animations\n```\n\n代表的なフェードイン・フェードアウトを書いてみましょう。\n\n```app.animations.ts\nimport {\n  style,\n  animate,\n  animation\n} from '@angular/animations';\n\nexport const slideFadeIn = animation([\n  style({\n    opacity: 0,\n    transform: 'translateX(2%)'\n  }),\n  animate('{{time}} {{easing}}', style({\n    opacity: 1,\n    transform: 'translateX(0)'\n  }))\n], {\n  params: {\n    time: '.5s',\n    easing: 'ease-out'\n  }\n});\nexport const slideFadeOut = animation([\n  style({\n    opacity: 1,\n    height: '*'\n  }),\n  animate('{{time}} {{easing}}', style({\n    opacity: 0,\n    height: 0\n  }))\n], {\n  params: {\n    time: '.5s',\n    easing: 'ease-out'\n  }\n});\n```\n\n## アニメーションを適用する\n\n必要な関数と先ほど作ったテンプレを呼び出します。\n\n```home/home.component.ts\nimport {\n  transition,\n  trigger,\n  state,\n  style,\n  animate,\n  useAnimation // \u003c- 追加\n} from '@angular/animations';\nimport { slideFadeIn, slideFadeOut } from '../app.animations'; // \u003c- 追加\n```\n\ncomponentのanimationsにアニメーションを定義を追加しますよ！\n\n```home/home.component.ts\n@Component({\n  selector: 'et-home',\n  templateUrl: './home.component.html',\n  styleUrls: ['./home.component.scss'], // \u003c- 「,」忘れずに\n  animations: [\n    // ...略\n    // 追加ここから\n    trigger('slideFade', [\n      transition(':enter', [\n        useAnimation(slideFadeIn)\n      ]),\n      transition(':leave', [\n        useAnimation(slideFadeOut)\n      ])\n    ])\n    // 追加ここまで\n  ]\n})\n```\n\n```home/home.component.html 21行目\n\u003cmd-list-item *ngFor=\"let item of items\" (click)=\"changeState(item.id)\" [@stateEffect]=\"item.state\" @slideFade\u003e\n```\n\n`@slideFade`を追加しました。\n\nuseAnimation animationで定義したテンプレを使ってアニメーションを実行します。第２引数にパラメータを渡せるので、\n\n```\ntransition(':enter', [\n  useAnimation(slideFadeIn, {\n    params: {\n      time: '300ms',\n      easing: 'ease-out'\n    }\n  })\n])\n```\nとしたら時間を変えたりイージング変えたりできます。\nいろいろと応用できそうですね。\n\n## リストアニメーション\n\nリストがあるとアイテムごとにアニメーションにディレイをつけたくなりますよね。\n\n```\nimport {\n  transition,\n  trigger,\n  state,\n  style,\n  animate,\n  useAnimation,\n  query, // \u003c- 追加\n  stagger // \u003c- 追加\n} from '@angular/animations';\n// ...略\n// slideFade置き換えここから\ntrigger('slideFade', [\n  transition('* =\u003e *', [\n    query(':leave', [\n      useAnimation(slideFadeOut)\n    ], { optional: true }),\n    query(':enter', [\n       stagger(50, [\n        useAnimation(slideFadeIn)\n      ])\n    ], { optional: true })\n  ])\n])\n// slideFade置き換えここまで\n```\n\n```home/home.component.html 20行目\n\u003cmd-list *ngIf=\"(list | async | todoSearch : searchWord) as items\" [@slideFade]=\"items\"\u003e\n  \u003cmd-list-item *ngFor=\"let item of items\" (click)=\"changeState(item.id)\" [@stateEffect]=\"item.state\"\u003e\n```\n\n`[@slideFade]=\"items\"` を追加しました。\n同時にmd-list-itemに書いていた`slideFade`の記述を削除しました。\n\nすべて・完了・未完了で切り替えてみるとディレイがついてアニメーションしています。\nが、ちょっとぎこちない動きですよね。\n\n## アニメーションを同時実行\n\nデフォルトではアニメーションは配列の順番に実行されていきます。\n現状は切り替えると、アイテムが消えてから、その後に出てくるアニメーションが実行されているためぎこちない動きになっています。\n\nアニメーション同時実行させますよ！\n\n```home/home.component.ts\nimport {\n  transition,\n  trigger,\n  state,\n  style,\n  animate,\n  useAnimation,\n  query,\n  stagger,\n  group // \u003c- 追加\n} from '@angular/animations';\n\n// ...略\n// slideFade置き換えここから\ntrigger('slideFade', [\n  transition('* =\u003e *', [\n    group([ // \u003c- 追加\n      query(':leave', [\n        useAnimation(slideFadeOut)\n      ], { optional: true }),\n      query(':enter', [\n        stagger(50, [\n          useAnimation(slideFadeIn)\n        ])\n      ], { optional: true })\n    ]) // \u003c- 追加\n  ])\n])\n// slideFade置き換えここまで\n```\n\nこれで消えつつ表示されるようになりました。\n\n## まとめ\n\n動的にパラメータ変更はまだできないですが、アプリケーション構築する分には問題ないレベルになっています。でもまだExperimentalのステータスなので今後も変わるかもしれませんけどね！\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrontainer%2Fpractice-angular-animations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffrontainer%2Fpractice-angular-animations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrontainer%2Fpractice-angular-animations/lists"}