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? } 屬性。其實現原理為:

  1. 使用插入文檔的隱藏文本框節點計算單行文本的高度,再由 minRows, maxRows 計算文本框的最大最小高度,並計算文本框的高度。計算獲得的文本框即 textareaStyles 變數。
  2. 調用組件實例的 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 源碼。

推薦閱讀:

查看原文 >>
相关文章