はじめに
最近業務でTypeScriptの使用が始まりました。
ですが、自分は勉強会で少し触れた程度で業務で使うには基本的な部分が少し心許ない…
なので、自己学習として環境構築からちょっとした処理の作成までをやってみようと思いました。
本記事では、研修では勉強したけど実務で使ったことはまだなく、
FE歴1年数ヶ月の私が、TypeScript実装のための環境構築から処理を実装してブラウザから動かしてみるまでの記録になっています。
TypeScriptの勉強に手をつけたいけど、「重い腰が上がらない」「ちょっと難しそうで1歩が踏み出せない」そんな気持ちを持つ方が、「これくらいなら自分でもできそう」「ちょっくらやってみるか」という気持ちで手をつけ始められるきっかけになれば嬉しいです。
今回の目的
今回、TypeScriptの勉強のゴールは、ひとまず以下で設定してます。
👉 tsがjsにコンパイルされ、ブラウザから動かしてみて想定した動作が確認できること
そもそもTypeScriptって何でしょう?
公式によると..
TypeScript adds additional syntax to JavaScript to support a tighter integration with your editor. Catch errors early in your editor.
TypeScript code converts to JavaScript, which runs anywhere JavaScript runs: In a browser, on Node.js or Deno and in your apps.
TypeScript understands JavaScript and uses type inference to give you great tooling without additional code.
すごく簡単に言うと、型定義が可能でエディタでエラーが発見できる、JavaScriptに+αな機能がついたプログラミング言語かと思います。
環境構築してみよう
今回、環境構築は、以下のサイトを参考に行なっていきます。
https://qiita.com/niwasawa/items/d6535bb1ca4d44299eae
また、今回のゴールにはブラウザからイベントを発火し、想定した動作の確認を行うところまでなので、TypeScriptの環境設定とは別に、HTMLとCSSの準備を行っていきます。
今回は、HTMLはJavaScriptのテンプレートエンジンであるEJSを使用し、CSSはSassを使用していきます!
※上記の選定理由は、普段扱っているからという理由だけですので、お好みで・・
※尚、今回gulpタスクは上記に合わせて設定していきます。
環境
今回エディタはVSCode
を使用します。
また、Node.js
は以下バージョンを使用しています。
v12.16.2
早速必要なものをインストールしていく
TypeScriptの実行にはtypescript
が必要なので、グローバルでインストールします。
また、コマンドで直接ファイルを指定して実行するためには、ts-node
が必要になります。
> npm i -g typescript ts-node
TypeScript
がインストールされたことを確認するため、
VSCodeを開き、VScodeの統合ターミナルを表示し、以下コマンドで、インストールされたことを確認します。
> tsc -v
Version 4.5.2
今回タスクランナーを使ってコンパイルなどを行いたいので、
以下のファイル階層になるように、gulpfile.jsを作成し配置します。
(本記事で扱わないファイルは省略しております。)
├── dist
├── gulpfile.js
├── package.json
├── src
│ ├── js
│ │ └── index.ts
│ ├── css
│ │ ├── reset.scss
│ │ └── index.scss
│ ├── _partials
│ │ ├── components
│ │ │ ├── button.ejs
│ │ │ ├── button.scss
│ │ │ ├── card.ejs
│ │ │ ├── card.scss
│ │ │ ├── modal.ejs
│ │ │ └── modal.scss
│ │ └── main.ejs
│ └── index.ejs
└── tsconfig.json
以下コマンドでpackage.jsonを作成します。
> npm init -y
次にコマンドでTypeScriptを実行できるようにするために、
gulp-cli
をグローバルにインストールします。
> npm i -g gulp-cli
次にgulpでTypeScriptをコンパイルするために必要なパッケージをインストールします。
> npm i typescript gulp gulp-typescript -D
上記を実行すると、package.json
のdevDependencies
下に、
インストールしたパッケージが追記されます。
次に、コンパイルするtsファイルやコンパイラの設定を行うことができるtsconfig.json
を作成します。
tsconfig.jsonは以下コマンドで自動生成することが可能です。
> tsc --init
ほぼほぼコメントアウトされた状態で、tsconfig.jsonが作成されるので、こちらのQiitaの記事を参考にしながら、必要そうなオプションを設定していきました!
オプションはたくさんあるため、本記事では各オプションの説明は割愛させていただきます。
**実際に修正した後のtsconfig.jsonはこちら**
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
"incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "es2015", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist/js", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
"strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
"noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/**/*",
],
}
gulpファイルに処理を記載しよう
今回、gulpを使ってTypeScriptのコンパイルを行いたいです。
そのため、gulpfile.jsを作成し、以下の処理を追記します。
const gulp = require('gulp');
const typeScript = require('gulp-typescript');
const tsProject = typeScript.createProject('tsconfig.json');
function tsTransPiler() {
return tsProject.src()
.pipe(tsProject())
.js.pipe(gulp.dest('dist/js/'))
};
function watch(done) {
gulp.watch(['src/js/*', 'src/js/**/*'], tsTransPiler);
done();
};
exports.default = watch;
試しに、src/js/index.ts
を作成し、以下の処理を書いてgulp
を実行してみると
dist/js
配下に、index.js
が作成されているのが確認できます。
const hoge = () => {
console.log('Hello!World!');
};
hoge();
実行するコマンド
> npx gulp
ここまでで、TypeScript
をコンパイルするタスクは完成なので、
次に、HTMLとCSSを修正しブラウザーに反映して確認できるようにgulpfile.js
に修正を加えていきます。
(EJSやSassのために インストールしたプラグインは以下をご参照ください)
**EJSやSassのプラグインとgulpソース**
npm i gulp-sass dart-sass gulp-plumber gulp-notify gulp-postcss postcss autoprefixer cssnano gulp-ejs gulp-rename browser-sync gulp-connect sass -D
■ 以下、gulpfile.jsの修正です!
※TypeScriptの処理は省いております。
const sass = require('gulp-sass')(require('sass'));
const plumber = require('gulp-plumber');
const notify = require('gulp-notify');
const postcss = require('gulp-postcss');
const autoprefixer =require('autoprefixer');
const cssnano = require('cssnano');
const ejs = require('gulp-ejs');
const rename = require('gulp-rename');
const browserSync = require('browser-sync');
const connect = require('gulp-connect');
const { parallel } = require('gulp');
sass.compiler = require('dart-sass');
function cssTranspiler() {
return gulp.src('src/**/*.scss')
.pipe(sass())
.pipe(postcss([
autoprefixer({
grid: true,
}),
cssnano({
autoprefixer: false,
}),
]))
.pipe(plumber({
errorHandler: notify.onError('<%= error.message %>'),
}))
.pipe(gulp.dest('dist/'))
.pipe(browserSync.reload({ stream: true }));
};
function ejsCompiler() {
const srcPath = 'src/**/*.ejs';
const notTargetSrcPath = '!src/_**/*.ejs';
const distPath = 'dist/';
return gulp.src([srcPath, notTargetSrcPath])
.pipe(plumber({
errorHandler: notify.onError('<%= error.message %>'),
}))
.pipe(ejs({}, {}, { ext: '.html' }))
.pipe(rename({
extname: '.html',
}))
.pipe(gulp.dest(distPath))
.pipe(browserSync.reload({ stream: true }));
}
function server(done) {
browserSync.init({
server: {
baseDir: 'src',
},
startPath: './dist/index.html'
});
done();
};
function connectServer(done) {
connect.server({
root: './',
port: 8080,
livereload: true,
}, function (){
browserSync.init({
proxy: 'http://localhost:8080',
startPath: 'dist/index.html'
});
});
done();
}
function watch(done) {
gulp.watch(['src/css/*', 'src/css/**/*'], cssTranspiler);
gulp.watch(['src/*', 'src/**/*'], ejsCompiler);
done();
};
exports.default = parallel(connectServer, watch);
先ほど記載したgulpfile.jsのTypeScriptの処理に、browser-syncの処理を追記します。
function tsTransPiler() {
return tsProject.src()
.pipe(tsProject())
.js.pipe(gulp.dest('dist/js/'))
.pipe(browserSync.reload({ stream: true })); // 追加行
};
// ...省略
function watch(done) {
gulp.watch(['src/css/*', 'src/css/**/*'], cssTranspiler);
gulp.watch(['src/js/*', 'src/js/**/*'], tsTransPiler); // 追加行
gulp.watch(['src/*', 'src/**/*'], ejsCompiler);
done();
};
**完成した最終的なgulpfile.jsはこちらをご確認ください**
const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass'));
const plumber = require('gulp-plumber');
const notify = require('gulp-notify');
const postcss = require('gulp-postcss');
const autoprefixer =require('autoprefixer');
const cssnano = require('cssnano');
const ejs = require('gulp-ejs');
const rename = require('gulp-rename');
const typeScript = require('gulp-typescript');
const tsProject = typeScript.createProject('tsconfig.json');
const browserSync = require('browser-sync');
const connect = require('gulp-connect');
const { parallel } = require('gulp');
sass.compiler = require('dart-sass');
function cssTranspiler() {
return gulp.src('src/**/*.scss')
.pipe(sass())
.pipe(postcss([
autoprefixer({
grid: true,
}),
cssnano({
autoprefixer: false,
}),
]))
.pipe(plumber({
errorHandler: notify.onError('<%= error.message %>'),
}))
.pipe(gulp.dest('dist/'))
.pipe(browserSync.reload({ stream: true }));
};
function tsTransPiler() {
return tsProject.src()
.pipe(tsProject())
.js.pipe(gulp.dest('dist/js/'))
.pipe(browserSync.reload({ stream: true }));
};
function ejsCompiler() {
const srcPath = 'src/**/*.ejs';
const notTargetSrcPath = '!src/_**/*.ejs';
const distPath = 'dist/';
return gulp.src([srcPath, notTargetSrcPath])
.pipe(plumber({
errorHandler: notify.onError('<%= error.message %>'),
}))
.pipe(ejs({}, {}, { ext: '.html' }))
.pipe(rename({
extname: '.html',
}))
.pipe(gulp.dest(distPath))
.pipe(browserSync.reload({ stream: true }));
}
function server(done) {
browserSync.init({
server: {
baseDir: 'src',
},
startPath: './dist/index.html'
});
done();
};
function connectServer(done) {
connect.server({
root: './',
port: 8080,
livereload: true,
}, function (){
browserSync.init({
proxy: 'http://localhost:8080',
startPath: 'dist/index.html'
});
});
done();
}
function watch(done) {
gulp.watch(['src/css/*', 'src/css/**/*'], cssTranspiler);
gulp.watch(['src/js/*', 'src/js/**/*'], tsTransPiler);
gulp.watch(['src/*', 'src/**/*'], ejsCompiler);
done();
};
exports.default = parallel(connectServer, watch);
ここまでで、環境構築は完了!
それでは、実際に処理を書いていく!
処理を書いてみよう
今回は、画面側にパーツを設置し、それをクリックしたといきの挙動を実装する。
実装する挙動は、3つ。
- アコーディオン
- モーダル
- 上部への自動スクロール
**今回用意するHTML,CSSはこちらをご覧ください**
<main class="tsContainer">
<div class="tsSection" data-target="scrollTop">
<h1 class="tsSection__title">みんなのおすすめ動画</h1>
<%_
const cardData = [
{
image: '../../image/card/main1.png',
attribute: {
'class': 'tsCard',
'data-trigger': 'openModal'
},
cardTitle: '1つ目のカード!',
cardDiscription: 'スライダー1枚目のカードです!',
},
{
image: '../../image/card/main2.png',
attribute: {
'class': 'tsCard',
'data-trigger': 'openModal'
},
cardTitle: '2つ目のカード!',
cardDiscription: 'スライダー2枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard',
'data-trigger': 'openModal'
},
cardTitle: '3つ目のカード!',
cardDiscription: 'スライダー3枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '4つ目のカード!',
cardDiscription: 'スライダー4枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '5つ目のカード!',
cardDiscription: 'スライダー5枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '6つ目のカード!',
cardDiscription: 'スライダー6枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '7つ目のカード!',
cardDiscription: 'スライダー7枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '8つ目のカード!',
cardDiscription: 'スライダー8枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '9つ目のカード!',
cardDiscription: 'スライダー9枚目のカードです!',
},
{
image: '../../image/card/main3.png',
attribute: {
'class': 'tsCard is-hidden',
'data-trigger': 'openModal'
},
cardTitle: '10つ目のカード!',
cardDiscription: 'スライダー10枚目のカードです!',
},
]
_%>
<section class="tsSection__List" data-target="accordion">
<%_ cardData.forEach((attrObj) => { _%>
<%- include('./components/card', attrObj) %>
<%_ }); _%>
</section>
<%- include('./components/modal'); %>
<%- include('./components/button', {
buttonText: 'もっと見る',
attribute: {
'class': 'tsButton',
'data-trigger': 'accordion'
}
}); %>
</div>
<div class="tsSection">
<h1 class="tsSection__title">スクロール</h1>
<%- include('./components/button', {
buttonText: 'Topへスクロール',
attribute: {
'class': 'tsButton',
'data-trigger': 'scrollTop'
}
}); %>
</div>
</main>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="format-detection" content="telephone=no">
<title>type script 練習ページ</title>
<link rel="stylesheet" href="./css/reset.css">
<link rel="stylesheet" href="./css/style.css">
<script type="text/javascript" src="./js/index.js"></script>
</head>
<body>
<%- include('./_partials/header.ejs') %>
<%- include('./_partials/main.ejs') %>
<%- include('./_partials/footer.ejs') %>
</body>
</html>
@use '../_partials/components/button.scss';
@use '../_partials/components/card.scss';
@use '../_partials/components/modal.scss';
body {
&.is-fixed {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
.tsHeader {
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 2px solid #cdcdcd;
&__image {
display: block;
}
}
.tsContainer {
padding: 16px 24px;
text-align: center;
}
.tsSection {
margin-top: 24px;
}
■ ボタン
<%_
const text = typeof buttonText !== 'undefined' ? buttonText : '';
const attrData = typeof attribute !== 'undefined' ? attribute : '';
const attr = attrData ? Object.keys(attrData).map(e => ` ${e}="${attrData[e]}"`).join('') : '';
_%>
<button<%- attr %>><%= text %></button>
.tsButton {
width: 100%;
height: 44px;
margin-top: 16px;
background: #fff;
border: 2px solid #1976d2;
border-radius: 6px;
font-size: 16px;
font-family: 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'sans-serif';
font-weight: 700;
line-height: 1.5;
color: #2d2d2d;
}
■ カード
<%_
const imagePath = typeof image !== 'undefined' ? image : '';
const attrData = typeof attribute !== 'undefined' ? attribute : '';
const attr = attrData ? Object.keys(attrData).map(e => ` ${e}="${attrData[e]}"`).join('') : '';
if (typeof attrData['class'] === 'undefined') return;
const parentClass = attrData['class'].split(/\s/)[0];
const title = typeof cardTitle !== 'undefined' ? cardTitle : '';
const discription = typeof cardDiscription !== 'undefined' ? cardDiscription : '';
_%>
<article<%- attr %>>
<img class="<%= `${parentClass}__image`%>">
<h2 class="<%= `${parentClass}__title`%>"><%- title %></h2>
<p class="<%= `${parentClass}__discription`%>"><%- discription %></p>
</article>
.tsCard {
margin-top: 8px;
border: 1px solid #8d8d8d;
border-radius: 4px;
&.is-hidden {
display: none;
}
&__title {
font-size: 12px;
font-family: 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'sans-serif';
font-weight: 700;
line-height: 1.5;
color: #2d2d2d;
}
&__discription {
padding: 8px;
font-size: 10px;
font-family: 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'sans-serif';
font-weight: 700;
line-height: 1.5;
color: #2d2d2d;
}
}
■ モーダル
<div class="tsModal" data-target="openModal">
<div class="tsModal__overlay"></div>
<div class="tsModal__header">
<span class="tsModal__close" data-trigger="closeModal">閉じる</span>
</div>
<div class="tsModal__contents">
<p class="tsModal__text">モーダルを開きました!</p>
</div>
</div>
.tsModal {
display: none;
&.is-open {
display: block;
}
&__overlay {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background: #2d2d2d;
opacity: .5;
}
&__header {
width: 100vw;
height: 50px;
z-index: 30;
position: absolute;
top: 0;
left: 0;
background-color: #FFF;
border-bottom: 1px solid #8d8d8d;
}
&__close {
display: block;
width: 30px;
height: 30px;
position: absolute;
top: calc(50% - 15px);
right: 20px;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 3px;
height: 27px;
background-color: #2d2d2d;
}
&::before {
transform: translate(-50%,-50%) rotate(45deg);
}
&::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
}
&__contents {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 20;
background-color: #FFF;
}
}
早速、TypeScriptで処理を書いていきます。
今回は、src/js
配下にindex.ts
を作成し、コンパイルされると、dist/js/index.js
が吐き出される想定で進めていきます。
1. アコーディオン
今回アコーディオンは、is-hidden
クラスの付け外しで実装します。
3つ目のカードまでは常に表示で、4つ目以降がアコーディオンになるイメージです。
(() => {
const accordion = {
toggle: (displayMax: Number, targets: NodeListOf<HTMLElement>) => {
targets.forEach((target, index) => {
if (index > displayMax) {
target.classList.toggle('is-hidden');
}
})
}
};
const bindEvent = () => {
// アコーディオンのイベントセット
const targetAccordion = document.querySelector<HTMLElement>('[data-trigger="accordion"]');
targetAccordion.addEventListener('click', () => {
const targetElement = document.querySelector<HTMLElement>('[data-target="accordion"]');
if (targetElement === null) return;
const cards = targetElement.querySelectorAll('.tsCard');
if (cards.length === 0) return;
accordion.toggle(3, cards);
targetAccordion.textContent = targetAccordion.textContent === '閉じる' ? 'もっと見る' : '閉じる';
});
}
})();
ここまで書いたところで2つエラーが出ているのを確認が確認されました。
1つ目のエラーは以下です。
オブジェクトは 'null' である可能性があります。ts(2531)
JavaScriptだと特にエラーなど起こっていなかったのですが、TypeScriptだと怒ってくれるようです!
今回本エラーが出ていたのは、Element.querySelector
で取得している値だったため、
targetAccordion
の宣言の次の行に以下を追記することで解消されました。
if (targetAccordion === null) return;
2つ目のエラーは以下でした。
型 'NodeListOf<Element>' の引数を型 'NodeListOf<HTMLElement>' のパラメーターに割り当てることはできません。
型 'Element' には 型 'HTMLElement' からの次のプロパティがありません: accessKey, accessKeyLabel, autocapitalize, dir、109 など。ts(2345)
querySelectorAll
でクラスセレクタを引数に指定していたけど、型引数を指定していなかったので、適切な型推論が行われずにエラーが出ていたようです。
以下のように、型引数を指定してあげるように修正してエラーを解消しました。
const cards = targetElement.querySelectorAll<HTMLElement>('.tsCard');
2. モーダル
今回モーダルは、カードをクリックした際に、モーダルが表示されるようにし、モーダルヘッダーのバツボタンでモーダルを閉じるように実装します。
(() => {
// ...省略
const modal = {
open: () => {
const target = document.querySelector<HTMLElement>('[data-target="openModal"]');
if (target === null) return;
target.classList.add('is-open');
document.body.classList.add('is-fixed');
},
close: () => {
const target = document.querySelector<HTMLElement>('[data-target="openModal"]');
if (target === null) return;
target.classList.remove('is-open');
document.body.classList.remove('is-fixed');
}
};
const bindEvent = () => {
// ...省略
// モーダルのイベントセット
const triggerOpenModal = document.querySelectorAll<HTMLElement>('[data-trigger="openModal"]');
if (triggerOpenModal.length === 0) return;
triggerOpenModal.forEach( target => target.addEventListener('click', modal.open));
const triggerCloseModal = document.querySelector<HTMLElement>('[data-trigger="closeModal"]');
if (triggerCloseModal === null) return;
triggerCloseModal.addEventListener('click', modal.close);
};
})();
3. スクロールトップ
最後にスクロールの処理を入れます。
(() => {
const scrollTo = {
top: () => {
const targetTop = document.querySelector<HTMLElement>('[data-target="scrollTop"]');
if (targetTop === null) return;
window.scrollTo({behavior: "smooth", top: targetTop.scrollTop});
}
}
const bindEvent = () => {
// ...省略
const triggerScrollTop = document.querySelector<HTMLElement>('[data-trigger="scrollTop"]');
if (triggerScrollTop === null) return;
triggerScrollTop.addEventListener('click', scrollTo.top);
}
})();
ここまでかけたら、以下コマンドでgulpを実行し、ブラウザからイベントを発火させて、
動くかどうかを確認します。
> npx gulp
デザインはさておき、とりあえずアコーディオンとモーダル、スクロールが動くことは確認ができたのではないでしょうか??
最終的なコードはこちらのGitHubをご確認ください!
おわりに
今回、ひとまず環境構築を行い、処理を書いてブラウザの画面から動かしてみると言うところまでを行いました。
今回は簡単な処理しか作成しなかったのですが、ここまでやったところで、もう少し処理を書きたい気持ちが芽生えてきたのではないでしょうか??
あとはTypeScriptで、どんどん処理を書いていくだけかと思います!
処理を書いて完全習得を目指しましょう!
参考
本記事における開発環境構築及び実装は、以下の記事を参考に進めております。