2021年1月12日 星期二

實作 angularJs 自訂驗證 select option list - 檢查 ng-model 的值有無在 option list 中

 AngularJs 原生的 select option  的 required 驗證,

只能對 ng-model 的值為 undefined , "", null , NaN來做 required error。

參考:

ngRequired

ngModel.NgModelController 的 $isEmpty

但如果 ng-model 不是 undefined , "", null , NaN ,且其值不在 select option 中,

此時希望能檢查出驗證錯誤的話 (validation) ,

就需要自己實作。


例如可能的情境為:

如果 option list 裡都是不為 0 的數字,當 ng-model 是數字 0 時 (可能是程式常態 assign 賦值的),此時 required (或 ng-required) 就無法驗證出 required 的 validation error。


以下為我寫出來的簡單實作,

建立了一個名為 selectOptionCheckValidator 的 directive 來進行驗證:



html : 

<div ng-app="app" ng-controller="controller as ctrl">
  <form name="form">
    <label><input type="radio" name="optionType" ng-value="1" ng-model="ctrl.optionType" /> Set option list 1</label>
    <label><input type="radio" name="optionType" ng-value="2" ng-model="ctrl.optionType" /> Set option list 2</label>

    <select name="mySelector" ng-model="ctrl.selectedVal" ng-options="selectOption.optionValue.id as selectOption.title for selectOption in ctrl.selectOptionList" select-option-check-validator ng-required="true">
    </select>
    
    <button ng-click="ctrl.selectedVal = 0">
      Set selectedVal to 0
    </button>

    <div>
      ctrl.selectedVal : {{ctrl.selectedVal}}
    </div>

    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.selectedValueInOptionList ? "invalid" : "valid"}}
    </div>
    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.required ? "invalid" : "valid"}}
    </div>

  </form>
</div>



Javascript :
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
    var self = this;
    self.optionType = 1;
    self.selectedVal = 0;

    self.optionList1 = [{
      optionValue: {
        id: 11
      },
      title: "id 11"
    }, {
      optionValue: {
        id: 12
      },
      title: "id 12"
    }];

    self.optionList2 = [{
      optionValue: {
        id: 21
      },
      title: "id 21"
    }, {
      optionValue: {
        id: 22
      },
      title: "id 22"
    }];

    self.selectOptionList = [];

    $scope.$watch(angular.bind(this, function() {
      return this.optionType;
    }), function(newVal, oldVal) {
      //if value of selectedValue can not be found in self.selectOptionList,
      // selectedValue will be assigned to null.
      if (newVal == 1) {
        self.selectOptionList = self.optionList1;
      } else if (newVal == 2) {
        self.selectOptionList = self.optionList2;
        self.selectedVal = 21;
      }
    });
  }]).directive('selectOptionCheckValidator', ["$parse", function($parse) {
    return {
      require: 'ngModel',
      link: function(scope, element, attrs, ngModel) {

        var regex = /(.+)\sas\s(.+)\sfor\s(.+)\sin\s(.+)/;
        var matchString = regex.exec(attrs.ngOptions);
        var collectionExpression = matchString[4]; // "ctrl.selectOptionList"
        var arryItemVarableName = matchString[3]; // "selectOption"
        var valueExpression = matchString[1]; // "selectOption.optionValue.id"

        scope.$watch(collectionExpression, function(newValue, oldValue) {
          checkIsSelectedValueInOptionList();
        });

        element.on("change", function() {
          scope.$apply(function() {
            checkIsSelectedValueInOptionList();
          });
        });

        function checkIsSelectedValueInOptionList() {
          var isSelectedValueInOptionList = false;
          var selectedValue = $parse(attrs.ngModel)(scope);
          var optionList = $parse(collectionExpression)(scope);

          if (optionList) {
            isSelectedValueInOptionList = optionList.some(function(option) {
            
              // create a custom scope object to render optionValue from valueExpression
              var optionTempObject = new function(){
              	this[arryItemVarableName] = option;
              };
              
              var optionValue = $parse(valueExpression)(optionTempObject);
              
              return optionValue == selectedValue;
            });
            
            ngModel.$setValidity("selectedValueInOptionList", isSelectedValueInOptionList);
          }          
        }
      }
    };
  }]);



在這個例子裡,設置了兩個 option list 作為 <select> 的 option 選項,
當按下例子中的兩個 input type="radio" 時會改變 optionType ,
而 optionType 被改變時,會根據所勾選的值來重新設置不同的 option 選項給 <select>。

在 html <select> 的下方,我把 selectedVal 和 <select> 的 validation error 印出來以利觀察。

可以看到的是,
一開始 selectedVal 被設定成 0,但兩個 option 選項中都並沒有 0 這個值存在,
如果我們希望當 selectedVal 不在 option 選項中時能夠被驗證出來產生 validation error,
就必須自己實作 directive 來達成,即在這個例子中的 selectOptionCheckValidator directive。

AngularJs 原生的 required (或 ng-required) 在 selectedVal = 0 時,
會無法產生 validation error,因為它的實作是在當 selecctedVal = undefined, null, "", NaN 時才會產生 validation required error.
-------------------------------------------------------------------------------------------------------------------
另一個可以注意到的是,
我在這裡對按下第二個 input type="radio" 時,對 selectedVal 賦與 option list 2 中的值,
而按下第一個 input type="radio" 時沒有賦值。
此時可以觀察到當在一個 AngularJs Render 生命週期中,
如果 option list 被改變了,且 selectedValue 在 option list 中找不到的話,
selectedValue 就會被重新設定成 null。

Note

此例中用來解析 ng-option 字串的正規表達式較為簡易,

如果想知道完整的正規表達式,也就是可以解析 AngularJs ng-option 所有可能字串的正規達式,

可以參考官方源碼,其中可以看到正規表達式為:

^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$

圖例



2021年1月11日 星期一

實作能偵測 textarea 內容並自動調整高度的 angularjs directive

實作了能偵測 textarea 內容並自動調整高度的 angularjs directive。

當 <textarea> 裡的文字變多需要高多行的 textarea 時,即需要更高的 textrea 時,

可以自動的調高高度,

而當文字變少時也能自動減少高度。

Note:

因為有使用到一些 jQuery 的 function,例如 innerHeight,所有需要先 include jQuery

直接上程式碼及範例:

html:
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>

This &lt;textarea&gt; will change its height to contain content.
<div ng-app="app" ng-controller="controller as ctrl">
  <textarea textarea-auto-resizer></textarea>
</div>

javascript:
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
  	var self = this;
    self.text = "long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words"
  }])
  .directive('textareaAutoResizer', ["$sce", function($sce) {
    return {
      link: function(scope, element, attrs, ngModel) {
        autoResizeTextArea();

        scope.$watch(attrs.ngModel, function(newValue, oldValue) {
        	autoResizeTextArea();
        });
        
        element.on("input", autoResizeTextArea);

        function autoResizeTextArea(event) {
          var dom = angular.element(element);

          dom.css({
            height: ""
          });
          if (dom.prop("scrollHeight") > dom.innerHeight()) {
            //set dom height(without padding part) to scrollHeight - padding part
            dom.height(dom.prop("scrollHeight") - (dom.innerHeight() - dom.height()));
          }
        }
      }
    };
  }]);


參考:

  1. Creating a Directive that Adds Event Listeners

2021年1月1日 星期五

Minecraft Java 版 (1.16.4, jdk 1.8) 自製 Mod - 使用 Forge - 以 Subscribe Event 為例子

 如果只是要在 Minecraft 中玩 Forge 的 Mod (自己做好的或別人做好的),

只需要以下步驟:

  1. Forge 官網下載對應 Minecraft 版本的 Installer。
  2. 執行下載下來的 Installer 並安裝 Client 端。
  3. 把要玩的 Mod (為 jar 檔) 放到 Minecraft 的 mods 資料夾中。
    預設 Minecraft 的根目錄會裝在這:
    C:\Users\XXX\AppData\Roaming\.minecraft
  4. 執行 Minecraft 的 Launcher 啟動 Minecraft,
    點開 "安裝檔" 分頁,可以看到 Forge 的開始遊戲方式,用 Forge 開始遊戲,
    即可開始使用 Mod 玩遊戲了 (在選單介面可以看到點),如下圖所示。



以下開始介紹使用 Forge 製作自己 Mod 的方法:

我的環境為 Win10

使用的 IDE 為 Eclipse

Note:

如果 Eclipse 所使用的 jdk 不是 jdk 1.8,

此時記得要把 Forge 的 Mod 專案的 compile Java 設定成 jdk1.8,

還有在執行 Forge Mod 專案中的 gradle task 時,要指定 jdk1.8,

例如:

gradlew build -Dorg.gradle.java.home={JDK_PATH}

或是在 Eclipse 裡的 gradle plugin run configuration 裡對 gradle task 設定 java_home 的位置


  1. 去 Forge 官網下載對應 Minecraft 版本的 Mdk。

  2. 解壓縮下載下來的檔案後,裡面的東西就是一個完整所需的 Minecraft Forge Mod Java 專案 (以下暫稱 Minecraft Forge Mod 專案),不過還要執行一些 forge 已經幫我們寫好的 gradle task 來設定一些東西。
  3. 以 Windows 環境為例,打開命令列模式視窗執行
    gradlew genEclipseRuns
  4. 執行完 genEclipseRuns 的 gradle task後,再使用 Eclipse 的 Import project - Exsiting Gradle Project 功能把 Minecraft Forge Mod 專案給 Import 進來,就可以開始撰寫程式了。
  5. 要測試自己寫的 Mod 的時候,可以執行 runClient 這個 gradle task,
    此時會有一個 Minecraft 遊戲視窗被打開,進去後應可以看到自己的 Mod 已被載入,開始遊戲後就可以在遊戲中使用自制的 Mod 了。
  6. 寫好 Mod 想要輸出時,可以執行 build 這個 grade task,就會在專案的
    /build/libs 看到輸出的 Mod 的 jar 檔,把此 jar 檔放到遊戲的 mods 資料夾就可以在遊戲中使用了。


我有找到幾個不錯的參考資料,裡面的教學對我幫助很大,

雖然可能 Forge 版本不同有些微差異,但還是能幫助理解使用 Forge 自制 Minecraft Mod 的許多相關知識。

請參考本篇文章下方的 "參考資料"。


接下來我會以 Minecraft Mod 中註冊事件 (Subscribe Event) 為例子,

來實作一個在 Minecraft 中, "丟棄物品時,會在其位置產生一個XXX" 的功能

Note:

在 Minecraft 中,"丟棄" 和 "丟擲" 不同 ,其事件對應到 Forge 的 ItemTossEventProjectileImpactEvent.Throwable

以下是專案的檔案結構:



可以看到裡面已經有一個範例 Mod, ExampleMod.java,

之後會建立自己的 Mod java class,所以可以 ExampleMod.java 刪掉或是把其中的
@Mod 那行刪掉,

被紅框框起來的部份是我自己建立的 Mod Class 檔: com.my.mode.MyMod.java,

接著要為此 Mod 決定一個 Mod 的名稱,也可以說是 Mod Id,只能英文小寫,

此例我們決定叫做 mymod,

決定好 Mod Id 後,要在 

main/resources/META-INF/mods.toml

裡設定 Mod Id,在 mods.toml 中,紀錄著 Mod 的相關資訊,例如 Mod Id, Mod 的名稱、敘述、作者資料等資訊,在 Minecraft 的 Mod 資訊頁面上也會顯示這些東西。

為了簡單,我們不修改其他東西,因為並不影響自制 Mod 的功能,

只要把下列這行修改成自己的 Mod Id 即可:

# The modid of the mod

modId="mymod" #mandatory


以下直接看程式碼:

mods.toml

# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here:  https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="[35,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="All rights reserved"
# A URL to refer people to when problems occur with this mod
issueTrackerURL="http://my.issue.tracker/" #optional
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId="mymod" #mandatory
# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
# ${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata
# see the associated build.gradle script for how to populate this completely automatically during a build
version="${file.jarVersion}" #mandatory
 # A display name for the mod
displayName="My Mod" #mandatory
# A URL to query for updates for this mod. See the JSON update specification <here>
updateJSONURL="http://myurl.me/" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
displayURL="http://example.com/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="mymod.png" #optional
# A text field displayed in the mod UI
credits="Thanks for this example mod goes to Java" #optional
# A text field displayed in the mod UI
authors="Love, Cheese and small house plants" #optional
# The description text for the mod (multi line!) (#mandatory)
description='''
This is a long form description of the mod. You can write whatever you want here

Have some lorem ipsum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed mollis lacinia magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sagittis luctus odio eu tempus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque volutpat ligula eget lacus auctor sagittis. In hac habitasse platea dictumst. Nunc gravida elit vitae sem vehicula efficitur. Donec mattis ipsum et arcu lobortis, eleifend sagittis sem rutrum. Cras pharetra quam eget posuere fermentum. Sed id tincidunt justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.examplemod]] #optional
    # the modid of the dependency
    modId="forge" #mandatory
    # Does this dependency have to exist - if not, ordering below must be specified
    mandatory=true #mandatory
    # The version range of the dependency
    versionRange="[35,)" #mandatory
    # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory
    ordering="NONE"
    # Side this dependency is applied on - BOTH, CLIENT or SERVER
    side="BOTH"
# Here's another dependency
[[dependencies.examplemod]]
    modId="minecraft"
    mandatory=true
# This version range declares a minimum of the current minecraft version up to but not including the next major version
    versionRange="[1.16.4,1.17)"
    ordering="NONE"
    side="BOTH"

com.my.mod.MyMod.java :

package com.my.mode;

import net.minecraftforge.fml.common.Mod;

@Mod("mymod") // 設定 Mod Id, 請先把 ExampleMod.java 拿掉或把其 @Mod 的那行註解掉
public class MyMod {
	
	public MyMod() {
		// 註冊 EventBus,
		// 先註冊 ModEventBus, 之後 onCommonSetupEvent 會被觸發執行,
		// 接著屬於 ModEventBus 的 Event 都會被觸發執行。
		// 再註冊 ForgeEventBus,
		// 之後屬於 ForgeEventBus 的 Event (例如 ItemTossEvent, ProjectileImpactEvent.Throwable 等) 都會被觸發執行。
		ModEventBusHandler.register();
	}
}

com.my.mod.ModEventBusHandler.java :

package com.my.mode;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;

public class ModEventBusHandler {
	public static void register() {
		IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
		modEventBus.register(ModEventBusHandler.class);
		modEventBus.register(new ModEventBusHandler());
	}
	
	@SubscribeEvent
	public void onCommonSetupEvent(FMLCommonSetupEvent event) {
		ForgeEventBusHandler.register();
	}
}

com.my.mod.ForgeEventBusHandler.java :

package com.my.mode;

import net.minecraft.entity.EntityType;
import net.minecraft.entity.item.ItemEntity;
import net.minecraft.entity.passive.FoxEntity;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class ForgeEventBusHandler {
	public static void register() {
		IEventBus forgeEventBus = MinecraftForge.EVENT_BUS;
		forgeEventBus.register(ForgeEventBusHandler.class);
		forgeEventBus.register(new ForgeEventBusHandler());
	}
	
	// Event 有分成在 ModEventUs 上的,或在 ForgeEventBus 上的,
	// 此例的 ItemTossEvent 為在 ForgeEventBus 上的 Event
	@SubscribeEvent
	public void onItemTossEvent(ItemTossEvent event) {
		//ItemTossEvent: 物品丟棄時觸發的 Event, 
		//在遊戲中可用按鍵 q 來丟棄物品
		ItemEntity item = event.getEntityItem();
		World world = item.getEntityWorld();
		
		if (!world.isRemote) { // 判斷是否為 logical server 端,即處理邏輯的那端
							   // 如果此例的 isRemote 為 true,即為 logical client 端
							   // 可參考
							   // https://hackmd.io/@immortalmice/Hkj9s-CvU/https%3A%2F%2Fhackmd.io%2F%40immortalmice%2FrJKayrf9U
			
			//建立一個狐狸物件
			FoxEntity fox = new FoxEntity(EntityType.FOX, world);
			fox.setPosition(item.getPosX(), item.getPosY(), item.getPosZ());
			//將物件放到世界中
			world.addEntity(fox);
		}
	}
}

資源分享: