ant design 組件庫:Input 組件
1. Input 組件
Input 組件只關乎布局,其餘均為常規處理。props.addonBefore 為前置標籤,props.addonAfter 為後置標籤,props.prefix 前綴圖標,props.suffix 後綴圖標。參看官方圖例:
Input 組件在 render 方法執行階段將繪製出 input 原生組件。鑒於 react 的實現機制,對於 input 原生組件的處理,有受控組件和非手空組件的區別。受控組件即根據組件的 state 變化渲染 input 節點的值,非受控組件只允許組件渲染出 input 節點的默認值,而其實時值將根據用戶的操作行為加以改變。ant design 中的 Input 組件對於受控組件和非受控組件的處理,即是當開發者同時傳入 props.value 和 props.defaultValue,props.value 的優先順序高於 props.defaultValue,且當 props.value 為 undefined 或 null,均會以空字元串渲染 input 節點的值。
在事件的綁定函數方面,常規的綁定函數均會透傳到 input 原生組件上,而 props.onPressEnter, props.onKeyDown 兩個綁定函數均會以 inputInstance.handleKeyDown 方法的形式掛載為 input 原生組件的 onKeyDown 綁定函數(inputInstance 為 Input 組件的實例)。
此外,inputInstance 實例內置 focus, blur, select 方法,其功用如 inputInstance.focus 可以在校驗表單時使校驗失敗的輸入框組件獲得焦點。inputInstance.input 屬性用於訪問 input 原生節點。
在樣式處理方面,筆者將作專文加以闡述。
2. TextArea 組件
TextArea 組件用於繪製 textarea 原生節點。不同於使用前後標籤、前後圖標影響渲染輸入框組件的布局,TextArea 組件只包含 textarea 節點,沒有其他布局元素。
TextArea 組件的特殊處理邏輯是,textarea 節點的高度隨用戶輸入內容而改變,受控於 props.autosize = { minRows?, maxRows? } 屬性。其實現原理為:
- 使用插入文檔的隱藏文本框節點計算單行文本的高度,再由 minRows, maxRows 計算文本框的最大最小高度,並計算文本框的高度。計算獲得的文本框即 textareaStyles 變數。
- 調用組件實例的 setState 方法,更新 state.textareaStyles,最終構成組件實例的 resizeTextarea 方法。對於受控組件,將在componentWillReceiveProps 生命周期中判斷組件獲得的 props.value 是否更新,再使用 window.requestAnimationFrame 方式調用 resizeTextarea 方法,以調整文本框的高度。對於非受控組件,在 onChange 事件發生時調用 resizeTextarea 方法。
calculateNodeHeight 函數計算文本框高度源碼:
const HIDDEN_TEXTAREA_STYLE = `
min-height:0 !important;
max-height:none !important;
height:0 !important;
visibility:hidden !important;
overflow:hidden !important;
position:absolute !important;
z-index:-1000 !important;
top:0 !important;
right:0 !important
`;
const SIZING_STYLE = [
letter-spacing,
line-height,
padding-top,
padding-bottom,
font-family,
font-weight,
font-size,
text-rendering,
text-transform,
width,
text-indent,
padding-left,
padding-right,
border-width,
box-sizing,
];
let computedStyleCache: {[key: string]: NodeType} = {};
let hiddenTextarea: HTMLTextAreaElement;
function calculateNodeStyling(node: HTMLElement, useCache = false) {
const nodeRef = (
node.getAttribute(id) ||
node.getAttribute(data-reactid) ||
node.getAttribute(name)
) as string;
if (useCache && computedStyleCache[nodeRef]) {
return computedStyleCache[nodeRef];
}
const style = window.getComputedStyle(node);
const boxSizing = (
style.getPropertyValue(box-sizing) ||
style.getPropertyValue(-moz-box-sizing) ||
style.getPropertyValue(-webkit-box-sizing)
);
const paddingSize = (
parseFloat(style.getPropertyValue(padding-bottom)) +
parseFloat(style.getPropertyValue(padding-top))
);
const borderSize = (
parseFloat(style.getPropertyValue(border-bottom-width)) +
parseFloat(style.getPropertyValue(border-top-width))
);
const sizingStyle = SIZING_STYLE
.map(name => `${name}:${style.getPropertyValue(name)}`)
.join(;);
const nodeInfo: NodeType = {
sizingStyle,
paddingSize,
borderSize,
boxSizing,
};
if (useCache && nodeRef) {
computedStyleCache[nodeRef] = nodeInfo;
}
return nodeInfo;
}
export default function calculateNodeHeight(
uiTextNode: HTMLTextAreaElement,
useCache = false,
minRows: number | null = null,
maxRows: number | null = null,
) {
if (!hiddenTextarea) {
hiddenTextarea = document.createElement(textarea);
document.body.appendChild(hiddenTextarea);
}
// Fix wrap="off" issue
// https://github.com/ant-design/ant-design/issues/6577
if (uiTextNode.getAttribute(wrap)) {
hiddenTextarea.setAttribute(wrap, uiTextNode.getAttribute(wrap) as string);
} else {
hiddenTextarea.removeAttribute(wrap);
}
// 將影響文本框節點高度的樣式屬性拷貝給隱藏節點
// overflow 樣式屬性置為 hidden,以此將滾動條排除在外
let {
paddingSize, borderSize,
boxSizing, sizingStyle,
} = calculateNodeStyling(uiTextNode, useCache);
hiddenTextarea.setAttribute(style, `${sizingStyle};${HIDDEN_TEXTAREA_STYLE}`);
hiddenTextarea.value = uiTextNode.value || uiTextNode.placeholder || ;
let minHeight = Number.MIN_SAFE_INTEGER;
let maxHeight = Number.MAX_SAFE_INTEGER;
let height = hiddenTextarea.scrollHeight;// 以隱藏節點計算文本框高度
let overflowY: any;
// 根據盒模式調整高度
if (boxSizing === border-box) {
height = height + borderSize;
} else if (boxSizing === content-box) {
height = height - paddingSize;
}
if (minRows !== null || maxRows !== null) {
// 計算文本框單行高度
hiddenTextarea.value = ;
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
if (minRows !== null) {
minHeight = singleRowHeight * minRows;
if (boxSizing === border-box) {
minHeight = minHeight + paddingSize + borderSize;
}
height = Math.max(minHeight, height);
}
if (maxRows !== null) {
maxHeight = singleRowHeight * maxRows;
if (boxSizing === border-box) {
maxHeight = maxHeight + paddingSize + borderSize;
}
overflowY = height > maxHeight ? : hidden;
height = Math.min(maxHeight, height);
}
}
if (!maxRows) {
overflowY = hidden;
}
return { height, minHeight, maxHeight, overflowY };
}
setState 更新文本框高度源碼:
// 不打斷本次渲染,在下一次渲染調整文本框的樣式
function onNextFrame(cb: () => void) {
if (window.requestAnimationFrame) {
return window.requestAnimationFrame(cb);
}
return window.setTimeout(cb, 1);
}
function clearNextFrameAction(nextFrameId: number) {
if (window.cancelAnimationFrame) {
window.cancelAnimationFrame(nextFrameId);
} else {
window.clearTimeout(nextFrameId);
}
}
class TextArea extends React.Component<TextAreaProps, TextAreaState> {
static defaultProps = {
prefixCls: ant-input,
};
nextFrameActionId: number;
state = {
textareaStyles: {},
};
componentDidMount() {
this.resizeTextarea();
}
componentWillReceiveProps(nextProps: TextAreaProps) {
if (this.props.value !== nextProps.value) {
if (this.nextFrameActionId) {
clearNextFrameAction(this.nextFrameActionId);
}
this.nextFrameActionId = onNextFrame(this.resizeTextarea);
}
}
resizeTextarea = () => {
const { autosize } = this.props;
if (!autosize || !this.textAreaRef) {
return;
}
const minRows = autosize ? (autosize as AutoSizeType).minRows : null;
const maxRows = autosize ? (autosize as AutoSizeType).maxRows : null;
const textareaStyles = calculateNodeHeight(this.textAreaRef, false, minRows, maxRows);
this.setState({ textareaStyles });
}
handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!(value in this.props)) {
this.resizeTextarea();
}
const { onChange } = this.props;
if (onChange) {
onChange(e);
}
}
// 余略
}
3. Search
Search 組件基於 Input 組件製作,其特殊處理是在 inputInstance.onPressEnter 方法執行過程中調用 props.onSearch 方法,適用於遠程搜索之類的場景;Search 組件還提供 props.enterButton 屬性用於配置輸入框的後綴圖標,默認使用 search 圖標,可渲染文本或按鈕。實現請參考 ant design 源碼。
4. Group
Group 組件主要以樣式控制多個表單項組件 props.children 的成組渲染。實現請參考 ant design 源碼。
推薦閱讀: