mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -05:00
feat: Checkbox adds rules prop
This commit is contained in:
parent
2ef47933b8
commit
a50f8ff3bf
7 changed files with 255 additions and 106 deletions
|
@ -1,18 +1,38 @@
|
||||||
|
use crate::{FieldInjection, FieldValidationState, Rule, RuleValueIsEmpty};
|
||||||
use leptos::{context::Provider, prelude::*};
|
use leptos::{context::Provider, prelude::*};
|
||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, ops::Deref};
|
||||||
use thaw_utils::{class_list, Model};
|
use thaw_utils::{class_list, Model};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CheckboxGroup(
|
pub fn CheckboxGroup(
|
||||||
#[prop(optional, into)] class: MaybeProp<String>,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
#[prop(optional, into)] id: MaybeProp<String>,
|
||||||
|
/// A string specifying a name for the input control.
|
||||||
|
/// This name is submitted along with the control's value when the form data is submitted.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
name: MaybeProp<String>,
|
||||||
|
#[prop(optional, into)] rules: Vec<CheckboxGroupRule>,
|
||||||
/// Sets the value of the checkbox group.
|
/// Sets the value of the checkbox group.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
value: Model<HashSet<String>>,
|
value: Model<HashSet<String>>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let (id, name) = FieldInjection::use_id_and_name(id, name);
|
||||||
|
let validate = Rule::validate(rules, value, name);
|
||||||
|
Effect::new(move |prev: Option<()>| {
|
||||||
|
value.with(|_| {});
|
||||||
|
if prev.is_some() {
|
||||||
|
validate.run(Some(CheckboxGroupRuleTrigger::Change));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Provider value=CheckboxGroupInjection(value)>
|
<Provider value=CheckboxGroupInjection{ value, name }>
|
||||||
<div class=class_list!["thaw-checkbox-group", class] role="group">
|
<div
|
||||||
|
class=class_list!["thaw-checkbox-group", class]
|
||||||
|
id=id
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
{children()}
|
{children()}
|
||||||
</div>
|
</div>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
@ -20,7 +40,10 @@ pub fn CheckboxGroup(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct CheckboxGroupInjection(pub Model<HashSet<String>>);
|
pub(crate) struct CheckboxGroupInjection {
|
||||||
|
pub value: Model<HashSet<String>>,
|
||||||
|
pub name: Signal<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Copy for CheckboxGroupInjection {}
|
impl Copy for CheckboxGroupInjection {}
|
||||||
|
|
||||||
|
@ -29,3 +52,50 @@ impl CheckboxGroupInjection {
|
||||||
use_context()
|
use_context()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum CheckboxGroupRuleTrigger {
|
||||||
|
#[default]
|
||||||
|
Change,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CheckboxGroupRule(Rule<HashSet<String>, CheckboxGroupRuleTrigger>);
|
||||||
|
|
||||||
|
impl CheckboxGroupRule {
|
||||||
|
pub fn required(required: MaybeSignal<bool>) -> Self {
|
||||||
|
Self(Rule::required(required))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_with_message(
|
||||||
|
required: MaybeSignal<bool>,
|
||||||
|
message: MaybeSignal<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self(Rule::required_with_message(required, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validator(
|
||||||
|
f: impl Fn(&HashSet<String>) -> Result<(), FieldValidationState> + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self(Rule::validator(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_trigger(mut self, trigger: CheckboxGroupRuleTrigger) -> Self {
|
||||||
|
self.0.trigger = trigger;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for CheckboxGroupRule {
|
||||||
|
type Target = Rule<HashSet<String>, CheckboxGroupRuleTrigger>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuleValueIsEmpty for HashSet<String> {
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
mod checkbox_group;
|
mod checkbox_group;
|
||||||
|
|
||||||
pub use checkbox_group::CheckboxGroup;
|
pub use checkbox_group::{CheckboxGroup, CheckboxGroupRule, CheckboxGroupRuleTrigger};
|
||||||
|
|
||||||
use checkbox_group::CheckboxGroupInjection;
|
use checkbox_group::CheckboxGroupInjection;
|
||||||
use leptos::{html, prelude::*};
|
use leptos::{html, prelude::*};
|
||||||
|
@ -30,7 +30,7 @@ pub fn Checkbox(
|
||||||
let Some(group) = group.as_ref() else {
|
let Some(group) = group.as_ref() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
group.0.with(|group_value| {
|
group.value.with(|group_value| {
|
||||||
item_value.with_value(|value| {
|
item_value.with_value(|value| {
|
||||||
let Some(value) = value else {
|
let Some(value) = value else {
|
||||||
return None;
|
return None;
|
||||||
|
@ -44,11 +44,11 @@ pub fn Checkbox(
|
||||||
let input = input_ref.get_untracked().unwrap();
|
let input = input_ref.get_untracked().unwrap();
|
||||||
if group_checked.get_untracked().is_some() {
|
if group_checked.get_untracked().is_some() {
|
||||||
if input.checked() {
|
if input.checked() {
|
||||||
group.as_ref().unwrap().0.update(move |group_value| {
|
group.as_ref().unwrap().value.update(move |group_value| {
|
||||||
group_value.insert(item_value.get_value().unwrap());
|
group_value.insert(item_value.get_value().unwrap());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
group.as_ref().unwrap().0.update(move |group_value| {
|
group.as_ref().unwrap().value.update(move |group_value| {
|
||||||
item_value.with_value(|value| {
|
item_value.with_value(|value| {
|
||||||
group_value.remove(value.as_ref().unwrap());
|
group_value.remove(value.as_ref().unwrap());
|
||||||
});
|
});
|
||||||
|
@ -71,6 +71,8 @@ pub fn Checkbox(
|
||||||
class="thaw-checkbox__input"
|
class="thaw-checkbox__input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id=id.clone()
|
id=id.clone()
|
||||||
|
name=move || group.map(|g| g.name.get()).flatten()
|
||||||
|
value=item_value.get_value()
|
||||||
checked=checked
|
checked=checked
|
||||||
node_ref=input_ref
|
node_ref=input_ref
|
||||||
on:change=on_change
|
on:change=on_change
|
||||||
|
|
|
@ -33,6 +33,11 @@ view! {
|
||||||
<Field label="Password" name="password">
|
<Field label="Password" name="password">
|
||||||
<Input input_type=InputType::Password rules=vec![InputRule::required(true.into())]/>
|
<Input input_type=InputType::Password rules=vec![InputRule::required(true.into())]/>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field name="remember">
|
||||||
|
<CheckboxGroup rules=vec![CheckboxGroupRule::required(true.into())] >
|
||||||
|
<Checkbox label="Remember me" value="true"/>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</Field>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
on:click={
|
on:click={
|
||||||
|
|
|
@ -11,7 +11,8 @@ pub fn Field(
|
||||||
label: MaybeProp<String>,
|
label: MaybeProp<String>,
|
||||||
/// A string specifying a name for the input control.
|
/// A string specifying a name for the input control.
|
||||||
/// This name is submitted along with the control's value when the form data is submitted.
|
/// This name is submitted along with the control's value when the form data is submitted.
|
||||||
#[prop(optional, into)] name: MaybeProp<String>,
|
#[prop(optional, into)]
|
||||||
|
name: MaybeProp<String>,
|
||||||
/// The orientation of the label relative to the field component.
|
/// The orientation of the label relative to the field component.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
orientation: MaybeSignal<FieldOrientation>,
|
orientation: MaybeSignal<FieldOrientation>,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
mod field;
|
mod field;
|
||||||
mod field_context_provider;
|
mod field_context_provider;
|
||||||
|
mod rule;
|
||||||
|
|
||||||
pub use field::*;
|
pub use field::*;
|
||||||
pub use field_context_provider::*;
|
pub use field_context_provider::*;
|
||||||
|
pub(crate) use rule::*;
|
151
thaw/src/field/rule.rs
Normal file
151
thaw/src/field/rule.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
use super::{FieldContextInjection, FieldInjection, FieldValidationState};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use send_wrapper::SendWrapper;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
enum RuleValidator<T> {
|
||||||
|
Required(MaybeSignal<bool>),
|
||||||
|
RequiredMessage(MaybeSignal<bool>, MaybeSignal<String>),
|
||||||
|
Validator(Box<dyn Fn(&T) -> Result<(), FieldValidationState>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Rule<T, Trigger> {
|
||||||
|
validator: RuleValidator<T>,
|
||||||
|
pub trigger: Trigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Trigger> Rule<T, Trigger>
|
||||||
|
where
|
||||||
|
Trigger: Default,
|
||||||
|
{
|
||||||
|
pub fn required(required: MaybeSignal<bool>) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: Default::default(),
|
||||||
|
validator: RuleValidator::Required(required),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_with_message(
|
||||||
|
required: MaybeSignal<bool>,
|
||||||
|
message: MaybeSignal<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: Default::default(),
|
||||||
|
validator: RuleValidator::RequiredMessage(required, message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validator(
|
||||||
|
f: impl Fn(&T) -> Result<(), FieldValidationState> + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: Default::default(),
|
||||||
|
validator: RuleValidator::Validator(Box::new(f)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Trigger> Rule<T, Trigger> {
|
||||||
|
pub fn with_trigger(mut self, trigger: Trigger) -> Self {
|
||||||
|
self.trigger = trigger;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: RuleValueIsEmpty, Trigger> Rule<T, Trigger> {
|
||||||
|
pub fn call_validator<V: WithUntracked<Value = T>>(
|
||||||
|
&self,
|
||||||
|
value: V,
|
||||||
|
name: Signal<Option<String>>,
|
||||||
|
) -> Result<(), FieldValidationState> {
|
||||||
|
value.with_untracked(|value| match &self.validator {
|
||||||
|
RuleValidator::Required(required) => {
|
||||||
|
if required.get_untracked() && RuleValueIsEmpty::is_empty(value) {
|
||||||
|
let message = name.get_untracked().map_or_else(
|
||||||
|
|| String::from("Please input!"),
|
||||||
|
|name| format!("Please input {name}!"),
|
||||||
|
);
|
||||||
|
Err(FieldValidationState::Error(message))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuleValidator::RequiredMessage(required, message) => {
|
||||||
|
if required.get_untracked() && RuleValueIsEmpty::is_empty(value) {
|
||||||
|
Err(FieldValidationState::Error(message.get_untracked()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuleValidator::Validator(f) => f(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate<V, R>(rules: Vec<R>, value: V, name: Signal<Option<String>>) -> Callback<Option<Trigger>, bool>
|
||||||
|
where
|
||||||
|
V: WithUntracked<Value = T>,
|
||||||
|
V: Send + Sync + Copy + 'static,
|
||||||
|
R: Deref<Target = Rule<T, Trigger>> + 'static,
|
||||||
|
Trigger: PartialEq + 'static,
|
||||||
|
{
|
||||||
|
let field_injection = FieldInjection::use_context();
|
||||||
|
let rules = StoredValue::new(SendWrapper::new(rules));
|
||||||
|
let validate = Callback::new(move |trigger: Option<Trigger>| {
|
||||||
|
let state = rules.with_value(move |rules| {
|
||||||
|
if rules.is_empty() {
|
||||||
|
return Some(Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rules_iter = rules.iter();
|
||||||
|
let mut call_count = 0;
|
||||||
|
loop {
|
||||||
|
let Some(rule) = rules_iter.next() else {
|
||||||
|
if call_count == 0 {
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
return Some(Ok(()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(trigger) = trigger.as_ref() {
|
||||||
|
if &rule.trigger != trigger {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call_count += 1;
|
||||||
|
|
||||||
|
let state = rule.call_validator(value, name);
|
||||||
|
if state.is_err() {
|
||||||
|
return Some(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(state) = state else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let rt = state.is_ok();
|
||||||
|
if let Some(field_injection) = field_injection.as_ref() {
|
||||||
|
field_injection.update_validation_state(state);
|
||||||
|
};
|
||||||
|
rt
|
||||||
|
});
|
||||||
|
if let Some(field_context) = FieldContextInjection::use_context() {
|
||||||
|
field_context.register_field(name, move || validate.run(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
validate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RuleValueIsEmpty {
|
||||||
|
fn is_empty(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuleValueIsEmpty for String {
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.is_empty()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{FieldContextInjection, FieldInjection, FieldValidationState};
|
use std::ops::Deref;
|
||||||
|
use crate::{FieldInjection, FieldValidationState, Rule};
|
||||||
use leptos::{ev, html, prelude::*};
|
use leptos::{ev, html, prelude::*};
|
||||||
use thaw_utils::{
|
use thaw_utils::{
|
||||||
class_list, mount_style, ArcOneCallback, BoxOneCallback, ComponentRef, Model, OptionalProp,
|
class_list, mount_style, ArcOneCallback, BoxOneCallback, ComponentRef, Model, OptionalProp,
|
||||||
|
@ -61,52 +62,7 @@ pub fn Input(
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("input", include_str!("./input.css"));
|
mount_style("input", include_str!("./input.css"));
|
||||||
let (id, name) = FieldInjection::use_id_and_name(id, name);
|
let (id, name) = FieldInjection::use_id_and_name(id, name);
|
||||||
let field_injection = FieldInjection::use_context();
|
let validate = Rule::validate(rules, value, name);
|
||||||
let rules = StoredValue::new(rules);
|
|
||||||
let validate = Callback::new(move |trigger: Option<InputRuleTrigger>| {
|
|
||||||
let state = rules.with_value(move |rules| {
|
|
||||||
if rules.is_empty() {
|
|
||||||
return Some(Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rules_iter = rules.iter();
|
|
||||||
let mut call_count = 0;
|
|
||||||
loop {
|
|
||||||
let Some(rule) = rules_iter.next() else {
|
|
||||||
if call_count == 0 {
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
return Some(Ok(()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(trigger) = trigger.as_ref() {
|
|
||||||
if &rule.trigger != trigger {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
call_count += 1;
|
|
||||||
|
|
||||||
let state = rule.call_validator(value, name);
|
|
||||||
if state.is_err() {
|
|
||||||
return Some(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(state) = state else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
let rt = state.is_ok();
|
|
||||||
if let Some(field_injection) = field_injection.as_ref() {
|
|
||||||
field_injection.update_validation_state(state);
|
|
||||||
};
|
|
||||||
rt
|
|
||||||
});
|
|
||||||
if let Some(field_context) = FieldContextInjection::use_context() {
|
|
||||||
field_context.register_field(name, move || validate.run(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let parser_none = parser.is_none();
|
let parser_none = parser.is_none();
|
||||||
let on_input = {
|
let on_input = {
|
||||||
|
@ -306,75 +262,37 @@ pub enum InputRuleTrigger {
|
||||||
Change,
|
Change,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InputRuleValidator {
|
pub struct InputRule(Rule<String, InputRuleTrigger>);
|
||||||
Required(MaybeSignal<bool>),
|
|
||||||
RequiredMessage(MaybeSignal<bool>, MaybeSignal<String>),
|
|
||||||
Validator(Callback<String, Result<(), FieldValidationState>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct InputRule {
|
|
||||||
validator: InputRuleValidator,
|
|
||||||
trigger: InputRuleTrigger,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputRule {
|
impl InputRule {
|
||||||
pub fn required(required: MaybeSignal<bool>) -> Self {
|
pub fn required(required: MaybeSignal<bool>) -> Self {
|
||||||
Self {
|
Self(Rule::required(required))
|
||||||
trigger: Default::default(),
|
|
||||||
validator: InputRuleValidator::Required(required),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn required_with_message(
|
pub fn required_with_message(
|
||||||
required: MaybeSignal<bool>,
|
required: MaybeSignal<bool>,
|
||||||
message: MaybeSignal<String>,
|
message: MaybeSignal<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self(Rule::required_with_message(required,message))
|
||||||
trigger: Default::default(),
|
|
||||||
validator: InputRuleValidator::RequiredMessage(required, message),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validator(
|
pub fn validator(
|
||||||
f: impl Fn(&String) -> Result<(), FieldValidationState> + Send + Sync + 'static,
|
f: impl Fn(&String) -> Result<(), FieldValidationState> + Send + Sync + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self(Rule::validator(f))
|
||||||
trigger: Default::default(),
|
|
||||||
validator: InputRuleValidator::Validator(Callback::from(move |v| f(&v))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_trigger(mut self, trigger: InputRuleTrigger) -> Self {
|
pub fn with_trigger(mut self, trigger: InputRuleTrigger) -> Self {
|
||||||
self.trigger = trigger;
|
self.0.trigger = trigger;
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn call_validator(
|
impl Deref for InputRule {
|
||||||
&self,
|
type Target = Rule<String, InputRuleTrigger>;
|
||||||
value: Model<String>,
|
|
||||||
name: Signal<Option<String>>,
|
fn deref(&self) -> &Self::Target {
|
||||||
) -> Result<(), FieldValidationState> {
|
&self.0
|
||||||
value.with_untracked(|value| match &self.validator {
|
|
||||||
InputRuleValidator::Required(required) => {
|
|
||||||
if required.get_untracked() && value.is_empty() {
|
|
||||||
let message = name.get_untracked().map_or_else(
|
|
||||||
|| String::from("Please input!"),
|
|
||||||
|name| format!("Please input {name}!"),
|
|
||||||
);
|
|
||||||
Err(FieldValidationState::Error(message))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputRuleValidator::RequiredMessage(required, message) => {
|
|
||||||
if required.get_untracked() && value.is_empty() {
|
|
||||||
Err(FieldValidationState::Error(message.get_untracked()))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputRuleValidator::Validator(f) => f.run(value.clone()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue