Skip to content

Commit

Permalink
fix(field): fix slot invoked outside render function (#906)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek authored Apr 23, 2024
1 parent 1063ffd commit f9d07c6
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 132 deletions.
37 changes: 19 additions & 18 deletions packages/docs/components/Field.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,28 @@ title: Field

### Props

| Prop name | Description | Type | Values | Default |
| ---------------- | ------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| addons | Field automatically attach controls together | boolean | - | <code style='white-space: nowrap; padding: 0;'>true</code> |
| groupMultiline | Allow controls to fill up multiple lines, making it responsive | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| grouped | Direct child components/elements of Field will be grouped horizontally<br/>(see which ones at the top of the page). | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| horizontal | Group label and control on the same line for horizontal forms | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| label | Field label | string | - | |
| labelFor | Same as native for set on the label | string | - | |
| labelSize | Vertical size of input | string | `small`, `medium`, `large` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>field: {<br>&nbsp;&nbsp;labelsize: undefined<br>}</code> |
| message | Help message text | string | - | |
| mobileBreakpoint | Mobile breakpoint as max-width value | string | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>field: {<br>&nbsp;&nbsp;mobileBreakpoint: undefined<br>}</code> |
| override | Override existing theme classes completely | boolean | - | |
| variant | Color of the field and help message, also adds a matching icon.<br/>Used by Input, Select and Autocomplete. | string | `primary`, `info`, `success`, `warning`, `danger`, `and any other custom color` | |
| Prop name | Description | Type | Values | Default |
| ---------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| addons | Field automatically attach controls together | boolean | - | <code style='white-space: nowrap; padding: 0;'>true</code> |
| groupMultiline | Allow controls to fill up multiple lines, making it responsive | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| grouped | Direct child components/elements of Field will be grouped horizontally<br/>(see which ones at the top of the page). | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| horizontal | Group label and control on the same line for horizontal forms | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| label | Field label | string | - | |
| labelFor | Same as native for set on the label | string | - | |
| labelSize | Vertical size of input | string | `small`, `medium`, `large` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>field: {<br>&nbsp;&nbsp;labelsize: undefined<br>}</code> |
| message | Help message text | string | - | |
| messageTag | | DynamicComponent | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>field: {<br>&nbsp;&nbsp;messageTag: "p"<br>}</code> |
| mobileBreakpoint | Mobile breakpoint as max-width value | string | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>field: {<br>&nbsp;&nbsp;mobileBreakpoint: undefined<br>}</code> |
| override | Override existing theme classes completely | boolean | - | |
| variant | Color of the field and help message, also adds a matching icon.<br/>Used by Input, Select and Autocomplete. | string | `primary`, `info`, `success`, `warning`, `danger`, `and any other custom color` | |

### Slots

| Name | Description | Bindings |
| ------- | -------------------- | -------- |
| label | Override the label | |
| default | Default content | |
| message | Override the message | |
| Name | Description | Bindings |
| ------- | -------------------- | ------------------------------------ |
| label | Override the label | **label** `string` - label property |
| message | Override the message | **message** `string` - field message |
| default | Default content | |

</div>

Expand Down
135 changes: 86 additions & 49 deletions packages/oruga/src/components/field/Field.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<script setup lang="ts">
import { computed, ref, useSlots, watch, type PropType } from "vue";
import OFieldBody from "./FieldBody.vue";
import {
computed,
ref,
useSlots,
watch,
type PropType,
type VNodeArrayChildren,
} from "vue";
import { getOption } from "@/utils/config";
import { isVNodeEmpty } from "@/utils/helpers";
import { defineClasses, useMatchMedia } from "@/composables";
import { injectField, provideField } from "./fieldInjection";
import type { ComponentClass } from "@/types";
import type { ComponentClass, DynamicComponent } from "@/types";
/**
* Fields are used to add functionality to controls and to attach/group components and elements together
Expand Down Expand Up @@ -44,6 +50,10 @@ const props = defineProps({
labelFor: { type: String, default: undefined },
/** Help message text */
message: { type: String, default: undefined },
messageTag: {
type: [String, Object, Function] as PropType<DynamicComponent>,
default: () => getOption<DynamicComponent>("field.messageTag", "p"),
},
/**
* Direct child components/elements of Field will be grouped horizontally
* (see which ones at the top of the page).
Expand Down Expand Up @@ -145,6 +155,12 @@ const props = defineProps({
const { isMobile } = useMatchMedia(props.mobileBreakpoint);
const inputId = ref(props.labelFor);
watch(
() => props.labelFor,
(v) => (inputId.value = v),
);
/** Set internal variant when prop change. */
const fieldVariant = ref(props.variant);
watch(
Expand All @@ -159,14 +175,6 @@ watch(
(v) => (fieldMessage.value = v),
);
/** this can be set from outside to update the focus state. */
const isFocused = ref(false);
/** this can be set from outside to update the filled state. */
const isFilled = ref(false);
// inject parent field component if used inside one
const { parentField } = injectField();
/** Set parent message if we use Field in Field. */
watch(
() => fieldMessage.value,
Expand All @@ -180,46 +188,50 @@ watch(
},
);
/** this can be set from outside to update the focus state */
const isFocused = ref(false);
/** this can be set from outside to update the filled state */
const isFilled = ref(false);
/** this can be set from sub fields to update the has inner field state */
const hasInnerField = ref<boolean>(false);
// inject parent field component if used inside one
const { parentField } = injectField();
// tell parent field it has an inner field
if (parentField?.value) parentField.value.addInnerField();
const slots = useSlots();
const hasLabel = computed(() => props.label || !!slots.label);
const inputId = ref(props.labelFor);
watch(
() => props.labelFor,
(v) => (inputId.value = v),
);
const hasMessage = computed(() => !!fieldMessage.value || !!slots.message);
const hasMessage = computed(
const isGrouped = computed(
() =>
!!(!parentField?.value?.hasInnerField && fieldMessage.value) ||
!!slots.message,
props.grouped ||
props.groupMultiline ||
hasInnerField.value ||
hasAddons.value,
);
const hasInnerField = computed(
() => props.grouped || props.groupMultiline || hasAddons(),
const hasAddons = computed(
() => props.addons && !props.horizontal && !!slots.default,
);
function hasAddons(): boolean {
if (!props.addons || props.horizontal) return false;
let renderedNode = 0;
// [Vue warn]: Slot "default" invoked outside of the render function: this will not track dependencies used in the slot. Invoke the slot function inside the render function instead.
const slot = slots.default();
if (slot) {
const children =
slot.length === 1 && Array.isArray(slot[0].children)
? slot[0].children
: slot;
renderedNode = children.filter((n) => !!n).length;
}
return renderedNode > 1 && props.addons && !props.horizontal;
function getInnerContent(vnode): VNodeArrayChildren {
const slot = vnode();
return slot.length === 1 && Array.isArray(slot[0].children)
? slot[0].children
: slot;
}
// --- Field Dependency Injection Feature ---
const rootRef = ref();
function addInnerField(): void {
hasInnerField.value = true;
}
function setFocus(value: boolean): void {
isFocused.value = value;
}
Expand All @@ -244,6 +256,7 @@ const provideData = computed(() => ({
hasMessage: hasMessage.value,
fieldVariant: fieldVariant.value,
fieldMessage: fieldMessage.value,
addInnerField,
setInputId,
setFocus,
setFilled,
Expand Down Expand Up @@ -321,7 +334,7 @@ const innerFieldClasses = defineClasses(
"addonsClass",
"o-field--addons",
null,
computed(() => !props.grouped && hasAddons()),
computed(() => !props.grouped && hasAddons.value),
],
);
</script>
Expand All @@ -332,27 +345,45 @@ const innerFieldClasses = defineClasses(
<label v-if="hasLabel" :for="inputId" :class="labelClasses">
<!--
@slot Override the label
@binding {string} label label property
-->
<slot name="label">{{ label }}</slot>
<slot name="label" :label="label">{{ label }}</slot>
</label>
</div>
<template v-else>
<label v-if="hasLabel" :for="inputId" :class="labelClasses">
<!--
@slot Override the label
@binding {string} label label property
-->
<slot name="label">{{ label }}</slot>
<slot name="label" :label="label">{{ label }}</slot>
</label>
</template>

<o-field-body v-if="horizontal" :classes="bodyHorizontalClasses">
<!--
@slot Default content
-->
<slot />
</o-field-body>
<div v-if="horizontal" :class="bodyHorizontalClasses">
<template
v-for="(element, index) in getInnerContent($slots.default)"
:key="element">
<component :is="element" v-if="isVNodeEmpty(element)" />
<OField
v-else
:variant="fieldVariant"
:addons="false"
:message-tag
:message-class>
<!-- render inner default slot element -->
<component :is="element" />
<!-- show field message here -->
<template v-if="index === 0" #message>
<slot name="message" :message="fieldMessage">
{{ fieldMessage }}
</slot>
</template>
</OField>
</template>
</div>

<div v-else-if="hasInnerField" :class="bodyClasses">
<div v-else-if="isGrouped" :class="bodyClasses">
<div :class="innerFieldClasses">
<!--
@slot Default content
Expand All @@ -368,11 +399,17 @@ const innerFieldClasses = defineClasses(
<slot />
</template>

<p v-if="hasMessage && !horizontal" :class="messageClasses">
<component
:is="messageTag"
v-if="hasMessage && !horizontal"
:class="messageClasses">
<!--
@slot Override the message
@binding {string} message field message
-->
<slot name="message"> {{ fieldMessage }} </slot>
</p>
<slot name="message" :message="fieldMessage">
{{ fieldMessage }}
</slot>
</component>
</div>
</template>
Loading

0 comments on commit f9d07c6

Please sign in to comment.