Help us understand the problem. What is going on with this article?

Mithril.js 試してみた(5) Excelの様な表計算アプリを3時間で作る m.component()

More than 5 years have passed since last update.

今度はふと思い立って Excel の様な表計算アプリを作ってみた。
もちろん使っているフレームワークは Mithril.js のみ。
jQuery はもちろん使ってはいない。というか jQuery で作ろうという気力は無い。
作ったアプリは HTML ファイルが1つだけ。CSS無し。埋め込んだ JavaScript のみだ。
3時間くらいでできた。

Mithril.js を始めてみて Excel っぽいアプリも作れそうな気がしたのだ。
できれば、みなさんも他のフレームワークでも似た様なものを作って晒して欲しい。
※他のフレームワークでできた Excel っぽいものを私が見つけられないだけかもしれない。
※できれば Angular.js か React.js のバージョンを見てみたい。

基本的な Excel の動作としては以下の様になると思う。
A1のセルに「10」と入力して、B1のセルに「=a1+2」、C1のセルに「=b1+2」と入れれば、
計算されて、A1は「10」、B1は「12」、C1は「14」となる。
そこで、A1に「=c1+2」とすると循環参照になり、計算が止まらなくなるので注意が必要だ。

Chrome, Firefox, Edge, ie11, ie9, ie8 で動く。
ギリギリというか、かろうじて IE8 でも動いた。

あまり良い例とは言えないかもしれないが、初心者の勉強にはなると思う。

サンプルは以下のURLで動きます。
https://lightspeedc.github.io/mithril/excel/excel-ex.html

qiita-excel.png

前回までの記事
Mithril.js 試してみた(1) todo アプリを作り始める所まで - Qiita
Mithril.js 試してみた(2) サーバーからデータを取得する m.request() - Qiita
Mithril.js 試してみた(3) console.logの様に画面に表示してみる - Qiita
Mithril.js 試してみた(4) todo アプリのフロント側まで - Qiita
Mithril.js 試してみた(5) Excelの様な表計算アプリを3時間で作る m.component() - Qiita ⇒ この記事

コードは以下の様になった。

m.component() を使ってみた。
子コンポーネントは最初に生成した時のままで、前後に移動したり、増減しなければ、これで動く。

excel-app.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Excel example - Mithril.js</title>

<script src="/js/mithril.min.js"></script>
<!--[if IE]><script src="/js/es5-shim.min.js"></script><![endif]-->

<body>
<div id="$excelElement"></div>
</body>

<script>
(function () {
    'use strict';

    var ROWS = 10, COLS = 6; // 縦と横の最大
    var HEADER_STYLE = {style: {backgroundColor: 'lightblue'}};

    // Excel Sheet コンポーネント定義
    var excelComponent = {
        // コントローラ
        controller: function () {
            // 子コンポーネント「行」の配列
            this.children = range(ROWS + 1).map(function (i) {
                return m.component(rowComponent, {row: i}); });
        },
        // ビュー
        view: function (ctrl) {
            return m('table', {border: 0, cellpadding: 0, cellspacing: 0}, ctrl.children);
        }
    };

    // Excel Row コンポーネント定義
    var rowComponent = {
        // コントローラ
        controller: function (attrs) {
            // 子コンポーネント「セル」の配列
            this.children = range(COLS + 1).map(function (i) {
                return m.component(cellComponent, {col: i, row: attrs.row}); });
        },
        // ビュー
        view: function (ctrl) { return m('tr', ctrl.children); }
    };

    var dispData = {}; // 計算結果
    var prevData = ''; // 前回の計算結果(script)
    var currData = ''; // 今回の計算結果(script)
    var timer = null;  // timer稼働中かどうか

    // 計算結果を集約する。計算結果が前回と違うなら再描画(再計算)する。
    function collectData() {
        currData = '';
        for (var v in dispData)
            currData += 'var ' + v + '=' + JSON.stringify(dispData[v]) + ';';

        if (currData !== prevData && !timer)
            timer = setTimeout(function () { timer = null; m.redraw(true); }, 300);

        prevData = currData;
    }

    // Excel Cell コンポーネント定義
    var cellComponent = {
        // コントローラ
        controller: function (attrs) {
            this.attrs = attrs;
            this.colNm = attrs.col ? String.fromCharCode(0x40 + attrs.col) : '';
            if (attrs.col && attrs.row) {
                this.cellNm = this.colNm + attrs.row;
                this.expr = ''; // 入力中の数式
                this.disp = ''; // 表示用: 入れた文字列もしくは計算結果
                this.cell = m.prop(''); // セルの値
                dispData[this.cellNm] = this.disp;
                this.focus = 0; // focusを取得しているかどうか
                this.onchange = m.withAttr('value', this.cell);
            }
        },
        // セルを計算する
        calcCell: function (ctrl) {
            collectData();

            // 入力したものが数式の場合、計算してみる
            if (ctrl.expr.charAt(0) === '=') {
                try { ctrl.disp = Function(currData +
                            'return ' + ctrl.expr.substr(1).toUpperCase())();
                } catch (e) { ctrl.disp = ctrl.expr + ' ' + e; }
            }
            // それ以外は、文字列か数値とする
            else {
                ctrl.disp = ctrl.expr;
                try { if (String(Number(ctrl.expr)) === ctrl.expr)
                            ctrl.disp = Number(ctrl.expr);
                } catch (e) {}
            }
            dispData[ctrl.cellNm] = ctrl.disp;

            collectData();
        },
        // ビュー
        view: function view(ctrl) {
            // 一番上の行 A, B, C, ...
            if (ctrl.attrs.row === 0)
                return m('th', HEADER_STYLE, ctrl.colNm);

            // 一番左の列 1, 2, 3, ...
            if (ctrl.attrs.col === 0)
                return m('th', HEADER_STYLE, ctrl.attrs.row);

            // 入力中は再計算および表示を抑制
            if (ctrl.focus === 2) return {subtree: 'retain'};
            if (ctrl.focus === 1) ctrl.focus = 2; // focus取得直後の1回だけ表示する

            // セルを計算
            cellComponent.calcCell(ctrl);

            return m('td', {},
                m('input', {
                    style: ctrl.focus ? {backgroundColor: 'lightgreen'} : {},
                    onchange: function (e) {
                        ctrl.onchange.call(this, e);
                        ctrl.expr = ctrl.cell();
                        cellComponent.calcCell(ctrl);
                    },
                    value: ctrl.focus ? ctrl.expr : ctrl.disp,
                    onfocus: function () { ctrl.focus = 1; ctrl.cell(ctrl.expr); },
                    onblur:  function () { ctrl.focus = 0; ctrl.cell(ctrl.disp); }
                })
            );
        }
    };

    // HTML要素のイベントと値にプロパティを接続するユーティリティ
    function m_on(eventName, propName, propFunc, attrs) {
        attrs = attrs || {};
        attrs['on' + eventName] = m.withAttr(propName, propFunc);
        attrs[propName] = propFunc();
        return attrs;
    }

    // componentのclosure化 (componentが静的で増減しない場合は不要)
    function m_comp(comp) {
        var ctrl = new comp.controller();
        return {view: function view() { return comp.view(ctrl); }};
    }

    // 0~n-1までの配列を返す
    function range(n) {
        var array = Array(n);
        for (var i = 0; i < n; ++i)
            array[i] = i;
        return array;
    }

    // マウント
    m.mount($excelElement, excelComponent);
})();
</script>

試行錯誤していた時間が長かったが、できてみると、コードは思ったより短いと思う。160行以下だ。

循環参照を検知できないので、試してはいけない。試すなって、ちょっ、試すなよ!(フリ)

LightSpeedC
TypeScript, JavaScript, Node.js, Java, Rust, Go, C# 等を勉強中。 React (Mithril), socket.io, D3, Leaflet 等で何か作りたいな。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away