base.jsで強力な単一ページアプリケーションを作成することに関する魅力的な一連の記事を続けます。
前回私たちは少し哲学をし、トークンにも会いました。これは、basis.jsで最も重要なことの1つです。
今日は、データの操作に焦点を当てます。
すぐに小さな発言をします。
このサイクルは、フレームワークbase.jsを使用して、SPAの構築分野におけるさまざまな問題の解決策を説明するマニュアルのセットです。
マニュアルは、 公式文書を複製するという目標を設定していませんが、そこに記載されている内容の実際的な応用を示しています。
読者は、ドキュメントを改めて語るのではなく、より詳細で実用的な例を参照したいと考えています。
一部の場所については、さらに詳しく説明します。 基本的に、これらは私自身の方法で説明する必要があると思う瞬間です。
状況を想像してみましょう。
インタラクティブなリストページを作成しています:
機能:
- エントリの追加/削除
- サーバーに記録を保存する
- ページをロードした後、保存されたレコードはサーバーから自動的にダウンロードされて表示されます
- レコードのロードおよび保存中は、 追加ボタンと保存ボタンをロックする必要があります
- レコードの読み込みおよび保存中に、「読み込み中...」というメッセージが表示されます
- すべてのエントリを削除すると、「エントリがありません」というメッセージが表示されます
おそらく、ループ、条件付きステートメントを記述し、イベントハンドラーを追加することで、この問題を解決する方法を既に考え始めているでしょう。
これを証明するには、basis.jsのいくつかの概念的な事柄に精通する必要があります。
base.jsには、異なるデータ型用のラッパーがいくつかあります。
値 -スカラー値のラッパー
DataObject-オブジェクトのラッパー
データセット -DataObject型の要素のセット
値は Token (前の記事で説明しました)に非常に似ていますが、機能が豊富であり、いくつかの追加メソッドがあります。
DataObjectは、データの変更を追跡できるオブジェクトです。 さらに、 DataObjectは委任メカニズムを提供します。
データセットは、オブジェクトのコレクションを操作するための便利なメカニズムを提供します。
また、basis.jsのデータの詳細については、ドキュメントの対応するセクションを参照することをお勧めします。 そして今、basis.jsの兵器庫から別の重要なことを分析します。
値::クエリ
静的メソッドValue :: queryは、basis.jsの最も強力な機能の1つです。
このメソッドを使用すると、 Value :: queryが適用されるオブジェクトに関連して、指定されたプロパティのチェーン全体から実際の値を取得できます。
これがどのように機能するかを理解するために、次のコードを書きましょう。
index.js
let Value = basis.require('basis.data').Value; let DataObject = basis.require('basis.data').Object; let Node = basis.require('basis.ui').Node; let group1 = new DataObject({ data: { name: ' 1' } }); let group2 = new DataObject({ data: { name: ' 2' } }); let user = new DataObject({ data: { name: '', lastName: '', group: group1 } }); new Node({ container: document.querySelector('.container'), template: resource('./template.tmpl'), binding: { group: Value.query(user, 'data.group.data.name') }, action: { setGroup1() { user.update({ group: group1 }) }, setGroup2() { user.update({ group: group2 }) } } });
template.tmpl
<div> <div> : {group} </div> <div class="btn-group"> <button class="btn btn-success" event-click="setGroup1"> 1</button> <button class="btn btn-danger" event-click="setGroup2"> 2</button> </div> </div>
ユーザーがいます。 ユーザーには、自分がメンバーになっているグループがあります。
ページ上のボタンを使用して、ユーザーグループを変更できます。
Value :: queryを呼び出した結果、指定されたオブジェクトに関連する指定された一連のプロパティの現在の値を含む新しいValueを取得します。
示されている例では、値がユーザーに指定されたグループの名前であるグループバインディングを作成します。
ただし、グループを切り替えることはできます。 この場合、値が更新されたことを理解するにはどうすればよいですか?
この質問に答えるには、basis.jsの深さを深く掘り下げる必要があります。
base.jsクラスのプロトタイプまたはインスタンスでは、 値を更新する必要があるときにValue ::クエリメソッドを「伝える」ことができる特別なpropertyDescriptors プロパティを指定できます。
base.jsのソースでDataObjectクラスがどのように記述されているかを見てみましょう。
var DataObject = AbstractData.subclass({ propertyDescriptors: { delegate: 'delegateChanged', target: 'targetChanged', root: 'rootChanged', data: { nested: true, events: 'update' } }, // ... }
要求でデータプロパティが指定されている場合、 Value ::クエリメカニズムは、このオブジェクトから更新イベントが発生するたびに(つまり、オブジェクトデータが変更されるたびに)値を更新します。
そして、私たちが行ったリクエストをもう一度見てみましょう。
Value.query(user, 'data.group.data.name')
Value ::クエリメカニズムは、指定された要求を部分に分割し、指定されたプロパティによってオブジェクトに深く入り込み、パスの各参加者のpropertyDescriptorsで指定されたイベントに自動的にサブスクライブします。
したがって、 Value :: queryの呼び出しの結果は、指定されたオブジェクトを基準にして、指定されたパスの現在の値を常に「認識」します。
データ状態
タスクに戻りましょう。
リストの要素は、追加、ロード、保存できるデータです。
ダウンロードと保存はデータ同期操作です。
base.jsには、状態の概念が組み込まれています。 これは、basis.jsの各データ型にはいくつかの状態があることを意味します。
- 未定義 -不明なデータ状態(デフォルト状態)
- 処理中 -ロード/処理中のデータ
- 準備完了-アップロード/処理され、すぐに使用できるデータ
- エラー -データのロード/処理中にエラーが発生しました
- 非推奨 -データが古くなっているため、再度同期する必要があります
現在の状況に応じて、これらの状態を切り替えることができます。
サーバーからリストをダウンロードする例を使用して、アクションのシーケンスを見てみましょう。
このメカニズムの使用に関する多くのケースを思い付くことができます。 それらのほんの一部を次に示します。
- データセットがPROCESSING状態の場合- 保存および追加ボタンをロックする必要があります
- データセットがERROR状態の場合-エラーメッセージを表示
データのダウンロードと保存はSPAで頻繁に行われる操作であるため、basis.jsにはそれらのモジュールのために別のモジュールbase.netがあります。
前述したように、同期段階に応じてデータ状態を切り替える必要があります。
状態を切り替える方法には2つのオプションがあります。
- 手動で、トランスポートコールバックを使用して
- base.net.actionを使用
base.net.actionは 、データ同期用のワークピース関数を作成するために設計されています。
一番下の行は、これらの空の関数自体が、いつ、どの状態でデータを切り替える必要があるかを知っているということです。
サーバーからデータをダウンロードし、テキストフィールドのリストとして表示し、編集および削除できるコンポーネントを作成しましょう。
骨が折れそうですか? まったくない!
index.js
let Dataset = require('basis.data').Dataset; let Node = require('basis.ui').Node; let action = require('basis.net.action'); // let cities = new Dataset({ // syncAction: action.create({ url: '/api/cities', success(response) { // , JS- DataObject this.set(response.map(data => new DataObject({ data }))) } }) }); new Node({ container: document.querySelector('.container'), active: true, dataSource: cities, template: resource('./template/list.tmpl'), // // childClass: { template: resource('./template/item.tmpl'), binding: { name: 'data:' }, action: { input(e) { // - this.update({ name: e.sender.value }); }, onDelete() { // "" - // , this.delegate.destroy(); } } } });
以上で、マークアップをスケッチして必要な値を転送するだけになりました。
list.tmpl
<b:style src="./list.css" ns="my"/> <div> <div class="my:list"> <div{childNodesElement}/> </div> </div>
item.tmpl
<b:style src="./item.css" ns="my"/> <div class="input-group my:item"> <input type="text" class="form-control input-lg" value="{name}" event-input="input"> <span class="input-group-btn"> <button class="btn btn-default btn-lg" event-click="onDelete"> <span class="glyphicon glyphicon-remove"></span> </button> </span> </div>
CSSはあなた次第です。 しかし、おそらく既に推測されているように、私はブートストラップを使用します。
そこで、Citiesデータセットを作成し、サーバーとの同期を構成しました。セットの要素はaddress / api / cityで取得する必要があることを示しました。
データは任意のソースから取得できますが、都市のリストを提供するサーバーが既にあります(記事のリポジトリにあります)。
データを受け取ったら、それらをセットに入れる必要があります。
これを行うには、 Dataset#setメソッドを使用します。 セットに入れる必要があるDataObjectから配列を受け取ります。
しかし、サーバーからの応答として、通常のJSオブジェクトの配列が入り、それらをセットに入れる前に、これらのオブジェクトをDataObjectに変換する必要があります。
記録
this.set(response.map(data => new DataObject({ data })))
補助関数「basis.data.wrap」を使用すると、大幅に削減できます。
let wrap = require('basis.data').wrap; // ... this.set(wrap(response, true));
wrapは通常のオブジェクトの配列を入力として受け取り、同じオブジェクトの配列を出力しますが、 DataObjectにラップされます。
また、コンポーネントにdataSourceプロパティを追加し、 activeプロパティをtrueに切り替えたことにも注意してください。
ドキュメントに記載されている内容に基づいて、セットにはアクティブなサブスクライバーがあります。つまり、誰かがこのセットのコンテンツを必要としていたということです。
セットは最初は空で、その状態はUNDEFINEDに設定されているため、アクティブなサブスクライバーの登録直後に、前述の規則に従ってセットが同期を開始します。 結果のコレクションオブジェクトは、ビューのDOMノードに関連付けられます。
この動作はNodeに既に備わっています。 コレクションがdataSourceプロパティに表示されるとすぐに、 Nodeは指定されたコレクションへの変更の追跡を開始します。
セットの各要素について、委任によってセット要素に関連付ける子ビュー(コンポーネント)を作成します。
セット内の要素の構成が変更されると、視覚表現も変更されます。
そのため、base.jsを使用すると、テンプレートのループやその他のロジックから保護され、データが視覚的な表現と確実に同期されます。
データバインディングは、セットの要素とその視覚的表現が委任を使用してデータを共有し始めることを意味します。
これにより、セット項目を更新するメカニズムが簡素化されます。
次に、ダイヤルの同期中に「loading ...」という碑文を表示します。
これを行うには、セットのステータスを監視し、セットがPROCESSING状態の場合にのみ碑文「loading ...」を表示します
index.js
let STATE = require('basis.data').STATE; let Value = require('basis.data').Value; // .... new Node({ // ... binding: { loading: Value.query('childNodesState').as(state => state == STATE.PROCESSING) } // ... });
テンプレートで新しいバインディングを使用します。
list.tmpl
<b:style src="./list.css" ns="my"/> <div> <div class="my:list"> <div class="alert alert-info" b:show="{loading}">...</div> <div{childNodesElement}/> </div> </div>
これで、セットの同期中に、メッセージ「loading ...」
示されている例では、同期プロセスが進行中であるかどうかについて話す必要があるローディングバインダーを作成します。 その値はデータセットの状態に依存します-セットがPROCESSING状態にある場合はtrue 、そうでない場合はfalseです。
Nodeに dataSourceが指定されている場合、 Node#childNodesStateプロパティは、指定されたデータソースの状態を複製します。
詳細はこちらをご覧ください 。
ちなみに、例からわかるように、 Value :: queryをバインディングとして指定し、指定されたパスの構築に関連するオブジェクトを指定しない場合、このオブジェクトはNodeになり、そのバインディングにはValue :: queryがあります 。
また、 Nodeのデータソースが変更された場合でも、バインディングをロードすると、 現在インストールされているデータソースに基づいて現在の値が保持されます。 この事実は、再びValue :: queryを使用する利点を示しています 。
参照用:
Value.query('childNodesState')
に置き換えることができます
Value.query('dataSource.state')
結果は同じになります。 しかし、 childNodesStateの場合、データソースから完全に抽象化し、basis.jsメカニズムに依存しています。
いいね! さらにいくつかの点を理解する必要があります。
セットにエントリがない場合、対応するメッセージが表示されます。
しかし、最初に考えてみましょう-この場合、このメッセージを表示する必要がありますか?
少なくとも、セットに要素がない場合(セットのitemCountプロパティはゼロです)。
適切なバインディングを作成しましょう。
new Node({ // ... binding: { // ... hasItems: Value.query('dataSource.itemCount'), // ... }, // ... };
しかし、リストに要素があるかどうかまだわからない期間があります。 たとえば、データがサーバーからダウンロードされている場合。 データがロードされている間、何かがそこにあるかどうかを確実に言うことはできません。 したがって、1つの値のみに依存するオプションは適切ではありません。
メッセージを表示するためのより適切な条件は、同期が完了して要素数がゼロの場合にメッセージを表示することです 。
つまり、バインディング値は2つのValueに依存します。
base.jsでは、このようなタスクは通常Expressionで実行されます。
Expressionは、渡された引数のいずれかの値が変更されたときに実行される、 トークンのようなオブジェクトを引数および関数として受け入れます。
次のようになります。
index.js
let Expression = require('basis.data.value').Expression; // ... new Node({ // ... binding: { // ... empty: node => new Expression( Value.query(node, 'childNodesState'), Value.query(node, 'dataSource.itemCount'), (state, itemCount) => !itemCount && (state == STATE.READY || state == STATE.ERROR) ), // ... }, // ... };
したがって、セットに要素がなく、セット自体が同期状態でない限り、 空のバインディングはtrueになります 。 それ以外の場合、 空はfalseになります 。
次に、作成したバインディングをマークアップに追加します。
list.tmpl
<b:style src="./list.css" ns="my"/> <div> <div class="my:list"> <div class="alert alert-info" b:show="{loading}">...</div> <div class="alert alert-warning" b:show="{empty}"> </div> <div{childNodesElement}/> </div> </div>
ここで、リストからすべてのアイテムを削除するか、空のリストがサーバーから送信されると、「リストは空です」というメッセージが画面に表示されます。
リストから最後の機会、つまりリストアイテムの追加と保存を実現することはまだ残っています。
ここでは、馴染みのあるものを使用します。
始めるには、マークアップにいくつかのボタンを追加します: saveとadd 。 したがって、マークアップの最終バージョンは次の形式を取ります。
list.tmpl
<b:style src="./list.css" ns="my"/> <div> <div class="navbar navbar-default navbar-fixed-top"> <div class="container"> <div class="my:buttons btn-group"> <button class="btn btn-success" event-click="add" disabled="{disabled}"></button> <button class="btn btn-danger" event-click="save" disabled="{disabled}"></button> </div> </div> </div> <div class="my:list"> <div class="alert alert-info" b:show="{loading}">...</div> <div class="alert alert-warning" b:show="{empty}"> </div> <div{childNodesElement}/> </div> </div>
この例からわかるように、 無効なバインディングがtrueに設定されている場合、ボタンはロックされている必要があります 。
次に、ボタンのクリックを処理し、要素の追加と保存を実装し、最後にコードの最終バージョンを確認します。
index.js
let Value = require('basis.data').Value; let Expression = require('basis.data.value').Expression; let Dataset = require('basis.data').Dataset; let DataObject = require('basis.data').Object; let STATE = require('basis.data').STATE; let wrap = require('basis.data').wrap; let Node = require('basis.ui').Node; let action = require('basis.net.action'); let cities = new Dataset({ syncAction: action.create({ url: '/api/cities', success(response) { this.set(wrap(response, true)) } }), // action save: action.create({ url: '/api/cities', method: 'post', contentType: 'application/json', encoding: 'utf8', // , "" body() { return { // // this , save items: this.getValues('data') }; } }) }); new Node({ container: document.querySelector('.container'), active: true, dataSource: cities, // Node#disabled - , binding , disabled: Value.query('childNodesState').as(state => state != STATE.READY), template: resource('./template/list.tmpl'), binding: { loading: Value.query('childNodesState').as(state => state == STATE.PROCESSING), empty: node => new Expression( Value.query(node, 'childNodesState'), Value.query(node, 'dataSource.itemCount'), (state, itemCount) => !itemCount && (state == STATE.READY || state == STATE.ERROR) ) }, action: { // add() { cities.add(new DataObject()) }, save() { cities.save() } }, childClass: { template: resource('./template/item.tmpl'), binding: { name: 'data:' }, action: { input(e) { this.update({ name: e.sender.value }) }, onDelete() { this.delegate.destroy() } } } });
saveActionはsyncActionとの類推によって作成されます。 保存ボタンがクリックされると、 保存が呼び出されます。
リストへの要素の追加は可能な限り簡単です。[ 追加 ]をクリックすると、別のオブジェクトをセットに追加するだけで十分です。内部リンクメカニズムはすべてを配置し、それに応じてセットの新しい要素が視覚表現で表示されるようにします。
前述のように、そのようなタスクは、ループや条件ステートメントを使用せずに、basis.jsで解決されます。 すべては、basis.jsメカニズムに基づいて実装されます。
もちろん、basis.js内にはループと条件文がありますが、重要なことは、basis.jsを使用してそれらの使用を最小限に抑えることができることです。 クライアントコード、特にテンプレート。
それだけです。 おもしろくて有益だったことを願っています。
次のマニュアルまで!
貴重なアドバイスをいただいたlahmatiyに感謝します;)
便利なリンク:
UPD: base.jsでgitter-chatを開始しました。 追加し、質問します。